mirror of
https://github.com/2revoemag/PSF-BotServer.git
synced 2026-02-24 00:53:35 +00:00
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:
parent
144804139f
commit
4634dffe00
66 changed files with 1560 additions and 1838 deletions
|
|
@ -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
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
10791
common/src/main/scala/net/psforever/actors/session/SessionActor.scala
Normal file
10791
common/src/main/scala/net/psforever/actors/session/SessionActor.scala
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,358 @@
|
|||
package net.psforever.login
|
||||
|
||||
import java.security.SecureRandom
|
||||
|
||||
import akka.actor.MDCContextAware.Implicits._
|
||||
import akka.actor.{Actor, ActorRef, MDCContextAware}
|
||||
import net.psforever.crypto.CryptoInterface
|
||||
import net.psforever.crypto.CryptoInterface.CryptoStateWithMAC
|
||||
import net.psforever.packet._
|
||||
import net.psforever.packet.control._
|
||||
import net.psforever.packet.crypto._
|
||||
import net.psforever.packet.game.PingMsg
|
||||
import org.log4s.MDC
|
||||
import scodec.Attempt.{Failure, Successful}
|
||||
import scodec.bits._
|
||||
|
||||
sealed trait CryptoSessionAPI
|
||||
final case class DropCryptoSession() extends CryptoSessionAPI
|
||||
|
||||
/**
|
||||
* Actor that stores crypto state for a connection, appropriately encrypts and decrypts packets,
|
||||
* and passes packets along to the next hop once processed.
|
||||
*/
|
||||
class CryptoSessionActor extends Actor with MDCContextAware {
|
||||
private[this] val log = org.log4s.getLogger
|
||||
|
||||
var sessionId: Long = 0
|
||||
var leftRef: ActorRef = ActorRef.noSender
|
||||
var rightRef: ActorRef = ActorRef.noSender
|
||||
|
||||
var cryptoDHState: Option[CryptoInterface.CryptoDHState] = None
|
||||
var cryptoState: Option[CryptoInterface.CryptoStateWithMAC] = None
|
||||
val random = new SecureRandom()
|
||||
|
||||
// crypto handshake state
|
||||
var serverChallenge = ByteVector.empty
|
||||
var serverChallengeResult = ByteVector.empty
|
||||
var serverMACBuffer = ByteVector.empty
|
||||
|
||||
var clientPublicKey = ByteVector.empty
|
||||
var clientChallenge = ByteVector.empty
|
||||
var clientChallengeResult = ByteVector.empty
|
||||
|
||||
var clientNonce: Long = 0
|
||||
var serverNonce: Long = 0
|
||||
|
||||
// Don't leak crypto object memory even on an exception
|
||||
override def postStop() = {
|
||||
cleanupCrypto()
|
||||
}
|
||||
|
||||
def receive = Initializing
|
||||
|
||||
def Initializing: Receive = {
|
||||
case HelloFriend(sharedSessionId, pipe) =>
|
||||
import MDCContextAware.Implicits._
|
||||
this.sessionId = sharedSessionId
|
||||
leftRef = sender()
|
||||
if (pipe.hasNext) {
|
||||
rightRef = pipe.next // who ever we send to has to send something back to us
|
||||
rightRef !> HelloFriend(sessionId, pipe)
|
||||
} else {
|
||||
rightRef = sender()
|
||||
}
|
||||
log.trace(s"Left sender ${leftRef.path.name}")
|
||||
context.become(NewClient)
|
||||
|
||||
case default =>
|
||||
log.error("Unknown message " + default)
|
||||
context.stop(self)
|
||||
}
|
||||
|
||||
def NewClient: Receive = {
|
||||
case RawPacket(msg) =>
|
||||
PacketCoding.UnmarshalPacket(msg) match {
|
||||
case Successful(p) =>
|
||||
log.trace("Initializing -> NewClient")
|
||||
|
||||
p match {
|
||||
case ControlPacket(_, ClientStart(nonce)) =>
|
||||
clientNonce = nonce
|
||||
serverNonce = Math.abs(random.nextInt())
|
||||
sendResponse(PacketCoding.CreateControlPacket(ServerStart(nonce, serverNonce)))
|
||||
log.trace(s"ClientStart($nonce), $serverNonce")
|
||||
|
||||
context.become(CryptoExchange)
|
||||
case _ =>
|
||||
log.error(s"Unexpected packet type $p in state NewClient")
|
||||
}
|
||||
case Failure(_) =>
|
||||
// There is a special case where no crypto is being used.
|
||||
// The only packet coming through looks like PingMsg. This is a hardcoded
|
||||
// feature of the client @ 0x005FD618
|
||||
PacketCoding.DecodePacket(msg) match {
|
||||
case Successful(packet) =>
|
||||
packet match {
|
||||
case ping @ PingMsg(_, _) =>
|
||||
// reflect the packet back to the sender
|
||||
sendResponse(ping)
|
||||
case _ =>
|
||||
log.error(s"Unexpected non-crypto packet type $packet in state NewClient")
|
||||
}
|
||||
case Failure(e) =>
|
||||
log.error("Could not decode packet: " + e + s" in state NewClient")
|
||||
}
|
||||
}
|
||||
case default =>
|
||||
log.error(s"Invalid message '$default' received in state NewClient")
|
||||
}
|
||||
|
||||
def CryptoExchange: Receive = {
|
||||
case RawPacket(msg) =>
|
||||
PacketCoding.UnmarshalPacket(msg, CryptoPacketOpcode.ClientChallengeXchg) match {
|
||||
case Failure(e) =>
|
||||
log.error("Could not decode packet in state CryptoExchange: " + e)
|
||||
|
||||
case Successful(pkt) =>
|
||||
log.trace("NewClient -> CryptoExchange")
|
||||
pkt match {
|
||||
case CryptoPacket(seq, ClientChallengeXchg(time, challenge, p, g)) =>
|
||||
cryptoDHState = Some(new CryptoInterface.CryptoDHState())
|
||||
val dh = cryptoDHState.get
|
||||
// initialize our crypto state from the client's P and G
|
||||
dh.start(p, g)
|
||||
// save the client challenge
|
||||
clientChallenge = ServerChallengeXchg.getCompleteChallenge(time, challenge)
|
||||
// save the packet we got for a MAC check later. drop the first 3 bytes
|
||||
serverMACBuffer ++= msg.drop(3)
|
||||
val serverTime = System.currentTimeMillis() / 1000L
|
||||
val randomChallenge = getRandBytes(0xc)
|
||||
// store the complete server challenge for later
|
||||
serverChallenge = ServerChallengeXchg.getCompleteChallenge(serverTime, randomChallenge)
|
||||
val packet =
|
||||
PacketCoding.CreateCryptoPacket(seq, ServerChallengeXchg(serverTime, randomChallenge, dh.getPublicKey))
|
||||
val sentPacket = sendResponse(packet)
|
||||
// save the sent packet a MAC check
|
||||
serverMACBuffer ++= sentPacket.drop(3)
|
||||
context.become(CryptoSetupFinishing)
|
||||
|
||||
case _ =>
|
||||
log.error(s"Unexpected packet type $pkt in state CryptoExchange")
|
||||
}
|
||||
}
|
||||
case default =>
|
||||
log.error(s"Invalid message '$default' received in state CryptoExchange")
|
||||
}
|
||||
|
||||
def CryptoSetupFinishing: Receive = {
|
||||
case RawPacket(msg) =>
|
||||
PacketCoding.UnmarshalPacket(msg, CryptoPacketOpcode.ClientFinished) match {
|
||||
case Failure(e) => log.error("Could not decode packet in state CryptoSetupFinishing: " + e)
|
||||
case Successful(p) =>
|
||||
log.trace("CryptoExchange -> CryptoSetupFinishing")
|
||||
|
||||
p match {
|
||||
case CryptoPacket(seq, ClientFinished(clientPubKey, clientChalResult)) =>
|
||||
clientPublicKey = clientPubKey
|
||||
clientChallengeResult = clientChalResult
|
||||
|
||||
// save the packet we got for a MAC check later
|
||||
serverMACBuffer ++= msg.drop(3)
|
||||
|
||||
val dh = cryptoDHState.get
|
||||
val agreedValue = dh.agree(clientPublicKey)
|
||||
|
||||
// we are now done with the DH crypto object
|
||||
dh.close
|
||||
|
||||
/*println("Agreed: " + agreedValue)
|
||||
println(s"Client challenge: $clientChallenge")*/
|
||||
val agreedMessage = ByteVector("master secret".getBytes) ++ clientChallenge ++
|
||||
hex"00000000" ++ serverChallenge ++ hex"00000000"
|
||||
|
||||
//println("In message: " + agreedMessage)
|
||||
|
||||
val masterSecret = CryptoInterface.MD5MAC(agreedValue, agreedMessage, 20)
|
||||
|
||||
//println("Master secret: " + masterSecret)
|
||||
|
||||
serverChallengeResult = CryptoInterface.MD5MAC(
|
||||
masterSecret,
|
||||
ByteVector("server finished".getBytes) ++ serverMACBuffer ++ hex"01",
|
||||
0xc
|
||||
)
|
||||
|
||||
// val clientChallengeResultCheck = CryptoInterface.MD5MAC(masterSecret,
|
||||
// ByteVector("client finished".getBytes) ++ serverMACBuffer ++ hex"01" ++ clientChallengeResult ++ hex"01",
|
||||
// 0xc)
|
||||
// println("Check result: " + CryptoInterface.verifyMAC(clientChallenge, clientChallengeResult))
|
||||
|
||||
val decExpansion = ByteVector("client expansion".getBytes) ++ hex"0000" ++ serverChallenge ++
|
||||
hex"00000000" ++ clientChallenge ++ hex"00000000"
|
||||
|
||||
val encExpansion = ByteVector("server expansion".getBytes) ++ hex"0000" ++ serverChallenge ++
|
||||
hex"00000000" ++ clientChallenge ++ hex"00000000"
|
||||
|
||||
/*println("DecExpansion: " + decExpansion)
|
||||
println("EncExpansion: " + encExpansion)*/
|
||||
|
||||
// expand the encryption and decryption keys
|
||||
// The first 20 bytes are for RC5, and the next 16 are for the MAC'ing keys
|
||||
val expandedDecKey =
|
||||
CryptoInterface.MD5MAC(masterSecret, decExpansion, 0x40) // this is what is visible in IDA
|
||||
|
||||
val expandedEncKey = CryptoInterface.MD5MAC(masterSecret, encExpansion, 0x40)
|
||||
|
||||
val decKey = expandedDecKey.take(20)
|
||||
val encKey = expandedEncKey.take(20)
|
||||
val decMACKey = expandedDecKey.drop(20).take(16)
|
||||
val encMACKey = expandedEncKey.drop(20).take(16)
|
||||
|
||||
/*println("**** DecKey: " + decKey)
|
||||
println("**** EncKey: " + encKey)
|
||||
println("**** DecMacKey: " + decMACKey)
|
||||
println("**** EncMacKey: " + encMACKey)*/
|
||||
|
||||
// spin up our encryption program
|
||||
cryptoState = Some(new CryptoStateWithMAC(decKey, encKey, decMACKey, encMACKey))
|
||||
|
||||
val packet = PacketCoding.CreateCryptoPacket(seq, ServerFinished(serverChallengeResult))
|
||||
|
||||
sendResponse(packet)
|
||||
|
||||
context.become(Established)
|
||||
case default => failWithError(s"Unexpected packet type $default in state CryptoSetupFinished")
|
||||
}
|
||||
}
|
||||
case default => failWithError(s"Invalid message '$default' received in state CryptoSetupFinished")
|
||||
}
|
||||
|
||||
def Established: Receive = {
|
||||
//same as having received ad hoc hexadecimal
|
||||
case RawPacket(msg) =>
|
||||
if (sender() == rightRef) {
|
||||
val packet = PacketCoding.encryptPacket(cryptoState.get, 0, msg).require
|
||||
sendResponse(packet)
|
||||
} else { //from network-side
|
||||
PacketCoding.UnmarshalPacket(msg) match {
|
||||
case Successful(p) =>
|
||||
p match {
|
||||
case encPacket @ EncryptedPacket(_ /*seq*/, _) =>
|
||||
PacketCoding.decryptPacketData(cryptoState.get, encPacket) match {
|
||||
case Successful(packet) =>
|
||||
MDC("sessionId") = sessionId.toString
|
||||
rightRef !> RawPacket(packet)
|
||||
case Failure(e) =>
|
||||
log.error("Failed to decode encrypted packet: " + e)
|
||||
}
|
||||
case default =>
|
||||
failWithError(s"Unexpected packet type $default in state Established")
|
||||
|
||||
}
|
||||
case Failure(e) =>
|
||||
log.error("Could not decode raw packet: " + e)
|
||||
}
|
||||
}
|
||||
//message to self?
|
||||
case api: CryptoSessionAPI =>
|
||||
api match {
|
||||
case DropCryptoSession() =>
|
||||
handleEstablishedPacket(
|
||||
sender(),
|
||||
PacketCoding.CreateControlPacket(TeardownConnection(clientNonce))
|
||||
)
|
||||
}
|
||||
//echo the session router? isn't that normally the leftRef?
|
||||
case sessionAPI: SessionRouterAPI =>
|
||||
leftRef !> sessionAPI
|
||||
//error
|
||||
case default =>
|
||||
failWithError(s"Invalid message '$default' received in state Established")
|
||||
}
|
||||
|
||||
def failWithError(error: String) = {
|
||||
log.error(error)
|
||||
}
|
||||
|
||||
def cleanupCrypto() = {
|
||||
if (cryptoDHState.isDefined) {
|
||||
cryptoDHState.get.close
|
||||
cryptoDHState = None
|
||||
}
|
||||
|
||||
if (cryptoState.isDefined) {
|
||||
cryptoState.get.close
|
||||
cryptoState = None
|
||||
}
|
||||
}
|
||||
|
||||
def resetState(): Unit = {
|
||||
context.become(receive)
|
||||
|
||||
// reset the crypto primitives
|
||||
cleanupCrypto()
|
||||
|
||||
serverChallenge = ByteVector.empty
|
||||
serverChallengeResult = ByteVector.empty
|
||||
serverMACBuffer = ByteVector.empty
|
||||
clientPublicKey = ByteVector.empty
|
||||
clientChallenge = ByteVector.empty
|
||||
clientChallengeResult = ByteVector.empty
|
||||
}
|
||||
|
||||
def handleEstablishedPacket(from: ActorRef, cont: PlanetSidePacketContainer): Unit = {
|
||||
//we are processing a packet that we decrypted
|
||||
if (from == self) { //to WSA, LSA, etc.
|
||||
rightRef !> cont
|
||||
} else if (from == rightRef) { //processing a completed packet from the right; to network-side
|
||||
PacketCoding.getPacketDataForEncryption(cont) match {
|
||||
case Successful((seq, data)) =>
|
||||
val packet = PacketCoding.encryptPacket(cryptoState.get, seq, data).require
|
||||
sendResponse(packet)
|
||||
case Failure(ex) =>
|
||||
log.error(s"$ex")
|
||||
}
|
||||
} else {
|
||||
log.error(s"Invalid sender when handling a message in Established $from")
|
||||
}
|
||||
}
|
||||
|
||||
def sendResponse(cont: PlanetSidePacketContainer): ByteVector = {
|
||||
log.trace("CRYPTO SEND: " + cont)
|
||||
val pkt = PacketCoding.MarshalPacket(cont)
|
||||
pkt match {
|
||||
case Failure(_) =>
|
||||
log.error(s"Failed to marshal packet ${cont.getClass.getName} when sending response")
|
||||
ByteVector.empty
|
||||
|
||||
case Successful(v) =>
|
||||
val bytes = v.toByteVector
|
||||
MDC("sessionId") = sessionId.toString
|
||||
leftRef !> ResponsePacket(bytes)
|
||||
bytes
|
||||
}
|
||||
}
|
||||
|
||||
def sendResponse(pkt: PlanetSideGamePacket): ByteVector = {
|
||||
log.trace("CRYPTO SEND GAME: " + pkt)
|
||||
val pktEncoded = PacketCoding.EncodePacket(pkt)
|
||||
pktEncoded match {
|
||||
case Failure(_) =>
|
||||
log.error(s"Failed to encode packet ${pkt.getClass.getName} when sending response")
|
||||
ByteVector.empty
|
||||
|
||||
case Successful(v) =>
|
||||
val bytes = v.toByteVector
|
||||
MDC("sessionId") = sessionId.toString
|
||||
leftRef !> ResponsePacket(bytes)
|
||||
bytes
|
||||
}
|
||||
}
|
||||
|
||||
def getRandBytes(amount: Int): ByteVector = {
|
||||
val array = Array.ofDim[Byte](amount)
|
||||
random.nextBytes(array)
|
||||
ByteVector.view(array)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,319 @@
|
|||
package net.psforever.login
|
||||
|
||||
import java.net.{InetAddress, InetSocketAddress}
|
||||
|
||||
import akka.actor.MDCContextAware.Implicits._
|
||||
import akka.actor.{Actor, ActorRef, Cancellable, MDCContextAware}
|
||||
import com.github.t3hnar.bcrypt._
|
||||
import net.psforever.objects.{Account, Default}
|
||||
import net.psforever.packet.control._
|
||||
import net.psforever.packet.game.LoginRespMessage.{LoginError, StationError, StationSubscriptionStatus}
|
||||
import net.psforever.packet.game._
|
||||
import net.psforever.packet.{PlanetSideGamePacket, _}
|
||||
import net.psforever.persistence
|
||||
import net.psforever.types.PlanetSideEmpire
|
||||
import net.psforever.util.Config
|
||||
import net.psforever.util.Database._
|
||||
import org.log4s.MDC
|
||||
import scodec.bits._
|
||||
import services.ServiceManager
|
||||
import services.ServiceManager.Lookup
|
||||
import services.account.{ReceiveIPAddress, RetrieveIPAddress, StoreAccountData}
|
||||
|
||||
import scala.concurrent.Future
|
||||
import scala.concurrent.duration._
|
||||
import scala.util.{Failure, Success}
|
||||
|
||||
class LoginSessionActor extends Actor with MDCContextAware {
|
||||
private[this] val log = org.log4s.getLogger
|
||||
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
private case class UpdateServerList()
|
||||
|
||||
val usernameRegex = """[A-Za-z0-9]{3,}""".r
|
||||
|
||||
var sessionId: Long = 0
|
||||
var leftRef: ActorRef = ActorRef.noSender
|
||||
var rightRef: ActorRef = ActorRef.noSender
|
||||
var accountIntermediary: ActorRef = ActorRef.noSender
|
||||
|
||||
var updateServerListTask: Cancellable = Default.Cancellable
|
||||
|
||||
var ipAddress: String = ""
|
||||
var hostName: String = ""
|
||||
var canonicalHostName: String = ""
|
||||
var port: Int = 0
|
||||
|
||||
val serverName = Config.app.world.serverName
|
||||
val publicAddress = new InetSocketAddress(InetAddress.getByName(Config.app.public), Config.app.world.port)
|
||||
|
||||
// Reference: https://stackoverflow.com/a/50470009
|
||||
private val numBcryptPasses = 10
|
||||
|
||||
override def postStop() = {
|
||||
if (updateServerListTask != null)
|
||||
updateServerListTask.cancel()
|
||||
}
|
||||
|
||||
def receive = Initializing
|
||||
|
||||
def Initializing: Receive = {
|
||||
case HelloFriend(aSessionId, pipe) =>
|
||||
this.sessionId = aSessionId
|
||||
leftRef = sender()
|
||||
if (pipe.hasNext) {
|
||||
rightRef = pipe.next
|
||||
rightRef !> HelloFriend(aSessionId, pipe)
|
||||
} else {
|
||||
rightRef = sender()
|
||||
}
|
||||
context.become(Started)
|
||||
ServiceManager.serviceManager ! Lookup("accountIntermediary")
|
||||
|
||||
case _ =>
|
||||
log.error("Unknown message")
|
||||
context.stop(self)
|
||||
}
|
||||
|
||||
def Started: Receive = {
|
||||
case ServiceManager.LookupResult("accountIntermediary", endpoint) =>
|
||||
accountIntermediary = endpoint
|
||||
case ReceiveIPAddress(address) =>
|
||||
ipAddress = address.Address
|
||||
hostName = address.HostName
|
||||
canonicalHostName = address.CanonicalHostName
|
||||
port = address.Port
|
||||
case UpdateServerList() =>
|
||||
updateServerList()
|
||||
case ControlPacket(_, ctrl) =>
|
||||
handleControlPkt(ctrl)
|
||||
case GamePacket(_, _, game) =>
|
||||
handleGamePkt(game)
|
||||
case default => failWithError(s"Invalid packet class received: $default")
|
||||
}
|
||||
|
||||
def handleControlPkt(pkt: PlanetSideControlPacket) = {
|
||||
pkt match {
|
||||
/// TODO: figure out what this is what what it does for the PS client
|
||||
/// I believe it has something to do with reliable packet transmission and resending
|
||||
case sync @ ControlSync(diff, _, _, _, _, _, fa, fb) =>
|
||||
log.trace(s"SYNC: $sync")
|
||||
val serverTick = Math.abs(System.nanoTime().toInt) // limit the size to prevent encoding error
|
||||
sendResponse(PacketCoding.CreateControlPacket(ControlSyncResp(diff, serverTick, fa, fb, fb, fa)))
|
||||
|
||||
case TeardownConnection(_) =>
|
||||
sendResponse(DropSession(sessionId, "client requested session termination"))
|
||||
|
||||
case default =>
|
||||
log.error(s"Unhandled ControlPacket $default")
|
||||
}
|
||||
}
|
||||
|
||||
def handleGamePkt(pkt: PlanetSideGamePacket) =
|
||||
pkt match {
|
||||
case LoginMessage(majorVersion, minorVersion, buildDate, username, password, token, revision) =>
|
||||
// TODO: prevent multiple LoginMessages from being processed in a row!! We need a state machine
|
||||
|
||||
val clientVersion = s"Client Version: $majorVersion.$minorVersion.$revision, $buildDate"
|
||||
|
||||
accountIntermediary ! RetrieveIPAddress(sessionId)
|
||||
|
||||
if (token.isDefined)
|
||||
log.info(s"New login UN:$username Token:${token.get}. $clientVersion")
|
||||
else {
|
||||
// log.info(s"New login UN:$username PW:$password. $clientVersion")
|
||||
log.info(s"New login UN:$username. $clientVersion")
|
||||
}
|
||||
|
||||
accountLogin(username, password.get)
|
||||
|
||||
case ConnectToWorldRequestMessage(name, _, _, _, _, _, _) =>
|
||||
log.info(s"Connect to world request for '$name'")
|
||||
val response = ConnectToWorldMessage(serverName, publicAddress.getAddress.getHostAddress, publicAddress.getPort)
|
||||
sendResponse(PacketCoding.CreateGamePacket(0, response))
|
||||
sendResponse(DropSession(sessionId, "user transferring to world"))
|
||||
|
||||
case _ =>
|
||||
log.debug(s"Unhandled GamePacket $pkt")
|
||||
}
|
||||
|
||||
def accountLogin(username: String, password: String): Unit = {
|
||||
import ctx._
|
||||
val newToken = this.generateToken()
|
||||
|
||||
val result = for {
|
||||
// backwards compatibility: prefer exact match first, then try lowercase
|
||||
accountsExact <- ctx.run(query[persistence.Account].filter(_.username == lift(username)))
|
||||
accountsLower <- accountsExact.headOption match {
|
||||
case None =>
|
||||
ctx.run(query[persistence.Account].filter(_.username.toLowerCase == lift(username).toLowerCase))
|
||||
case Some(_) =>
|
||||
Future.successful(Seq())
|
||||
}
|
||||
accountOption <- accountsExact.headOption orElse accountsLower.headOption match {
|
||||
case Some(account) => Future.successful(Some(account))
|
||||
case None => {
|
||||
Config.app.login.createMissingAccounts match {
|
||||
case true =>
|
||||
val passhash: String = password.bcrypt(numBcryptPasses)
|
||||
ctx.run(
|
||||
query[persistence.Account]
|
||||
.insert(_.passhash -> lift(passhash), _.username -> lift(username))
|
||||
.returningGenerated(_.id)
|
||||
) flatMap { id => ctx.run(query[persistence.Account].filter(_.id == lift(id))) } map { accounts =>
|
||||
Some(accounts.head)
|
||||
}
|
||||
case false =>
|
||||
loginFailureResponse(username, newToken)
|
||||
Future.successful(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
login <- accountOption match {
|
||||
case Some(account) =>
|
||||
(account.inactive, password.isBcrypted(account.passhash)) match {
|
||||
case (false, true) =>
|
||||
accountIntermediary ! StoreAccountData(newToken, new Account(account.id, account.username, account.gm))
|
||||
val future = ctx.run(
|
||||
query[persistence.Login].insert(
|
||||
_.accountId -> lift(account.id),
|
||||
_.ipAddress -> lift(ipAddress),
|
||||
_.canonicalHostname -> lift(canonicalHostName),
|
||||
_.hostname -> lift(hostName),
|
||||
_.port -> lift(port)
|
||||
)
|
||||
)
|
||||
loginSuccessfulResponse(username, newToken)
|
||||
updateServerListTask =
|
||||
context.system.scheduler.scheduleWithFixedDelay(0 seconds, 2 seconds, self, UpdateServerList())
|
||||
future
|
||||
case (_, false) =>
|
||||
loginPwdFailureResponse(username, newToken)
|
||||
Future.successful(None)
|
||||
case (true, _) =>
|
||||
loginAccountFailureResponse(username, newToken)
|
||||
Future.successful(None)
|
||||
}
|
||||
case None => Future.successful(None)
|
||||
}
|
||||
} yield login
|
||||
|
||||
result.onComplete {
|
||||
case Success(_) =>
|
||||
case Failure(e) => log.error(e.getMessage())
|
||||
}
|
||||
}
|
||||
|
||||
def loginSuccessfulResponse(username: String, newToken: String) = {
|
||||
sendResponse(
|
||||
PacketCoding.CreateGamePacket(
|
||||
0,
|
||||
LoginRespMessage(
|
||||
newToken,
|
||||
LoginError.Success,
|
||||
StationError.AccountActive,
|
||||
StationSubscriptionStatus.Active,
|
||||
0,
|
||||
username,
|
||||
10001
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
def loginPwdFailureResponse(username: String, newToken: String) = {
|
||||
log.info(s"Failed login to account $username")
|
||||
sendResponse(
|
||||
PacketCoding.CreateGamePacket(
|
||||
0,
|
||||
LoginRespMessage(
|
||||
newToken,
|
||||
LoginError.BadUsernameOrPassword,
|
||||
StationError.AccountActive,
|
||||
StationSubscriptionStatus.Active,
|
||||
685276011,
|
||||
username,
|
||||
10001
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
def loginFailureResponse(username: String, newToken: String) = {
|
||||
log.info("DB problem")
|
||||
sendResponse(
|
||||
PacketCoding.CreateGamePacket(
|
||||
0,
|
||||
LoginRespMessage(
|
||||
newToken,
|
||||
LoginError.unk1,
|
||||
StationError.AccountActive,
|
||||
StationSubscriptionStatus.Active,
|
||||
685276011,
|
||||
username,
|
||||
10001
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
def loginAccountFailureResponse(username: String, newToken: String) = {
|
||||
log.info(s"Account $username inactive")
|
||||
sendResponse(
|
||||
PacketCoding.CreateGamePacket(
|
||||
0,
|
||||
LoginRespMessage(
|
||||
newToken,
|
||||
LoginError.BadUsernameOrPassword,
|
||||
StationError.AccountClosed,
|
||||
StationSubscriptionStatus.Active,
|
||||
685276011,
|
||||
username,
|
||||
10001
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
def generateToken() = {
|
||||
val r = new scala.util.Random
|
||||
val sb = new StringBuilder
|
||||
for (_ <- 1 to 31) {
|
||||
sb.append(r.nextPrintableChar)
|
||||
}
|
||||
sb.toString
|
||||
}
|
||||
|
||||
def updateServerList() = {
|
||||
val msg = VNLWorldStatusMessage(
|
||||
"Welcome to PlanetSide! ",
|
||||
Vector(
|
||||
WorldInformation(
|
||||
serverName,
|
||||
WorldStatus.Up,
|
||||
Config.app.world.serverType,
|
||||
Vector(WorldConnectionInfo(publicAddress)),
|
||||
PlanetSideEmpire.VS
|
||||
)
|
||||
)
|
||||
)
|
||||
sendResponse(PacketCoding.CreateGamePacket(0, msg))
|
||||
}
|
||||
|
||||
def failWithError(error: String) = {
|
||||
log.error(error)
|
||||
//sendResponse(PacketCoding.CreateControlPacket(ConnectionClose()))
|
||||
}
|
||||
|
||||
def sendResponse(cont: Any) = {
|
||||
log.trace("LOGIN SEND: " + cont)
|
||||
MDC("sessionId") = sessionId.toString
|
||||
rightRef !> cont
|
||||
}
|
||||
|
||||
def sendRawResponse(pkt: ByteVector) = {
|
||||
log.trace("LOGIN SEND RAW: " + pkt)
|
||||
MDC("sessionId") = sessionId.toString
|
||||
rightRef !> RawPacket(pkt)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,484 @@
|
|||
package net.psforever.login
|
||||
|
||||
import akka.actor.MDCContextAware.Implicits._
|
||||
import akka.actor.{Actor, ActorRef, Cancellable, MDCContextAware}
|
||||
import net.psforever.objects.Default
|
||||
import net.psforever.packet._
|
||||
import net.psforever.packet.control.{HandleGamePacket, _}
|
||||
import org.log4s.MDC
|
||||
import scodec.Attempt.{Failure, Successful}
|
||||
import scodec.bits._
|
||||
|
||||
import scala.annotation.tailrec
|
||||
import scala.collection.mutable
|
||||
import scala.collection.mutable.ArrayBuffer
|
||||
import scala.concurrent.duration._
|
||||
|
||||
/**
|
||||
* In between the network side and the higher functioning side of the simulation:
|
||||
* accept packets and transform them into a sequence of data (encoding), and
|
||||
* accept a sequence of data and transform it into s packet (decoding).<br>
|
||||
* <br>
|
||||
* Following the standardization of the `SessionRouter` pipeline, the throughput of this `Actor` has directionality.
|
||||
* The "network," where the encoded data comes and goes, is assumed to be `leftRef`.
|
||||
* The "simulation", where the decoded packets come and go, is assumed to be `rightRef`.
|
||||
* `rightRef` can accept a sequence that looks like encoded data but it will merely pass out the same sequence.
|
||||
* Likewise, `leftRef` accepts decoded packets but merely ejects the same packets without doing any work on them.
|
||||
* The former functionality is anticipated.
|
||||
* The latter functionality is deprecated.<br>
|
||||
* <br>
|
||||
* Encoded data leaving the `Actor` (`leftRef`) is limited by an upper bound capacity.
|
||||
* Sequences can not be larger than that bound or else they will be dropped.
|
||||
* This maximum transmission unit (MTU) is used to divide the encoded sequence into chunks of encoded data,
|
||||
* re-packaged into nested `ControlPacket` units, and each unit encoded.
|
||||
* The outer packaging is numerically consistent with a `subslot` that starts counting once the simulation starts.
|
||||
* The client is very specific about the `subslot` number and will reject out-of-order packets.
|
||||
* It resets to 0 each time this `Actor` starts up and the client reflects this functionality.
|
||||
*/
|
||||
class PacketCodingActor extends Actor with MDCContextAware {
|
||||
private var sessionId: Long = 0
|
||||
private var subslotOutbound: Int = 0
|
||||
private var subslotInbound: Int = 0
|
||||
private var leftRef: ActorRef = ActorRef.noSender
|
||||
private var rightRef: ActorRef = ActorRef.noSender
|
||||
private[this] val log = org.log4s.getLogger
|
||||
|
||||
/*
|
||||
Since the client can indicate missing packets when sending SlottedMetaPackets we should keep a history of them to resend to the client when requested with a RelatedA packet
|
||||
Since the subslot counter can wrap around, we need to use a LinkedHashMap to maintain the order packets are inserted, then we can drop older entries as required
|
||||
For example when a RelatedB packet arrives we can remove any entries to the left of the received ones without risking removing newer entries if the subslot counter wraps around back to 0
|
||||
*/
|
||||
private var slottedPacketLog: mutable.LinkedHashMap[Int, ByteVector] = mutable.LinkedHashMap()
|
||||
|
||||
// Due to the fact the client can send `RelatedA` packets out of order, we need to keep a buffer of which subslots arrived correctly, order them
|
||||
// and then act accordingly to send the missing subslot packet after a specified timeout
|
||||
private var relatedALog: ArrayBuffer[Int] = ArrayBuffer()
|
||||
private var relatedABufferTimeout: Cancellable = Default.Cancellable
|
||||
|
||||
def AddSlottedPacketToLog(subslot: Int, packet: ByteVector): Unit = {
|
||||
val log_limit = 500 // Number of SlottedMetaPackets to keep in history
|
||||
if (slottedPacketLog.size > log_limit) {
|
||||
slottedPacketLog = slottedPacketLog.drop(slottedPacketLog.size - log_limit)
|
||||
}
|
||||
|
||||
slottedPacketLog { subslot } = packet
|
||||
}
|
||||
|
||||
override def postStop() = {
|
||||
subslotOutbound = 0 //in case this `Actor` restarts
|
||||
super.postStop()
|
||||
}
|
||||
|
||||
def receive = Initializing
|
||||
|
||||
def Initializing: Receive = {
|
||||
case HelloFriend(sharedSessionId, pipe) =>
|
||||
import MDCContextAware.Implicits._
|
||||
this.sessionId = sharedSessionId
|
||||
leftRef = sender()
|
||||
if (pipe.hasNext) {
|
||||
rightRef = pipe.next
|
||||
rightRef !> HelloFriend(sessionId, pipe)
|
||||
} else {
|
||||
rightRef = sender()
|
||||
}
|
||||
log.trace(s"Left sender ${leftRef.path.name}")
|
||||
context.become(Established)
|
||||
|
||||
case default =>
|
||||
log.error("Unknown message " + default)
|
||||
context.stop(self)
|
||||
}
|
||||
|
||||
def Established: Receive = {
|
||||
case PacketCodingActor.SubslotResend() => {
|
||||
log.trace(s"Subslot resend timeout reached, session: ${sessionId}")
|
||||
relatedABufferTimeout.cancel()
|
||||
log.trace(s"Client indicated successful subslots ${relatedALog.sortBy(x => x).mkString(" ")}")
|
||||
|
||||
// If a non-contiguous range of RelatedA packets were received we may need to send multiple missing packets, thus split the array into contiguous ranges
|
||||
val sorted_log = relatedALog.sortBy(x => x)
|
||||
|
||||
val split_logs: ArrayBuffer[ArrayBuffer[Int]] = new ArrayBuffer[ArrayBuffer[Int]]()
|
||||
var curr: ArrayBuffer[Int] = ArrayBuffer()
|
||||
for (i <- 0 to sorted_log.size - 1) {
|
||||
if (i == 0 || (sorted_log(i) != sorted_log(i - 1) + 1)) {
|
||||
curr = new ArrayBuffer()
|
||||
split_logs.append(curr)
|
||||
}
|
||||
curr.append(sorted_log(i))
|
||||
}
|
||||
|
||||
if (split_logs.size > 1) log.trace(s"Split successful subslots into ${split_logs.size} contiguous chunks")
|
||||
|
||||
for (range <- split_logs) {
|
||||
log.trace(s"Processing chunk ${range.mkString(" ")}")
|
||||
val first_accepted_subslot = range.min
|
||||
val missing_subslot = first_accepted_subslot - 1
|
||||
slottedPacketLog.get(missing_subslot) match {
|
||||
case Some(packet: ByteVector) =>
|
||||
log.info(s"Resending packet with subslot: $missing_subslot to session: ${sessionId}")
|
||||
sendResponseLeft(packet)
|
||||
case None =>
|
||||
log.error(s"Couldn't find packet with subslot: ${missing_subslot} to resend to session ${sessionId}.")
|
||||
}
|
||||
}
|
||||
|
||||
relatedALog.clear()
|
||||
}
|
||||
case RawPacket(msg) =>
|
||||
if (sender == rightRef) { //from LSA, WSA, etc., to network - encode
|
||||
mtuLimit(msg)
|
||||
} else { //from network, to LSA, WSA, etc. - decode
|
||||
UnmarshalInnerPacket(msg, "a packet")
|
||||
}
|
||||
//known elevated packet type
|
||||
case ctrl @ ControlPacket(_, packet) =>
|
||||
if (sender == rightRef) { //from LSA, WSA, to network - encode
|
||||
PacketCoding.EncodePacket(packet) match {
|
||||
case Successful(data) =>
|
||||
mtuLimit(data.toByteVector)
|
||||
case Failure(ex) =>
|
||||
log.error(s"Failed to encode a ControlPacket: $ex")
|
||||
}
|
||||
} else { //deprecated; ControlPackets should not be coming from this direction
|
||||
log.warn(s"DEPRECATED CONTROL PACKET SEND: $ctrl")
|
||||
MDC("sessionId") = sessionId.toString
|
||||
handlePacketContainer(ctrl) //sendResponseRight
|
||||
}
|
||||
//known elevated packet type
|
||||
case game @ GamePacket(_, _, packet) =>
|
||||
if (sender == rightRef) { //from LSA, WSA, etc., to network - encode
|
||||
PacketCoding.EncodePacket(packet) match {
|
||||
case Successful(data) =>
|
||||
mtuLimit(data.toByteVector)
|
||||
case Failure(ex) =>
|
||||
log.error(s"Failed to encode a GamePacket: $ex")
|
||||
}
|
||||
} else { //deprecated; GamePackets should not be coming from this direction
|
||||
log.warn(s"DEPRECATED GAME PACKET SEND: $game")
|
||||
MDC("sessionId") = sessionId.toString
|
||||
sendResponseRight(game)
|
||||
}
|
||||
//bundling packets into a SlottedMetaPacket0/MultiPacketEx
|
||||
case msg @ MultiPacketBundle(list) =>
|
||||
log.trace(s"BUNDLE PACKET REQUEST SEND, LEFT (always): $msg")
|
||||
handleBundlePacket(list)
|
||||
//etc
|
||||
case msg =>
|
||||
if (sender == rightRef) {
|
||||
log.trace(s"BASE CASE PACKET SEND, LEFT: $msg")
|
||||
MDC("sessionId") = sessionId.toString
|
||||
leftRef !> msg
|
||||
} else {
|
||||
log.trace(s"BASE CASE PACKET SEND, RIGHT: $msg")
|
||||
MDC("sessionId") = sessionId.toString
|
||||
rightRef !> msg
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the current subslot number.
|
||||
* Increment the `subslot` for the next time it is needed.
|
||||
* @return a `16u` number starting at 0
|
||||
*/
|
||||
def Subslot: Int = {
|
||||
if (subslotOutbound == 65536) { //TODO what is the actual wrap number?
|
||||
subslotOutbound = 0
|
||||
subslotOutbound
|
||||
} else {
|
||||
val curr = subslotOutbound
|
||||
subslotOutbound += 1
|
||||
curr
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that an outbound packet is not too big to get stuck by the MTU.
|
||||
* If it is larger than the MTU, divide it up and re-package the sections.
|
||||
* Otherwise, send the data out like normal.
|
||||
* @param msg the encoded packet data
|
||||
*/
|
||||
def mtuLimit(msg: ByteVector): Unit = {
|
||||
if (msg.length > PacketCodingActor.MTU_LIMIT_BYTES) {
|
||||
handleSplitPacket(PacketCoding.CreateControlPacket(HandleGamePacket(msg)))
|
||||
} else {
|
||||
sendResponseLeft(msg)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a `ControlPacket` into `ByteVector` data for splitting.
|
||||
* @param cont the original `ControlPacket`
|
||||
*/
|
||||
def handleSplitPacket(cont: ControlPacket): Unit = {
|
||||
PacketCoding.getPacketDataForEncryption(cont) match {
|
||||
case Successful((_, data)) =>
|
||||
handleSplitPacket(data)
|
||||
case Failure(ex) =>
|
||||
log.error(s"$ex")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Accept `ByteVector` data, representing a `ControlPacket`, and split it into chunks.
|
||||
* The chunks should not be blocked by the MTU.
|
||||
* Send each chunk (towards the network) as it is converted.
|
||||
* @param data `ByteVector` data to be split
|
||||
*/
|
||||
def handleSplitPacket(data: ByteVector): Unit = {
|
||||
val lim = PacketCodingActor.MTU_LIMIT_BYTES - 4 //4 bytes is the base size of SlottedMetaPacket
|
||||
data
|
||||
.grouped(lim)
|
||||
.foreach(bvec => {
|
||||
val subslot = Subslot
|
||||
PacketCoding.EncodePacket(SlottedMetaPacket(4, subslot, bvec)) match {
|
||||
case Successful(bdata) =>
|
||||
AddSlottedPacketToLog(subslot, bdata.toByteVector)
|
||||
sendResponseLeft(bdata.toByteVector)
|
||||
case f: Failure =>
|
||||
log.error(s"$f")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Accept a `List` of packets and sequentially re-package the elements from the list into multiple container packets.<br>
|
||||
* <br>
|
||||
* The original packets are encoded then paired with their encoding lengths plus extra space to prefix the length.
|
||||
* Encodings from these pairs are drawn from the list until into buckets that fit a maximum byte stream length.
|
||||
* The size limitation on any bucket is the MTU limit.
|
||||
* less by the base sizes of `MultiPacketEx` (2) and of `SlottedMetaPacket` (4).
|
||||
* @param bundle the packets to be bundled
|
||||
*/
|
||||
def handleBundlePacket(bundle: List[PlanetSidePacket]): Unit = {
|
||||
val packets: List[ByteVector] = recursiveEncode(bundle.iterator)
|
||||
recursiveFillPacketBuckets(packets.iterator, PacketCodingActor.MTU_LIMIT_BYTES - 6)
|
||||
.foreach(list => {
|
||||
handleBundlePacket(list.toVector)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Accept a `Vector` of encoded packets and re-package them.
|
||||
* The normal order is to package the elements of the vector into a `MultiPacketEx`.
|
||||
* If the vector only has one element, it will get packaged by itself in a `SlottedMetaPacket`.
|
||||
* If that one element risks being too big for the MTU, however, it will be handled off to be split.
|
||||
* Splitting should preserve `Subslot` ordering with the rest of the bundling.
|
||||
* @param vec a specific number of byte streams
|
||||
*/
|
||||
def handleBundlePacket(vec: Vector[ByteVector]): Unit = {
|
||||
if (vec.size == 1) {
|
||||
val elem = vec.head
|
||||
if (elem.length > PacketCodingActor.MTU_LIMIT_BYTES - 4) {
|
||||
handleSplitPacket(PacketCoding.CreateControlPacket(HandleGamePacket(elem)))
|
||||
} else {
|
||||
handleBundlePacket(elem)
|
||||
}
|
||||
} else {
|
||||
PacketCoding.EncodePacket(MultiPacketEx(vec)) match {
|
||||
case Successful(bdata) =>
|
||||
handleBundlePacket(bdata.toByteVector)
|
||||
case Failure(e) =>
|
||||
log.warn(s"bundling failed on MultiPacketEx creation: - $e")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Accept `ByteVector` data and package it into a `SlottedMetaPacket`.
|
||||
* Send it (towards the network) upon successful encoding.
|
||||
* @param data an encoded packet
|
||||
*/
|
||||
def handleBundlePacket(data: ByteVector): Unit = {
|
||||
val subslot = Subslot
|
||||
PacketCoding.EncodePacket(SlottedMetaPacket(0, subslot, data)) match {
|
||||
case Successful(bdata) =>
|
||||
AddSlottedPacketToLog(subslot, bdata.toByteVector)
|
||||
sendResponseLeft(bdata.toByteVector)
|
||||
case Failure(e) =>
|
||||
log.warn(s"bundling failed on SlottedMetaPacket creation: - $e")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encoded sequence of data going towards the network.
|
||||
* @param cont the data
|
||||
*/
|
||||
def sendResponseLeft(cont: ByteVector): Unit = {
|
||||
log.trace("PACKET SEND, LEFT: " + cont)
|
||||
MDC("sessionId") = sessionId.toString
|
||||
leftRef !> RawPacket(cont)
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform data into a container packet and re-submit that container to the process that handles the packet.
|
||||
* @param data the packet data
|
||||
* @param description an explanation of the input `data`
|
||||
*/
|
||||
def UnmarshalInnerPacket(data: ByteVector, description: String): Unit = {
|
||||
PacketCoding.unmarshalPayload(0, data) match { //TODO is it safe for this to always be 0?
|
||||
case Successful(packet) =>
|
||||
handlePacketContainer(packet)
|
||||
case Failure(ex) =>
|
||||
log.info(s"Failed to unmarshal $description: $ex. Data : $data")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort and redirect a container packet bound for the server by type of contents.
|
||||
* `GamePacket` objects can just onwards without issue.
|
||||
* `ControlPacket` objects may need to be dequeued.
|
||||
* All other container types are invalid.
|
||||
* @param container the container packet
|
||||
*/
|
||||
def handlePacketContainer(container: PlanetSidePacketContainer): Unit = {
|
||||
container match {
|
||||
case _: GamePacket =>
|
||||
sendResponseRight(container)
|
||||
case ControlPacket(_, ctrlPkt) =>
|
||||
handleControlPacket(container, ctrlPkt)
|
||||
case default =>
|
||||
log.warn(s"Invalid packet container class received: ${default.getClass.getName}") //do not spill contents in log
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a control packet or determine that it does not need to be processed at this level.
|
||||
* Primarily, if the packet is of a type that contains another packet that needs be be unmarshalled,
|
||||
* that/those packet must be unwound.<br>
|
||||
* <br>
|
||||
* The subslot information is used to identify these nested packets after arriving at their destination,
|
||||
* to establish order for sequential packets and relation between divided packets.
|
||||
* @param container the original container packet
|
||||
* @param packet the packet that was extracted from the container
|
||||
*/
|
||||
def handleControlPacket(container: PlanetSidePacketContainer, packet: PlanetSideControlPacket) = {
|
||||
packet match {
|
||||
case SlottedMetaPacket(slot, subslot, innerPacket) =>
|
||||
subslotInbound = subslot
|
||||
self.tell(PacketCoding.CreateControlPacket(RelatedB(slot, subslot)), rightRef) //will go to the network
|
||||
UnmarshalInnerPacket(innerPacket, "the inner packet of a SlottedMetaPacket")
|
||||
|
||||
case MultiPacket(packets) =>
|
||||
packets.foreach { UnmarshalInnerPacket(_, "the inner packet of a MultiPacket") }
|
||||
|
||||
case MultiPacketEx(packets) =>
|
||||
packets.foreach { UnmarshalInnerPacket(_, "the inner packet of a MultiPacketEx") }
|
||||
|
||||
case RelatedA(slot, subslot) =>
|
||||
log.trace(s"Client indicated a packet is missing prior to slot: $slot subslot: $subslot, session: ${sessionId}")
|
||||
|
||||
relatedALog += subslot
|
||||
|
||||
// (re)start the timeout period, if no more RelatedA packets are sent before the timeout period elapses the missing packet(s) will be resent
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
relatedABufferTimeout.cancel()
|
||||
relatedABufferTimeout =
|
||||
context.system.scheduler.scheduleOnce(100 milliseconds, self, PacketCodingActor.SubslotResend())
|
||||
|
||||
case RelatedB(slot, subslot) =>
|
||||
log.trace(s"result $slot: subslot $subslot accepted, session: ${sessionId}")
|
||||
|
||||
// The client has indicated it's received up to a certain subslot, that means we can purge the log of any subslots prior to and including the confirmed subslot
|
||||
// Find where this subslot is stored in the packet log (if at all) and drop anything to the left of it, including itself
|
||||
if (relatedABufferTimeout.isCancelled || relatedABufferTimeout == Default.Cancellable) {
|
||||
val pos = slottedPacketLog.keySet.toArray.indexOf(subslot)
|
||||
if (pos != -1) {
|
||||
slottedPacketLog = slottedPacketLog.drop(pos + 1)
|
||||
log.trace(s"Subslots left in log: ${slottedPacketLog.keySet.toString()}")
|
||||
}
|
||||
}
|
||||
case _ =>
|
||||
sendResponseRight(container)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decoded packet going towards the simulation.
|
||||
* @param cont the packet
|
||||
*/
|
||||
def sendResponseRight(cont: PlanetSidePacketContainer): Unit = {
|
||||
log.trace("PACKET SEND, RIGHT: " + cont)
|
||||
MDC("sessionId") = sessionId.toString
|
||||
rightRef !> cont
|
||||
}
|
||||
|
||||
/**
|
||||
* Accept a series of packets and transform it into a series of packet encodings.
|
||||
* Packets that do not encode properly are simply excluded from the product.
|
||||
* This is not treated as an error or exception; a warning will merely be logged.
|
||||
* @param iter the `Iterator` for a series of packets
|
||||
* @param out updated series of byte stream data produced through successful packet encoding;
|
||||
* defaults to an empty list
|
||||
* @return a series of byte stream data produced through successful packet encoding
|
||||
*/
|
||||
@tailrec private def recursiveEncode(
|
||||
iter: Iterator[PlanetSidePacket],
|
||||
out: List[ByteVector] = List()
|
||||
): List[ByteVector] = {
|
||||
if (!iter.hasNext) {
|
||||
out
|
||||
} else {
|
||||
import net.psforever.packet.{PlanetSideControlPacket, PlanetSideGamePacket}
|
||||
iter.next match {
|
||||
case msg: PlanetSideGamePacket =>
|
||||
PacketCoding.EncodePacket(msg) match {
|
||||
case Successful(bytecode) =>
|
||||
recursiveEncode(iter, out :+ bytecode.toByteVector)
|
||||
case Failure(e) =>
|
||||
log.warn(s"game packet $msg, part of a bundle, did not encode - $e")
|
||||
recursiveEncode(iter, out)
|
||||
}
|
||||
case msg: PlanetSideControlPacket =>
|
||||
PacketCoding.EncodePacket(msg) match {
|
||||
case Successful(bytecode) =>
|
||||
recursiveEncode(iter, out :+ bytecode.toByteVector)
|
||||
case Failure(e) =>
|
||||
log.warn(s"control packet $msg, part of a bundle, did not encode - $e")
|
||||
recursiveEncode(iter, out)
|
||||
}
|
||||
case _ =>
|
||||
recursiveEncode(iter, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Accept a series of byte stream data and sort into sequential size-limited buckets of the same byte streams.
|
||||
* Note that elements that exceed `lim` by themselves are always sorted into their own buckets.
|
||||
* @param iter an `Iterator` of a series of byte stream data
|
||||
* @param lim the maximum stream length permitted
|
||||
* @param curr the stream length of the current bucket
|
||||
* @param out updated series of byte stream data stored in buckets
|
||||
* @return a series of byte stream data stored in buckets
|
||||
*/
|
||||
@tailrec private def recursiveFillPacketBuckets(
|
||||
iter: Iterator[ByteVector],
|
||||
lim: Int,
|
||||
curr: Int = 0,
|
||||
out: List[mutable.ListBuffer[ByteVector]] = List(mutable.ListBuffer())
|
||||
): List[mutable.ListBuffer[ByteVector]] = {
|
||||
if (!iter.hasNext) {
|
||||
out
|
||||
} else {
|
||||
val data = iter.next
|
||||
var len = data.length.toInt
|
||||
len = len + (if (len < 256) { 1 }
|
||||
else if (len < 65536) { 2 }
|
||||
else { 4 }) //space for the prefixed length byte(s)
|
||||
if (curr + len > lim && out.last.nonEmpty) { //bucket must have something in it before swapping
|
||||
recursiveFillPacketBuckets(iter, lim, len, out :+ mutable.ListBuffer(data))
|
||||
} else {
|
||||
out.last += data
|
||||
recursiveFillPacketBuckets(iter, lim, curr + len, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object PacketCodingActor {
|
||||
final val MTU_LIMIT_BYTES: Int = 467
|
||||
|
||||
private final case class SubslotResend()
|
||||
}
|
||||
103
common/src/main/scala/net/psforever/login/Session.scala
Normal file
103
common/src/main/scala/net/psforever/login/Session.scala
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
package net.psforever.login
|
||||
|
||||
import java.net.InetSocketAddress
|
||||
|
||||
import akka.actor.MDCContextAware.Implicits._
|
||||
import akka.actor.{ActorContext, ActorRef, PoisonPill, _}
|
||||
import com.github.nscala_time.time.Imports._
|
||||
import scodec.bits._
|
||||
|
||||
sealed trait SessionState
|
||||
final case class New() extends SessionState
|
||||
final case class Related() extends SessionState
|
||||
final case class Handshaking() extends SessionState
|
||||
final case class Established() extends SessionState
|
||||
final case class Closing() extends SessionState
|
||||
final case class Closed() extends SessionState
|
||||
|
||||
class Session(
|
||||
val sessionId: Long,
|
||||
val socketAddress: InetSocketAddress,
|
||||
returnActor: ActorRef,
|
||||
sessionPipeline: List[SessionPipeline]
|
||||
)(
|
||||
implicit val context: ActorContext,
|
||||
implicit val self: ActorRef
|
||||
) {
|
||||
|
||||
var state: SessionState = New()
|
||||
val sessionCreatedTime: DateTime = DateTime.now
|
||||
var sessionEndedTime: DateTime = DateTime.now
|
||||
|
||||
val pipeline = sessionPipeline.map { actor =>
|
||||
val a = context.actorOf(actor.props, actor.nameTemplate + sessionId.toString)
|
||||
context.watch(a)
|
||||
a
|
||||
}
|
||||
|
||||
val pipelineIter = pipeline.iterator
|
||||
if (pipelineIter.hasNext) {
|
||||
pipelineIter.next ! HelloFriend(sessionId, pipelineIter)
|
||||
}
|
||||
|
||||
// statistics
|
||||
var bytesSent: Long = 0
|
||||
var bytesReceived: Long = 0
|
||||
var inboundPackets: Long = 0
|
||||
var outboundPackets: Long = 0
|
||||
|
||||
var lastInboundEvent: Long = System.nanoTime()
|
||||
var lastOutboundEvent: Long = System.nanoTime()
|
||||
|
||||
var inboundPacketRate: Double = 0.0
|
||||
var outboundPacketRate: Double = 0.0
|
||||
var inboundBytesPerSecond: Double = 0.0
|
||||
var outboundBytesPerSecond: Double = 0.0
|
||||
|
||||
def receive(packet: RawPacket): Unit = {
|
||||
bytesReceived += packet.data.size
|
||||
inboundPackets += 1
|
||||
lastInboundEvent = System.nanoTime()
|
||||
|
||||
pipeline.head !> packet
|
||||
}
|
||||
|
||||
def send(packet: ByteVector): Unit = {
|
||||
bytesSent += packet.size
|
||||
outboundPackets += 1
|
||||
lastOutboundEvent = System.nanoTime()
|
||||
|
||||
returnActor ! SendPacket(packet, socketAddress)
|
||||
}
|
||||
|
||||
def dropSession(graceful: Boolean) = {
|
||||
pipeline.foreach(context.unwatch)
|
||||
pipeline.foreach(_ ! PoisonPill)
|
||||
|
||||
sessionEndedTime = DateTime.now
|
||||
setState(Closed())
|
||||
}
|
||||
|
||||
def getState = state
|
||||
|
||||
def setState(newState: SessionState): Unit = {
|
||||
state = newState
|
||||
}
|
||||
def getPipeline: List[ActorRef] = pipeline
|
||||
|
||||
def getTotalBytes = {
|
||||
bytesSent + bytesReceived
|
||||
}
|
||||
|
||||
def timeSinceLastInboundEvent = {
|
||||
(System.nanoTime() - lastInboundEvent) / 1000000
|
||||
}
|
||||
|
||||
def timeSinceLastOutboundEvent = {
|
||||
(System.nanoTime() - lastOutboundEvent) / 1000000
|
||||
}
|
||||
|
||||
override def toString: String = {
|
||||
s"Session($sessionId, $getTotalBytes)"
|
||||
}
|
||||
}
|
||||
194
common/src/main/scala/net/psforever/login/SessionRouter.scala
Normal file
194
common/src/main/scala/net/psforever/login/SessionRouter.scala
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
package net.psforever.login
|
||||
|
||||
import java.net.InetSocketAddress
|
||||
|
||||
import akka.actor.SupervisorStrategy.Stop
|
||||
import akka.actor._
|
||||
import net.psforever.packet.PacketCoding
|
||||
import net.psforever.packet.control.ConnectionClose
|
||||
import net.psforever.util.Config
|
||||
import org.log4s.MDC
|
||||
import scodec.bits._
|
||||
import services.ServiceManager
|
||||
import services.ServiceManager.Lookup
|
||||
import services.account.{IPAddress, StoreIPAddress}
|
||||
import scala.collection.mutable
|
||||
import scala.concurrent.duration._
|
||||
|
||||
sealed trait SessionRouterAPI
|
||||
final case class RawPacket(data: ByteVector) extends SessionRouterAPI
|
||||
final case class ResponsePacket(data: ByteVector) extends SessionRouterAPI
|
||||
final case class DropSession(id: Long, reason: String) extends SessionRouterAPI
|
||||
final case class SessionReaper() extends SessionRouterAPI
|
||||
|
||||
case class SessionPipeline(nameTemplate: String, props: Props)
|
||||
|
||||
/**
|
||||
* Login sessions are divided between two actors. The crypto session actor transparently handles all of the cryptographic
|
||||
* setup of the connection. Once a correct crypto session has been established, all packets, after being decrypted
|
||||
* will be passed on to the login session actor. This actor has important state that is used to maintain the login
|
||||
* session.
|
||||
*
|
||||
* > PlanetSide Session Pipeline <
|
||||
*
|
||||
* read() route decrypt
|
||||
* UDP Socket -----> [Session Router] -----> [Crypto Actor] -----> [Session Actor]
|
||||
* /|\ | /|\ | /|\ |
|
||||
* | write() | | encrypt | | response |
|
||||
* +--------------+ +-----------+ +-----------------+
|
||||
*/
|
||||
class SessionRouter(role: String, pipeline: List[SessionPipeline]) extends Actor with MDCContextAware {
|
||||
private[this] val log = org.log4s.getLogger(self.path.name)
|
||||
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
val sessionReaper = context.system.scheduler.scheduleWithFixedDelay(10 seconds, 5 seconds, self, SessionReaper())
|
||||
|
||||
val idBySocket = mutable.Map[InetSocketAddress, Long]()
|
||||
val sessionById = mutable.Map[Long, Session]()
|
||||
val sessionByActor = mutable.Map[ActorRef, Session]()
|
||||
val closePacket = PacketCoding.EncodePacket(ConnectionClose()).require.bytes
|
||||
var accountIntermediary: ActorRef = ActorRef.noSender
|
||||
|
||||
var sessionId = 0L // this is a connection session, not an actual logged in session ID
|
||||
var inputRef: ActorRef = ActorRef.noSender
|
||||
|
||||
override def supervisorStrategy = OneForOneStrategy() { case _ => Stop }
|
||||
|
||||
override def preStart = {
|
||||
log.info(s"SessionRouter (for ${role}s) initializing ...")
|
||||
}
|
||||
|
||||
def receive = initializing
|
||||
|
||||
def initializing: Receive = {
|
||||
case Hello() =>
|
||||
inputRef = sender()
|
||||
ServiceManager.serviceManager ! Lookup("accountIntermediary")
|
||||
case ServiceManager.LookupResult("accountIntermediary", endpoint) =>
|
||||
accountIntermediary = endpoint
|
||||
log.info(s"SessionRouter starting; ready for $role sessions")
|
||||
context.become(started)
|
||||
case default =>
|
||||
log.error(s"Unknown or unexpected message $default before being properly started. Stopping completely...")
|
||||
context.stop(self)
|
||||
}
|
||||
|
||||
override def postStop() = {
|
||||
sessionReaper.cancel()
|
||||
}
|
||||
|
||||
def started: Receive = {
|
||||
case _ @ReceivedPacket(msg, from) =>
|
||||
var session: Session = null
|
||||
|
||||
if (!idBySocket.contains(from)) {
|
||||
session = createNewSession(from)
|
||||
} else {
|
||||
val id = idBySocket { from }
|
||||
session = sessionById { id }
|
||||
}
|
||||
|
||||
if (session.state != Closed()) {
|
||||
MDC("sessionId") = session.sessionId.toString
|
||||
log.trace(s"RECV: $msg -> ${session.getPipeline.head.path.name}")
|
||||
session.receive(RawPacket(msg))
|
||||
MDC.clear()
|
||||
}
|
||||
case ResponsePacket(msg) =>
|
||||
val session = sessionByActor.get(sender())
|
||||
|
||||
if (session.isDefined) {
|
||||
if (session.get.state != Closed()) {
|
||||
MDC("sessionId") = session.get.sessionId.toString
|
||||
log.trace(s"SEND: $msg -> ${inputRef.path.name}")
|
||||
session.get.send(msg)
|
||||
MDC.clear()
|
||||
}
|
||||
} else {
|
||||
log.error("Dropped old response packet from actor " + sender().path.name)
|
||||
}
|
||||
case DropSession(id, reason) =>
|
||||
val session = sessionById.get(id)
|
||||
|
||||
if (session.isDefined) {
|
||||
removeSessionById(id, reason, graceful = true)
|
||||
} else {
|
||||
log.error(s"Requested to drop non-existent session ID=$id from ${sender()}")
|
||||
}
|
||||
case SessionReaper() =>
|
||||
val inboundGrace = Config.app.network.session.inboundGraceTime.toMillis
|
||||
val outboundGrace = Config.app.network.session.outboundGraceTime.toMillis
|
||||
|
||||
sessionById.foreach {
|
||||
case (id, session) =>
|
||||
log.trace(session.toString)
|
||||
if (session.getState == Closed()) {
|
||||
// clear mappings
|
||||
session.getPipeline.foreach(sessionByActor remove)
|
||||
sessionById.remove(id)
|
||||
idBySocket.remove(session.socketAddress)
|
||||
log.debug(s"Reaped session ID=$id")
|
||||
} else if (session.timeSinceLastInboundEvent > inboundGrace) {
|
||||
removeSessionById(id, "session timed out (inbound)", graceful = false)
|
||||
} else if (session.timeSinceLastOutboundEvent > outboundGrace) {
|
||||
removeSessionById(id, "session timed out (outbound)", graceful = true) // tell client to STFU
|
||||
}
|
||||
}
|
||||
case Terminated(actor) =>
|
||||
val terminatedSession = sessionByActor.get(actor)
|
||||
|
||||
if (terminatedSession.isDefined) {
|
||||
removeSessionById(terminatedSession.get.sessionId, s"${actor.path.name} died", graceful = true)
|
||||
} else {
|
||||
log.error("Received an invalid actor Termination from " + actor.path.name)
|
||||
}
|
||||
case default =>
|
||||
log.error(s"Unknown message $default from " + sender().path)
|
||||
}
|
||||
|
||||
def createNewSession(address: InetSocketAddress) = {
|
||||
val id = newSessionId
|
||||
val session = new Session(id, address, inputRef, pipeline)
|
||||
|
||||
// establish mappings for easy lookup
|
||||
idBySocket { address } = id
|
||||
sessionById { id } = session
|
||||
|
||||
session.getPipeline.foreach { actor =>
|
||||
sessionByActor { actor } = session
|
||||
}
|
||||
|
||||
log.info(s"New session ID=$id from " + address.toString)
|
||||
|
||||
if (role == "Login") {
|
||||
accountIntermediary ! StoreIPAddress(id, new IPAddress(address))
|
||||
}
|
||||
|
||||
session
|
||||
}
|
||||
|
||||
def removeSessionById(id: Long, reason: String, graceful: Boolean): Unit = {
|
||||
val sessionOption = sessionById.get(id)
|
||||
|
||||
if (sessionOption.isEmpty)
|
||||
return
|
||||
|
||||
val session: Session = sessionOption.get
|
||||
|
||||
if (graceful) {
|
||||
for (_ <- 0 to 5) {
|
||||
session.send(closePacket)
|
||||
}
|
||||
}
|
||||
|
||||
// kill all session specific actors
|
||||
session.dropSession(graceful)
|
||||
log.info(s"Dropping session ID=$id (reason: $reason)")
|
||||
}
|
||||
|
||||
def newSessionId = {
|
||||
val oldId = sessionId
|
||||
sessionId += 1
|
||||
oldId
|
||||
}
|
||||
}
|
||||
50
common/src/main/scala/net/psforever/login/TcpListener.scala
Normal file
50
common/src/main/scala/net/psforever/login/TcpListener.scala
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
package net.psforever.login
|
||||
|
||||
import java.net.{InetAddress, InetSocketAddress}
|
||||
|
||||
import akka.actor.SupervisorStrategy.Stop
|
||||
import akka.actor.{Actor, ActorRef, OneForOneStrategy, Props}
|
||||
import akka.io._
|
||||
|
||||
class TcpListener[T <: Actor](actorClass: Class[T], nextActorName: String, listenAddress: InetAddress, port: Int)
|
||||
extends Actor {
|
||||
private val log = org.log4s.getLogger(self.path.name)
|
||||
|
||||
override def supervisorStrategy =
|
||||
OneForOneStrategy() {
|
||||
case _ => Stop
|
||||
}
|
||||
|
||||
import context.system
|
||||
|
||||
IO(Tcp) ! Tcp.Bind(self, new InetSocketAddress(listenAddress, port))
|
||||
|
||||
var sessionId = 0L
|
||||
var bytesRecevied = 0L
|
||||
var bytesSent = 0L
|
||||
var nextActor: ActorRef = ActorRef.noSender
|
||||
|
||||
def receive = {
|
||||
case Tcp.Bound(local) =>
|
||||
log.info(s"Now listening on TCP:$local")
|
||||
|
||||
context.become(ready(sender()))
|
||||
case Tcp.CommandFailed(Tcp.Bind(_, address, _, _, _)) =>
|
||||
log.error("Failed to bind to the network interface: " + address)
|
||||
context.system.terminate()
|
||||
case default =>
|
||||
log.error(s"Unexpected message $default")
|
||||
}
|
||||
|
||||
def ready(socket: ActorRef): Receive = {
|
||||
case Tcp.Connected(remote, local) =>
|
||||
val connection = sender()
|
||||
val session = sessionId
|
||||
val handler = context.actorOf(Props(actorClass, remote, connection), nextActorName + session)
|
||||
connection ! Tcp.Register(handler)
|
||||
sessionId += 1
|
||||
case Tcp.Unbind => socket ! Tcp.Unbind
|
||||
case Tcp.Unbound => context.stop(self)
|
||||
case default => log.error(s"Unhandled message: $default")
|
||||
}
|
||||
}
|
||||
79
common/src/main/scala/net/psforever/login/UdpListener.scala
Normal file
79
common/src/main/scala/net/psforever/login/UdpListener.scala
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
package net.psforever.login
|
||||
|
||||
import java.net.{InetAddress, InetSocketAddress}
|
||||
|
||||
import akka.actor.SupervisorStrategy.Stop
|
||||
import akka.actor.{Actor, ActorRef, OneForOneStrategy, Props, Terminated}
|
||||
import akka.io._
|
||||
import scodec.bits._
|
||||
import scodec.interop.akka._
|
||||
|
||||
final case class ReceivedPacket(msg: ByteVector, from: InetSocketAddress)
|
||||
final case class SendPacket(msg: ByteVector, to: InetSocketAddress)
|
||||
final case class Hello()
|
||||
final case class HelloFriend(sessionId: Long, next: Iterator[ActorRef])
|
||||
|
||||
class UdpListener(
|
||||
nextActorProps: Props,
|
||||
nextActorName: String,
|
||||
listenAddress: InetAddress,
|
||||
port: Int,
|
||||
netParams: Option[NetworkSimulatorParameters]
|
||||
) extends Actor {
|
||||
private val log = org.log4s.getLogger(self.path.name)
|
||||
|
||||
override def supervisorStrategy =
|
||||
OneForOneStrategy() {
|
||||
case _ => Stop
|
||||
}
|
||||
|
||||
import context.system
|
||||
|
||||
// If we have network parameters, start the network simulator
|
||||
if (netParams.isDefined) {
|
||||
// See http://www.cakesolutions.net/teamblogs/understanding-akkas-recommended-practice-for-actor-creation-in-scala
|
||||
// For why we cant do Props(new Actor) here
|
||||
val sim = context.actorOf(Props(classOf[UdpNetworkSimulator], self, netParams.get))
|
||||
IO(Udp).tell(Udp.Bind(sim, new InetSocketAddress(listenAddress, port)), sim)
|
||||
} else {
|
||||
IO(Udp) ! Udp.Bind(self, new InetSocketAddress(listenAddress, port))
|
||||
}
|
||||
|
||||
var bytesRecevied = 0L
|
||||
var bytesSent = 0L
|
||||
var nextActor: ActorRef = ActorRef.noSender
|
||||
|
||||
def receive = {
|
||||
case Udp.Bound(local) =>
|
||||
log.info(s"Now listening on UDP:$local")
|
||||
|
||||
createNextActor()
|
||||
context.become(ready(sender()))
|
||||
case Udp.CommandFailed(Udp.Bind(_, address, _)) =>
|
||||
log.error("Failed to bind to the network interface: " + address)
|
||||
context.system.terminate()
|
||||
case default =>
|
||||
log.error(s"Unexpected message $default")
|
||||
}
|
||||
|
||||
def ready(socket: ActorRef): Receive = {
|
||||
case SendPacket(msg, to) =>
|
||||
bytesSent += msg.size
|
||||
socket ! Udp.Send(msg.toByteString, to)
|
||||
case Udp.Received(data, remote) =>
|
||||
bytesRecevied += data.size
|
||||
nextActor ! ReceivedPacket(data.toByteVector, remote)
|
||||
case Udp.Unbind => socket ! Udp.Unbind
|
||||
case Udp.Unbound => context.stop(self)
|
||||
case Terminated(actor) =>
|
||||
log.error(s"Next actor ${actor.path.name} has died...restarting")
|
||||
createNextActor()
|
||||
case default => log.error(s"Unhandled message: $default")
|
||||
}
|
||||
|
||||
def createNextActor() = {
|
||||
nextActor = context.actorOf(nextActorProps, nextActorName)
|
||||
context.watch(nextActor)
|
||||
nextActor ! Hello()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
package net.psforever.login
|
||||
|
||||
import akka.actor.{Actor, ActorRef}
|
||||
import akka.io._
|
||||
|
||||
import scala.collection.mutable
|
||||
import scala.concurrent.duration._
|
||||
import scala.util.Random
|
||||
|
||||
/** Parameters for the Network simulator
|
||||
*
|
||||
* @param packetLoss The percentage from [0.0, 1.0] that a packet will be lost
|
||||
* @param packetDelay The end-to-end delay (ping) of all packets
|
||||
* @param packetReorderingChance The percentage from [0.0, 1.0] that a packet will be reordered
|
||||
* @param packetReorderingTime The absolute adjustment in milliseconds that a packet can have (either
|
||||
* forward or backwards in time)
|
||||
*/
|
||||
case class NetworkSimulatorParameters(
|
||||
packetLoss: Double,
|
||||
packetDelay: Long,
|
||||
packetReorderingChance: Double,
|
||||
packetReorderingTime: Long
|
||||
) {
|
||||
assert(packetLoss >= 0.0 && packetLoss <= 1.0)
|
||||
assert(packetDelay >= 0)
|
||||
assert(packetReorderingChance >= 0.0 && packetReorderingChance <= 1.0)
|
||||
assert(packetReorderingTime >= 0)
|
||||
|
||||
override def toString =
|
||||
"NetSimParams: loss %.2f%% / delay %dms / reorder %.2f%% / reorder +/- %dms".format(
|
||||
packetLoss * 100,
|
||||
packetDelay,
|
||||
packetReorderingChance * 100,
|
||||
packetReorderingTime
|
||||
)
|
||||
}
|
||||
|
||||
class UdpNetworkSimulator(server: ActorRef, params: NetworkSimulatorParameters) extends Actor {
|
||||
private val log = org.log4s.getLogger
|
||||
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
|
||||
//******* Variables
|
||||
val packetDelayDuration = (params.packetDelay / 2).milliseconds
|
||||
|
||||
type QueueItem = (Udp.Message, Long)
|
||||
|
||||
// sort in ascending order (older things get dequeued first)
|
||||
implicit val QueueItem = Ordering.by[QueueItem, Long](_._2).reverse
|
||||
|
||||
val inPacketQueue = mutable.PriorityQueue[QueueItem]()
|
||||
val outPacketQueue = mutable.PriorityQueue[QueueItem]()
|
||||
|
||||
val chaos = new Random()
|
||||
var interface = ActorRef.noSender
|
||||
|
||||
def receive = {
|
||||
case UdpNetworkSimulator.ProcessInputQueue() =>
|
||||
val time = System.nanoTime()
|
||||
var exit = false
|
||||
|
||||
while (inPacketQueue.nonEmpty && !exit) {
|
||||
val lastTime = time - inPacketQueue.head._2
|
||||
|
||||
// this packet needs to be sent within 20 milliseconds or more
|
||||
if (lastTime >= 20000000) {
|
||||
server.tell(inPacketQueue.dequeue._1, interface)
|
||||
} else {
|
||||
schedule(lastTime.nanoseconds, outbound = false)
|
||||
exit = true
|
||||
}
|
||||
}
|
||||
case UdpNetworkSimulator.ProcessOutputQueue() =>
|
||||
val time = System.nanoTime()
|
||||
var exit = false
|
||||
|
||||
while (outPacketQueue.nonEmpty && !exit) {
|
||||
val lastTime = time - outPacketQueue.head._2
|
||||
|
||||
// this packet needs to be sent within 20 milliseconds or more
|
||||
if (lastTime >= 20000000) {
|
||||
interface.tell(outPacketQueue.dequeue._1, server)
|
||||
} else {
|
||||
schedule(lastTime.nanoseconds, outbound = true)
|
||||
exit = true
|
||||
}
|
||||
}
|
||||
// outbound messages
|
||||
case msg @ Udp.Send(payload, target, _) =>
|
||||
handlePacket(msg, outPacketQueue, outbound = true)
|
||||
// inbound messages
|
||||
case msg @ Udp.Received(payload, sender) =>
|
||||
handlePacket(msg, inPacketQueue, outbound = false)
|
||||
case msg @ Udp.Bound(address) =>
|
||||
interface = sender()
|
||||
log.info(s"Hooked ${server.path} for network simulation")
|
||||
server.tell(msg, self) // make sure the server sends *us* the packets
|
||||
case default =>
|
||||
val from = sender()
|
||||
|
||||
if (from == server)
|
||||
interface.tell(default, server)
|
||||
else if (from == interface)
|
||||
server.tell(default, interface)
|
||||
else
|
||||
log.error("Unexpected sending Actor " + from.path)
|
||||
}
|
||||
|
||||
def handlePacket(message: Udp.Message, queue: mutable.PriorityQueue[QueueItem], outbound: Boolean) = {
|
||||
val name: String = if (outbound) "OUT" else "IN"
|
||||
val queue: mutable.PriorityQueue[QueueItem] = if (outbound) outPacketQueue else inPacketQueue
|
||||
|
||||
if (chaos.nextDouble() > params.packetLoss) {
|
||||
// if the message queue is empty, then we need to reschedule our task
|
||||
if (queue.isEmpty)
|
||||
schedule(packetDelayDuration, outbound)
|
||||
|
||||
// perform a reordering
|
||||
if (chaos.nextDouble() <= params.packetReorderingChance) {
|
||||
// creates the range (-1.0, 1.0)
|
||||
// time adjustment to move the packet (forward or backwards in time)
|
||||
val adj = (2 * (chaos.nextDouble() - 0.5) * params.packetReorderingTime).toLong
|
||||
queue += ((message, System.nanoTime() + adj * 1000000))
|
||||
|
||||
log.debug(s"Reordered $name by ${adj}ms - $message")
|
||||
} else { // normal message
|
||||
queue += ((message, System.nanoTime()))
|
||||
}
|
||||
} else {
|
||||
log.debug(s"Dropped $name - $message")
|
||||
}
|
||||
}
|
||||
|
||||
def schedule(duration: FiniteDuration, outbound: Boolean) =
|
||||
context.system.scheduler.scheduleOnce(
|
||||
packetDelayDuration,
|
||||
self,
|
||||
if (outbound) UdpNetworkSimulator.ProcessOutputQueue() else UdpNetworkSimulator.ProcessInputQueue()
|
||||
)
|
||||
}
|
||||
|
||||
object UdpNetworkSimulator {
|
||||
//******* Internal messages
|
||||
private final case class ProcessInputQueue()
|
||||
private final case class ProcessOutputQueue()
|
||||
}
|
||||
650
common/src/main/scala/net/psforever/login/WorldSession.scala
Normal file
650
common/src/main/scala/net/psforever/login/WorldSession.scala
Normal file
|
|
@ -0,0 +1,650 @@
|
|||
package net.psforever.login
|
||||
|
||||
import akka.actor.ActorRef
|
||||
import akka.pattern.{AskTimeoutException, ask}
|
||||
import akka.util.Timeout
|
||||
import net.psforever.objects.equipment.{Ammo, Equipment}
|
||||
import net.psforever.objects.guid.{GUIDTask, Task, TaskResolver}
|
||||
import net.psforever.objects.inventory.{Container, InventoryItem}
|
||||
import net.psforever.objects.serverobject.PlanetSideServerObject
|
||||
import net.psforever.objects.serverobject.containable.Containable
|
||||
import net.psforever.objects.zones.Zone
|
||||
import net.psforever.objects.{AmmoBox, GlobalDefinitions, Player, Tool}
|
||||
import net.psforever.packet.game.ObjectHeldMessage
|
||||
import net.psforever.types.{PlanetSideGUID, TransactionType, Vector3}
|
||||
import services.Service
|
||||
import services.avatar.{AvatarAction, AvatarServiceMessage}
|
||||
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.concurrent.Future
|
||||
import scala.concurrent.duration._
|
||||
import scala.language.implicitConversions
|
||||
import scala.util.{Failure, Success}
|
||||
|
||||
object WorldSession {
|
||||
|
||||
/**
|
||||
* Convert a boolean value into an integer value.
|
||||
* Use: `true:Int` or `false:Int`
|
||||
* @param b `true` or `false` (or `null`)
|
||||
* @return 1 for `true`; 0 for `false`
|
||||
*/
|
||||
implicit def boolToInt(b: Boolean): Int = if (b) 1 else 0
|
||||
private implicit val timeout = new Timeout(5000 milliseconds)
|
||||
|
||||
/**
|
||||
* Use this for placing equipment that has yet to be registered into a container,
|
||||
* such as in support of changing ammunition types in `Tool` objects (weapons).
|
||||
* If the object can not be placed into the container, it will be dropped onto the ground.
|
||||
* It will also be dropped if it takes too long to be placed.
|
||||
* Item swapping during the placement is not allowed.
|
||||
* @see `ask`
|
||||
* @see `ChangeAmmoMessage`
|
||||
* @see `Containable.CanNotPutItemInSlot`
|
||||
* @see `Containable.PutItemAway`
|
||||
* @see `Future.onComplete`
|
||||
* @see `Future.recover`
|
||||
* @see `tell`
|
||||
* @see `Zone.Ground.DropItem`
|
||||
* @param obj the container
|
||||
* @param item the item being manipulated
|
||||
* @return a `Future` that anticipates the resolution to this manipulation
|
||||
*/
|
||||
def PutEquipmentInInventoryOrDrop(obj: PlanetSideServerObject with Container)(item: Equipment): Future[Any] = {
|
||||
val localContainer = obj
|
||||
val localItem = item
|
||||
val result = ask(localContainer.Actor, Containable.PutItemAway(localItem))
|
||||
result.onComplete {
|
||||
case Failure(_) | Success(_: Containable.CanNotPutItemInSlot) =>
|
||||
localContainer.Zone.Ground.tell(
|
||||
Zone.Ground.DropItem(localItem, localContainer.Position, Vector3.z(localContainer.Orientation.z)),
|
||||
localContainer.Actor
|
||||
)
|
||||
case _ => ;
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this for placing equipment that has yet to be registered into a container,
|
||||
* such as in support of changing ammunition types in `Tool` objects (weapons).
|
||||
* Equipment will go wherever it fits in containing object, or be dropped if it fits nowhere.
|
||||
* Item swapping during the placement is not allowed.
|
||||
* @see `ChangeAmmoMessage`
|
||||
* @see `GUIDTask.RegisterEquipment`
|
||||
* @see `PutEquipmentInInventoryOrDrop`
|
||||
* @see `Task`
|
||||
* @see `TaskResolver.GiveTask`
|
||||
* @param obj the container
|
||||
* @param item the item being manipulated
|
||||
* @return a `TaskResolver` object
|
||||
*/
|
||||
def PutNewEquipmentInInventoryOrDrop(
|
||||
obj: PlanetSideServerObject with Container
|
||||
)(item: Equipment): TaskResolver.GiveTask = {
|
||||
val localZone = obj.Zone
|
||||
TaskResolver.GiveTask(
|
||||
new Task() {
|
||||
private val localContainer = obj
|
||||
private val localItem = item
|
||||
|
||||
override def isComplete: Task.Resolution.Value = Task.Resolution.Success
|
||||
|
||||
def Execute(resolver: ActorRef): Unit = {
|
||||
PutEquipmentInInventoryOrDrop(localContainer)(localItem)
|
||||
resolver ! Success(this)
|
||||
}
|
||||
},
|
||||
List(GUIDTask.RegisterEquipment(item)(localZone.GUID))
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this for obtaining new equipment from a loadout specification.
|
||||
* The loadout specification contains a specific slot position for placing the item.
|
||||
* This request will (probably) be coincidental with a number of other such requests based on that loadout
|
||||
* so items must be rigidly placed else cascade into a chaostic order.
|
||||
* Item swapping during the placement is not allowed.
|
||||
* @see `ask`
|
||||
* @see `AvatarAction.ObjectDelete`
|
||||
* @see `ChangeAmmoMessage`
|
||||
* @see `Containable.CanNotPutItemInSlot`
|
||||
* @see `Containable.PutItemAway`
|
||||
* @see `Future.onComplete`
|
||||
* @see `Future.recover`
|
||||
* @see `GUIDTask.UnregisterEquipment`
|
||||
* @see `tell`
|
||||
* @see `Zone.AvatarEvents`
|
||||
* @param obj the container
|
||||
* @param taskResolver na
|
||||
* @param item the item being manipulated
|
||||
* @param slot na
|
||||
* @return a `Future` that anticipates the resolution to this manipulation
|
||||
*/
|
||||
def PutEquipmentInInventorySlot(
|
||||
obj: PlanetSideServerObject with Container,
|
||||
taskResolver: ActorRef
|
||||
)(item: Equipment, slot: Int): Future[Any] = {
|
||||
val localContainer = obj
|
||||
val localItem = item
|
||||
val localResolver = taskResolver
|
||||
val result = ask(localContainer.Actor, Containable.PutItemInSlotOnly(localItem, slot))
|
||||
result.onComplete {
|
||||
case Failure(_) | Success(_: Containable.CanNotPutItemInSlot) =>
|
||||
localResolver ! GUIDTask.UnregisterEquipment(localItem)(localContainer.Zone.GUID)
|
||||
case _ => ;
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this for obtaining new equipment from a loadout specification.
|
||||
* The loadout specification contains a specific slot position for placing the item.
|
||||
* This request will (probably) be coincidental with a number of other such requests based on that loadout
|
||||
* so items must be rigidly placed else cascade into a chaostic order.
|
||||
* Item swapping during the placement is not allowed.
|
||||
* @see `GUIDTask.RegisterEquipment`
|
||||
* @see `PutEquipmentInInventorySlot`
|
||||
* @see `Task`
|
||||
* @see `TaskResolver.GiveTask`
|
||||
* @param obj the container
|
||||
* @param taskResolver na
|
||||
* @param item the item being manipulated
|
||||
* @param slot where the item will be placed in the container
|
||||
* @return a `TaskResolver` object
|
||||
*/
|
||||
def PutLoadoutEquipmentInInventory(
|
||||
obj: PlanetSideServerObject with Container,
|
||||
taskResolver: ActorRef
|
||||
)(item: Equipment, slot: Int): TaskResolver.GiveTask = {
|
||||
val localZone = obj.Zone
|
||||
TaskResolver.GiveTask(
|
||||
new Task() {
|
||||
private val localContainer = obj
|
||||
private val localItem = item
|
||||
private val localSlot = slot
|
||||
private val localFunc: (Equipment, Int) => Future[Any] = PutEquipmentInInventorySlot(obj, taskResolver)
|
||||
|
||||
override def Timeout: Long = 1000
|
||||
|
||||
override def isComplete: Task.Resolution.Value = {
|
||||
if (localItem.HasGUID && localContainer.Find(localItem).nonEmpty)
|
||||
Task.Resolution.Success
|
||||
else
|
||||
Task.Resolution.Incomplete
|
||||
}
|
||||
|
||||
override def Description: String = s"PutEquipmentInInventorySlot - ${localItem.Definition.Name}"
|
||||
|
||||
def Execute(resolver: ActorRef): Unit = {
|
||||
localFunc(localItem, localSlot)
|
||||
resolver ! Success(this)
|
||||
}
|
||||
},
|
||||
List(GUIDTask.RegisterEquipment(item)(localZone.GUID))
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Used for purchasing new equipment from a terminal and placing it somewhere in a player's loadout.
|
||||
* Two levels of query are performed here based on the behavior expected of the item.
|
||||
* First, an attempt is made to place the item anywhere in the target container as long as it does not cause swap items to be generated.
|
||||
* Second, if it fails admission to the target container, an attempt is made to place it into the target player's free hand.
|
||||
* If the container and the suggested player are the same, it will skip the second attempt.
|
||||
* As a terminal operation, the player must receive a report regarding whether the transaction was successful.
|
||||
* @see `ask`
|
||||
* @see `Containable.CanNotPutItemInSlot`
|
||||
* @see `Containable.PutItemInSlotOnly`
|
||||
* @see `GUIDTask.RegisterEquipment`
|
||||
* @see `GUIDTask.UnregisterEquipment`
|
||||
* @see `Future.onComplete`
|
||||
* @see `PutEquipmentInInventorySlot`
|
||||
* @see `TerminalMessageOnTimeout`
|
||||
* @param obj the container
|
||||
* @param taskResolver na
|
||||
* @param player na
|
||||
* @param term na
|
||||
* @param item the item being manipulated
|
||||
* @return a `TaskResolver` object
|
||||
*/
|
||||
def BuyNewEquipmentPutInInventory(
|
||||
obj: PlanetSideServerObject with Container,
|
||||
taskResolver: ActorRef,
|
||||
player: Player,
|
||||
term: PlanetSideGUID
|
||||
)(item: Equipment): TaskResolver.GiveTask = {
|
||||
val localZone = obj.Zone
|
||||
TaskResolver.GiveTask(
|
||||
new Task() {
|
||||
private val localContainer = obj
|
||||
private val localItem = item
|
||||
private val localPlayer = player
|
||||
private val localResolver = taskResolver
|
||||
private val localTermMsg: Boolean => Unit = TerminalResult(term, localPlayer, TransactionType.Buy)
|
||||
|
||||
override def Timeout: Long = 1000
|
||||
|
||||
override def isComplete: Task.Resolution.Value = {
|
||||
if (localItem.HasGUID && localContainer.Find(localItem).nonEmpty)
|
||||
Task.Resolution.Success
|
||||
else
|
||||
Task.Resolution.Incomplete
|
||||
}
|
||||
|
||||
def Execute(resolver: ActorRef): Unit = {
|
||||
TerminalMessageOnTimeout(
|
||||
ask(localContainer.Actor, Containable.PutItemAway(localItem)),
|
||||
localTermMsg
|
||||
)
|
||||
.onComplete {
|
||||
case Failure(_) | Success(_: Containable.CanNotPutItemInSlot) =>
|
||||
if (localContainer != localPlayer) {
|
||||
TerminalMessageOnTimeout(
|
||||
PutEquipmentInInventorySlot(localPlayer, localResolver)(localItem, Player.FreeHandSlot),
|
||||
localTermMsg
|
||||
)
|
||||
.onComplete {
|
||||
case Failure(_) | Success(_: Containable.CanNotPutItemInSlot) =>
|
||||
localTermMsg(false)
|
||||
case _ =>
|
||||
localTermMsg(true)
|
||||
}
|
||||
} else {
|
||||
localResolver ! GUIDTask.UnregisterEquipment(localItem)(localContainer.Zone.GUID)
|
||||
localTermMsg(false)
|
||||
}
|
||||
case _ =>
|
||||
localTermMsg(true)
|
||||
}
|
||||
resolver ! Success(this)
|
||||
}
|
||||
},
|
||||
List(GUIDTask.RegisterEquipment(item)(localZone.GUID))
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The primary use is to register new mechanized assault exo-suit armaments,
|
||||
* place the newly registered weapon in hand,
|
||||
* and then raise that hand (draw that slot) so that the weapon is active.
|
||||
* (Players in MAX suits can not manipulate their drawn slot manually.)
|
||||
* In general, this can be used for any equipment that is to be equipped to a player's hand then immediately drawn.
|
||||
* Do not allow the item to be (mis)placed in any available slot.
|
||||
* Item swapping during the placement is not allowed and the possibility should be proactively avoided.
|
||||
* @throws `RuntimeException` if slot is not a player visible slot (holsters)
|
||||
* @see `ask`
|
||||
* @see `AvatarAction.ObjectDelete`
|
||||
* @see `AvatarAction.SendResponse`
|
||||
* @see `Containable.CanNotPutItemInSlot`
|
||||
* @see `Containable.PutItemInSlotOnly`
|
||||
* @see `GUIDTask.RegisterEquipment`
|
||||
* @see `GUIDTask.UnregisterEquipment`
|
||||
* @see `Future.onComplete`
|
||||
* @see `ObjectHeldMessage`
|
||||
* @see `Player.DrawnSlot`
|
||||
* @see `Player.LastDrawnSlot`
|
||||
* @see `Service.defaultPlayerGUID`
|
||||
* @see `TaskResolver.GiveTask`
|
||||
* @see `Zone.AvatarEvents`
|
||||
* @param player the player whose visible slot will be equipped and drawn
|
||||
* @param taskResolver na
|
||||
* @param item the item to equip
|
||||
* @param slot the slot in which the item will be equipped
|
||||
* @return a `TaskResolver` object
|
||||
*/
|
||||
def HoldNewEquipmentUp(player: Player, taskResolver: ActorRef)(item: Equipment, slot: Int): TaskResolver.GiveTask = {
|
||||
if (player.VisibleSlots.contains(slot)) {
|
||||
val localZone = player.Zone
|
||||
TaskResolver.GiveTask(
|
||||
new Task() {
|
||||
private val localPlayer = player
|
||||
private val localGUID = player.GUID
|
||||
private val localItem = item
|
||||
private val localSlot = slot
|
||||
private val localResolver = taskResolver
|
||||
|
||||
override def Timeout: Long = 1000
|
||||
|
||||
override def isComplete: Task.Resolution.Value = {
|
||||
if (localPlayer.DrawnSlot == localSlot)
|
||||
Task.Resolution.Success
|
||||
else
|
||||
Task.Resolution.Incomplete
|
||||
}
|
||||
|
||||
def Execute(resolver: ActorRef): Unit = {
|
||||
ask(localPlayer.Actor, Containable.PutItemInSlotOnly(localItem, localSlot))
|
||||
.onComplete {
|
||||
case Failure(_) | Success(_: Containable.CanNotPutItemInSlot) =>
|
||||
localResolver ! GUIDTask.UnregisterEquipment(localItem)(localZone.GUID)
|
||||
case _ =>
|
||||
if (localPlayer.DrawnSlot != Player.HandsDownSlot) {
|
||||
localPlayer.DrawnSlot = Player.HandsDownSlot
|
||||
localZone.AvatarEvents ! AvatarServiceMessage(
|
||||
localPlayer.Name,
|
||||
AvatarAction.SendResponse(
|
||||
Service.defaultPlayerGUID,
|
||||
ObjectHeldMessage(localGUID, Player.HandsDownSlot, false)
|
||||
)
|
||||
)
|
||||
localZone.AvatarEvents ! AvatarServiceMessage(
|
||||
localZone.Id,
|
||||
AvatarAction.ObjectHeld(localGUID, localPlayer.LastDrawnSlot)
|
||||
)
|
||||
}
|
||||
localPlayer.DrawnSlot = localSlot
|
||||
localZone.AvatarEvents ! AvatarServiceMessage(
|
||||
localZone.Id,
|
||||
AvatarAction.SendResponse(Service.defaultPlayerGUID, ObjectHeldMessage(localGUID, localSlot, false))
|
||||
)
|
||||
}
|
||||
resolver ! Success(this)
|
||||
}
|
||||
},
|
||||
List(GUIDTask.RegisterEquipment(item)(localZone.GUID))
|
||||
)
|
||||
} else {
|
||||
//TODO log.error
|
||||
throw new RuntimeException(s"provided slot $slot is not a player visible slot (holsters)")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an item from the ground and put it into the given container.
|
||||
* The zone in which the item is found is expected to be the same in which the container object is located.
|
||||
* If the object can not be placed into the container, it is put back on the ground.
|
||||
* The item that was collected off the ground, if it is placed back on the ground,
|
||||
* will be positioned with respect to the container object rather than its original location.
|
||||
* @see `ask`
|
||||
* @see `AvatarAction.ObjectDelete`
|
||||
* @see `Future.onComplete`
|
||||
* @see `Zone.AvatarEvents`
|
||||
* @see `Zone.Ground.CanNotPickUpItem`
|
||||
* @see `Zone.Ground.ItemInHand`
|
||||
* @see `Zone.Ground.PickUpItem`
|
||||
* @see `PutEquipmentInInventoryOrDrop`
|
||||
* @param obj the container into which the item will be placed
|
||||
* @param item the item being collected from off the ground of the container's zone
|
||||
* @return a `Future` that anticipates the resolution to this manipulation
|
||||
*/
|
||||
def PickUpEquipmentFromGround(obj: PlanetSideServerObject with Container)(item: Equipment): Future[Any] = {
|
||||
val localZone = obj.Zone
|
||||
val localContainer = obj
|
||||
val localItem = item
|
||||
val future = ask(localZone.Ground, Zone.Ground.PickupItem(item.GUID))
|
||||
future.onComplete {
|
||||
case Success(Zone.Ground.ItemInHand(_)) =>
|
||||
PutEquipmentInInventoryOrDrop(localContainer)(localItem)
|
||||
case Success(Zone.Ground.CanNotPickupItem(_, item_guid, _)) =>
|
||||
localZone.GUID(item_guid) match {
|
||||
case Some(_) => ;
|
||||
case None => //acting on old data?
|
||||
localZone.AvatarEvents ! AvatarServiceMessage(
|
||||
localZone.Id,
|
||||
AvatarAction.ObjectDelete(Service.defaultPlayerGUID, item_guid)
|
||||
)
|
||||
}
|
||||
case _ => ;
|
||||
}
|
||||
future
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an item from a container and drop it on the ground.
|
||||
* @see `ask`
|
||||
* @see `AvatarAction.ObjectDelete`
|
||||
* @see `Containable.ItemFromSlot`
|
||||
* @see `Containable.RemoveItemFromSlot`
|
||||
* @see `Future.onComplete`
|
||||
* @see `Future.recover`
|
||||
* @see `tell`
|
||||
* @see `Zone.AvatarEvents`
|
||||
* @see `Zone.Ground.DropItem`
|
||||
* @param obj the container to search
|
||||
* @param item the item to find and remove from the container
|
||||
* @param pos an optional position where to drop the item on the ground;
|
||||
* expected override from original container's position
|
||||
* @return a `Future` that anticipates the resolution to this manipulation
|
||||
*/
|
||||
def DropEquipmentFromInventory(
|
||||
obj: PlanetSideServerObject with Container
|
||||
)(item: Equipment, pos: Option[Vector3] = None): Future[Any] = {
|
||||
val localContainer = obj
|
||||
val localItem = item
|
||||
val localPos = pos
|
||||
val result = ask(localContainer.Actor, Containable.RemoveItemFromSlot(localItem))
|
||||
result.onComplete {
|
||||
case Success(Containable.ItemFromSlot(_, Some(_), Some(_))) =>
|
||||
localContainer.Zone.Ground.tell(
|
||||
Zone.Ground
|
||||
.DropItem(localItem, localPos.getOrElse(localContainer.Position), Vector3.z(localContainer.Orientation.z)),
|
||||
localContainer.Actor
|
||||
)
|
||||
case _ => ;
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an item from a container and delete it.
|
||||
* @see `ask`
|
||||
* @see `AvatarAction.ObjectDelete`
|
||||
* @see `Containable.ItemFromSlot`
|
||||
* @see `Containable.RemoveItemFromSlot`
|
||||
* @see `Future.onComplete`
|
||||
* @see `Future.recover`
|
||||
* @see `GUIDTask.UnregisterEquipment`
|
||||
* @see `Zone.AvatarEvents`
|
||||
* @param obj the container to search
|
||||
* @param taskResolver na
|
||||
* @param item the item to find and remove from the container
|
||||
* @return a `Future` that anticipates the resolution to this manipulation
|
||||
*/
|
||||
def RemoveOldEquipmentFromInventory(obj: PlanetSideServerObject with Container, taskResolver: ActorRef)(
|
||||
item: Equipment
|
||||
): Future[Any] = {
|
||||
val localContainer = obj
|
||||
val localItem = item
|
||||
val localResolver = taskResolver
|
||||
val result = ask(localContainer.Actor, Containable.RemoveItemFromSlot(localItem))
|
||||
result.onComplete {
|
||||
case Success(Containable.ItemFromSlot(_, Some(_), Some(_))) =>
|
||||
localResolver ! GUIDTask.UnregisterEquipment(localItem)(localContainer.Zone.GUID)
|
||||
case _ =>
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/**
|
||||
* Primarily, remove an item from a container and delete it.
|
||||
* As a terminal operation, the player must receive a report regarding whether the transaction was successful.
|
||||
* At the end of a successful transaction, and only a successful transaction,
|
||||
* the item that was removed is no longer considered a valid game object.
|
||||
* Contrasting `RemoveOldEquipmentFromInventory` which identifies the actual item to be eliminated,
|
||||
* this function uses the slot where the item is (should be) located.
|
||||
* @see `ask`
|
||||
* @see `Containable.ItemFromSlot`
|
||||
* @see `Containable.RemoveItemFromSlot`
|
||||
* @see `Future.onComplete`
|
||||
* @see `Future.recover`
|
||||
* @see `GUIDTask.UnregisterEquipment`
|
||||
* @see `RemoveOldEquipmentFromInventory`
|
||||
* @see `TerminalMessageOnTimeout`
|
||||
* @see `TerminalResult`
|
||||
* @param obj the container to search
|
||||
* @param taskResolver na
|
||||
* @param player the player who used the terminal
|
||||
* @param term the unique identifier number of the terminal
|
||||
* @param slot from which slot the equipment is to be removed
|
||||
* @return a `Future` that anticipates the resolution to this manipulation
|
||||
*/
|
||||
def SellEquipmentFromInventory(
|
||||
obj: PlanetSideServerObject with Container,
|
||||
taskResolver: ActorRef,
|
||||
player: Player,
|
||||
term: PlanetSideGUID
|
||||
)(slot: Int): Future[Any] = {
|
||||
val localContainer = obj
|
||||
val localPlayer = player
|
||||
val localSlot = slot
|
||||
val localResolver = taskResolver
|
||||
val localTermMsg: Boolean => Unit = TerminalResult(term, localPlayer, TransactionType.Sell)
|
||||
val result = TerminalMessageOnTimeout(
|
||||
ask(localContainer.Actor, Containable.RemoveItemFromSlot(localSlot)),
|
||||
localTermMsg
|
||||
)
|
||||
result.onComplete {
|
||||
case Success(Containable.ItemFromSlot(_, Some(item), Some(_))) =>
|
||||
localResolver ! GUIDTask.UnregisterEquipment(item)(localContainer.Zone.GUID)
|
||||
localTermMsg(true)
|
||||
case _ =>
|
||||
localTermMsg(false)
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/**
|
||||
* If a timeout occurs on the manipulation, declare a terminal transaction failure.
|
||||
* @see `AskTimeoutException`
|
||||
* @see `recover`
|
||||
* @param future the item manipulation's `Future` object
|
||||
* @param terminalMessage how to call the terminal message
|
||||
* @return a `Future` that anticipates the resolution to this manipulation
|
||||
*/
|
||||
def TerminalMessageOnTimeout(future: Future[Any], terminalMessage: Boolean => Unit): Future[Any] = {
|
||||
future.recover {
|
||||
case _: AskTimeoutException =>
|
||||
terminalMessage(false)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Announced the result of this player's terminal use, to the player that used the terminal.
|
||||
* This is a necessary step for regaining terminal use which is naturally blocked by the client after a transaction request.
|
||||
* @see `AvatarAction.TerminalOrderResult`
|
||||
* @see `ItemTransactionResultMessage`
|
||||
* @see `TransactionType`
|
||||
* @param guid the terminal's unique identifier
|
||||
* @param player the player who used the terminal
|
||||
* @param transaction what kind of transaction was involved in terminal use
|
||||
* @param result the result of that transaction
|
||||
*/
|
||||
def TerminalResult(guid: PlanetSideGUID, player: Player, transaction: TransactionType.Value)(
|
||||
result: Boolean
|
||||
): Unit = {
|
||||
player.Zone.AvatarEvents ! AvatarServiceMessage(
|
||||
player.Name,
|
||||
AvatarAction.TerminalOrderResult(guid, transaction, result)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop some items on the ground is a given location.
|
||||
* The location corresponds to the previous container for those items.
|
||||
* @see `Zone.Ground.DropItem`
|
||||
* @param container the original object that contained the items
|
||||
* @param drops the items to be dropped on the ground
|
||||
*/
|
||||
def DropLeftovers(container: PlanetSideServerObject with Container)(drops: List[InventoryItem]): Unit = {
|
||||
//drop or retire
|
||||
val zone = container.Zone
|
||||
val pos = container.Position
|
||||
val orient = Vector3.z(container.Orientation.z)
|
||||
//TODO make a sound when dropping stuff?
|
||||
drops.foreach { entry => zone.Ground.tell(Zone.Ground.DropItem(entry.obj, pos, orient), container.Actor) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Within a specified `Container`, find the smallest number of `Equipment` objects of a certain qualifying type
|
||||
* whose sum count is greater than, or equal to, a `desiredAmount` based on an accumulator method.<br>
|
||||
* <br>
|
||||
* In an occupied `List` of returned `Inventory` entries, all but the last entry is typically considered "emptied."
|
||||
* For objects with contained quantities, the last entry may require having that quantity be set to a non-zero number.
|
||||
* @param obj the `Container` to search
|
||||
* @param filterTest test used to determine inclusivity of `Equipment` collection
|
||||
* @param desiredAmount how much is requested
|
||||
* @param counting test used to determine value of found `Equipment`;
|
||||
* defaults to one per entry
|
||||
* @return a `List` of all discovered entries totaling approximately the amount requested
|
||||
*/
|
||||
def FindEquipmentStock(
|
||||
obj: Container,
|
||||
filterTest: Equipment => Boolean,
|
||||
desiredAmount: Int,
|
||||
counting: Equipment => Int = DefaultCount
|
||||
): List[InventoryItem] = {
|
||||
var currentAmount: Int = 0
|
||||
obj.Inventory.Items
|
||||
.filter(item => filterTest(item.obj))
|
||||
.sortBy(_.start)
|
||||
.takeWhile(entry => {
|
||||
val previousAmount = currentAmount
|
||||
currentAmount += counting(entry.obj)
|
||||
previousAmount < desiredAmount
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* The default counting function for an item.
|
||||
* Counts the number of item(s).
|
||||
* @param e the `Equipment` object
|
||||
* @return the quantity;
|
||||
* always one
|
||||
*/
|
||||
def DefaultCount(e: Equipment): Int = 1
|
||||
|
||||
/**
|
||||
* The counting function for an item of `AmmoBox`.
|
||||
* Counts the `Capacity` of the ammunition.
|
||||
* @param e the `Equipment` object
|
||||
* @return the quantity
|
||||
*/
|
||||
def CountAmmunition(e: Equipment): Int = {
|
||||
e match {
|
||||
case a: AmmoBox => a.Capacity
|
||||
case _ => 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The counting function for an item of `Tool` where the item is also a grenade.
|
||||
* Counts the number of grenades.
|
||||
* @see `GlobalDefinitions.isGrenade`
|
||||
* @param e the `Equipment` object
|
||||
* @return the quantity
|
||||
*/
|
||||
def CountGrenades(e: Equipment): Int = {
|
||||
e match {
|
||||
case t: Tool => (GlobalDefinitions.isGrenade(t.Definition): Int) * t.Magazine
|
||||
case _ => 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flag an `AmmoBox` object that matches for the given ammunition type.
|
||||
* @param ammo the type of `Ammo` to check
|
||||
* @param e the `Equipment` object
|
||||
* @return `true`, if the object is an `AmmoBox` of the correct ammunition type; `false`, otherwise
|
||||
*/
|
||||
def FindAmmoBoxThatUses(ammo: Ammo.Value)(e: Equipment): Boolean = {
|
||||
e match {
|
||||
case t: AmmoBox => t.AmmoType == ammo
|
||||
case _ => false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flag a `Tool` object that matches for loading the given ammunition type.
|
||||
* @param ammo the type of `Ammo` to check
|
||||
* @param e the `Equipment` object
|
||||
* @return `true`, if the object is a `Tool` that loads the correct ammunition type; `false`, otherwise
|
||||
*/
|
||||
def FindToolThatUses(ammo: Ammo.Value)(e: Equipment): Boolean = {
|
||||
e match {
|
||||
case t: Tool =>
|
||||
t.Definition.AmmoTypes.map { _.AmmoType }.contains(ammo)
|
||||
case _ =>
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package net.psforever.login.psadmin
|
||||
|
||||
import net.psforever.util.Config
|
||||
import scala.collection.mutable
|
||||
import scala.jdk.CollectionConverters._
|
||||
|
||||
object CmdInternal {
|
||||
|
||||
def cmdDumpConfig(args: Array[String]) = {
|
||||
val config =
|
||||
Config.config.root.keySet.asScala.map(key => key -> Config.config.getAnyRef(key).asInstanceOf[Any]).toMap
|
||||
CommandGoodResponse(s"Dump of WorldConfig", mutable.Map(config.toSeq: _*))
|
||||
}
|
||||
|
||||
def cmdThreadDump(args: Array[String]) = {
|
||||
|
||||
var data = mutable.Map[String, Any]()
|
||||
val traces = Thread.getAllStackTraces().asScala
|
||||
var traces_fmt = List[String]()
|
||||
|
||||
for ((thread, trace) <- traces) {
|
||||
val info = s"Thread ${thread.getId} - ${thread.getName}\n"
|
||||
traces_fmt = traces_fmt ++ List(info + trace.mkString("\n"))
|
||||
}
|
||||
|
||||
data { "trace" } = traces_fmt
|
||||
|
||||
CommandGoodResponse(s"Dump of ${traces.size} threads", data)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package net.psforever.login.psadmin
|
||||
|
||||
import akka.actor.{Actor, ActorRef}
|
||||
import net.psforever.objects.zones.InterstellarCluster
|
||||
|
||||
import scala.collection.mutable.Map
|
||||
|
||||
class CmdListPlayers(args: Array[String], services: Map[String, ActorRef]) extends Actor {
|
||||
private[this] val log = org.log4s.getLogger(self.path.name)
|
||||
|
||||
override def preStart = {
|
||||
services { "cluster" } ! InterstellarCluster.ListPlayers()
|
||||
}
|
||||
|
||||
override def receive = {
|
||||
case InterstellarCluster.PlayerList(players) =>
|
||||
val data = Map[String, Any]()
|
||||
data { "player_count" } = players.size
|
||||
data { "player_list" } = Array[String]()
|
||||
|
||||
if (players.isEmpty) {
|
||||
context.parent ! CommandGoodResponse("No players currently online!", data)
|
||||
} else {
|
||||
data { "player_list" } = players
|
||||
context.parent ! CommandGoodResponse(s"${players.length} players online\n", data)
|
||||
}
|
||||
case default => log.error(s"Unexpected message $default")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package net.psforever.login.psadmin
|
||||
|
||||
import akka.actor.{Actor, ActorRef}
|
||||
|
||||
import scala.collection.mutable.Map
|
||||
|
||||
class CmdShutdown(args: Array[String], services: Map[String, ActorRef]) extends Actor {
|
||||
override def preStart = {
|
||||
var data = Map[String, Any]()
|
||||
context.parent ! CommandGoodResponse("Shutting down", data)
|
||||
context.system.terminate()
|
||||
}
|
||||
|
||||
override def receive = {
|
||||
case default =>
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,187 @@
|
|||
package net.psforever.login.psadmin
|
||||
|
||||
import java.net.InetSocketAddress
|
||||
|
||||
import akka.actor.{Actor, ActorRef, Props, Stash}
|
||||
import akka.io.Tcp
|
||||
import akka.util.ByteString
|
||||
import org.json4s._
|
||||
import org.json4s.native.Serialization.write
|
||||
import scodec.bits._
|
||||
import scodec.interop.akka._
|
||||
import services.ServiceManager.Lookup
|
||||
import services._
|
||||
|
||||
import scala.collection.mutable.Map
|
||||
|
||||
object PsAdminActor {
|
||||
val whiteSpaceRegex = """\s+""".r
|
||||
}
|
||||
|
||||
class PsAdminActor(peerAddress: InetSocketAddress, connection: ActorRef) extends Actor with Stash {
|
||||
private[this] val log = org.log4s.getLogger(self.path.name)
|
||||
|
||||
val services = Map[String, ActorRef]()
|
||||
val servicesToResolve = Array("cluster")
|
||||
var buffer = ByteString()
|
||||
|
||||
implicit val formats = DefaultFormats // for JSON serialization
|
||||
|
||||
case class CommandCall(operation: String, args: Array[String])
|
||||
|
||||
override def preStart() = {
|
||||
log.trace(s"PsAdmin connection started $peerAddress")
|
||||
|
||||
for (service <- servicesToResolve) {
|
||||
ServiceManager.serviceManager ! Lookup(service)
|
||||
}
|
||||
}
|
||||
|
||||
override def receive = ServiceLookup
|
||||
|
||||
def ServiceLookup: Receive = {
|
||||
case ServiceManager.LookupResult(service, endpoint) =>
|
||||
services { service } = endpoint
|
||||
|
||||
if (services.size == servicesToResolve.size) {
|
||||
unstashAll()
|
||||
context.become(ReceiveCommand)
|
||||
}
|
||||
|
||||
case default => stash()
|
||||
}
|
||||
|
||||
def ReceiveCommand: Receive = {
|
||||
case Tcp.Received(data) =>
|
||||
buffer ++= data
|
||||
|
||||
var pos = -1;
|
||||
var amount = 0
|
||||
do {
|
||||
pos = buffer.indexOf('\n')
|
||||
if (pos != -1) {
|
||||
val (cmd, rest) = buffer.splitAt(pos)
|
||||
buffer = rest.drop(1); // drop the newline
|
||||
|
||||
// make sure the CN cant crash us
|
||||
val line = cmd.decodeString("utf-8").trim
|
||||
|
||||
if (line != "") {
|
||||
val tokens = PsAdminActor.whiteSpaceRegex.split(line)
|
||||
val cmd = tokens.head
|
||||
val args = tokens.tail
|
||||
|
||||
amount += 1
|
||||
self ! CommandCall(cmd, args)
|
||||
}
|
||||
}
|
||||
} while (pos != -1)
|
||||
|
||||
if (amount > 0)
|
||||
context.become(ProcessCommands)
|
||||
|
||||
case Tcp.PeerClosed =>
|
||||
context.stop(self)
|
||||
|
||||
case default =>
|
||||
log.error(s"Unexpected message $default")
|
||||
}
|
||||
|
||||
/// Process all buffered commands and stash other ones
|
||||
def ProcessCommands: Receive = {
|
||||
case c: CommandCall =>
|
||||
stash()
|
||||
unstashAll()
|
||||
context.become(ProcessCommand)
|
||||
|
||||
case default =>
|
||||
stash()
|
||||
unstashAll()
|
||||
context.become(ReceiveCommand)
|
||||
}
|
||||
|
||||
/// Process a single command
|
||||
def ProcessCommand: Receive = {
|
||||
case CommandCall(cmd, args) =>
|
||||
val data = Map[String, Any]()
|
||||
|
||||
if (cmd == "help" || cmd == "?") {
|
||||
if (args.size == 0) {
|
||||
var resp = "PsAdmin command usage\n"
|
||||
|
||||
for ((command, info) <- PsAdminCommands.commands) {
|
||||
resp += s"${command} - ${info.usage}\n"
|
||||
}
|
||||
|
||||
data { "message" } = resp
|
||||
} else {
|
||||
if (PsAdminCommands.commands.contains(args(0))) {
|
||||
val info = PsAdminCommands.commands { args(0) }
|
||||
|
||||
data { "message" } = s"${args(0)} - ${info.usage}"
|
||||
} else {
|
||||
data { "message" } = s"Unknown command ${args(0)}"
|
||||
data { "error" } = true
|
||||
}
|
||||
}
|
||||
|
||||
sendLine(write(data.toMap))
|
||||
} else if (PsAdminCommands.commands.contains(cmd)) {
|
||||
val cmd_template = PsAdminCommands.commands { cmd }
|
||||
|
||||
cmd_template match {
|
||||
case PsAdminCommands.Command(usage, handler) =>
|
||||
context.actorOf(Props(handler, args, services))
|
||||
|
||||
case PsAdminCommands.CommandInternal(usage, handler) =>
|
||||
val resp = handler(args)
|
||||
|
||||
resp match {
|
||||
case CommandGoodResponse(msg, data) =>
|
||||
data { "message" } = msg
|
||||
sendLine(write(data.toMap))
|
||||
|
||||
case CommandErrorResponse(msg, data) =>
|
||||
data { "message" } = msg
|
||||
data { "error" } = true
|
||||
sendLine(write(data.toMap))
|
||||
}
|
||||
|
||||
context.become(ProcessCommands)
|
||||
}
|
||||
} else {
|
||||
data { "message" } = "Unknown command"
|
||||
data { "error" } = true
|
||||
sendLine(write(data.toMap))
|
||||
context.become(ProcessCommands)
|
||||
}
|
||||
|
||||
case resp: CommandResponse =>
|
||||
resp match {
|
||||
case CommandGoodResponse(msg, data) =>
|
||||
data { "message" } = msg
|
||||
sendLine(write(data.toMap))
|
||||
|
||||
case CommandErrorResponse(msg, data) =>
|
||||
data { "message" } = msg
|
||||
data { "error" } = true
|
||||
sendLine(write(data.toMap))
|
||||
}
|
||||
|
||||
context.become(ProcessCommands)
|
||||
context.stop(sender())
|
||||
case default =>
|
||||
stash()
|
||||
unstashAll()
|
||||
context.become(ProcessCommands)
|
||||
}
|
||||
|
||||
def sendLine(line: String) = {
|
||||
ByteVector.encodeUtf8(line + "\n") match {
|
||||
case Left(e) =>
|
||||
log.error(s"Message encoding failure: $e")
|
||||
case Right(bv) =>
|
||||
connection ! Tcp.Write(bv.toByteString)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package net.psforever.login.psadmin
|
||||
|
||||
import scala.collection.mutable
|
||||
|
||||
sealed trait CommandResponse
|
||||
case class CommandGoodResponse(message: String, data: mutable.Map[String, Any]) extends CommandResponse
|
||||
case class CommandErrorResponse(message: String, data: mutable.Map[String, Any]) extends CommandResponse
|
||||
|
||||
object PsAdminCommands {
|
||||
import CmdInternal._
|
||||
|
||||
val commands: Map[String, CommandInfo] = Map(
|
||||
"list_players" -> Command(
|
||||
"""Return a list of players connected to the interstellar cluster.""",
|
||||
classOf[CmdListPlayers]
|
||||
),
|
||||
"dump_config" -> CommandInternal("""Dumps entire running config.""", cmdDumpConfig),
|
||||
"shutdown" -> Command("""Shuts down the server forcefully.""", classOf[CmdShutdown]),
|
||||
"thread_dump" -> CommandInternal("""Returns all thread's stack traces.""", cmdThreadDump)
|
||||
)
|
||||
|
||||
sealed trait CommandInfo {
|
||||
def usage: String
|
||||
}
|
||||
|
||||
/// A command with full access to the ActorSystem and WorldServer services.
|
||||
/// Spawns an Actor to handle the request and the service queries
|
||||
case class Command[T](usage: String, handler: Class[T]) extends CommandInfo
|
||||
|
||||
/// A command without access to the ActorSystem or any services
|
||||
case class CommandInternal(usage: String, handler: ((Array[String]) => CommandResponse)) extends CommandInfo
|
||||
}
|
||||
17
common/src/main/scala/net/psforever/objects/Session.scala
Normal file
17
common/src/main/scala/net/psforever/objects/Session.scala
Normal 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
|
||||
)
|
||||
|
|
@ -63,8 +63,8 @@ object ChatMessageType extends Enumeration {
|
|||
CMT_GMTELL, // /gmtell (actually causes normal /tell 0x20 when not a gm???)
|
||||
CMT_NOTE, // /note
|
||||
CMT_GMBROADCASTPOPUP, // /gmpopup
|
||||
U_CMT_GMTELLFROM, // ??? Recipient of /gmtell?
|
||||
U_CMT_TELLFROM, // ??? Recipient of /t?
|
||||
U_CMT_GMTELLFROM, // Acknowledgement of /gmtell for sender
|
||||
U_CMT_TELLFROM, // Acknowledgement of /tell for sender
|
||||
UNK_45, // ??? empty
|
||||
CMT_CULLWATERMARK, // ??? This actually causes the client to ping back to the server with some stringified numbers "80 120" (with the same 46 chatmsg causing infinite loop?) - may be incorrect decoding
|
||||
CMT_INSTANTACTION, // /instantaction OR /ia
|
||||
|
|
|
|||
120
common/src/main/scala/net/psforever/util/Config.scala
Normal file
120
common/src/main/scala/net/psforever/util/Config.scala
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
package net.psforever.util
|
||||
|
||||
import java.nio.file.Paths
|
||||
|
||||
import com.typesafe.config.{Config => TypesafeConfig}
|
||||
import enumeratum.values.{IntEnum, IntEnumEntry}
|
||||
import net.psforever.packet.game.ServerType
|
||||
import pureconfig.ConfigConvert.viaNonEmptyStringOpt
|
||||
import pureconfig.ConfigReader.Result
|
||||
import pureconfig.{ConfigConvert, ConfigSource}
|
||||
import scala.concurrent.duration._
|
||||
import scala.reflect.ClassTag
|
||||
import pureconfig.generic.auto._ // intellij: this is not unused
|
||||
|
||||
object Config {
|
||||
// prog.home is defined when we are running from SBT pack
|
||||
val directory: String = System.getProperty("prog.home") match {
|
||||
case null =>
|
||||
Paths.get("config").toAbsolutePath.toString
|
||||
case home =>
|
||||
Paths.get(home, "config").toAbsolutePath.toString
|
||||
}
|
||||
|
||||
implicit def enumeratumIntConfigConvert[A <: IntEnumEntry](implicit
|
||||
enum: IntEnum[A],
|
||||
ct: ClassTag[A]
|
||||
): ConfigConvert[A] =
|
||||
viaNonEmptyStringOpt[A](
|
||||
v =>
|
||||
enum.values.toList.collectFirst {
|
||||
case (e: ServerType) if e.name == v => e.asInstanceOf[A]
|
||||
},
|
||||
_.value.toString
|
||||
)
|
||||
|
||||
private val source = {
|
||||
val configFile = Paths.get(directory, "psforever.conf").toFile()
|
||||
if (configFile.exists)
|
||||
ConfigSource.file(configFile).withFallback(ConfigSource.defaultApplication)
|
||||
else
|
||||
ConfigSource.defaultApplication
|
||||
}
|
||||
|
||||
val result: Result[AppConfig] = source.load[AppConfig]
|
||||
|
||||
// Raw config object - prefer app when possible
|
||||
lazy val config: TypesafeConfig = source.config().toOption.get
|
||||
|
||||
// Typed config object
|
||||
lazy val app: AppConfig = result.toOption.get
|
||||
}
|
||||
|
||||
case class AppConfig(
|
||||
bind: String,
|
||||
public: String,
|
||||
login: LoginConfig,
|
||||
world: WorldConfig,
|
||||
admin: AdminConfig,
|
||||
database: DatabaseConfig,
|
||||
antiCheat: AntiCheatConfig,
|
||||
network: NetworkConfig,
|
||||
developer: DeveloperConfig,
|
||||
kamon: KamonConfig
|
||||
)
|
||||
|
||||
case class LoginConfig(
|
||||
port: Int,
|
||||
createMissingAccounts: Boolean
|
||||
)
|
||||
|
||||
case class WorldConfig(
|
||||
port: Int,
|
||||
serverName: String,
|
||||
serverType: ServerType
|
||||
)
|
||||
|
||||
case class AdminConfig(
|
||||
port: Int,
|
||||
bind: String
|
||||
)
|
||||
|
||||
case class DatabaseConfig(
|
||||
host: String,
|
||||
port: Int,
|
||||
username: String,
|
||||
password: String,
|
||||
database: String,
|
||||
sslmode: String
|
||||
) {
|
||||
def toJdbc = s"jdbc:postgresql://$host:$port/$database"
|
||||
}
|
||||
|
||||
case class AntiCheatConfig(
|
||||
hitPositionDiscrepancyThreshold: Int
|
||||
)
|
||||
|
||||
case class NetworkConfig(
|
||||
session: SessionConfig
|
||||
)
|
||||
|
||||
case class SessionConfig(
|
||||
inboundGraceTime: Duration,
|
||||
outboundGraceTime: Duration
|
||||
)
|
||||
|
||||
case class DeveloperConfig(
|
||||
netSim: NetSimConfig
|
||||
)
|
||||
|
||||
case class NetSimConfig(
|
||||
enable: Boolean,
|
||||
loss: Double,
|
||||
delay: Duration,
|
||||
reorderChance: Double,
|
||||
reorderTime: Duration
|
||||
)
|
||||
|
||||
case class KamonConfig(
|
||||
enable: Boolean
|
||||
)
|
||||
20
common/src/main/scala/net/psforever/util/Database.scala
Normal file
20
common/src/main/scala/net/psforever/util/Database.scala
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
package net.psforever.util
|
||||
|
||||
import io.getquill.{PostgresJAsyncContext, SnakeCase}
|
||||
import net.psforever.persistence
|
||||
|
||||
object Database {
|
||||
implicit val accountSchemaMeta = ctx.schemaMeta[persistence.Account]("accounts", _.id -> "id")
|
||||
implicit val characterSchemaMeta = ctx.schemaMeta[persistence.Character]("characters", _.id -> "id")
|
||||
implicit val loadoutSchemaMeta = ctx.schemaMeta[persistence.Loadout]("loadouts", _.id -> "id")
|
||||
implicit val lockerSchemaMeta = ctx.schemaMeta[persistence.Locker]("lockers", _.id -> "id")
|
||||
implicit val loginSchemaMeta = ctx.schemaMeta[persistence.Login]("logins", _.id -> "id")
|
||||
|
||||
// TODO remove if this gets merged https://github.com/getquill/quill/pull/1765
|
||||
implicit class ILike(s1: String) {
|
||||
import ctx._
|
||||
def ilike(s2: String) = quote(infix"$s1 ilike $s2".as[Boolean])
|
||||
}
|
||||
|
||||
val ctx = new PostgresJAsyncContext(SnakeCase, Config.config.getConfig("database"))
|
||||
}
|
||||
581
common/src/main/scala/net/psforever/util/PointOfInterest.scala
Normal file
581
common/src/main/scala/net/psforever/util/PointOfInterest.scala
Normal file
|
|
@ -0,0 +1,581 @@
|
|||
package net.psforever.util
|
||||
|
||||
import net.psforever.types.Vector3
|
||||
|
||||
import scala.collection.mutable
|
||||
import scala.util.Random
|
||||
|
||||
/**
|
||||
* 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 exception is `alias` which is maintained for cosmetic purposes and clarification.)
|
||||
* @param alias the common name of the zone
|
||||
* @param map the map name of the zone (this map is loaded)
|
||||
* @param zonename the zone's internal name
|
||||
*/
|
||||
class PointOfInterest(val alias: String, val map: String, val zonename: String) {
|
||||
|
||||
/**
|
||||
* A listing of warpgates, geowarps, and island warpgates in this zone.
|
||||
* The coordinates specified will only ever drop the user on a specific point within the protective bubble of the warpgate.
|
||||
* This breaks from the expected zoning functionality where the user is placed in a random spot under the bubble.
|
||||
* There is no prior usage details for the searchability format of this field's key values.
|
||||
*/
|
||||
private val gates: mutable.HashMap[String, Vector3] = mutable.HashMap()
|
||||
|
||||
/**
|
||||
* A listing of special locations in this zone, i.e., major faciities, and some landmarks of interest.
|
||||
* There is no prior usage details for the searchability format of this field's key values.
|
||||
*/
|
||||
private val locations: mutable.HashMap[String, Vector3] = mutable.HashMap()
|
||||
}
|
||||
|
||||
object PointOfInterest {
|
||||
|
||||
/**
|
||||
* A listing of all zones that can be visited by their internal name.
|
||||
* The keys in this map should be directly usable by the `/zone` command.
|
||||
*/
|
||||
private val zones = Map[String, PointOfInterest](
|
||||
"z1" -> PointOfInterest("Solsar", "map01", "z1"),
|
||||
"z2" -> PointOfInterest("Hossin", "map02", "z2"),
|
||||
"z3" -> PointOfInterest("Cyssor", "map03", "z3"),
|
||||
"z4" -> PointOfInterest("Ishundar", "map04", "z4"),
|
||||
"z5" -> PointOfInterest("Forseral", "map05", "z5"),
|
||||
"z6" -> PointOfInterest("Ceryshen", "map06", "z6"),
|
||||
"z7" -> PointOfInterest("Esamir", "map07", "z7"),
|
||||
"z8" -> PointOfInterest("Oshur", "map08", "z8"),
|
||||
"z9" -> PointOfInterest("Searhus", "map09", "z9"),
|
||||
"z10" -> PointOfInterest("Amerish", "map10", "z10"),
|
||||
"home1" -> PointOfInterest("NC Sanctuary", "map11", "home1"),
|
||||
"home2" -> PointOfInterest("TR Sanctuary", "map12", "home2"),
|
||||
"home3" -> PointOfInterest("VS Sanctuary", "map13", "home3"),
|
||||
"tzshtr" -> PointOfInterest("VR Shooting Range TR", "map14", "tzshtr"),
|
||||
"tzdrtr" -> PointOfInterest("VR Driving Range TR", "map15", "tzdrtr"),
|
||||
"tzcotr" -> PointOfInterest("VR Combat Zone TR", "map16", "tzcotr"),
|
||||
"tzshvs" -> PointOfInterest("VR Shooting Range VS", "map14", "tzshvs"),
|
||||
"tzdrvs" -> PointOfInterest("VR Driving Range VS", "map15", "tzdrvs"),
|
||||
"tzcovs" -> PointOfInterest("VR Combat Zone VS", "map16", "tzcovs"),
|
||||
"tzshnc" -> PointOfInterest("VR Shooting Range NC", "map14", "tzshnc"),
|
||||
"tzdrnc" -> PointOfInterest("VR Driving Range NC", "map15", "tzdrnc"),
|
||||
"tzconc" -> PointOfInterest("VR Combat Zone NC", "map16", "tzconc"),
|
||||
"c1" -> PointOfInterest("Supai", "ugd01", "c1"),
|
||||
"c2" -> PointOfInterest("Hunhau", "ugd02", "c2"),
|
||||
"c3" -> PointOfInterest("Adlivun", "ugd03", "c3"),
|
||||
"c4" -> PointOfInterest("Byblos", "ugd04", "c4"),
|
||||
"c5" -> PointOfInterest("Annwn", "ugd05", "c5"),
|
||||
"c6" -> PointOfInterest("Drugaskan", "ugd06", "c6"),
|
||||
"i4" -> PointOfInterest("Nexus", "map96", "i4"),
|
||||
"i3" -> PointOfInterest("Desolation", "map97", "i3"),
|
||||
"i2" -> PointOfInterest("Ascension", "map98", "i2"),
|
||||
"i1" -> PointOfInterest("Extinction", "map99", "i1"),
|
||||
"homebo" -> PointOfInterest("Black_ops_hq", "Black_ops_hq", "homebo"),
|
||||
"station1" -> PointOfInterest("TR Station", "Station1", "station1"),
|
||||
"station2" -> PointOfInterest("NC Station", "Station2", "station2"),
|
||||
"station3" -> PointOfInterest("VS Station", "Station3", "station3")
|
||||
)
|
||||
|
||||
/**
|
||||
* A listing of all zones that can be visited by their common name.
|
||||
* The keys in this map should be directly usable by the `/zone` command.
|
||||
* Though the behavior is undocumented, access to this alias list is for the benefit of the user.
|
||||
*/
|
||||
private val alias = Map[String, String](
|
||||
"solsar" -> "z1",
|
||||
"hossin" -> "z2",
|
||||
"cyssor" -> "z3",
|
||||
"ishundar" -> "z4",
|
||||
"forseral" -> "z5",
|
||||
"ceryshen" -> "z6",
|
||||
"esamir" -> "z7",
|
||||
"oshur" -> "z8",
|
||||
"searhus" -> "z9",
|
||||
"amerish" -> "z10",
|
||||
"nc-sanctuary" -> "home1",
|
||||
"tr-sanctuary" -> "home2",
|
||||
"vs-sanctuary" -> "home3",
|
||||
"tr-shooting" -> "tzshtr",
|
||||
"tr-driving" -> "tzdrtr",
|
||||
"tr-combat" -> "tzcotr",
|
||||
"vs-shooting" -> "tzshvs",
|
||||
"vs-driving" -> "tzdrvs",
|
||||
"vs-combat" -> "tzcovs",
|
||||
"nc-shooting" -> "tzshnc",
|
||||
"nc-driving" -> "tzdrnc",
|
||||
"nc-combat" -> "tzconc",
|
||||
"supai" -> "c1",
|
||||
"hunhau" -> "c2",
|
||||
"adlivun" -> "c3",
|
||||
"byblos" -> "c4",
|
||||
"annwn" -> "c5",
|
||||
"drugaskan" -> "c6",
|
||||
"nexus" -> "i4",
|
||||
"desolation" -> "i3",
|
||||
"ascension" -> "i2",
|
||||
"extinction" -> "i1",
|
||||
"Black_ops_hq" -> "homebo",
|
||||
"TR-Station" -> "station1",
|
||||
"NC-Station" -> "station2",
|
||||
"VS-Station" -> "station3"
|
||||
)
|
||||
|
||||
/**
|
||||
* A value used for selecting where to appear in a zone from the list of locations when the user has no indicated one.
|
||||
*/
|
||||
private val rand = Random
|
||||
setup()
|
||||
|
||||
/**
|
||||
* An abbreviated constructor for creating `CSRZone`s without invocation of `new`.
|
||||
* @param alias the common name of the zone
|
||||
* @param map the map name of the zone (this map is loaded)
|
||||
* @param zonename the zone's internal name
|
||||
*/
|
||||
def apply(alias: String, map: String, zonename: String): PointOfInterest = new PointOfInterest(alias, map, zonename)
|
||||
|
||||
/**
|
||||
* Get a valid `CSRZone`'s information.
|
||||
* @param zoneId a name that describes the zone and should be searchable
|
||||
* @return the `CSRZone`, or `None`
|
||||
*/
|
||||
def get(zoneId: String): Option[PointOfInterest] = {
|
||||
var zId = zoneId.toLowerCase
|
||||
if (alias.get(zId).isDefined)
|
||||
zId = alias(zId)
|
||||
zones.get(zId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a location within the `CSRZone`.
|
||||
* The location should be a facility or a warpgate or interesting.
|
||||
* @param zoneId the `CSRZone`
|
||||
* @param locationId a name that describes a known location in the provided `CSRZone` and is searchable
|
||||
* @return the coordinates of that location, or None
|
||||
*/
|
||||
def getWarpLocation(zoneId: String, locationId: String): Option[Vector3] = {
|
||||
get(zoneId) match {
|
||||
case Some(poi) =>
|
||||
poi.locations.get(locationId) match {
|
||||
case Some(position) => Some(position)
|
||||
case None => poi.gates.get(locationId)
|
||||
}
|
||||
case None => None
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the position of a warpgate within the zone.
|
||||
* @param zone the `CSRZone`
|
||||
* @param gateId a name that describes a known warpgate in the provided `CSRZone` and is searchable
|
||||
* @return the coordinates of that warpgate, or None
|
||||
*/
|
||||
def getWarpgate(zone: PointOfInterest, gateId: String): Option[Vector3] = {
|
||||
zone.gates.get(gateId.toLowerCase)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the names for all of the `CSRZones` that can be visited.
|
||||
* @return all of the zonenames
|
||||
*/
|
||||
def list: String = {
|
||||
"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"
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name for all of the locations that can be visited in this `CSRZone`, excluding warpgates.
|
||||
* @param zone the `CSRZone`
|
||||
* @return all of the location keys
|
||||
*/
|
||||
def listLocations(zone: PointOfInterest): String = {
|
||||
var out: String = "warps: "
|
||||
if (zone.locations.nonEmpty) {
|
||||
out += zone.locations.keys.toArray.sorted.mkString(", ")
|
||||
} else
|
||||
out = "none"
|
||||
out
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name for all of the warpgates that can be visited in this `CSRZone`.
|
||||
* @param zone the `CSRZone`
|
||||
* @return all of the warpgate keys
|
||||
*/
|
||||
def listWarpgates(zone: PointOfInterest): String = {
|
||||
var out: String = "gatenames: "
|
||||
if (zone.gates.isEmpty)
|
||||
out += "none"
|
||||
else
|
||||
out += zone.gates.keys.toArray.sorted.mkString(", ")
|
||||
out
|
||||
}
|
||||
|
||||
/**
|
||||
* Select, of all the `CSRZone` locations and warpgates, a pseudorandom destination to spawn the player in the zone if none has been specified.
|
||||
* @param zone the `CSRZone`
|
||||
* @return the coordinates of the spawn point
|
||||
*/
|
||||
def selectRandom(zone: PointOfInterest): Vector3 = {
|
||||
var outlets = zone.locations //random location?
|
||||
if (outlets.nonEmpty) {
|
||||
return outlets.values.toArray.apply(rand.nextInt(outlets.size))
|
||||
}
|
||||
outlets = zone.gates //random warpgate?
|
||||
if (outlets.nonEmpty) {
|
||||
return outlets.values.toArray.apply(rand.nextInt(outlets.size))
|
||||
}
|
||||
Vector3.Zero //fallback coordinates (that will always be valid)
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all zones with selected places of interest and the coordinates to place the player nearby that given place of interest.
|
||||
* All of these keys should be searchable under the `/warp` command.
|
||||
* Only the warpgate keys are searchable by the `/zone` command.
|
||||
*/
|
||||
def setup(): Unit = {
|
||||
zones("z1").gates ++= Map(
|
||||
"gate1" -> Vector3(4150, 7341, 82),
|
||||
"gate2" -> Vector3(5698, 3404, 129),
|
||||
"gate3" -> Vector3(2650, 5363, 176),
|
||||
"gate4" -> Vector3(3022, 1225, 66),
|
||||
"geowarp1" -> Vector3(3678, 2895, 108),
|
||||
"geowarp2" -> Vector3(5672, 4750, 70)
|
||||
)
|
||||
zones("z1").locations ++= Map(
|
||||
"amun" -> Vector3(4337, 2278, 68),
|
||||
"aton" -> Vector3(3772, 5463, 54),
|
||||
"bastet" -> Vector3(5412, 5588, 56),
|
||||
"hapi" -> Vector3(4256, 4436, 59),
|
||||
"horus" -> Vector3(3725, 2114, 73),
|
||||
"mont" -> Vector3(3354, 4205, 83),
|
||||
"seth" -> Vector3(4495, 6026, 58),
|
||||
"sobek" -> Vector3(3094, 3027, 75),
|
||||
"thoth" -> Vector3(4615, 3373, 53),
|
||||
"lake" -> Vector3(4317, 4008, 37),
|
||||
"monolith" -> Vector3(5551, 5047, 64)
|
||||
)
|
||||
zones("z2").gates ++= Map(
|
||||
"gate1" -> Vector3(1881, 4873, 19),
|
||||
"gate2" -> Vector3(4648, 4625, 28),
|
||||
"gate3" -> Vector3(3296, 2045, 21),
|
||||
"gate4" -> Vector3(5614, 1781, 32),
|
||||
"geowarp1" -> Vector3(5199, 4869, 39),
|
||||
"geowarp2" -> Vector3(3911, 2407, 15)
|
||||
)
|
||||
zones("z2").locations ++= Map(
|
||||
"acan" -> Vector3(3534, 4015, 30),
|
||||
"bitol" -> Vector3(4525, 2632, 30),
|
||||
"chac" -> Vector3(4111, 5950, 39),
|
||||
"ghanon" -> Vector3(2565, 3707, 41),
|
||||
"hurakan" -> Vector3(1840, 2934, 38),
|
||||
"ixtab" -> Vector3(3478, 3143, 40),
|
||||
"kisin" -> Vector3(3356, 5374, 31),
|
||||
"mulac" -> Vector3(5592, 2738, 37),
|
||||
"naum" -> Vector3(5390, 3454, 28),
|
||||
"voltan" -> Vector3(4529, 3414, 28),
|
||||
"zotz" -> Vector3(6677, 2342, 129),
|
||||
"monolith" -> Vector3(2938, 2485, 14)
|
||||
)
|
||||
zones("z3").gates ++= Map(
|
||||
"gate1" -> Vector3(2616, 6567, 58),
|
||||
"gate2" -> Vector3(6980, 5336, 57),
|
||||
"gate3" -> Vector3(1199, 1332, 66),
|
||||
"gate4" -> Vector3(5815, 1974, 63),
|
||||
"geowarp1" -> Vector3(2403, 4278, 60),
|
||||
"geowarp2" -> Vector3(4722, 2665, 78)
|
||||
)
|
||||
zones("z3").locations ++= Map(
|
||||
"aja" -> Vector3(754, 5435, 48),
|
||||
"chuku" -> Vector3(4208, 7021, 54),
|
||||
"bomazi" -> Vector3(1198, 4492, 58),
|
||||
"ekera" -> Vector3(5719, 6555, 51),
|
||||
"faro" -> Vector3(5030, 5700, 57),
|
||||
"gunuku" -> Vector3(4994, 4286, 54),
|
||||
"honsi" -> Vector3(4042, 4588, 89),
|
||||
"itan" -> Vector3(5175, 3393, 48),
|
||||
"kaang" -> Vector3(5813, 3862, 62),
|
||||
"leza" -> Vector3(2691, 1561, 64),
|
||||
"mukuru" -> Vector3(661, 2380, 54),
|
||||
"nzame" -> Vector3(1670, 2706, 45),
|
||||
"orisha" -> Vector3(7060, 1327, 59),
|
||||
"pamba" -> Vector3(7403, 3123, 63),
|
||||
"shango" -> Vector3(6846, 2319, 63),
|
||||
"tore" -> Vector3(3017, 2272, 58),
|
||||
"wele" -> Vector3(436, 7040, 60),
|
||||
"monolith" -> Vector3(4515, 4105, 38),
|
||||
"peak" -> Vector3(3215, 5063, 579)
|
||||
)
|
||||
zones("z4").gates ++= Map(
|
||||
"gate1" -> Vector3(4702, 6768, 30),
|
||||
"gate2" -> Vector3(5515, 3368, 69),
|
||||
"gate3" -> Vector3(1564, 3356, 46),
|
||||
"gate4" -> Vector3(3889, 1118, 56),
|
||||
"geowarp1" -> Vector3(4202, 4325, 68),
|
||||
"geowarp2" -> Vector3(2384, 1925, 37)
|
||||
)
|
||||
zones("z4").locations ++= Map(
|
||||
"akkan" -> Vector3(2746, 4260, 39),
|
||||
"baal" -> Vector3(825, 5470, 72),
|
||||
"dagon" -> Vector3(1739, 5681, 40),
|
||||
"enkidu" -> Vector3(3217, 3574, 37),
|
||||
"girru" -> Vector3(4475, 5853, 78),
|
||||
"hanish" -> Vector3(3794, 5540, 89),
|
||||
"irkalla" -> Vector3(4742, 5270, 66),
|
||||
"kusag" -> Vector3(6532, 4692, 46),
|
||||
"lahar" -> Vector3(6965, 5306, 38),
|
||||
"marduk" -> Vector3(3059, 2144, 70),
|
||||
"neti" -> Vector3(3966, 2417, 80),
|
||||
"zaqar" -> Vector3(4796, 2177, 75),
|
||||
"monolith" -> Vector3(5165, 4083, 35),
|
||||
"stonehenge" -> Vector3(4992, 3776, 56)
|
||||
)
|
||||
zones("z5").gates ++= Map(
|
||||
"gate1" -> Vector3(3432, 6498, 73),
|
||||
"gate2" -> Vector3(7196, 3917, 47),
|
||||
"gate3" -> Vector3(1533, 3540, 56),
|
||||
"gate4" -> Vector3(3197, 1390, 45),
|
||||
"geowarp1" -> Vector3(4899, 5633, 38),
|
||||
"geowarp2" -> Vector3(5326, 2558, 54)
|
||||
)
|
||||
zones("z5").locations ++= Map(
|
||||
"anu" -> Vector3(3479, 2556, 56),
|
||||
"bel" -> Vector3(3665, 4626, 58),
|
||||
"caer" -> Vector3(4570, 2601, 56),
|
||||
"dagda" -> Vector3(5825, 4449, 55),
|
||||
"eadon" -> Vector3(2725, 2853, 53),
|
||||
"gwydion" -> Vector3(5566, 3739, 61),
|
||||
"lugh" -> Vector3(6083, 5069, 72),
|
||||
"neit" -> Vector3(4345, 4319, 76),
|
||||
"ogma" -> Vector3(3588, 3227, 114),
|
||||
"pwyll" -> Vector3(4683, 4764, 104),
|
||||
"monolith" -> Vector3(3251, 3245, 160),
|
||||
"islands1" -> Vector3(6680, 6217, 125),
|
||||
"islands2" -> Vector3(1059, 6213, 120)
|
||||
)
|
||||
zones("z6").gates ++= Map(
|
||||
"gate1" -> Vector3(5040, 4327, 46),
|
||||
"gate2" -> Vector3(2187, 5338, 30),
|
||||
"gate3" -> Vector3(4960, 1922, 15),
|
||||
"gate4" -> Vector3(2464, 3088, 189),
|
||||
"geowarp1" -> Vector3(3221, 5328, 242),
|
||||
"geowarp2" -> Vector3(2237, 1783, 238)
|
||||
)
|
||||
zones("z6").locations ++= Map(
|
||||
"akna" -> Vector3(4509, 3732, 219),
|
||||
"anguta" -> Vector3(3999, 4170, 266),
|
||||
"igaluk" -> Vector3(3241, 5658, 235),
|
||||
"keelut" -> Vector3(3630, 1904, 265),
|
||||
"nerrivik" -> Vector3(3522, 3703, 322),
|
||||
"pinga" -> Vector3(5938, 3545, 96),
|
||||
"sedna" -> Vector3(3932, 5160, 232),
|
||||
"tarqaq" -> Vector3(2980, 2155, 237),
|
||||
"tootega" -> Vector3(5171, 3251, 217),
|
||||
"monolith" -> Vector3(4011, 4851, 32),
|
||||
"bridge" -> Vector3(3729, 4859, 234)
|
||||
)
|
||||
zones("z7").gates ++= Map(
|
||||
"gate1" -> Vector3(1516, 6448, 61),
|
||||
"gate2" -> Vector3(5249, 3819, 69),
|
||||
"gate3" -> Vector3(2763, 2961, 86),
|
||||
"gate4" -> Vector3(6224, 1152, 78),
|
||||
"geowarp1" -> Vector3(6345, 4802, 90),
|
||||
"geowarp2" -> Vector3(3800, 2197, 64)
|
||||
)
|
||||
zones("z7").locations ++= Map(
|
||||
"andvari" -> Vector3(3233, 7207, 78),
|
||||
"dagur" -> Vector3(4026, 6191, 60),
|
||||
"eisa" -> Vector3(3456, 4513, 75),
|
||||
"freyr" -> Vector3(2853, 3840, 56),
|
||||
"gjallar" -> Vector3(1056, 2656, 74),
|
||||
"helheim" -> Vector3(5542, 2532, 53),
|
||||
"jarl" -> Vector3(1960, 5462, 68),
|
||||
"kvasir" -> Vector3(4096, 1571, 69),
|
||||
"mani" -> Vector3(5057, 4989, 58),
|
||||
"nott" -> Vector3(6783, 4329, 46),
|
||||
"ran" -> Vector3(2378, 1919, 85),
|
||||
"vidar" -> Vector3(3772, 3024, 67),
|
||||
"ymir" -> Vector3(1911, 4008, 69),
|
||||
"monolith" -> Vector3(6390, 1622, 63)
|
||||
)
|
||||
zones("z8").gates ++= Map(
|
||||
"gate1" -> Vector3(5437, 5272, 32),
|
||||
"gate2" -> Vector3(3251, 5650, 60),
|
||||
"gate3" -> Vector3(5112, 2616, 40),
|
||||
"gate4" -> Vector3(2666, 1665, 45),
|
||||
"geowarp1" -> Vector3(3979, 5370, 47),
|
||||
"geowarp2" -> Vector3(6018, 3136, 35)
|
||||
)
|
||||
zones("z8").locations ++= Map(
|
||||
"atar" -> Vector3(3609, 2730, 47),
|
||||
"dahaka" -> Vector3(4633, 5379, 54),
|
||||
"hvar" -> Vector3(3857, 4764, 49),
|
||||
"izha" -> Vector3(5396, 3852, 51),
|
||||
"jamshid" -> Vector3(2371, 3378, 52),
|
||||
"mithra" -> Vector3(2480, 4456, 44),
|
||||
"rashnu" -> Vector3(3098, 3961, 59),
|
||||
"yazata" -> Vector3(4620, 3983, 62),
|
||||
"zal" -> Vector3(3966, 2164, 61),
|
||||
"arch1" -> Vector3(4152, 3285, 31),
|
||||
"arch2" -> Vector3(4688, 5272, 68),
|
||||
"pride" -> Vector3(2913, 4412, 63)
|
||||
)
|
||||
zones("z9").gates ++= Map(
|
||||
"gate1" -> Vector3(1505, 6981, 65),
|
||||
"gate2" -> Vector3(6835, 3517, 56),
|
||||
"gate3" -> Vector3(1393, 1376, 53),
|
||||
"geowarp1" -> Vector3(7081, 5552, 46),
|
||||
"geowarp2" -> Vector3(3776, 1092, 49)
|
||||
)
|
||||
zones("z9").locations ++= Map(
|
||||
"akua" -> Vector3(5258, 4041, 346),
|
||||
"drakulu" -> Vector3(3806, 2647, 151),
|
||||
"hiro" -> Vector3(4618, 5761, 190),
|
||||
"iva" -> Vector3(6387, 5199, 55),
|
||||
"karihi" -> Vector3(3879, 5574, 236),
|
||||
"laka" -> Vector3(4720, 6718, 49),
|
||||
"matagi" -> Vector3(5308, 5093, 239),
|
||||
"ngaru" -> Vector3(4103, 4077, 205),
|
||||
"oro" -> Vector3(4849, 4456, 208),
|
||||
"pele" -> Vector3(4549, 3712, 208),
|
||||
"rehua" -> Vector3(3843, 2195, 60),
|
||||
"sina" -> Vector3(5919, 2177, 91),
|
||||
"tara" -> Vector3(1082, 4225, 60),
|
||||
"wakea" -> Vector3(1785, 5241, 63),
|
||||
"monolith" -> Vector3(3246, 6507, 105)
|
||||
)
|
||||
zones("z10").gates ++= Map(
|
||||
"gate1" -> Vector3(6140, 6599, 71),
|
||||
"gate2" -> Vector3(4814, 4608, 59),
|
||||
"gate3" -> Vector3(3152, 3480, 54),
|
||||
"gate4" -> Vector3(1605, 1446, 40),
|
||||
"geowarp1" -> Vector3(3612, 6918, 38),
|
||||
"geowarp2" -> Vector3(3668, 3327, 55)
|
||||
)
|
||||
zones("z10").locations ++= Map(
|
||||
"azeban" -> Vector3(6316, 5160, 62),
|
||||
"cetan" -> Vector3(3587, 2522, 48),
|
||||
"heyoka" -> Vector3(4395, 2327, 47),
|
||||
"ikanam" -> Vector3(2740, 2412, 57),
|
||||
"kyoi" -> Vector3(5491, 2284, 62),
|
||||
"mekala" -> Vector3(6087, 2925, 59),
|
||||
"onatha" -> Vector3(3397, 5799, 48),
|
||||
"qumu" -> Vector3(3990, 5152, 46),
|
||||
"sungrey" -> Vector3(4609, 5624, 72),
|
||||
"tumas" -> Vector3(4687, 6392, 69),
|
||||
"verica" -> Vector3(4973, 3459, 47),
|
||||
"xelas" -> Vector3(6609, 4479, 56),
|
||||
"monolith" -> Vector3(5651, 6024, 38)
|
||||
)
|
||||
zones("home1").gates ++= Map(
|
||||
"gate1" -> Vector3(4158, 6344, 44),
|
||||
"gate2" -> Vector3(2214, 5797, 48),
|
||||
"gate3" -> Vector3(5032, 3241, 53)
|
||||
)
|
||||
zones("home1").locations += "hart_c" -> Vector3(2352, 5523, 66)
|
||||
zones("home2").gates ++= Map(
|
||||
"gate1" -> Vector3(5283, 4317, 44),
|
||||
"gate2" -> Vector3(3139, 4809, 40),
|
||||
"gate3" -> Vector3(3659, 2894, 26)
|
||||
)
|
||||
zones("home2").locations += "hart_c" -> Vector3(3125, 2864, 35)
|
||||
zones("home3").gates ++= Map(
|
||||
"gate1" -> Vector3(5657, 4681, 98),
|
||||
"gate2" -> Vector3(2639, 5366, 57),
|
||||
"gate3" -> Vector3(4079, 2467, 155)
|
||||
)
|
||||
zones("home3").locations += "hart_c" -> Vector3(3675, 2727, 91)
|
||||
zones("tzshtr").locations += "roof" -> Vector3(499, 1568, 25)
|
||||
zones("tzcotr").locations += "spawn" -> Vector3(960, 1002, 32)
|
||||
zones("tzdrtr").locations ++= Map(
|
||||
"start" -> Vector3(2457, 1864, 23),
|
||||
"air_pad" -> Vector3(1700, 1900, 32)
|
||||
)
|
||||
zones("tzshvs").locations += "roof" -> Vector3(499, 1568, 25)
|
||||
zones("tzcovs").locations += "spawn" -> Vector3(960, 1002, 32)
|
||||
zones("tzdrvs").locations ++= Map(
|
||||
"start" -> Vector3(2457, 1864, 23),
|
||||
"air_pad" -> Vector3(1700, 1900, 32)
|
||||
)
|
||||
zones("tzshnc").locations += "roof" -> Vector3(499, 1568, 25)
|
||||
zones("tzconc").locations += "spawn" -> Vector3(960, 1002, 32)
|
||||
zones("tzdrnc").locations ++= Map(
|
||||
"start" -> Vector3(2457, 1864, 23),
|
||||
"air_pad" -> Vector3(1700, 1900, 32)
|
||||
)
|
||||
zones("c1").gates ++= Map(
|
||||
"geowarp1" -> Vector3(998, 2038, 103),
|
||||
"geowarp2" -> Vector3(231, 1026, 82),
|
||||
"geowarp3" -> Vector3(2071, 1405, 102),
|
||||
"geowarp4" -> Vector3(1051, 370, 103)
|
||||
)
|
||||
zones("c2").gates ++= Map(
|
||||
"geowarp1" -> Vector3(999, 2386, 243),
|
||||
"geowarp2" -> Vector3(283, 1249, 172),
|
||||
"geowarp3" -> Vector3(1887, 1307, 192),
|
||||
"geowarp4" -> Vector3(1039, 155, 143)
|
||||
)
|
||||
zones("c3").gates ++= Map(
|
||||
"geowarp1" -> Vector3(1095, 1725, 25),
|
||||
"geowarp2" -> Vector3(226, 832, 42),
|
||||
"geowarp3" -> Vector3(1832, 1026, 43),
|
||||
"geowarp4" -> Vector3(981, 320, 46)
|
||||
)
|
||||
zones("c4").gates ++= Map(
|
||||
"geowarp1" -> Vector3(902, 1811, 93),
|
||||
"geowarp2" -> Vector3(185, 922, 113),
|
||||
"geowarp3" -> Vector3(1696, 1188, 92),
|
||||
"geowarp4" -> Vector3(887, 227, 115)
|
||||
)
|
||||
zones("c5").gates ++= Map(
|
||||
"geowarp1" -> Vector3(1195, 1752, 244),
|
||||
"geowarp2" -> Vector3(290, 1104, 235),
|
||||
"geowarp3" -> Vector3(1803, 899, 243),
|
||||
"geowarp4" -> Vector3(1042, 225, 246)
|
||||
)
|
||||
zones("c6").gates ++= Map(
|
||||
"geowarp1" -> Vector3(1067, 2044, 95),
|
||||
"geowarp2" -> Vector3(290, 693, 73),
|
||||
"geowarp3" -> Vector3(1922, 928, 33),
|
||||
"geowarp4" -> Vector3(1174, 249, 114)
|
||||
)
|
||||
zones("i3").gates ++= Map(
|
||||
"gate1" -> Vector3(1219, 2580, 30),
|
||||
"gate2" -> Vector3(2889, 2919, 33),
|
||||
"gate3" -> Vector3(2886, 1235, 32)
|
||||
)
|
||||
zones("i3").locations ++= Map(
|
||||
"dahaka" -> Vector3(1421, 2216, 30),
|
||||
"jamshid" -> Vector3(2500, 2543, 30),
|
||||
"izha" -> Vector3(2569, 1544, 30),
|
||||
"oasis" -> Vector3(2084, 1935, 40)
|
||||
)
|
||||
zones("i2").gates ++= Map(
|
||||
"gate1" -> Vector3(1243, 1393, 12),
|
||||
"gate2" -> Vector3(2510, 2544, 12),
|
||||
"gate3" -> Vector3(2634, 1477, 12)
|
||||
)
|
||||
zones("i2").locations ++= Map(
|
||||
"rashnu" -> Vector3(1709, 1802, 91),
|
||||
"sraosha" -> Vector3(2729, 2349, 91),
|
||||
"zal" -> Vector3(1888, 2728, 91),
|
||||
"center" -> Vector3(2082, 2192, 160),
|
||||
"vpad" -> Vector3(1770, 2686, 92)
|
||||
)
|
||||
zones("i1").gates ++= Map(
|
||||
"gate1" -> Vector3(1225, 2036, 67),
|
||||
"gate2" -> Vector3(2548, 2801, 65),
|
||||
"gate3" -> Vector3(2481, 1194, 89)
|
||||
)
|
||||
zones("i1").locations ++= Map(
|
||||
"hvar" -> Vector3(1559, 1268, 88),
|
||||
"mithra" -> Vector3(2855, 2850, 89),
|
||||
"yazata" -> Vector3(1254, 2583, 88),
|
||||
"south_of_volcano" -> Vector3(2068, 1686, 88)
|
||||
)
|
||||
zones("i4").gates ++= Map(
|
||||
"gate1" -> Vector3(2359, 2717, 36),
|
||||
"gate2" -> Vector3(2732, 1355, 36),
|
||||
"geowarp" -> Vector3(1424, 1640, 45)
|
||||
)
|
||||
zones("i4").locations += "atar" -> Vector3(1915, 1936, 43)
|
||||
}
|
||||
}
|
||||
65
common/src/main/scala/net/psforever/zones/Maps.scala
Normal file
65
common/src/main/scala/net/psforever/zones/Maps.scala
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
package net.psforever.zones
|
||||
|
||||
import net.psforever.objects.LocalProjectile
|
||||
import net.psforever.objects.ballistics.Projectile
|
||||
import net.psforever.objects.zones.ZoneMap
|
||||
import net.psforever.zones.zonemaps._
|
||||
|
||||
import scala.concurrent.Future
|
||||
import scala.util.{Failure, Success}
|
||||
|
||||
object Maps {
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
|
||||
val map01 = InitZoneMap(Future { Map01.ZoneMap })
|
||||
val map02 = InitZoneMap(Future { Map02.ZoneMap })
|
||||
val map03 = InitZoneMap(Future { Map03.ZoneMap })
|
||||
val map04 = InitZoneMap(Future { Map04.ZoneMap })
|
||||
val map05 = InitZoneMap(Future { Map05.ZoneMap })
|
||||
val map06 = InitZoneMap(Future { Map06.ZoneMap })
|
||||
val map07 = InitZoneMap(Future { Map07.ZoneMap })
|
||||
val map08 = InitZoneMap(Future { Map08.ZoneMap })
|
||||
val map09 = InitZoneMap(Future { Map09.ZoneMap })
|
||||
val map10 = InitZoneMap(Future { Map10.ZoneMap })
|
||||
val map11 = InitZoneMap(Future { Map11.ZoneMap })
|
||||
val map12 = InitZoneMap(Future { Map12.ZoneMap })
|
||||
val map13 = InitZoneMap(Future { Map13.ZoneMap })
|
||||
|
||||
val map14 = new ZoneMap("map14") {
|
||||
Projectiles(this)
|
||||
}
|
||||
|
||||
val map15 = new ZoneMap("map15") {
|
||||
Projectiles(this)
|
||||
}
|
||||
|
||||
val map16 = new ZoneMap("map16") {
|
||||
Projectiles(this)
|
||||
}
|
||||
|
||||
val ugd01 = InitZoneMap(Future { Ugd01.ZoneMap })
|
||||
val ugd02 = InitZoneMap(Future { Ugd02.ZoneMap })
|
||||
val ugd03 = InitZoneMap(Future { Ugd03.ZoneMap })
|
||||
val ugd04 = InitZoneMap(Future { Ugd04.ZoneMap })
|
||||
val ugd05 = InitZoneMap(Future { Ugd05.ZoneMap })
|
||||
val ugd06 = InitZoneMap(Future { Ugd06.ZoneMap })
|
||||
|
||||
val map96 = InitZoneMap(Future { Map96.ZoneMap })
|
||||
val map97 = InitZoneMap(Future { Map97.ZoneMap })
|
||||
val map98 = InitZoneMap(Future { Map98.ZoneMap })
|
||||
val map99 = InitZoneMap(Future { Map99.ZoneMap })
|
||||
|
||||
def Projectiles(zmap: ZoneMap): Unit = {
|
||||
(Projectile.BaseUID until Projectile.RangeUID) foreach {
|
||||
zmap.LocalObject(_, LocalProjectile.Constructor)
|
||||
}
|
||||
}
|
||||
|
||||
def InitZoneMap(future: Future[ZoneMap]): Future[ZoneMap] = {
|
||||
future onComplete {
|
||||
case Success(x) => Projectiles(x)
|
||||
case Failure(_) => throw new RuntimeException("Maps: failure when setting up map") //should not fail?
|
||||
}
|
||||
future
|
||||
}
|
||||
}
|
||||
572
common/src/main/scala/net/psforever/zones/Zones.scala
Normal file
572
common/src/main/scala/net/psforever/zones/Zones.scala
Normal file
|
|
@ -0,0 +1,572 @@
|
|||
package net.psforever.zones
|
||||
|
||||
import akka.actor.ActorContext
|
||||
import net.psforever.objects.GlobalDefinitions
|
||||
import net.psforever.objects.serverobject.resourcesilo.ResourceSilo
|
||||
import net.psforever.objects.serverobject.structures.WarpGate
|
||||
import net.psforever.objects.zones.Zone
|
||||
import net.psforever.types.PlanetSideEmpire
|
||||
import scala.concurrent.duration._
|
||||
import scala.collection.immutable.HashMap
|
||||
import scala.concurrent.Await
|
||||
|
||||
object Zones {
|
||||
val zones: HashMap[String, Zone] = HashMap(
|
||||
(
|
||||
"z1",
|
||||
new Zone("z1", Await.result(Maps.map01, 30 seconds), 1) {
|
||||
override def Init(implicit context: ActorContext): Unit = {
|
||||
super.Init(context)
|
||||
HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
|
||||
HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
|
||||
|
||||
InitZoneAmenities(zone = this)
|
||||
}
|
||||
}
|
||||
),
|
||||
(
|
||||
"z2",
|
||||
new Zone("z2", Await.result(Maps.map02, 30 seconds), 2) {
|
||||
override def Init(implicit context: ActorContext): Unit = {
|
||||
super.Init(context)
|
||||
HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
|
||||
HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
|
||||
|
||||
InitZoneAmenities(zone = this)
|
||||
}
|
||||
}
|
||||
),
|
||||
(
|
||||
"z3",
|
||||
new Zone("z3", Await.result(Maps.map03, 30 seconds), 3) {
|
||||
override def Init(implicit context: ActorContext): Unit = {
|
||||
super.Init(context)
|
||||
HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
|
||||
HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
|
||||
|
||||
InitZoneAmenities(zone = this)
|
||||
}
|
||||
}
|
||||
),
|
||||
(
|
||||
"z4",
|
||||
new Zone("z4", Await.result(Maps.map04, 30 seconds), 4) {
|
||||
override def Init(implicit context: ActorContext): Unit = {
|
||||
super.Init(context)
|
||||
HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
|
||||
HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
|
||||
|
||||
InitZoneAmenities(zone = this)
|
||||
|
||||
BuildingByMapId(5).get.Faction = PlanetSideEmpire.TR //Akkan
|
||||
BuildingByMapId(6).get.Faction = PlanetSideEmpire.TR //Baal
|
||||
BuildingByMapId(7).get.Faction = PlanetSideEmpire.TR //Dagon
|
||||
BuildingByMapId(8).get.Faction = PlanetSideEmpire.NC //Enkidu
|
||||
BuildingByMapId(9).get.Faction = PlanetSideEmpire.VS //Girru
|
||||
BuildingByMapId(10).get.Faction = PlanetSideEmpire.VS //Hanish
|
||||
BuildingByMapId(11).get.Faction = PlanetSideEmpire.VS //Irkalla
|
||||
BuildingByMapId(12).get.Faction = PlanetSideEmpire.VS //Kusag
|
||||
BuildingByMapId(13).get.Faction = PlanetSideEmpire.VS //Lahar
|
||||
BuildingByMapId(14).get.Faction = PlanetSideEmpire.NC //Marduk
|
||||
BuildingByMapId(15).get.Faction = PlanetSideEmpire.NC //Neti
|
||||
BuildingByMapId(16).get.Faction = PlanetSideEmpire.NC //Zaqar
|
||||
BuildingByMapId(17).get.Faction = PlanetSideEmpire.NC //S_Marduk_Tower
|
||||
BuildingByMapId(18).get.Faction = PlanetSideEmpire.NC //W_Neti_Tower
|
||||
BuildingByMapId(19).get.Faction = PlanetSideEmpire.NC //W_Zaqar_Tower
|
||||
BuildingByMapId(20).get.Faction = PlanetSideEmpire.NC //E_Zaqar_Tower
|
||||
BuildingByMapId(21).get.Faction = PlanetSideEmpire.NC //NE_Neti_Tower
|
||||
BuildingByMapId(22).get.Faction = PlanetSideEmpire.NC //SE_Ceryshen_Warpgate_Tower
|
||||
BuildingByMapId(23).get.Faction = PlanetSideEmpire.VS //S_Kusag_Tower
|
||||
BuildingByMapId(24).get.Faction = PlanetSideEmpire.VS //NW_Kusag_Tower
|
||||
BuildingByMapId(25).get.Faction = PlanetSideEmpire.VS //N_Ceryshen_Warpgate_Tower
|
||||
BuildingByMapId(26).get.Faction = PlanetSideEmpire.VS //SE_Irkalla_Tower
|
||||
BuildingByMapId(27).get.Faction = PlanetSideEmpire.VS //S_Irkalla_Tower
|
||||
BuildingByMapId(28).get.Faction = PlanetSideEmpire.TR //NE_Enkidu_Tower
|
||||
BuildingByMapId(29).get.Faction = PlanetSideEmpire.NC //SE_Akkan_Tower
|
||||
BuildingByMapId(30).get.Faction = PlanetSideEmpire.NC //SW_Enkidu_Tower
|
||||
BuildingByMapId(31).get.Faction = PlanetSideEmpire.TR //E_Searhus_Warpgate_Tower
|
||||
BuildingByMapId(32).get.Faction = PlanetSideEmpire.TR //N_Searhus_Warpgate_Tower
|
||||
BuildingByMapId(33).get.Faction = PlanetSideEmpire.VS //E_Girru_Tower
|
||||
BuildingByMapId(34).get.Faction = PlanetSideEmpire.VS //SE_Hanish_Tower
|
||||
BuildingByMapId(35).get.Faction = PlanetSideEmpire.TR //SW_Hanish_Tower
|
||||
BuildingByMapId(36).get.Faction = PlanetSideEmpire.VS //W_Girru_Tower
|
||||
BuildingByMapId(37).get.Faction = PlanetSideEmpire.TR //E_Dagon_Tower
|
||||
BuildingByMapId(38).get.Faction = PlanetSideEmpire.TR //NE_Baal_Tower
|
||||
BuildingByMapId(39).get.Faction = PlanetSideEmpire.TR //SE_Baal_Tower
|
||||
BuildingByMapId(40).get.Faction = PlanetSideEmpire.TR //S_Dagon_Tower
|
||||
BuildingByMapId(41).get.Faction = PlanetSideEmpire.NC //W_Ceryshen_Warpgate_Tower
|
||||
BuildingByMapId(42).get.Faction = PlanetSideEmpire.NEUTRAL //dagon bunker
|
||||
BuildingByMapId(43).get.Faction = PlanetSideEmpire.NEUTRAL //Akkan North Bunker
|
||||
BuildingByMapId(44).get.Faction = PlanetSideEmpire.NEUTRAL //Enkidu East Bunker
|
||||
BuildingByMapId(45).get.Faction = PlanetSideEmpire.NEUTRAL //Neti bunker
|
||||
BuildingByMapId(46).get.Faction = PlanetSideEmpire.NEUTRAL //Hanish West Bunker
|
||||
BuildingByMapId(47).get.Faction = PlanetSideEmpire.NEUTRAL //Irkalla East Bunker
|
||||
BuildingByMapId(48).get.Faction = PlanetSideEmpire.NEUTRAL //Zaqar bunker
|
||||
BuildingByMapId(49).get.Faction = PlanetSideEmpire.NEUTRAL //Kusag West Bunker
|
||||
BuildingByMapId(50).get.Faction = PlanetSideEmpire.NEUTRAL //marduk bunker
|
||||
BuildingByMapId(51).get.Faction = PlanetSideEmpire.TR //baal bunker
|
||||
BuildingByMapId(52).get.Faction = PlanetSideEmpire.NEUTRAL //girru bunker
|
||||
BuildingByMapId(53).get.Faction = PlanetSideEmpire.NEUTRAL //lahar bunker
|
||||
BuildingByMapId(54).get.Faction = PlanetSideEmpire.NEUTRAL //akkan bunker
|
||||
BuildingByMapId(55).get.Faction = PlanetSideEmpire.VS //Irkalla_Tower
|
||||
BuildingByMapId(56).get.Faction = PlanetSideEmpire.VS //Hanish_Tower
|
||||
BuildingByMapId(57).get.Faction = PlanetSideEmpire.VS //E_Ceryshen_Warpgate_Tower
|
||||
BuildingByMapId(58).get.Faction = PlanetSideEmpire.VS //Lahar_Tower
|
||||
BuildingByMapId(59).get.Faction = PlanetSideEmpire.VS //VSSanc_Warpgate_Tower
|
||||
BuildingByMapId(60).get.Faction = PlanetSideEmpire.TR //Akkan_Tower
|
||||
BuildingByMapId(61).get.Faction = PlanetSideEmpire.NC //TRSanc_Warpgate_Tower
|
||||
BuildingByMapId(62).get.Faction = PlanetSideEmpire.NC //Marduk_Tower
|
||||
BuildingByMapId(63).get.Faction = PlanetSideEmpire.TR //NW_Dagon_Tower
|
||||
BuildingByMapId(64).get.Faction = PlanetSideEmpire.NEUTRAL //E7 East Bunker (at north from bridge)
|
||||
BuildingByMapId(65).get.Faction = PlanetSideEmpire.VS //W_Hanish_Tower
|
||||
}
|
||||
}
|
||||
),
|
||||
(
|
||||
"z5",
|
||||
new Zone("z5", Await.result(Maps.map05, 30 seconds), 5) {
|
||||
override def Init(implicit context: ActorContext): Unit = {
|
||||
super.Init(context)
|
||||
HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
|
||||
HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
|
||||
|
||||
InitZoneAmenities(zone = this)
|
||||
}
|
||||
}
|
||||
),
|
||||
(
|
||||
"z6",
|
||||
new Zone("z6", Await.result(Maps.map06, 30 seconds), 6) {
|
||||
override def Init(implicit context: ActorContext): Unit = {
|
||||
super.Init(context)
|
||||
HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
|
||||
HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
|
||||
|
||||
import net.psforever.types.PlanetSideEmpire
|
||||
BuildingByMapId(2).get.Faction = PlanetSideEmpire.VS
|
||||
BuildingByMapId(48).get.Faction = PlanetSideEmpire.VS
|
||||
BuildingByMapId(49).get.Faction = PlanetSideEmpire.VS
|
||||
|
||||
InitZoneAmenities(zone = this)
|
||||
}
|
||||
}
|
||||
),
|
||||
(
|
||||
"z7",
|
||||
new Zone("z7", Await.result(Maps.map07, 30 seconds), 7) {
|
||||
override def Init(implicit context: ActorContext): Unit = {
|
||||
super.Init(context)
|
||||
HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
|
||||
HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
|
||||
|
||||
InitZoneAmenities(zone = this)
|
||||
}
|
||||
}
|
||||
),
|
||||
(
|
||||
"z8",
|
||||
new Zone("z8", Await.result(Maps.map08, 30 seconds), 8) {
|
||||
override def Init(implicit context: ActorContext): Unit = {
|
||||
super.Init(context)
|
||||
HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
|
||||
HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
|
||||
|
||||
InitZoneAmenities(zone = this)
|
||||
}
|
||||
}
|
||||
),
|
||||
(
|
||||
"z9",
|
||||
new Zone("z9", Await.result(Maps.map09, 30 seconds), 9) {
|
||||
override def Init(implicit context: ActorContext): Unit = {
|
||||
super.Init(context)
|
||||
HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
|
||||
HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
|
||||
|
||||
InitZoneAmenities(zone = this)
|
||||
}
|
||||
}
|
||||
),
|
||||
(
|
||||
"z10",
|
||||
new Zone("z10", Await.result(Maps.map10, 30 seconds), 10) {
|
||||
override def Init(implicit context: ActorContext): Unit = {
|
||||
super.Init(context)
|
||||
HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
|
||||
HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
|
||||
|
||||
InitZoneAmenities(zone = this)
|
||||
}
|
||||
}
|
||||
),
|
||||
(
|
||||
"home1",
|
||||
new Zone("home1", Await.result(Maps.map11, 30 seconds), 11) {
|
||||
override def Init(implicit context: ActorContext): Unit = {
|
||||
super.Init(context)
|
||||
|
||||
import net.psforever.types.PlanetSideEmpire
|
||||
Buildings.values.foreach { _.Faction = PlanetSideEmpire.NC }
|
||||
|
||||
InitZoneAmenities(zone = this)
|
||||
}
|
||||
}
|
||||
),
|
||||
(
|
||||
"home2",
|
||||
new Zone("home2", Await.result(Maps.map12, 30 seconds), 12) {
|
||||
override def Init(implicit context: ActorContext): Unit = {
|
||||
super.Init(context)
|
||||
|
||||
import net.psforever.types.PlanetSideEmpire
|
||||
Buildings.values.foreach { _.Faction = PlanetSideEmpire.TR }
|
||||
|
||||
InitZoneAmenities(zone = this)
|
||||
}
|
||||
}
|
||||
),
|
||||
(
|
||||
"home3",
|
||||
new Zone("home3", Await.result(Maps.map13, 30 seconds), 13) {
|
||||
override def Init(implicit context: ActorContext): Unit = {
|
||||
super.Init(context)
|
||||
|
||||
import net.psforever.types.PlanetSideEmpire
|
||||
Buildings.values.foreach { _.Faction = PlanetSideEmpire.VS }
|
||||
|
||||
InitZoneAmenities(zone = this)
|
||||
}
|
||||
}
|
||||
),
|
||||
(
|
||||
"tzshtr",
|
||||
new Zone("tzshtr", Maps.map14, 14)
|
||||
),
|
||||
(
|
||||
"tzdrtr",
|
||||
new Zone("tzsdrtr", Maps.map15, 15)
|
||||
),
|
||||
(
|
||||
"tzcotr",
|
||||
new Zone("tzcotr", Maps.map16, 16)
|
||||
),
|
||||
(
|
||||
"tzshnc",
|
||||
new Zone("tzshnc", Maps.map14, 17)
|
||||
),
|
||||
(
|
||||
"tzdrnc",
|
||||
new Zone("tzdrnc", Maps.map15, 18)
|
||||
),
|
||||
(
|
||||
"tzconc",
|
||||
new Zone("tzconc", Maps.map16, 19)
|
||||
),
|
||||
(
|
||||
"tzshvs",
|
||||
new Zone("tzshvs", Maps.map14, 20)
|
||||
),
|
||||
(
|
||||
"tzdrvs",
|
||||
new Zone("tzdrvs", Maps.map15, 21)
|
||||
),
|
||||
(
|
||||
"tzcovs",
|
||||
new Zone("tzcovs", Maps.map16, 22)
|
||||
),
|
||||
(
|
||||
"c1",
|
||||
new Zone("c1", Await.result(Maps.ugd01, 30 seconds), 23) {
|
||||
override def Init(implicit context: ActorContext): Unit = {
|
||||
super.Init(context)
|
||||
HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
|
||||
HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
|
||||
|
||||
InitZoneAmenities(zone = this)
|
||||
}
|
||||
}
|
||||
),
|
||||
(
|
||||
"c2",
|
||||
new Zone("c2", Await.result(Maps.ugd02, 30 seconds), 24) {
|
||||
override def Init(implicit context: ActorContext): Unit = {
|
||||
super.Init(context)
|
||||
HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
|
||||
HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
|
||||
|
||||
InitZoneAmenities(zone = this)
|
||||
}
|
||||
}
|
||||
),
|
||||
(
|
||||
"c3",
|
||||
new Zone("c3", Await.result(Maps.ugd03, 30 seconds), 25) {
|
||||
override def Init(implicit context: ActorContext): Unit = {
|
||||
super.Init(context)
|
||||
HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
|
||||
HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
|
||||
|
||||
InitZoneAmenities(zone = this)
|
||||
}
|
||||
}
|
||||
),
|
||||
(
|
||||
"c4",
|
||||
new Zone("c4", Await.result(Maps.ugd04, 30 seconds), 26) {
|
||||
override def Init(implicit context: ActorContext): Unit = {
|
||||
super.Init(context)
|
||||
HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
|
||||
HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
|
||||
|
||||
InitZoneAmenities(zone = this)
|
||||
}
|
||||
}
|
||||
),
|
||||
(
|
||||
"c5",
|
||||
new Zone("c5", Await.result(Maps.ugd05, 30 seconds), 27) {
|
||||
override def Init(implicit context: ActorContext): Unit = {
|
||||
super.Init(context)
|
||||
HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
|
||||
HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
|
||||
|
||||
InitZoneAmenities(zone = this)
|
||||
}
|
||||
}
|
||||
),
|
||||
(
|
||||
"c6",
|
||||
new Zone("c6", Await.result(Maps.ugd06, 30 seconds), 28) {
|
||||
override def Init(implicit context: ActorContext): Unit = {
|
||||
super.Init(context)
|
||||
HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
|
||||
HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
|
||||
|
||||
InitZoneAmenities(zone = this)
|
||||
}
|
||||
}
|
||||
),
|
||||
(
|
||||
"i1",
|
||||
new Zone("i1", Await.result(Maps.map99, 30 seconds), 29) {
|
||||
override def Init(implicit context: ActorContext): Unit = {
|
||||
super.Init(context)
|
||||
HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
|
||||
HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
|
||||
|
||||
InitZoneAmenities(zone = this)
|
||||
}
|
||||
}
|
||||
),
|
||||
(
|
||||
"i2",
|
||||
new Zone("i2", Await.result(Maps.map98, 30 seconds), 30) {
|
||||
override def Init(implicit context: ActorContext): Unit = {
|
||||
super.Init(context)
|
||||
HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
|
||||
HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
|
||||
|
||||
InitZoneAmenities(zone = this)
|
||||
}
|
||||
}
|
||||
),
|
||||
(
|
||||
"i3",
|
||||
new Zone("i3", Await.result(Maps.map97, 30 seconds), 31) {
|
||||
override def Init(implicit context: ActorContext): Unit = {
|
||||
super.Init(context)
|
||||
HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
|
||||
HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
|
||||
|
||||
InitZoneAmenities(zone = this)
|
||||
}
|
||||
}
|
||||
),
|
||||
(
|
||||
"i4",
|
||||
new Zone("i4", Await.result(Maps.map96, 30 seconds), 32) {
|
||||
override def Init(implicit context: ActorContext): Unit = {
|
||||
super.Init(context)
|
||||
HotSpotCoordinateFunction = Zones.HotSpots.StandardRemapping(Map.Scale, 80, 80)
|
||||
HotSpotTimeFunction = Zones.HotSpots.StandardTimeRules
|
||||
|
||||
InitZoneAmenities(zone = this)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
def InitZoneAmenities(zone: Zone): Unit = {
|
||||
InitResourceSilos(zone)
|
||||
InitWarpGates(zone)
|
||||
|
||||
def InitWarpGates(zone: Zone): Unit = {
|
||||
// todo: work out which faction owns links to this warpgate and if they should be marked as broadcast or not
|
||||
// todo: enable geowarps to go to the correct cave
|
||||
zone.Buildings.values.collect {
|
||||
case wg: WarpGate
|
||||
if wg.Definition == GlobalDefinitions.warpgate || wg.Definition == GlobalDefinitions.warpgate_small =>
|
||||
wg.Active = true
|
||||
wg.Faction = PlanetSideEmpire.NEUTRAL
|
||||
wg.Broadcast = true
|
||||
case geowarp: WarpGate
|
||||
if geowarp.Definition == GlobalDefinitions.warpgate_cavern || geowarp.Definition == GlobalDefinitions.hst =>
|
||||
geowarp.Faction = PlanetSideEmpire.NEUTRAL
|
||||
geowarp.Active = false
|
||||
}
|
||||
}
|
||||
|
||||
def InitResourceSilos(zone: Zone): Unit = {
|
||||
// todo: load silo charge from database
|
||||
zone.Buildings.values.flatMap {
|
||||
_.Amenities.collect {
|
||||
case silo : ResourceSilo =>
|
||||
silo.Actor ! ResourceSilo.UpdateChargeLevel(silo.MaxNtuCapacitor)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the zone identifier name for the sanctuary continent of a given empire.
|
||||
* @param faction the empire
|
||||
* @return the zone id, with a blank string as an invalidating result
|
||||
*/
|
||||
def SanctuaryZoneId(faction: PlanetSideEmpire.Value): String = {
|
||||
faction match {
|
||||
case PlanetSideEmpire.NC => "home1"
|
||||
case PlanetSideEmpire.TR => "home2"
|
||||
case PlanetSideEmpire.VS => "home3"
|
||||
case PlanetSideEmpire.NEUTRAL => "" //invalid, not black ops
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the zone number for the sanctuary continent of a given empire.
|
||||
* @param faction the empire
|
||||
* @return the zone number, within the sequence 1-32, and with 0 as an invalidating result
|
||||
*/
|
||||
def SanctuaryZoneNumber(faction: PlanetSideEmpire.Value): Int = {
|
||||
faction match {
|
||||
case PlanetSideEmpire.NC => 11
|
||||
case PlanetSideEmpire.TR => 12
|
||||
case PlanetSideEmpire.VS => 13
|
||||
case PlanetSideEmpire.NEUTRAL => 0 //invalid, not black ops
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a zone identification string, provide that zone's ordinal number.
|
||||
* As zone identification naming is extremely formulaic,
|
||||
* just being able to poll the zone's identifier by its first few letters will produce its ordinal position.
|
||||
* @param id a zone id string
|
||||
* @return a zone number
|
||||
*/
|
||||
def NumberFromId(id: String): Int = {
|
||||
if (id.startsWith("z")) { //z2 -> 2
|
||||
id.substring(1).toInt
|
||||
} else if (id.startsWith("home")) { //home2 -> 2 + 10 = 12
|
||||
id.substring(4).toInt + 10
|
||||
} else if (id.startsWith("tz")) { //tzconc -> (14 + (3 * 1) + 2) -> 19
|
||||
(List("tr", "nc", "vs").indexOf(id.substring(4)) * 3) + List("sh", "dr", "co").indexOf(id.substring(2, 4)) + 14
|
||||
} else if (id.startsWith("c")) { //c2 -> 2 + 21 = 23
|
||||
id.substring(1).toInt + 21
|
||||
} else if (id.startsWith("i")) { //i2 -> 2 + 28 = 30
|
||||
id.substring(1).toInt + 28
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
object HotSpots {
|
||||
import net.psforever.objects.ballistics.SourceEntry
|
||||
import net.psforever.objects.zones.MapScale
|
||||
import net.psforever.types.Vector3
|
||||
|
||||
import scala.concurrent.duration._
|
||||
|
||||
/**
|
||||
* Produce hotspot coordinates based on map coordinates.
|
||||
* @see `FindClosestDivision`
|
||||
* @param scale the map's scale (width and height)
|
||||
* @param longDivNum the number of division lines spanning the width of the `scale`
|
||||
* @param latDivNum the number of division lines spanning the height of the `scale`
|
||||
* @param pos the absolute position of the activity reported
|
||||
* @return the position for a hotspot
|
||||
*/
|
||||
def StandardRemapping(scale: MapScale, longDivNum: Int, latDivNum: Int)(pos: Vector3): Vector3 = {
|
||||
Vector3(
|
||||
//x
|
||||
FindClosestDivision(pos.x, scale.width, longDivNum.toFloat),
|
||||
//y
|
||||
FindClosestDivision(pos.y, scale.height, latDivNum.toFloat),
|
||||
//z is always zero - maps are flat 2D planes
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Produce hotspot coordinates based on map coordinates.<br>
|
||||
* <br>
|
||||
* Transform a reported number by mapping it
|
||||
* into a division from a regular pattern of divisions
|
||||
* defined by the scale divided evenly a certain number of times.
|
||||
* The depicted number of divisions is actually one less than the parameter number
|
||||
* as the first division is used to represent everything before that first division (there is no "zero").
|
||||
* Likewise, the last division occurs before the farther edge of the scale is counted
|
||||
* and is used to represent everything after that last division.
|
||||
* This is not unlike rounding.
|
||||
* @param coordinate the point to scale
|
||||
* @param scale the map's scale (width and height)
|
||||
* @param divisions the number of division lines spanning across the `scale`
|
||||
* @return the closest regular division
|
||||
*/
|
||||
private def FindClosestDivision(coordinate: Float, scale: Float, divisions: Float): Float = {
|
||||
val divLength: Float = scale / divisions
|
||||
if (coordinate >= scale - divLength) {
|
||||
scale - divLength
|
||||
} else if (coordinate >= divLength) {
|
||||
val sector: Float = (coordinate * divisions / scale).toInt * divLength
|
||||
val nextSector: Float = sector + divLength
|
||||
if (coordinate - sector < nextSector - coordinate) {
|
||||
sector
|
||||
} else {
|
||||
nextSector
|
||||
}
|
||||
} else {
|
||||
divLength
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine a duration for which the hotspot will be displayed on the zone map.
|
||||
* Friendly fire is not recognized.
|
||||
* @param defender the defending party
|
||||
* @param attacker the attacking party
|
||||
* @return the duration
|
||||
*/
|
||||
def StandardTimeRules(defender: SourceEntry, attacker: SourceEntry): FiniteDuration = {
|
||||
import net.psforever.objects.GlobalDefinitions
|
||||
import net.psforever.objects.ballistics._
|
||||
if (attacker.Faction == defender.Faction) {
|
||||
0 seconds
|
||||
} else {
|
||||
//TODO is target occupy-able and occupied, or jammer-able and jammered?
|
||||
defender match {
|
||||
case _: PlayerSource =>
|
||||
60 seconds
|
||||
case _: VehicleSource =>
|
||||
60 seconds
|
||||
case t: ObjectSource if t.Definition == GlobalDefinitions.manned_turret =>
|
||||
60 seconds
|
||||
case _: DeployableSource =>
|
||||
30 seconds
|
||||
case _: ComplexDeployableSource =>
|
||||
30 seconds
|
||||
case _ =>
|
||||
0 seconds
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
6171
common/src/main/scala/net/psforever/zones/zonemaps/Map01.scala
Normal file
6171
common/src/main/scala/net/psforever/zones/zonemaps/Map01.scala
Normal file
File diff suppressed because it is too large
Load diff
7464
common/src/main/scala/net/psforever/zones/zonemaps/Map02.scala
Normal file
7464
common/src/main/scala/net/psforever/zones/zonemaps/Map02.scala
Normal file
File diff suppressed because it is too large
Load diff
10857
common/src/main/scala/net/psforever/zones/zonemaps/Map03.scala
Normal file
10857
common/src/main/scala/net/psforever/zones/zonemaps/Map03.scala
Normal file
File diff suppressed because it is too large
Load diff
9202
common/src/main/scala/net/psforever/zones/zonemaps/Map04.scala
Normal file
9202
common/src/main/scala/net/psforever/zones/zonemaps/Map04.scala
Normal file
File diff suppressed because it is too large
Load diff
6174
common/src/main/scala/net/psforever/zones/zonemaps/Map05.scala
Normal file
6174
common/src/main/scala/net/psforever/zones/zonemaps/Map05.scala
Normal file
File diff suppressed because it is too large
Load diff
7329
common/src/main/scala/net/psforever/zones/zonemaps/Map06.scala
Normal file
7329
common/src/main/scala/net/psforever/zones/zonemaps/Map06.scala
Normal file
File diff suppressed because it is too large
Load diff
8255
common/src/main/scala/net/psforever/zones/zonemaps/Map07.scala
Normal file
8255
common/src/main/scala/net/psforever/zones/zonemaps/Map07.scala
Normal file
File diff suppressed because it is too large
Load diff
6358
common/src/main/scala/net/psforever/zones/zonemaps/Map08.scala
Normal file
6358
common/src/main/scala/net/psforever/zones/zonemaps/Map08.scala
Normal file
File diff suppressed because it is too large
Load diff
8488
common/src/main/scala/net/psforever/zones/zonemaps/Map09.scala
Normal file
8488
common/src/main/scala/net/psforever/zones/zonemaps/Map09.scala
Normal file
File diff suppressed because it is too large
Load diff
7753
common/src/main/scala/net/psforever/zones/zonemaps/Map10.scala
Normal file
7753
common/src/main/scala/net/psforever/zones/zonemaps/Map10.scala
Normal file
File diff suppressed because it is too large
Load diff
4540
common/src/main/scala/net/psforever/zones/zonemaps/Map11.scala
Normal file
4540
common/src/main/scala/net/psforever/zones/zonemaps/Map11.scala
Normal file
File diff suppressed because it is too large
Load diff
4254
common/src/main/scala/net/psforever/zones/zonemaps/Map12.scala
Normal file
4254
common/src/main/scala/net/psforever/zones/zonemaps/Map12.scala
Normal file
File diff suppressed because it is too large
Load diff
4226
common/src/main/scala/net/psforever/zones/zonemaps/Map13.scala
Normal file
4226
common/src/main/scala/net/psforever/zones/zonemaps/Map13.scala
Normal file
File diff suppressed because it is too large
Load diff
1046
common/src/main/scala/net/psforever/zones/zonemaps/Map96.scala
Normal file
1046
common/src/main/scala/net/psforever/zones/zonemaps/Map96.scala
Normal file
File diff suppressed because it is too large
Load diff
2104
common/src/main/scala/net/psforever/zones/zonemaps/Map97.scala
Normal file
2104
common/src/main/scala/net/psforever/zones/zonemaps/Map97.scala
Normal file
File diff suppressed because it is too large
Load diff
2992
common/src/main/scala/net/psforever/zones/zonemaps/Map98.scala
Normal file
2992
common/src/main/scala/net/psforever/zones/zonemaps/Map98.scala
Normal file
File diff suppressed because it is too large
Load diff
2452
common/src/main/scala/net/psforever/zones/zonemaps/Map99.scala
Normal file
2452
common/src/main/scala/net/psforever/zones/zonemaps/Map99.scala
Normal file
File diff suppressed because it is too large
Load diff
3709
common/src/main/scala/net/psforever/zones/zonemaps/Ugd01.scala
Normal file
3709
common/src/main/scala/net/psforever/zones/zonemaps/Ugd01.scala
Normal file
File diff suppressed because it is too large
Load diff
6942
common/src/main/scala/net/psforever/zones/zonemaps/Ugd02.scala
Normal file
6942
common/src/main/scala/net/psforever/zones/zonemaps/Ugd02.scala
Normal file
File diff suppressed because it is too large
Load diff
5065
common/src/main/scala/net/psforever/zones/zonemaps/Ugd03.scala
Normal file
5065
common/src/main/scala/net/psforever/zones/zonemaps/Ugd03.scala
Normal file
File diff suppressed because it is too large
Load diff
3856
common/src/main/scala/net/psforever/zones/zonemaps/Ugd04.scala
Normal file
3856
common/src/main/scala/net/psforever/zones/zonemaps/Ugd04.scala
Normal file
File diff suppressed because it is too large
Load diff
3105
common/src/main/scala/net/psforever/zones/zonemaps/Ugd05.scala
Normal file
3105
common/src/main/scala/net/psforever/zones/zonemaps/Ugd05.scala
Normal file
File diff suppressed because it is too large
Load diff
3776
common/src/main/scala/net/psforever/zones/zonemaps/Ugd06.scala
Normal file
3776
common/src/main/scala/net/psforever/zones/zonemaps/Ugd06.scala
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -15,14 +15,14 @@ object Service {
|
|||
}
|
||||
|
||||
trait GenericEventBusMsg {
|
||||
def toChannel: String
|
||||
def channel: String
|
||||
}
|
||||
|
||||
class GenericEventBus[A <: GenericEventBusMsg] extends ActorEventBus with SubchannelClassification {
|
||||
type Event = A
|
||||
type Classifier = String
|
||||
|
||||
protected def classify(event: Event): Classifier = event.toChannel
|
||||
protected def classify(event: Event): Classifier = event.channel
|
||||
|
||||
protected def subclassification =
|
||||
new Subclassification[Classifier] {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import net.psforever.types.{ExoSuitType, PlanetSideEmpire, PlanetSideGUID, Trans
|
|||
import services.GenericEventBusMsg
|
||||
|
||||
final case class AvatarServiceResponse(
|
||||
toChannel: String,
|
||||
channel: String,
|
||||
avatar_guid: PlanetSideGUID,
|
||||
replyMessage: AvatarResponse.Response
|
||||
) extends GenericEventBusMsg
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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]
|
||||
)
|
||||
}
|
||||
|
|
@ -1,227 +1,188 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package services.chat
|
||||
|
||||
import akka.actor.Actor
|
||||
import net.psforever.objects.LivePlayerList
|
||||
import akka.actor.typed.receptionist.{Receptionist, ServiceKey}
|
||||
import akka.actor.typed.{ActorRef, Behavior}
|
||||
import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext, Behaviors}
|
||||
import net.psforever.objects.Session
|
||||
import net.psforever.packet.game.ChatMsg
|
||||
import net.psforever.types.ChatMessageType
|
||||
import services.{GenericEventBus, Service}
|
||||
import net.psforever.types.{ChatMessageType, PlanetSideEmpire, PlanetSideGUID}
|
||||
|
||||
class ChatService extends Actor {
|
||||
private[this] val log = org.log4s.getLogger
|
||||
object ChatService {
|
||||
val ChatServiceKey: ServiceKey[Command] = ServiceKey[ChatService.Command]("chatService")
|
||||
|
||||
override def preStart = {
|
||||
log.info("Starting....")
|
||||
def apply(): Behavior[Command] =
|
||||
Behaviors.setup { context =>
|
||||
context.system.receptionist ! Receptionist.Register(ChatServiceKey, context.self)
|
||||
new ChatService(context)
|
||||
}
|
||||
|
||||
sealed trait Command
|
||||
|
||||
final case class JoinChannel(actor: ActorRef[MessageResponse], session: Session, channel: ChatChannel) extends Command
|
||||
final case class LeaveChannel(actor: ActorRef[MessageResponse], channel: ChatChannel) extends Command
|
||||
final case class LeaveAllChannels(actor: ActorRef[MessageResponse]) extends Command
|
||||
|
||||
final case class Message(session: Session, message: ChatMsg, channel: ChatChannel) extends Command
|
||||
final case class MessageResponse(session: Session, message: ChatMsg, channel: ChatChannel)
|
||||
|
||||
trait ChatChannel
|
||||
object ChatChannel {
|
||||
// one of the default channels that the player is always subscribed to (local, broadcast, command...)
|
||||
final case class Default() extends ChatChannel
|
||||
final case class Squad(guid: PlanetSideGUID) extends ChatChannel
|
||||
}
|
||||
|
||||
val ChatEvents = new GenericEventBus[ChatServiceResponse]
|
||||
}
|
||||
|
||||
def receive = {
|
||||
case Service.Join(channel) =>
|
||||
val path = s"/Chat/$channel"
|
||||
val who = sender()
|
||||
log.info(s"$who has joined $path")
|
||||
ChatEvents.subscribe(who, path)
|
||||
case Service.Leave(None) =>
|
||||
ChatEvents.unsubscribe(sender())
|
||||
case Service.Leave(Some(channel)) =>
|
||||
val path = s"/Chat/$channel"
|
||||
val who = sender()
|
||||
log.info(s"$who has left $path")
|
||||
ChatEvents.unsubscribe(who, path)
|
||||
case Service.LeaveAll() =>
|
||||
ChatEvents.unsubscribe(sender())
|
||||
class ChatService(context: ActorContext[ChatService.Command]) extends AbstractBehavior[ChatService.Command](context) {
|
||||
|
||||
case ChatServiceMessage(forChannel, action) =>
|
||||
action match {
|
||||
case ChatAction.Local(player_guid, player_name, cont, player_pos, player_faction, msg) => // local
|
||||
ChatEvents.publish(
|
||||
ChatServiceResponse(
|
||||
s"/Chat/$forChannel",
|
||||
player_guid,
|
||||
player_name,
|
||||
cont,
|
||||
player_pos,
|
||||
player_faction,
|
||||
2,
|
||||
ChatMsg(ChatMessageType.CMT_OPEN, msg.wideContents, player_name, msg.contents, None)
|
||||
)
|
||||
)
|
||||
case ChatAction.Tell(player_guid, player_name, msg) => // tell
|
||||
var good: Boolean = false
|
||||
LivePlayerList
|
||||
.WorldPopulation(_ => true)
|
||||
.foreach(char => {
|
||||
if (char.name.equalsIgnoreCase(msg.recipient)) {
|
||||
good = true
|
||||
}
|
||||
})
|
||||
if (good) {
|
||||
ChatEvents.publish(
|
||||
ChatServiceResponse(
|
||||
s"/Chat/$forChannel",
|
||||
player_guid,
|
||||
player_name,
|
||||
target = 0,
|
||||
replyMessage = ChatMsg(ChatMessageType.CMT_TELL, msg.wideContents, msg.recipient, msg.contents, None)
|
||||
)
|
||||
)
|
||||
ChatEvents.publish(
|
||||
ChatServiceResponse(
|
||||
s"/Chat/$forChannel",
|
||||
player_guid,
|
||||
player_name,
|
||||
target = 1,
|
||||
replyMessage =
|
||||
ChatMsg(ChatMessageType.U_CMT_TELLFROM, msg.wideContents, msg.recipient, msg.contents, None)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
ChatEvents.publish(
|
||||
ChatServiceResponse(
|
||||
s"/Chat/$forChannel",
|
||||
player_guid,
|
||||
player_name,
|
||||
target = 1,
|
||||
replyMessage =
|
||||
ChatMsg(ChatMessageType.U_CMT_TELLFROM, msg.wideContents, msg.recipient, msg.contents, None)
|
||||
)
|
||||
)
|
||||
ChatEvents.publish(
|
||||
ChatServiceResponse(
|
||||
s"/Chat/$forChannel",
|
||||
player_guid,
|
||||
player_name,
|
||||
target = 1,
|
||||
replyMessage = ChatMsg(ChatMessageType.UNK_45, msg.wideContents, "", "@NoTell_Target", None)
|
||||
)
|
||||
)
|
||||
}
|
||||
case ChatAction.Broadcast(player_guid, player_name, cont, player_pos, player_faction, msg) => // broadcast
|
||||
ChatEvents.publish(
|
||||
ChatServiceResponse(
|
||||
s"/Chat/$forChannel",
|
||||
player_guid,
|
||||
player_name,
|
||||
cont,
|
||||
player_pos,
|
||||
player_faction,
|
||||
2,
|
||||
ChatMsg(msg.messageType, msg.wideContents, player_name, msg.contents, None)
|
||||
)
|
||||
)
|
||||
case ChatAction.Voice(player_guid, player_name, cont, player_pos, player_faction, msg) => // voice
|
||||
ChatEvents.publish(
|
||||
ChatServiceResponse(
|
||||
s"/Chat/$forChannel",
|
||||
player_guid,
|
||||
player_name,
|
||||
cont,
|
||||
player_pos,
|
||||
player_faction,
|
||||
2,
|
||||
ChatMsg(ChatMessageType.CMT_VOICE, false, player_name, msg.contents, None)
|
||||
)
|
||||
)
|
||||
import ChatService._
|
||||
import ChatMessageType._
|
||||
|
||||
case ChatAction.Note(player_guid, player_name, msg) => // note
|
||||
ChatEvents.publish(
|
||||
ChatServiceResponse(
|
||||
s"/Chat/$forChannel",
|
||||
player_guid,
|
||||
player_name,
|
||||
target = 1,
|
||||
replyMessage = ChatMsg(ChatMessageType.U_CMT_GMTELLFROM, true, msg.recipient, msg.contents, None)
|
||||
)
|
||||
)
|
||||
ChatEvents.publish(
|
||||
ChatServiceResponse(
|
||||
s"/Chat/$forChannel",
|
||||
player_guid,
|
||||
player_name,
|
||||
target = 1,
|
||||
replyMessage = ChatMsg(
|
||||
ChatMessageType.CMT_GMTELL,
|
||||
true,
|
||||
"Server",
|
||||
"Why do you try to /note ? That's a GM command ! ... Or not, nobody can /note",
|
||||
None
|
||||
)
|
||||
)
|
||||
)
|
||||
case ChatAction.Squad(player_guid, player_name, cont, player_pos, player_faction, msg) => // squad
|
||||
ChatEvents.publish(
|
||||
ChatServiceResponse(
|
||||
s"/Chat/$forChannel",
|
||||
player_guid,
|
||||
player_name,
|
||||
cont,
|
||||
player_pos,
|
||||
player_faction,
|
||||
2,
|
||||
ChatMsg(ChatMessageType.CMT_SQUAD, msg.wideContents, player_name, msg.contents, None)
|
||||
)
|
||||
)
|
||||
case ChatAction.Platoon(player_guid, player_name, cont, player_pos, player_faction, msg) => // platoon
|
||||
ChatEvents.publish(
|
||||
ChatServiceResponse(
|
||||
s"/Chat/$forChannel",
|
||||
player_guid,
|
||||
player_name,
|
||||
cont,
|
||||
player_pos,
|
||||
player_faction,
|
||||
2,
|
||||
ChatMsg(ChatMessageType.CMT_PLATOON, msg.wideContents, player_name, msg.contents, None)
|
||||
)
|
||||
)
|
||||
case ChatAction.Command(player_guid, player_name, cont, player_pos, player_faction, msg) => // command
|
||||
ChatEvents.publish(
|
||||
ChatServiceResponse(
|
||||
s"/Chat/$forChannel",
|
||||
player_guid,
|
||||
player_name,
|
||||
cont,
|
||||
player_pos,
|
||||
player_faction,
|
||||
2,
|
||||
ChatMsg(ChatMessageType.CMT_COMMAND, msg.wideContents, player_name, msg.contents, None)
|
||||
)
|
||||
)
|
||||
case ChatAction.GM(player_guid, player_name, msg) => // GM
|
||||
msg.messageType match {
|
||||
case ChatMessageType.CMT_SILENCE =>
|
||||
ChatEvents.publish(
|
||||
ChatServiceResponse(
|
||||
s"/Chat/$forChannel",
|
||||
player_guid,
|
||||
msg.contents,
|
||||
target = 0,
|
||||
replyMessage = ChatMsg(ChatMessageType.CMT_SILENCE, true, "", "", None)
|
||||
private[this] val log = org.log4s.getLogger
|
||||
var subscriptions: List[JoinChannel] = List[JoinChannel]()
|
||||
|
||||
override def onMessage(msg: Command): Behavior[Command] = {
|
||||
msg match {
|
||||
case subscription: JoinChannel =>
|
||||
subscriptions ++= List(subscription)
|
||||
this
|
||||
|
||||
case LeaveChannel(actor, channel) =>
|
||||
subscriptions = subscriptions.filter {
|
||||
case JoinChannel(a, _, c) => actor != a && channel != c
|
||||
}
|
||||
this
|
||||
|
||||
case LeaveAllChannels(actor) =>
|
||||
subscriptions = subscriptions.filter {
|
||||
case JoinChannel(a, _, _) => actor != a
|
||||
}
|
||||
this
|
||||
|
||||
case Message(session, message, channel) =>
|
||||
(channel, message.messageType) match {
|
||||
case (ChatChannel.Squad(_), CMT_SQUAD) =>
|
||||
case (ChatChannel.Default(), messageType) if messageType != CMT_SQUAD =>
|
||||
case _ =>
|
||||
log.error(s"invalid chat channel $channel for messageType ${message.messageType}")
|
||||
return this
|
||||
}
|
||||
val subs = subscriptions.filter(_.channel == channel)
|
||||
message.messageType match {
|
||||
case CMT_TELL | CMT_GMTELL =>
|
||||
subs.find(_.session.player.Name == session.player.Name).foreach {
|
||||
case JoinChannel(sender, _, _) =>
|
||||
sender ! MessageResponse(
|
||||
session,
|
||||
message.copy(messageType = if (message.messageType == CMT_TELL) U_CMT_TELLFROM else U_CMT_GMTELLFROM),
|
||||
channel
|
||||
)
|
||||
)
|
||||
// if(player_guid != PlanetSideGUID(0)) {
|
||||
//
|
||||
// val args = msg.contents.split(" ")
|
||||
// var silence_name : String = ""
|
||||
// var silence_time : Int = 5
|
||||
// if (args.length == 1) {
|
||||
// silence_name = args(0)
|
||||
// }
|
||||
// else if (args.length == 2) {
|
||||
// silence_name = args(0)
|
||||
// silence_time = args(1).toInt
|
||||
// }
|
||||
// ChatEvents.publish(
|
||||
// ChatServiceResponse(s"/Chat/$forChannel", player_guid, player_name, target = 1, replyMessage = ChatMsg(ChatMessageType.UNK_45, true, "", silence_name + " silenced for " + silence_time + " min(s)", None))
|
||||
// )
|
||||
// }
|
||||
case _ => ;
|
||||
}
|
||||
case _ => ;
|
||||
}
|
||||
subs.find(_.session.player.Name == message.recipient) match {
|
||||
case Some(JoinChannel(receiver, _, _)) =>
|
||||
receiver ! MessageResponse(session, message, channel)
|
||||
case None =>
|
||||
sender ! MessageResponse(
|
||||
session,
|
||||
ChatMsg(ChatMessageType.UNK_45, false, "", "@NoTell_Target", None),
|
||||
channel
|
||||
)
|
||||
}
|
||||
|
||||
case msg =>
|
||||
log.info(s"Unhandled message $msg from $sender")
|
||||
}
|
||||
|
||||
case CMT_SILENCE =>
|
||||
val args = message.contents.split(" ")
|
||||
val (name, time, error) = (args.lift(0), args.lift(1)) match {
|
||||
case (Some(name), None) => (Some(name), Some(5), None)
|
||||
case (Some(name), Some(time)) =>
|
||||
time.toIntOption match {
|
||||
case Some(time) =>
|
||||
(Some(name), Some(time), None)
|
||||
case None =>
|
||||
(None, None, Some("bad time format"))
|
||||
}
|
||||
case _ => (None, None, None)
|
||||
}
|
||||
|
||||
val sender = subs.find(_.session.player.Name == session.player.Name)
|
||||
|
||||
(sender, name, time, error) match {
|
||||
case (Some(sender), Some(name), Some(_), None) =>
|
||||
val recipient = subs.find(_.session.player.Name == name)
|
||||
recipient match {
|
||||
case Some(recipient) =>
|
||||
if (recipient.session.player.silenced) {
|
||||
sender.actor ! MessageResponse(
|
||||
session,
|
||||
ChatMsg(UNK_71, true, "", "@silence_disabled_ack", None),
|
||||
channel
|
||||
)
|
||||
} else {
|
||||
sender.actor ! MessageResponse(
|
||||
session,
|
||||
ChatMsg(UNK_71, true, "", "@silence_enabled_ack", None),
|
||||
channel
|
||||
)
|
||||
}
|
||||
recipient.actor ! MessageResponse(session, message, channel)
|
||||
case None =>
|
||||
sender.actor ! MessageResponse(
|
||||
session,
|
||||
ChatMsg(UNK_71, true, "", s"unknown player '$name'", None),
|
||||
channel
|
||||
)
|
||||
}
|
||||
case (Some(sender), _, _, error) =>
|
||||
sender.actor ! MessageResponse(
|
||||
session,
|
||||
ChatMsg(UNK_71, false, "", error.getOrElse("usage: /silence <name> [<time>]"), None),
|
||||
channel
|
||||
)
|
||||
|
||||
case (None, _, _, _) =>
|
||||
log.error("received message from non-subscribed actor")
|
||||
|
||||
}
|
||||
|
||||
case CMT_NOTE =>
|
||||
subs.filter(_.session.player.Name == message.recipient).foreach {
|
||||
case JoinChannel(actor, _, _) =>
|
||||
actor ! MessageResponse(session, message.copy(recipient = session.player.Name), channel)
|
||||
}
|
||||
|
||||
// faction commands
|
||||
case CMT_OPEN | CMT_PLATOON | CMT_COMMAND =>
|
||||
subs.filter(_.session.player.Faction == session.player.Faction).foreach {
|
||||
case JoinChannel(actor, _, _) => actor ! MessageResponse(session, message, channel)
|
||||
}
|
||||
|
||||
case CMT_GMBROADCAST_NC =>
|
||||
subs.filter(_.session.player.Faction == PlanetSideEmpire.NC).foreach {
|
||||
case JoinChannel(actor, _, _) => actor ! MessageResponse(session, message, channel)
|
||||
}
|
||||
|
||||
case CMT_GMBROADCAST_TR =>
|
||||
subs.filter(_.session.player.Faction == PlanetSideEmpire.TR).foreach {
|
||||
case JoinChannel(actor, _, _) => actor ! MessageResponse(session, message, channel)
|
||||
}
|
||||
|
||||
case CMT_GMBROADCAST_VS =>
|
||||
subs.filter(_.session.player.Faction == PlanetSideEmpire.VS).foreach {
|
||||
case JoinChannel(actor, _, _) => actor ! MessageResponse(session, message, channel)
|
||||
}
|
||||
|
||||
// cross faction commands
|
||||
case CMT_BROADCAST | CMT_VOICE | CMT_GMBROADCAST | CMT_GMBROADCASTPOPUP | UNK_227 =>
|
||||
subs.foreach {
|
||||
case JoinChannel(actor, _, _) => actor ! MessageResponse(session, message, channel)
|
||||
}
|
||||
|
||||
case _ =>
|
||||
log.warn(s"unhandled chat message, add a case for $message")
|
||||
}
|
||||
this
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +0,0 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package services.chat
|
||||
|
||||
final case class ChatServiceMessage(forChannel: String, actionMessage: ChatAction.Action)
|
||||
|
|
@ -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
|
||||
|
|
@ -8,7 +8,7 @@ import net.psforever.packet.game.BuildingInfoUpdateMessage
|
|||
import net.psforever.types.PlanetSideGUID
|
||||
import services.GenericEventBusMsg
|
||||
|
||||
final case class GalaxyServiceResponse(toChannel: String, replyMessage: GalaxyResponse.Response)
|
||||
final case class GalaxyServiceResponse(channel: String, replyMessage: GalaxyResponse.Response)
|
||||
extends GenericEventBusMsg
|
||||
|
||||
object GalaxyResponse {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, Vector3}
|
|||
import services.GenericEventBusMsg
|
||||
|
||||
final case class LocalServiceResponse(
|
||||
toChannel: String,
|
||||
channel: String,
|
||||
avatar_guid: PlanetSideGUID,
|
||||
replyMessage: LocalResponse.Response
|
||||
) extends GenericEventBusMsg
|
||||
|
|
|
|||
|
|
@ -2,15 +2,22 @@
|
|||
package services.teamwork
|
||||
|
||||
import akka.actor.{Actor, ActorRef, Terminated}
|
||||
import net.psforever.objects.{Avatar, LivePlayerList, Player}
|
||||
import net.psforever.objects.definition.converter.StatConverter
|
||||
import net.psforever.objects.loadouts.SquadLoadout
|
||||
import net.psforever.objects.teamwork.{Member, Squad, SquadFeatures}
|
||||
import net.psforever.objects.zones.Zone
|
||||
import net.psforever.packet.game._
|
||||
import net.psforever.objects.{Avatar, LivePlayerList, Player}
|
||||
import net.psforever.packet.game.{
|
||||
SquadDetail,
|
||||
SquadInfo,
|
||||
WaypointEventAction,
|
||||
SquadPositionEntry,
|
||||
SquadPositionDetail,
|
||||
WaypointInfo,
|
||||
PlanetSideZoneID
|
||||
}
|
||||
import net.psforever.types._
|
||||
import services.{GenericEventBus, Service}
|
||||
import services.teamwork.SquadAction
|
||||
|
||||
import scala.collection.concurrent.TrieMap
|
||||
import scala.collection.mutable
|
||||
|
|
|
|||
|
|
@ -2,12 +2,11 @@
|
|||
package services.teamwork
|
||||
|
||||
import net.psforever.objects.teamwork.Squad
|
||||
import net.psforever.packet.game._
|
||||
import net.psforever.packet.game.{SquadDetail, SquadInfo, WaypointEventAction, WaypointInfo}
|
||||
import net.psforever.types.{PlanetSideGUID, SquadResponseType, SquadWaypoints}
|
||||
import services.GenericEventBusMsg
|
||||
import services.teamwork.SquadAction
|
||||
|
||||
final case class SquadServiceResponse(toChannel: String, exclude: Iterable[Long], response: SquadResponse.Response)
|
||||
final case class SquadServiceResponse(channel: String, exclude: Iterable[Long], response: SquadResponse.Response)
|
||||
extends GenericEventBusMsg
|
||||
|
||||
object SquadServiceResponse {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import net.psforever.types.{BailType, DriveState, PlanetSideGUID, Vector3}
|
|||
import services.GenericEventBusMsg
|
||||
|
||||
final case class VehicleServiceResponse(
|
||||
toChannel: String,
|
||||
channel: String,
|
||||
avatar_guid: PlanetSideGUID,
|
||||
replyMessage: VehicleResponse.Response
|
||||
) extends GenericEventBusMsg
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue