diff --git a/src/main/scala/net/psforever/actors/session/ChatActor.scala b/src/main/scala/net/psforever/actors/session/ChatActor.scala
index 14e74b912..b68258390 100644
--- a/src/main/scala/net/psforever/actors/session/ChatActor.scala
+++ b/src/main/scala/net/psforever/actors/session/ChatActor.scala
@@ -10,6 +10,7 @@ import net.psforever.actors.session.spectator.{SpectatorMode => SessionSpectator
import net.psforever.actors.zone.ZoneActor
import net.psforever.objects.sourcing.PlayerSource
import net.psforever.objects.zones.ZoneInfo
+import net.psforever.services.chat.{DefaultChannel, SquadChannel}
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import net.psforever.types.ChatMessageType.CMT_QUIT
import org.log4s.Logger
@@ -33,7 +34,7 @@ import net.psforever.packet.game.objectcreate.DrawnSlot
import net.psforever.packet.game.{ChatMsg, CreateShortcutMessage, DeadState, RequestDestroyMessage, Shortcut}
import net.psforever.services.{CavernRotationService, InterstellarClusterService}
import net.psforever.services.chat.ChatService
-import net.psforever.services.chat.ChatService.ChatChannel
+import net.psforever.services.chat.ChatChannel
import net.psforever.types.ChatMessageType.{CMT_GMOPEN, UNK_227, UNK_229}
import net.psforever.types.{ChatMessageType, Cosmetic, ExperienceType, ImplantType, PlanetSideEmpire, PlanetSideGUID, Vector3}
import net.psforever.util.{Config, PointOfInterest}
@@ -385,7 +386,7 @@ object ChatActor {
sendTo ! ChatService.Message(
session,
ChatMsg(UNK_227, wideContents=true, "", content.mkString(" "), None),
- ChatChannel.Default()
+ DefaultChannel
)
true
}
@@ -688,7 +689,7 @@ class ChatActor(
case ListingResponse(ChatService.ChatServiceKey.Listing(listings)) =>
chatService = Some(listings.head)
- channels ++= List(ChatChannel.Default())
+ channels ++= List(DefaultChannel)
postStartBehaviour()
case SetSession(newSession) =>
@@ -704,7 +705,7 @@ class ChatActor(
def postStartBehaviour(): Behavior[Command] = {
(session, chatService, cluster) match {
case (Some(_session), Some(_chatService), Some(_cluster)) if _session.player != null =>
- _chatService ! ChatService.JoinChannel(chatServiceAdapter, _session, ChatChannel.Default())
+ _chatService ! ChatService.JoinChannel(chatServiceAdapter, null, DefaultChannel)
logic = NormalMode.init(_chatService, _cluster)
buffer.unstashAll(active(_session, _chatService, _cluster))
case _ =>
@@ -724,7 +725,7 @@ class ChatActor(
active(newSession, chatService, cluster)
case JoinChannel(channel) =>
- chatService ! ChatService.JoinChannel(chatServiceAdapter, session, channel)
+ chatService ! ChatService.JoinChannel(chatServiceAdapter, null, channel)
channels ++= List(channel)
Behaviors.same
@@ -1057,7 +1058,7 @@ class ChatActor(
// SH prefix are tactical voice macros only sent to squad
if (contents.startsWith("SH")) {
channels.foreach {
- case _/*channel*/: ChatChannel.Squad =>
+ case _/*channel*/: SquadChannel =>
commandSendToRecipient(session, message, chatService)
case _ => ()
}
@@ -1082,7 +1083,7 @@ class ChatActor(
def commandSquad(session: Session, message: ChatMsg, chatService: ActorRef[ChatService.Command]): Unit = {
channels.foreach {
- case _/*channel*/: ChatChannel.Squad =>
+ case _/*channel*/: SquadChannel =>
commandSendToRecipient(session, message, chatService)
case _ => ()
}
@@ -1415,7 +1416,7 @@ class ChatActor(
chatService ! ChatService.Message(
session,
message,
- ChatChannel.Default()
+ DefaultChannel
)
}
@@ -1423,7 +1424,7 @@ class ChatActor(
chatService ! ChatService.Message(
session,
message.copy(recipient = session.player.Name),
- ChatChannel.Default()
+ DefaultChannel
)
}
diff --git a/src/main/scala/net/psforever/actors/session/SessionActor.scala b/src/main/scala/net/psforever/actors/session/SessionActor.scala
index 81a1fbe8f..7ea23d760 100644
--- a/src/main/scala/net/psforever/actors/session/SessionActor.scala
+++ b/src/main/scala/net/psforever/actors/session/SessionActor.scala
@@ -113,7 +113,9 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
middlewareActor ! MiddlewareActor.Send(KeepAliveMessage())
case SessionActor.SetMode(newMode) =>
- logic.switchFrom(data.session)
+ if (mode != newMode) {
+ logic.switchFrom(data.session)
+ }
mode = newMode
logic = mode.setup(data)
logic.switchTo(data.session)
diff --git a/src/main/scala/net/psforever/actors/session/normal/ChatLogic.scala b/src/main/scala/net/psforever/actors/session/normal/ChatLogic.scala
new file mode 100644
index 000000000..af585ad6d
--- /dev/null
+++ b/src/main/scala/net/psforever/actors/session/normal/ChatLogic.scala
@@ -0,0 +1,234 @@
+// Copyright (c) 2024 PSForever
+package net.psforever.actors.session.normal
+
+import akka.actor.ActorContext
+import net.psforever.actors.session.support.{ChatFunctions, ChatOperations, SessionData}
+import net.psforever.objects.Session
+import net.psforever.packet.game.{ChatMsg, SetChatFilterMessage}
+import net.psforever.services.chat.DefaultChannel
+import net.psforever.types.ChatMessageType
+import net.psforever.util.Config
+
+object ChatLogic {
+ def apply(ops: ChatOperations): ChatLogic = {
+ new ChatLogic(ops, ops.context)
+ }
+}
+
+class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) extends ChatFunctions {
+ def sessionLogic: SessionData = ops.sessionLogic
+
+ def handleChatMsg(session: Session, message: ChatMsg): Unit = {
+ import net.psforever.types.ChatMessageType._
+ val gmCommandAllowed =
+ session.account.gm || Config.app.development.unprivilegedGmCommands.contains(message.messageType)
+ (message.messageType, message.recipient.trim, message.contents.trim) match {
+ /** Messages starting with ! are custom chat commands */
+ case (_, _, contents) if contents.startsWith("!") &&
+ customCommandMessages(message, session) => ()
+
+ case (CMT_FLY, recipient, contents) if gmCommandAllowed =>
+ ops.commandFly(contents, recipient)
+
+ case (CMT_ANONYMOUS, _, _) =>
+ // ?
+
+ case (CMT_TOGGLE_GM, _, _) =>
+ // ?
+
+ case (CMT_CULLWATERMARK, _, contents) =>
+ ops.commandWatermark(contents)
+
+ case (CMT_SPEED, _, contents) if gmCommandAllowed =>
+ ops.commandSpeed(message, contents)
+
+ case (CMT_TOGGLESPECTATORMODE, _, contents) if gmCommandAllowed =>
+ ops.commandToggleSpectatorMode(session, contents)
+
+ case (CMT_RECALL, _, _) =>
+ ops.commandRecall(session)
+
+ case (CMT_INSTANTACTION, _, _) =>
+ ops.commandInstantAction(session)
+
+ case (CMT_QUIT, _, _) =>
+ ops.commandQuit(session)
+
+ case (CMT_SUICIDE, _, _) =>
+ ops.commandSuicide(session)
+
+ case (CMT_DESTROY, _, contents) if contents.matches("\\d+") =>
+ ops.commandDestroy(session, message, contents)
+
+ case (CMT_SETBASERESOURCES, _, contents) if gmCommandAllowed =>
+ ops.commandSetBaseResources(session, contents)
+
+ case (CMT_ZONELOCK, _, contents) if gmCommandAllowed =>
+ ops.commandZoneLock(contents)
+
+ case (U_CMT_ZONEROTATE, _, _) if gmCommandAllowed =>
+ ops.commandZoneRotate()
+
+ case (CMT_CAPTUREBASE, _, contents) if gmCommandAllowed =>
+ ops.commandCaptureBase(session, message, contents)
+
+ case (CMT_GMBROADCAST | CMT_GMBROADCAST_NC | CMT_GMBROADCAST_VS | CMT_GMBROADCAST_TR, _, _)
+ if gmCommandAllowed =>
+ ops.commandSendToRecipient(session, message, DefaultChannel)
+
+ case (CMT_GMTELL, _, _) if gmCommandAllowed =>
+ ops.commandSend(session, message, DefaultChannel)
+
+ case (CMT_GMBROADCASTPOPUP, _, _) if gmCommandAllowed =>
+ ops.commandSendToRecipient(session, message, DefaultChannel)
+
+ case (CMT_OPEN, _, _) if !session.player.silenced =>
+ ops.commandSendToRecipient(session, message, DefaultChannel)
+
+ case (CMT_VOICE, _, contents) =>
+ ops.commandVoice(session, message, contents, DefaultChannel)
+
+ case (CMT_TELL, _, _) if !session.player.silenced =>
+ ops.commandTellOrIgnore(session, message, DefaultChannel)
+
+ case (CMT_BROADCAST, _, _) if !session.player.silenced =>
+ ops.commandSendToRecipient(session, message, DefaultChannel)
+
+ case (CMT_PLATOON, _, _) if !session.player.silenced =>
+ ops.commandSendToRecipient(session, message, DefaultChannel)
+
+ case (CMT_COMMAND, _, _) if gmCommandAllowed =>
+ ops.commandSendToRecipient(session, message, DefaultChannel)
+
+ case (CMT_NOTE, _, _) =>
+ ops.commandSend(session, message, DefaultChannel)
+
+ case (CMT_SILENCE, _, _) if gmCommandAllowed =>
+ ops.commandSend(session, message, DefaultChannel)
+
+ case (CMT_SQUAD, _, _) =>
+ ops.commandSquad(session, message, DefaultChannel) //todo SquadChannel, but what is the guid
+
+ case (CMT_WHO | CMT_WHO_CSR | CMT_WHO_CR | CMT_WHO_PLATOONLEADERS | CMT_WHO_SQUADLEADERS | CMT_WHO_TEAMS, _, _) =>
+ ops.commandWho(session)
+
+ case (CMT_ZONE, _, contents) if gmCommandAllowed =>
+ ops.commandZone(message, contents)
+
+ case (CMT_WARP, _, contents) if gmCommandAllowed =>
+ ops.commandWarp(session, message, contents)
+
+ case (CMT_SETBATTLERANK, _, contents) if gmCommandAllowed =>
+ ops.commandSetBattleRank(session, message, contents)
+
+ case (CMT_SETCOMMANDRANK, _, contents) if gmCommandAllowed =>
+ ops.commandSetCommandRank(session, message, contents)
+
+ case (CMT_ADDBATTLEEXPERIENCE, _, contents) if gmCommandAllowed =>
+ ops.commandAddBattleExperience(message, contents)
+
+ case (CMT_ADDCOMMANDEXPERIENCE, _, contents) if gmCommandAllowed =>
+ ops.commandAddCommandExperience(message, contents)
+
+ case (CMT_TOGGLE_HAT, _, contents) =>
+ ops.commandToggleHat(session, message, contents)
+
+ case (CMT_HIDE_HELMET | CMT_TOGGLE_SHADES | CMT_TOGGLE_EARPIECE, _, contents) =>
+ ops.commandToggleCosmetics(session, message, contents)
+
+ case (CMT_ADDCERTIFICATION, _, contents) if gmCommandAllowed =>
+ ops.commandAddCertification(session, message, contents)
+
+ case (CMT_KICK, _, contents) if gmCommandAllowed =>
+ ops.commandKick(session, message, contents)
+
+ case _ =>
+ log.warn(s"Unhandled chat message $message")
+ }
+ }
+
+ def handleChatFilter(pkt: SetChatFilterMessage): Unit = {
+ val SetChatFilterMessage(_, _, _) = pkt
+ }
+
+ def handleIncomingMessage(session: Session, message: ChatMsg, fromSession: Session): Unit = {
+ import ChatMessageType._
+ message.messageType match {
+ case CMT_BROADCAST | CMT_SQUAD | CMT_PLATOON | CMT_COMMAND | CMT_NOTE =>
+ ops.commandIncomingSendAllIfOnline(session, message)
+
+ case CMT_OPEN =>
+ ops.commandIncomingSendToLocalIfOnline(session, fromSession, message)
+
+ case CMT_TELL | U_CMT_TELLFROM |
+ CMT_GMOPEN | CMT_GMBROADCAST | CMT_GMBROADCAST_NC | CMT_GMBROADCAST_TR | CMT_GMBROADCAST_VS |
+ CMT_GMBROADCASTPOPUP | CMT_GMTELL | U_CMT_GMTELLFROM | UNK_45 | UNK_71 | UNK_227 | UNK_229 =>
+ ops.commandIncomingSend(message)
+
+ case CMT_VOICE =>
+ ops.commandIncomingVoice(session, fromSession, message)
+
+ case CMT_SILENCE =>
+ ops.commandIncomingSilence(session, message)
+
+ case _ =>
+ log.warn(s"Unexpected messageType $message")
+ }
+ }
+
+ private def customCommandMessages(
+ message: ChatMsg,
+ session: Session
+ ): Boolean = {
+ val contents = message.contents
+ if (contents.startsWith("!")) {
+ val (command, params) = ops.cliTokenization(contents.drop(1)) match {
+ case a :: b => (a, b)
+ case _ => ("", Seq(""))
+ }
+ val gmBangCommandAllowed = session.account.gm || Config.app.development.unprivilegedGmBangCommands.contains(command)
+ //try gm commands
+ val tryGmCommandResult = if (gmBangCommandAllowed) {
+ command match {
+ case "whitetext" => Some(ops.customCommandWhitetext(session, params))
+ case "list" => Some(ops.customCommandList(session, params, message))
+ case "ntu" => Some(ops.customCommandNtu(session, params))
+ case "zonerotate" => Some(ops.customCommandZonerotate(params))
+ case "nearby" => Some(ops.customCommandNearby(session))
+ case _ => None
+ }
+ } else {
+ None
+ }
+ //try commands for all players if not caught as a gm command
+ val result = tryGmCommandResult match {
+ case None =>
+ command match {
+ case "loc" => ops.customCommandLoc(session, message)
+ case "suicide" => ops.customCommandSuicide(session)
+ case "grenade" => ops.customCommandGrenade(session, log)
+ case "macro" => ops.customCommandMacro(session, params)
+ case "progress" => ops.customCommandProgress(session, params)
+ case _ => false
+ }
+ case Some(out) =>
+ out
+ }
+ if (!result) {
+ // command was not handled
+ sendResponse(
+ ChatMsg(
+ ChatMessageType.CMT_GMOPEN, // CMT_GMTELL
+ message.wideContents,
+ "Server",
+ s"Unknown command !$command",
+ message.note
+ )
+ )
+ }
+ result
+ } else {
+ false // not a handled command
+ }
+ }
+}
diff --git a/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala b/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala
index 78e6d9732..7c493e365 100644
--- a/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala
+++ b/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala
@@ -2,11 +2,11 @@
package net.psforever.actors.session.normal
import akka.actor.typed.scaladsl.adapter._
-import akka.actor.{ActorContext, Cancellable, typed}
-import net.psforever.actors.session.{AvatarActor, ChatActor, SessionActor}
+import akka.actor.{ActorContext, typed}
+import net.psforever.actors.session.{AvatarActor, SessionActor}
import net.psforever.actors.session.support.{GeneralFunctions, GeneralOperations, SessionData}
import net.psforever.login.WorldSession.{CallBackForTask, ContainableMoveItem, DropEquipmentFromInventory, PickUpEquipmentFromGround, RemoveOldEquipmentFromInventory}
-import net.psforever.objects.{Account, BoomerDeployable, BoomerTrigger, ConstructionItem, Default, Deployables, GlobalDefinitions, Kit, LivePlayerList, PlanetSideGameObject, Player, SensorDeployable, ShieldGeneratorDeployable, SpecialEmp, TelepadDeployable, Tool, TrapDeployable, TurretDeployable, Vehicle}
+import net.psforever.objects.{Account, BoomerDeployable, BoomerTrigger, ConstructionItem, Deployables, GlobalDefinitions, Kit, LivePlayerList, PlanetSideGameObject, Player, SensorDeployable, ShieldGeneratorDeployable, SpecialEmp, TelepadDeployable, Tool, TrapDeployable, TurretDeployable, Vehicle}
import net.psforever.objects.avatar.{Avatar, PlayerControl, SpecialCarry}
import net.psforever.objects.ballistics.Projectile
import net.psforever.objects.ce.{Deployable, DeployedItem, TelepadLike}
@@ -39,13 +39,12 @@ import net.psforever.objects.vital.interaction.DamageInteraction
import net.psforever.objects.zones.{Zone, ZoneProjectile, Zoning}
import net.psforever.packet.PlanetSideGamePacket
import net.psforever.packet.game.objectcreate.ObjectClass
-import net.psforever.packet.game.{ActionCancelMessage, ActionResultMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, BindStatus, BugReportMessage, ChangeFireModeMessage, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestAction, CharacterRequestMessage, ChatMsg, CollisionIs, ConnectToWorldRequestMessage, CreateShortcutMessage, DeadState, DeployObjectMessage, DisplayedAwardMessage, DropItemMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FriendsRequest, GenericAction, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, ImplantAction, InvalidTerrainMessage, ItemTransactionMessage, LootItemMessage, MoveItemMessage, ObjectDeleteMessage, ObjectDetectedMessage, ObjectHeldMessage, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, PlayerStateShiftMessage, RequestDestroyMessage, SetChatFilterMessage, ShiftState, TargetInfo, TargetingImplantRequest, TargetingInfoMessage, TerrainCondition, TradeMessage, UnuseItemMessage, UseItemMessage, VoiceHostInfo, VoiceHostKill, VoiceHostRequest, ZipLineMessage}
+import net.psforever.packet.game.{ActionCancelMessage, ActionResultMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, BindStatus, BugReportMessage, ChangeFireModeMessage, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestAction, CharacterRequestMessage, ChatMsg, CollisionIs, ConnectToWorldRequestMessage, CreateShortcutMessage, DeadState, DeployObjectMessage, DisplayedAwardMessage, DropItemMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FriendsRequest, GenericAction, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, ImplantAction, InvalidTerrainMessage, ItemTransactionMessage, LootItemMessage, MoveItemMessage, ObjectDeleteMessage, ObjectDetectedMessage, ObjectHeldMessage, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, PlayerStateShiftMessage, RequestDestroyMessage, ShiftState, TargetInfo, TargetingImplantRequest, TargetingInfoMessage, TerrainCondition, TradeMessage, UnuseItemMessage, UseItemMessage, VoiceHostInfo, VoiceHostKill, VoiceHostRequest, ZipLineMessage}
import net.psforever.services.RemoverActor
import net.psforever.services.account.{AccountPersistenceService, RetrieveAccountData}
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import net.psforever.services.local.support.CaptureFlagManager
-import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
import net.psforever.types.{CapacitorStateType, ChatMessageType, Cosmetic, DriveState, ExoSuitType, ImplantType, PlanetSideEmpire, PlanetSideGUID, SpawnGroup, TransactionType, Vector3}
import net.psforever.util.Config
@@ -62,8 +61,6 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor
- private val chatActor: typed.ActorRef[ChatActor.Command] = ops.chatActor
-
def handleConnectToWorldRequest(pkt: ConnectToWorldRequestMessage): Unit = {
val ConnectToWorldRequestMessage(_, token, majorVersion, minorVersion, revision, buildDate, _, _) = pkt
log.trace(
@@ -192,14 +189,6 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
player.zoneInteractions()
}
- def handleChat(pkt: ChatMsg): Unit = {
- chatActor ! ChatActor.Message(pkt)
- }
-
- def handleChatFilter(pkt: SetChatFilterMessage): Unit = {
- val SetChatFilterMessage(_, _, _) = pkt
- }
-
def handleVoiceHostRequest(pkt: VoiceHostRequest): Unit = {
log.debug(s"$pkt")
sendResponse(VoiceHostKill())
diff --git a/src/main/scala/net/psforever/actors/session/normal/NormalMode.scala b/src/main/scala/net/psforever/actors/session/normal/NormalMode.scala
index 217f19533..bf1874762 100644
--- a/src/main/scala/net/psforever/actors/session/normal/NormalMode.scala
+++ b/src/main/scala/net/psforever/actors/session/normal/NormalMode.scala
@@ -3,10 +3,9 @@ package net.psforever.actors.session.normal
import akka.actor.Actor.Receive
import akka.actor.ActorRef
-import net.psforever.actors.session.ChatActor
-import net.psforever.actors.session.support.{GeneralFunctions, LocalHandlerFunctions, MountHandlerFunctions, SquadHandlerFunctions, TerminalHandlerFunctions, VehicleFunctions, VehicleHandlerFunctions, WeaponAndProjectileFunctions}
-import net.psforever.objects.Session
+import net.psforever.actors.session.support.{ChatFunctions, GeneralFunctions, LocalHandlerFunctions, MountHandlerFunctions, SquadHandlerFunctions, TerminalHandlerFunctions, VehicleFunctions, VehicleHandlerFunctions, WeaponAndProjectileFunctions}
import net.psforever.packet.game.UplinkRequest
+import net.psforever.services.chat.ChatService
//
import net.psforever.actors.session.{AvatarActor, SessionActor}
import net.psforever.actors.session.support.{ModeLogic, PlayerMode, SessionData, ZoningOperations}
@@ -34,6 +33,7 @@ import net.psforever.util.Config
class NormalModeLogic(data: SessionData) extends ModeLogic {
val avatarResponse: AvatarHandlerLogic = AvatarHandlerLogic(data.avatarResponse)
+ val chat: ChatFunctions = ChatLogic(data.chat)
val galaxy: GalaxyHandlerLogic = GalaxyHandlerLogic(data.galaxyResponseHandlers)
val general: GeneralFunctions = GeneralLogic(data.general)
val local: LocalHandlerFunctions = LocalHandlerLogic(data.localResponse)
@@ -44,11 +44,6 @@ class NormalModeLogic(data: SessionData) extends ModeLogic {
val vehicles: VehicleFunctions = VehicleLogic(data.vehicles)
val vehicleResponse: VehicleHandlerFunctions = VehicleHandlerLogic(data.vehicleResponseOperations)
- override def switchTo(session: Session): Unit = {
- data.general.chatActor ! ChatActor.SetMode("normal")
- super.switchTo(session)
- }
-
def parse(sender: ActorRef): Receive = {
/* really common messages (very frequently, every life) */
case packet: PlanetSideGamePacket =>
@@ -75,6 +70,9 @@ class NormalModeLogic(data: SessionData) extends ModeLogic {
case VehicleServiceResponse(toChannel, guid, reply) =>
vehicleResponse.handle(toChannel, guid, reply)
+ case ChatService.MessageResponse(fromSession, message, _) =>
+ chat.handleIncomingMessage(data.session, message, fromSession)
+
case SessionActor.SendResponse(packet) =>
data.sendResponse(packet)
@@ -315,10 +313,10 @@ class NormalModeLogic(data: SessionData) extends ModeLogic {
data.zoning.spawn.handleSpawnRequest(packet)
case packet: ChatMsg =>
- general.handleChat(packet)
+ chat.handleChatMsg(data.session, packet)
case packet: SetChatFilterMessage =>
- general.handleChatFilter(packet)
+ chat.handleChatFilter(packet)
case packet: VoiceHostRequest =>
general.handleVoiceHostRequest(packet)
diff --git a/src/main/scala/net/psforever/actors/session/normal/SquadHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/normal/SquadHandlerLogic.scala
index 643f97958..9197211f8 100644
--- a/src/main/scala/net/psforever/actors/session/normal/SquadHandlerLogic.scala
+++ b/src/main/scala/net/psforever/actors/session/normal/SquadHandlerLogic.scala
@@ -3,12 +3,12 @@ package net.psforever.actors.session.normal
import akka.actor.{ActorContext, ActorRef, typed}
import net.psforever.actors.session.support.SessionSquadHandlers.SquadUIElement
-import net.psforever.actors.session.{AvatarActor, ChatActor}
+import net.psforever.actors.session.AvatarActor
import net.psforever.actors.session.support.{SessionData, SessionSquadHandlers, SquadHandlerFunctions}
import net.psforever.objects.{Default, LivePlayerList}
import net.psforever.objects.avatar.Avatar
import net.psforever.packet.game.{CharacterKnowledgeInfo, CharacterKnowledgeMessage, ChatMsg, MemberEvent, PlanetsideAttributeMessage, ReplicationStreamMessage, SquadAction, SquadDefinitionActionMessage, SquadDetailDefinitionUpdateMessage, SquadListing, SquadMemberEvent, SquadMembershipRequest, SquadMembershipResponse, SquadState, SquadStateInfo, SquadWaypointEvent, SquadWaypointRequest, WaypointEvent, WaypointEventAction}
-import net.psforever.services.chat.ChatService
+import net.psforever.services.chat.SquadChannel
import net.psforever.services.teamwork.{SquadResponse, SquadServiceMessage, SquadAction => SquadServiceAction}
import net.psforever.types.{ChatMessageType, PlanetSideGUID, SquadListDecoration, SquadResponseType, WaypointSubtype}
@@ -23,8 +23,6 @@ class SquadHandlerLogic(val ops: SessionSquadHandlers, implicit val context: Act
private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor
- private val chatActor: typed.ActorRef[ChatActor.Command] = ops.chatActor
-
private val squadService: ActorRef = ops.squadService
private var waypointCooldown: Long = 0L
@@ -194,8 +192,8 @@ class SquadHandlerLogic(val ops: SessionSquadHandlers, implicit val context: Act
sendResponse(SquadDefinitionActionMessage(squad.GUID, 0, SquadAction.Unknown(18)))
squadService ! SquadServiceMessage(player, continent, SquadServiceAction.ReloadDecoration())
ops.updateSquadRef = ref
- ops. updateSquad = ops.PeriodicUpdatesWhenEnrolledInSquad
- chatActor ! ChatActor.JoinChannel(ChatService.ChatChannel.Squad(squad.GUID))
+ ops.updateSquad = ops.PeriodicUpdatesWhenEnrolledInSquad
+ sessionLogic.chat.JoinChannel(SquadChannel(squad.GUID))
case _ =>
//other player is joining our squad
//load each member's entry
@@ -245,7 +243,7 @@ class SquadHandlerLogic(val ops: SessionSquadHandlers, implicit val context: Act
ops.squad_supplement_id = 0
ops.squadUpdateCounter = 0
ops.updateSquad = ops.NoSquadUpdates
- chatActor ! ChatActor.LeaveChannel(ChatService.ChatChannel.Squad(squad.GUID))
+ sessionLogic.chat.LeaveChannel(SquadChannel(squad.GUID))
case _ =>
//remove each member's entry
ops.GiveSquadColorsToMembers(
diff --git a/src/main/scala/net/psforever/actors/session/spectator/ChatLogic.scala b/src/main/scala/net/psforever/actors/session/spectator/ChatLogic.scala
new file mode 100644
index 000000000..e0c16c7c4
--- /dev/null
+++ b/src/main/scala/net/psforever/actors/session/spectator/ChatLogic.scala
@@ -0,0 +1,143 @@
+// Copyright (c) 2024 PSForever
+package net.psforever.actors.session.spectator
+
+import akka.actor.ActorContext
+import net.psforever.actors.session.SessionActor
+import net.psforever.actors.session.normal.NormalMode
+import net.psforever.actors.session.support.{ChatFunctions, ChatOperations, SessionData}
+import net.psforever.objects.Session
+import net.psforever.packet.game.{ChatMsg, SetChatFilterMessage}
+import net.psforever.services.chat.SpectatorChannel
+import net.psforever.types.ChatMessageType
+
+import scala.collection.Seq
+
+object ChatLogic {
+ def apply(ops: ChatOperations): ChatLogic = {
+ new ChatLogic(ops, ops.context)
+ }
+}
+
+class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) extends ChatFunctions {
+ def sessionLogic: SessionData = ops.sessionLogic
+
+ def handleChatMsg(session: Session, message: ChatMsg): Unit = {
+ import ChatMessageType._
+ (message.messageType, message.recipient.trim, message.contents.trim) match {
+ /** Messages starting with ! are custom chat commands */
+ case (_, _, contents) if contents.startsWith("!") &&
+ customCommandMessages(message, session) => ()
+
+ case (CMT_FLY, recipient, contents) =>
+ ops.commandFly(contents, recipient)
+
+ case (CMT_ANONYMOUS, _, _) =>
+ // ?
+
+ case (CMT_TOGGLE_GM, _, _) =>
+ // ?
+
+ case (CMT_CULLWATERMARK, _, contents) =>
+ ops.commandWatermark(contents)
+
+ case (CMT_SPEED, _, contents) =>
+ ops.commandSpeed(message, contents)
+
+ case (CMT_TOGGLESPECTATORMODE, _, contents) =>
+ commandToggleSpectatorMode(contents)
+
+ case (CMT_RECALL, _, _) =>
+ commandToggleSpectatorMode(contents = "off")
+
+ case (CMT_QUIT, _, _) =>
+ ops.commandQuit(session)
+
+ case (CMT_SUICIDE, _, _) =>
+ commandToggleSpectatorMode(contents = "off")
+
+ case (CMT_OPEN, _, _) =>
+ ops.commandSendToRecipient(session, message, SpectatorChannel)
+
+ case (CMT_VOICE, _, contents) =>
+ ops.commandVoice(session, message, contents, SpectatorChannel)
+
+ case (CMT_TELL, _, _) =>
+ ops.commandTellOrIgnore(session, message, SpectatorChannel)
+
+ case (CMT_BROADCAST, _, _) =>
+ ops.commandSendToRecipient(session, message, SpectatorChannel)
+
+ case (CMT_PLATOON, _, _) =>
+ ops.commandSendToRecipient(session, message, SpectatorChannel)
+
+ case (CMT_GMTELL, _, _) =>
+ ops.commandSend(session, message, SpectatorChannel)
+
+ case (CMT_NOTE, _, _) =>
+ ops.commandSend(session, message, SpectatorChannel)
+
+ case (CMT_WHO | CMT_WHO_CSR | CMT_WHO_CR | CMT_WHO_PLATOONLEADERS | CMT_WHO_SQUADLEADERS | CMT_WHO_TEAMS, _, _) =>
+ ops.commandWho(session)
+
+ case (CMT_ZONE, _, contents) =>
+ ops.commandZone(message, contents)
+
+ case (CMT_WARP, _, contents) =>
+ ops.commandWarp(session, message, contents)
+
+ case _ => ()
+ }
+ }
+
+ def handleChatFilter(pkt: SetChatFilterMessage): Unit = {
+ val SetChatFilterMessage(_, _, _) = pkt
+ }
+
+ def handleIncomingMessage(session: Session, message: ChatMsg, fromSession: Session): Unit = {
+ import ChatMessageType._
+ message.messageType match {
+ case CMT_BROADCAST | CMT_SQUAD | CMT_PLATOON | CMT_COMMAND | CMT_NOTE =>
+ ops.commandIncomingSendAllIfOnline(session, message)
+
+ case CMT_OPEN =>
+ ops.commandIncomingSendToLocalIfOnline(session, fromSession, message)
+
+ case CMT_TELL | U_CMT_TELLFROM |
+ CMT_GMOPEN | CMT_GMBROADCAST | CMT_GMBROADCAST_NC | CMT_GMBROADCAST_TR | CMT_GMBROADCAST_VS |
+ CMT_GMBROADCASTPOPUP | CMT_GMTELL | U_CMT_GMTELLFROM | UNK_45 | UNK_71 | UNK_227 | UNK_229 =>
+ ops.commandIncomingSend(message)
+
+ case _ => ()
+ }
+ }
+
+ private def customCommandMessages(
+ message: ChatMsg,
+ session: Session
+ ): Boolean = {
+ val contents = message.contents
+ if (contents.startsWith("!")) {
+ val (command, params) = ops.cliTokenization(contents.drop(1)) match {
+ case a :: b => (a, b)
+ case _ => ("", Seq(""))
+ }
+ command match {
+ case "list" => ops.customCommandList(session, params, message)
+ case "nearby" => ops.customCommandNearby(session)
+ case "loc" => ops.customCommandLoc(session, message)
+ case "macro" => ops.customCommandMacro(session, params)
+ case _ => false
+ }
+ } else {
+ false
+ }
+ }
+
+ private def commandToggleSpectatorMode(contents: String): Unit = {
+ contents.toLowerCase() match {
+ case "off" | "of" =>
+ context.self ! SessionActor.SetMode(NormalMode)
+ case _ => ()
+ }
+ }
+}
diff --git a/src/main/scala/net/psforever/actors/session/spectator/GeneralLogic.scala b/src/main/scala/net/psforever/actors/session/spectator/GeneralLogic.scala
index f40f3e9a1..e749a9a30 100644
--- a/src/main/scala/net/psforever/actors/session/spectator/GeneralLogic.scala
+++ b/src/main/scala/net/psforever/actors/session/spectator/GeneralLogic.scala
@@ -2,7 +2,7 @@
package net.psforever.actors.session.spectator
import akka.actor.{ActorContext, typed}
-import net.psforever.actors.session.{AvatarActor, ChatActor}
+import net.psforever.actors.session.AvatarActor
import net.psforever.actors.session.support.{GeneralFunctions, GeneralOperations, SessionData}
import net.psforever.login.WorldSession.RemoveOldEquipmentFromInventory
import net.psforever.objects.{Account, BoomerDeployable, BoomerTrigger, GlobalDefinitions, LivePlayerList, PlanetSideGameObject, Player, TelepadDeployable, Tool, Vehicle}
@@ -21,7 +21,7 @@ import net.psforever.objects.vehicles.Utility.InternalTelepad
import net.psforever.objects.vital.{VehicleDismountActivity, VehicleMountActivity}
import net.psforever.objects.zones.{Zone, ZoneProjectile}
import net.psforever.packet.PlanetSideGamePacket
-import net.psforever.packet.game.{ActionCancelMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, BugReportMessage, ChangeFireModeMessage, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestMessage, ChatMsg, ConnectToWorldRequestMessage, CreateShortcutMessage, DeployObjectMessage, DisplayedAwardMessage, DropItemMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FriendsRequest, GenericAction, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, ImplantAction, InvalidTerrainMessage, LootItemMessage, MoveItemMessage, ObjectDeleteMessage, ObjectDetectedMessage, ObjectHeldMessage, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, PlayerStateShiftMessage, RequestDestroyMessage, SetChatFilterMessage, ShiftState, TargetInfo, TargetingImplantRequest, TargetingInfoMessage, TradeMessage, UnuseItemMessage, UseItemMessage, VoiceHostInfo, VoiceHostKill, VoiceHostRequest, ZipLineMessage}
+import net.psforever.packet.game.{ActionCancelMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, BugReportMessage, ChangeFireModeMessage, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestMessage, ChatMsg, ConnectToWorldRequestMessage, CreateShortcutMessage, DeployObjectMessage, DisplayedAwardMessage, DropItemMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FriendsRequest, GenericAction, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, ImplantAction, InvalidTerrainMessage, LootItemMessage, MoveItemMessage, ObjectDeleteMessage, ObjectDetectedMessage, ObjectHeldMessage, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, PlayerStateShiftMessage, RequestDestroyMessage, ShiftState, TargetInfo, TargetingImplantRequest, TargetingInfoMessage, TradeMessage, UnuseItemMessage, UseItemMessage, VoiceHostInfo, VoiceHostKill, VoiceHostRequest, ZipLineMessage}
import net.psforever.services.RemoverActor
import net.psforever.services.account.AccountPersistenceService
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
@@ -40,8 +40,6 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor
- private val chatActor: typed.ActorRef[ChatActor.Command] = ops.chatActor
-
private var customImplants = SpectatorModeLogic.SpectatorImplants.map(_.get)
def handleConnectToWorldRequest(pkt: ConnectToWorldRequestMessage): Unit = { /* intentionally blank */ }
@@ -85,12 +83,6 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
}
}
- def handleChat(pkt: ChatMsg): Unit = {
- chatActor ! ChatActor.Message(pkt)
- }
-
- def handleChatFilter(pkt: SetChatFilterMessage): Unit = { /* intentionally blank */ }
-
def handleVoiceHostRequest(pkt: VoiceHostRequest): Unit = {
log.debug(s"$pkt")
sendResponse(VoiceHostKill())
diff --git a/src/main/scala/net/psforever/actors/session/spectator/SpectatorMode.scala b/src/main/scala/net/psforever/actors/session/spectator/SpectatorMode.scala
index 1120a2265..b261538c7 100644
--- a/src/main/scala/net/psforever/actors/session/spectator/SpectatorMode.scala
+++ b/src/main/scala/net/psforever/actors/session/spectator/SpectatorMode.scala
@@ -3,8 +3,7 @@ package net.psforever.actors.session.spectator
import akka.actor.Actor.Receive
import akka.actor.ActorRef
-import net.psforever.actors.session.ChatActor
-import net.psforever.actors.session.support.{AvatarHandlerFunctions, GalaxyHandlerFunctions, GeneralFunctions, LocalHandlerFunctions, MountHandlerFunctions, SquadHandlerFunctions, TerminalHandlerFunctions, VehicleFunctions, VehicleHandlerFunctions, WeaponAndProjectileFunctions}
+import net.psforever.actors.session.support.{AvatarHandlerFunctions, ChatFunctions, GalaxyHandlerFunctions, GeneralFunctions, LocalHandlerFunctions, MountHandlerFunctions, SquadHandlerFunctions, TerminalHandlerFunctions, VehicleFunctions, VehicleHandlerFunctions, WeaponAndProjectileFunctions}
import net.psforever.actors.zone.ZoneActor
import net.psforever.objects.avatar.{BattleRank, CommandRank, DeployableToolbox, FirstTimeEvents, Implant, ProgressDecoration, Shortcut => AvatarShortcut}
import net.psforever.objects.serverobject.ServerObject
@@ -13,6 +12,7 @@ import net.psforever.packet.PlanetSidePacket
import net.psforever.packet.game.{ObjectCreateDetailedMessage, ObjectDeleteMessage}
import net.psforever.packet.game.objectcreate.{ObjectClass, ObjectCreateMessageParent, RibbonBars}
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
+import net.psforever.services.chat.{ChatService, SpectatorChannel}
import net.psforever.services.teamwork.{SquadAction, SquadServiceMessage}
import net.psforever.types.{CapacitorStateType, ChatMessageType, ExoSuitType, MeritCommendation, SquadRequestType}
//
@@ -41,6 +41,7 @@ import net.psforever.services.vehicle.VehicleServiceResponse
class SpectatorModeLogic(data: SessionData) extends ModeLogic {
val avatarResponse: AvatarHandlerFunctions = AvatarHandlerLogic(data.avatarResponse)
+ val chat: ChatFunctions = ChatLogic(data.chat)
val galaxy: GalaxyHandlerFunctions = GalaxyHandlerLogic(data.galaxyResponseHandlers)
val general: GeneralFunctions = GeneralLogic(data.general)
val local: LocalHandlerFunctions = LocalHandlerLogic(data.localResponse)
@@ -115,9 +116,12 @@ class SpectatorModeLogic(data: SessionData) extends ModeLogic {
val originalEvent = player.History.headOption
player.ClearHistory()
player.LogActivity(originalEvent)
- player.spectator = true
//
- data.general.chatActor ! ChatActor.SetMode("spectator")
+ player.spectator = true
+ if (player.silenced) {
+ data.chat.commandIncomingSilence(session, ChatMsg(ChatMessageType.CMT_SILENCE, "player 0"))
+ }
+ data.chat.JoinChannel(SpectatorChannel)
val newPlayer = SpectatorModeLogic.spectatorCharacter(player)
val cud = new SimpleItem(GlobalDefinitions.command_detonater)
cud.GUID = player.avatar.locker.GUID
@@ -145,6 +149,7 @@ class SpectatorModeLogic(data: SessionData) extends ModeLogic {
val zoning = data.zoning
val sendResponse: PlanetSidePacket => Unit = data.sendResponse
//
+ data.chat.LeaveChannel(SpectatorChannel)
player.spectator = false
sendResponse(ObjectDeleteMessage(player.avatar.locker.GUID, 0)) //free up the slot (from cud)
sendResponse(ChatMsg(ChatMessageType.CMT_TOGGLESPECTATORMODE, "off"))
@@ -178,6 +183,9 @@ class SpectatorModeLogic(data: SessionData) extends ModeLogic {
case VehicleServiceResponse(toChannel, guid, reply) =>
vehicleResponse.handle(toChannel, guid, reply)
+ case ChatService.MessageResponse(fromSession, message, _) =>
+ chat.handleIncomingMessage(data.session, message, fromSession)
+
case SessionActor.SendResponse(packet) =>
data.sendResponse(packet)
@@ -409,10 +417,10 @@ class SpectatorModeLogic(data: SessionData) extends ModeLogic {
data.zoning.spawn.handleSpawnRequest(packet)
case packet: ChatMsg =>
- general.handleChat(packet)
+ chat.handleChatMsg(data.session, packet)
case packet: SetChatFilterMessage =>
- general.handleChatFilter(packet)
+ chat.handleChatFilter(packet)
case packet: VoiceHostRequest =>
general.handleVoiceHostRequest(packet)
diff --git a/src/main/scala/net/psforever/actors/session/spectator/SquadHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/spectator/SquadHandlerLogic.scala
index fae35e07e..fae3a6817 100644
--- a/src/main/scala/net/psforever/actors/session/spectator/SquadHandlerLogic.scala
+++ b/src/main/scala/net/psforever/actors/session/spectator/SquadHandlerLogic.scala
@@ -3,12 +3,12 @@ package net.psforever.actors.session.spectator
import akka.actor.{ActorContext, typed}
import net.psforever.actors.session.support.SessionSquadHandlers.SquadUIElement
-import net.psforever.actors.session.{AvatarActor, ChatActor}
+import net.psforever.actors.session.AvatarActor
import net.psforever.actors.session.support.{SessionData, SessionSquadHandlers, SquadHandlerFunctions}
import net.psforever.objects.{Default, LivePlayerList}
import net.psforever.objects.avatar.Avatar
import net.psforever.packet.game.{CharacterKnowledgeInfo, CharacterKnowledgeMessage, PlanetsideAttributeMessage, ReplicationStreamMessage, SquadAction, SquadDefinitionActionMessage, SquadDetailDefinitionUpdateMessage, SquadListing, SquadMemberEvent, SquadMembershipRequest, SquadMembershipResponse, SquadState, SquadStateInfo, SquadWaypointEvent, SquadWaypointRequest, WaypointEventAction}
-import net.psforever.services.chat.ChatService
+import net.psforever.services.chat.SquadChannel
import net.psforever.services.teamwork.SquadResponse
import net.psforever.types.{PlanetSideGUID, SquadListDecoration, SquadResponseType}
@@ -23,10 +23,6 @@ class SquadHandlerLogic(val ops: SessionSquadHandlers, implicit val context: Act
private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor
- private val chatActor: typed.ActorRef[ChatActor.Command] = ops.chatActor
-
- //private val squadService: ActorRef = ops.squadService
-
/* packet */
def handleSquadDefinitionAction(pkt: SquadDefinitionActionMessage): Unit = { /* intentionally blank */ }
@@ -122,7 +118,7 @@ class SquadHandlerLogic(val ops: SessionSquadHandlers, implicit val context: Act
ops.squad_supplement_id = 0
ops.squadUpdateCounter = 0
ops.updateSquad = ops.NoSquadUpdates
- chatActor ! ChatActor.LeaveChannel(ChatService.ChatChannel.Squad(squad.GUID))
+ sessionLogic.chat.LeaveChannel(SquadChannel(squad.GUID))
case _ =>
//remove each member's entry
ops.GiveSquadColorsToMembers(
diff --git a/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala b/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala
new file mode 100644
index 000000000..520515465
--- /dev/null
+++ b/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala
@@ -0,0 +1,1292 @@
+// Copyright (c) 2024 PSForever
+package net.psforever.actors.session.support
+
+import akka.actor.Cancellable
+import akka.actor.typed.ActorRef
+import akka.actor.{ActorContext, typed}
+import net.psforever.actors.session.{AvatarActor, SessionActor}
+import net.psforever.actors.session.normal.{NormalMode => SessionNormalMode}
+import net.psforever.actors.session.spectator.{SpectatorMode => SessionSpectatorMode}
+import net.psforever.actors.zone.ZoneActor
+import net.psforever.objects.sourcing.PlayerSource
+import net.psforever.objects.zones.ZoneInfo
+import net.psforever.packet.game.SetChatFilterMessage
+import net.psforever.services.chat.{DefaultChannel, SquadChannel}
+import net.psforever.services.local.{LocalAction, LocalServiceMessage}
+import net.psforever.types.ChatMessageType.CMT_QUIT
+import org.log4s.Logger
+
+import scala.annotation.unused
+import scala.collection.{Seq, mutable}
+import scala.concurrent.duration._
+//
+import net.psforever.actors.zone.BuildingActor
+import net.psforever.login.WorldSession
+import net.psforever.objects.{Default, Player, Session}
+import net.psforever.objects.avatar.{BattleRank, Certification, CommandRank, Shortcut => AvatarShortcut}
+import net.psforever.objects.definition.ImplantDefinition
+import net.psforever.objects.serverobject.pad.{VehicleSpawnControl, VehicleSpawnPad}
+import net.psforever.objects.serverobject.resourcesilo.ResourceSilo
+import net.psforever.objects.serverobject.structures.{Amenity, Building}
+import net.psforever.objects.serverobject.turret.{FacilityTurret, TurretUpgrade, WeaponTurrets}
+import net.psforever.objects.zones.Zoning
+import net.psforever.packet.game.objectcreate.DrawnSlot
+import net.psforever.packet.game.{ChatMsg, CreateShortcutMessage, DeadState, RequestDestroyMessage, Shortcut}
+import net.psforever.services.{CavernRotationService, InterstellarClusterService}
+import net.psforever.services.chat.ChatService
+import net.psforever.services.chat.ChatChannel
+import net.psforever.types.ChatMessageType.{CMT_GMOPEN, UNK_227, UNK_229}
+import net.psforever.types.{ChatMessageType, Cosmetic, ExperienceType, ImplantType, PlanetSideEmpire, PlanetSideGUID, Vector3}
+import net.psforever.util.{Config, PointOfInterest}
+import net.psforever.zones.Zones
+
+trait ChatFunctions extends CommonSessionInterfacingFunctionality {
+ def ops: ChatOperations
+
+ def handleChatMsg(session: Session, message: ChatMsg): Unit
+
+ def handleChatFilter(pkt: SetChatFilterMessage): Unit
+
+ def handleIncomingMessage(session: Session, message: ChatMsg, fromSession: Session): Unit
+}
+
+class ChatOperations(
+ val sessionLogic: SessionData,
+ val avatarActor: typed.ActorRef[AvatarActor.Command],
+ val chatService: typed.ActorRef[ChatService.Command],
+ val cluster: typed.ActorRef[InterstellarClusterService.Command],
+ implicit val context: ActorContext
+ ) extends CommonSessionInterfacingFunctionality {
+ private var channels: List[ChatChannel] = List()
+ private var silenceTimer: Cancellable = Default.Cancellable
+ /**
+ * when another player is listed as one of our ignored players,
+ * and that other player sends an emote,
+ * that player is assigned a cooldown and only one emote per period will be seen
+ * key - character unique avatar identifier, value - when the current cooldown period will end
+ */
+ private val ignoredEmoteCooldown: mutable.LongMap[Long] = mutable.LongMap[Long]()
+
+ import akka.actor.typed.scaladsl.adapter._
+ private val chatServiceAdapter: ActorRef[ChatService.MessageResponse] = context.self.toTyped[ChatService.MessageResponse]
+
+ def JoinChannel(channel: ChatChannel): Unit = {
+ chatService ! ChatService.JoinChannel(chatServiceAdapter, sessionLogic, channel)
+ channels ++= List(channel)
+ }
+
+ def LeaveChannel(channel: ChatChannel): Unit = {
+ chatService ! ChatService.LeaveChannel(chatServiceAdapter, channel)
+ channels = channels.filterNot(_ == channel)
+ }
+
+ def commandFly(contents: String, recipient: String): Unit = {
+ val (token, flying) = contents match {
+ case "on" => (contents, true)
+ case "off" => (contents, false)
+ case _ => ("off", false)
+ }
+ context.self ! SessionActor.SetFlying(flying)
+ sendResponse(ChatMsg(ChatMessageType.CMT_FLY, wideContents=false, recipient, token, None))
+ }
+
+ def commandWatermark(contents: String): Unit = {
+ val connectionState =
+ if (contents.contains("40 80")) 100
+ else if (contents.contains("120 200")) 25
+ else 50
+ context.self ! SessionActor.SetConnectionState(connectionState)
+ }
+
+ def commandSpeed(message: ChatMsg, contents: String): Unit = {
+ val speed =
+ try {
+ contents.toFloat
+ } catch {
+ case _: Throwable =>
+ 1f
+ }
+ context.self ! SessionActor.SetSpeed(speed)
+ sendResponse(message.copy(contents = f"$speed%.3f"))
+ }
+
+ def commandToggleSpectatorMode(session: Session, contents: String): Unit = {
+ val currentSpectatorActivation = session.player.spectator
+ contents.toLowerCase() match {
+ case "on" | "o" | "" if !currentSpectatorActivation =>
+ context.self ! SessionActor.SetMode(SessionSpectatorMode)
+ case "off" | "of" if currentSpectatorActivation =>
+ context.self ! SessionActor.SetMode(SessionNormalMode)
+ case _ => ()
+ }
+ }
+
+ def commandRecall(session: Session): Unit = {
+ val player = session.player
+ 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 == Zones.sanctuaryZoneId(player.Faction) =>
+ Some("You can't recall to your sanctuary when you are already in your sanctuary")
+ case _ if !player.isAlive || session.deadState != DeadState.Alive =>
+ Some(if (player.isAlive) "@norecall_deconstructing" else "@norecall_dead")
+ case _ if player.VehicleSeated.nonEmpty =>
+ Some("@norecall_invehicle")
+ case _ =>
+ None
+ }
+ errorMessage match {
+ case Some(errorMessage) =>
+ sendResponse(ChatMsg(CMT_QUIT, errorMessage))
+ case None =>
+ context.self ! SessionActor.Recall()
+ }
+ }
+
+ def commandInstantAction(session: Session): Unit = {
+ val player = session.player
+ if (session.zoningType == Zoning.Method.Quit) {
+ sendResponse(ChatMsg(CMT_QUIT, "You can't instant action while quitting."))
+ } else if (session.zoningType == Zoning.Method.InstantAction) {
+ sendResponse(ChatMsg(CMT_QUIT, "@noinstantaction_instantactionting"))
+ } else if (session.zoningType == Zoning.Method.Recall) {
+ sendResponse(
+ ChatMsg(CMT_QUIT, "You won't instant action. You already requested to recall to your sanctuary continent")
+ )
+ } else if (!player.isAlive || session.deadState != DeadState.Alive) {
+ if (player.isAlive) {
+ sendResponse(ChatMsg(CMT_QUIT, "@noinstantaction_deconstructing"))
+ } else {
+ sendResponse(ChatMsg(CMT_QUIT, "@noinstantaction_dead"))
+ }
+ } else if (player.VehicleSeated.nonEmpty) {
+ sendResponse(ChatMsg(CMT_QUIT, "@noinstantaction_invehicle"))
+ } else {
+ context.self ! SessionActor.InstantAction()
+ }
+ }
+
+ def commandQuit(session: Session): Unit = {
+ val player = session.player
+ if (session.zoningType == Zoning.Method.Quit) {
+ sendResponse(ChatMsg(CMT_QUIT, "@noquit_quitting"))
+ } else if (!player.isAlive || session.deadState != DeadState.Alive) {
+ if (player.isAlive) {
+ sendResponse(ChatMsg(CMT_QUIT, "@noquit_deconstructing"))
+ } else {
+ sendResponse(ChatMsg(CMT_QUIT, "@noquit_dead"))
+ }
+ } else if (player.VehicleSeated.nonEmpty) {
+ sendResponse(ChatMsg(CMT_QUIT, "@noquit_invehicle"))
+ } else {
+ context.self ! SessionActor.Quit()
+ }
+ }
+
+ def commandSuicide(session: Session): Unit = {
+ if (session.player.isAlive && session.deadState != DeadState.Release) {
+ context.self ! SessionActor.Suicide()
+ }
+ }
+
+ def commandDestroy(session: Session, message: ChatMsg, contents: String): Unit = {
+ 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 _ =>
+ // FIXME we shouldn't do it like that
+ context.self ! RequestDestroyMessage(PlanetSideGUID(guid))
+ }
+ sendResponse(message)
+ }
+
+ def commandSetBaseResources(session: Session, contents: String): Unit = {
+ val buffer = cliTokenization(contents)
+ val customNtuValue = buffer.lift(1) match {
+ case Some(x) if x.toIntOption.nonEmpty => Some(x.toInt)
+ case _ => None
+ }
+ val silos = {
+ val position = session.player.Position
+ session.zone.Buildings.values
+ .filter { building =>
+ val soi2 = building.Definition.SOIRadius * building.Definition.SOIRadius
+ Vector3.DistanceSquared(building.Position, position) < soi2
+ }
+ }
+ .flatMap { building => building.Amenities.filter { _.isInstanceOf[ResourceSilo] } }
+ setBaseResources(customNtuValue, silos, debugContent="")
+ }
+
+ def commandZoneLock(contents: String): Unit = {
+ val buffer = cliTokenization(contents)
+ val (zoneOpt, lockVal) = (buffer.lift(1), buffer.lift(2)) match {
+ case (Some(x), Some(y)) =>
+ val zone = if (x.toIntOption.nonEmpty) {
+ val xInt = x.toInt
+ Zones.zones.find(_.Number == xInt)
+ } else {
+ Zones.zones.find(z => z.id.equals(x))
+ }
+ val value = if (y.toIntOption.nonEmpty && y.toInt == 0) {
+ 0
+ } else {
+ 1
+ }
+ (zone, Some(value))
+ case _ =>
+ (None, None)
+ }
+ (zoneOpt, lockVal) match {
+ case (Some(zone), Some(lock)) if zone.map.cavern =>
+ //caverns must be rotated in an order
+ if (lock == 0) {
+ cluster ! InterstellarClusterService.CavernRotation(CavernRotationService.HurryRotationToZoneUnlock(zone.id))
+ } else {
+ cluster ! InterstellarClusterService.CavernRotation(CavernRotationService.HurryRotationToZoneLock(zone.id))
+ }
+ case (Some(_), Some(_)) =>
+ //normal zones can lock when all facilities and towers on it belong to the same faction
+ //normal zones can lock when ???
+ case _ => ()
+ }
+ }
+
+ def commandZoneRotate(): Unit = {
+ cluster ! InterstellarClusterService.CavernRotation(CavernRotationService.HurryNextRotation)
+ }
+
+ def commandCaptureBase(session: Session, message: ChatMsg, contents: String): Unit = {
+ val buffer = cliTokenization(contents).take(3)
+ //walk through the param buffer
+ val (foundFacilities, foundFacilitiesTag, factionBuffer) = firstParam(session, buffer, captureBaseParamFacilities)
+ val (foundFaction, foundFactionTag, timerBuffer) = firstParam(session, factionBuffer, captureBaseParamFaction)
+ val (foundTimer, foundTimerTag, _) = firstParam(session, timerBuffer, captureBaseParamTimer)
+ //resolve issues with the initial params
+ var facilityError: Int = 0
+ var factionError: Boolean = false
+ var timerError: Boolean = false
+ var usageMessage: Boolean = false
+ val resolvedFacilities = foundFacilities
+ .orElse {
+ if (foundFacilitiesTag.nonEmpty) {
+ if (foundFaction.isEmpty) {
+ /* /capturebase OR /capturebase */
+ //malformed facility tag error
+ facilityError = 2
+ None
+ } else if (!foundFacilitiesTag.contains("curr")) { //did we do this next check already
+ /* /capturebase , potentially */
+ val buildings = captureBaseCurrSoi(session)
+ if (buildings.nonEmpty) {
+ //convert facilities to faction
+ Some(buildings.toSeq)
+ } else {
+ //no facilities error
+ facilityError = 1
+ None
+ }
+ } else {
+ //no facilities error
+ facilityError = 1
+ None
+ }
+ } else {
+ //no params; post command usage reminder
+ usageMessage = true
+ None
+ }
+ }
+ val resolvedFaction = foundFaction
+ .orElse {
+ if (resolvedFacilities.nonEmpty) {
+ /* /capturebase OR /capturebase */
+ if (foundFactionTag.isEmpty || foundTimer.nonEmpty) {
+ //convert facilities to OUR PLAYER'S faction
+ Some(session.player.Faction)
+ } else {
+ //malformed faction tag error
+ factionError = true
+ None
+ }
+ } else {
+ //incorrect params; already posted an error message
+ None
+ }
+ }
+ val resolvedTimer = foundTimer
+ .orElse {
+ //todo stop command execution? post command usage reminder?
+ if (resolvedFaction.nonEmpty && foundTimerTag.nonEmpty) {
+ /* /capturebase > > */
+ //malformed timer tag error
+ timerError = true
+ None
+ } else {
+ //eh
+ Some(1)
+ }
+ }
+ //evaluate results
+ (resolvedFacilities, resolvedFaction, resolvedTimer) match {
+ case (Some(buildings), Some(faction), Some(_)) =>
+ buildings.foreach { building =>
+ //TODO implement timer
+ val terminal = building.CaptureTerminal.get
+ val zone = building.Zone
+ val zoneActor = zone.actor
+ val buildingActor = building.Actor
+ //clear any previous hack
+ if (building.CaptureTerminalIsHacked) {
+ zone.LocalEvents ! LocalServiceMessage(
+ zone.id,
+ LocalAction.ResecureCaptureTerminal(terminal, PlayerSource.Nobody)
+ )
+ }
+ //push any updates this might cause
+ zoneActor ! ZoneActor.ZoneMapUpdate()
+ //convert faction affiliation
+ buildingActor ! BuildingActor.SetFaction(faction)
+ buildingActor ! BuildingActor.AmenityStateChange(terminal, Some(false))
+ //push for map updates again
+ zoneActor ! ZoneActor.ZoneMapUpdate()
+ }
+ case _ =>
+ if (usageMessage) {
+ sendResponse(
+ message.copy(messageType = UNK_229, contents = "@CMT_CAPTUREBASE_usage")
+ )
+ } else {
+ val msg = if (facilityError == 1) { "can not contextually determine building target" }
+ else if (facilityError == 2) { s"\'${foundFacilitiesTag.get}\' is not a valid building name" }
+ else if (factionError) { s"\'${foundFactionTag.get}\' is not a valid faction designation" }
+ else if (timerError) { s"\'${foundTimerTag.get}\' is not a valid timer value" }
+ else { "malformed params; check usage" }
+ sendResponse(ChatMsg(UNK_229, wideContents=true, "", s"\\#FF4040ERROR - $msg", None))
+ }
+ }
+ }
+
+ def commandVoice(session: Session, message: ChatMsg, contents: String, toChannel: ChatChannel): Unit = {
+ // SH prefix are tactical voice macros only sent to squad
+ if (contents.startsWith("SH")) {
+ channels.foreach {
+ case _/*channel*/: SquadChannel =>
+ commandSendToRecipient(session, message, toChannel)
+ case _ => ()
+ }
+ } else {
+ commandSendToRecipient(session, message, toChannel)
+ }
+ }
+
+ def commandTellOrIgnore(session: Session, message: ChatMsg, toChannel: ChatChannel): Unit = {
+ if (AvatarActor.onlineIfNotIgnored(message.recipient, session.avatar.name)) {
+ commandSend(session, message, toChannel)
+ } else if (AvatarActor.getLiveAvatarForFunc(message.recipient, (_,_,_)=>{}).isEmpty) {
+ sendResponse(
+ ChatMsg(ChatMessageType.UNK_45, wideContents=false, "none", "@notell_target", None)
+ )
+ } else {
+ sendResponse(
+ ChatMsg(ChatMessageType.UNK_45, wideContents=false, "none", "@notell_ignore", None)
+ )
+ }
+ }
+
+ def commandSquad(session: Session, message: ChatMsg, toChannel: ChatChannel): Unit = {
+ channels.foreach {
+ case _/*channel*/: SquadChannel =>
+ commandSendToRecipient(session, message, toChannel)
+ case _ => ()
+ }
+ }
+
+ def commandWho(session: Session): Unit = {
+ 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)
+ if (popNC + popTR + popVS == 0) {
+ sendResponse(ChatMsg(ChatMessageType.CMT_WHO, "@Nomatches"))
+ } else {
+ val contName = session.zone.map.name
+ sendResponse(
+ ChatMsg(ChatMessageType.CMT_WHO, wideContents=true, "", "That command doesn't work for now, but : ", None)
+ )
+ sendResponse(
+ ChatMsg(ChatMessageType.CMT_WHO, wideContents=true, "", "NC online : " + popNC + " on " + contName, None)
+ )
+ sendResponse(
+ ChatMsg(ChatMessageType.CMT_WHO, wideContents=true, "", "TR online : " + popTR + " on " + contName, None)
+ )
+ sendResponse(
+ ChatMsg(ChatMessageType.CMT_WHO, wideContents=true, "", "VS online : " + popVS + " on " + contName, None)
+ )
+ }
+ }
+
+ def commandZone(message: ChatMsg, contents: String): Unit = {
+ val buffer = cliTokenization(contents)
+ val (zone, gate, list) = (buffer.headOption, 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) =>
+ sendResponse(ChatMsg(UNK_229, wideContents=true, "", PointOfInterest.list, None))
+ case (Some(zone), None, true) =>
+ sendResponse(
+ ChatMsg(UNK_229, wideContents=true, "", PointOfInterest.listWarpgates(zone), None)
+ )
+ case (Some(zone), Some(gate), false) =>
+ context.self ! SessionActor.SetZone(zone.zonename, gate)
+ case (_, None, false) =>
+ sendResponse(
+ ChatMsg(UNK_229, wideContents=true, "", "Gate id not defined (use '/zone -list')", None)
+ )
+ case (_, _, _) if buffer.isEmpty || buffer.headOption.contains("-help") =>
+ sendResponse(
+ message.copy(messageType = UNK_229, contents = "@CMT_ZONE_usage")
+ )
+ case _ => ()
+ }
+ }
+
+ def commandWarp(session: Session, message: ChatMsg, contents: String): Unit = {
+ val buffer = cliTokenization(contents)
+ val (coordinates, waypoint) = (buffer.headOption, 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) if waypoint.nonEmpty => (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
+ } =>
+ context.self ! SessionActor.SetPosition(Vector3(x.toFloat, y.toFloat, z.toFloat))
+ case (None, Some(waypoint)) if waypoint == "-list" =>
+ val zone = PointOfInterest.get(session.player.Zone.id)
+ zone match {
+ case Some(zone: PointOfInterest) =>
+ sendResponse(
+ ChatMsg(UNK_229, wideContents=true, "", PointOfInterest.listAll(zone), None)
+ )
+ case _ =>
+ sendResponse(
+ ChatMsg(UNK_229, wideContents=true, "", s"unknown player zone '${session.player.Zone.id}'", None)
+ )
+ }
+ case (None, Some(waypoint)) if waypoint != "-help" =>
+ PointOfInterest.getWarpLocation(session.zone.id, waypoint) match {
+ case Some(location) =>
+ context.self ! SessionActor.SetPosition(location)
+ case None =>
+ sendResponse(
+ ChatMsg(UNK_229, wideContents=true, "", s"unknown location '$waypoint'", None)
+ )
+ }
+ case _ =>
+ sendResponse(
+ message.copy(messageType = UNK_229, contents = "@CMT_WARP_usage")
+ )
+ }
+ }
+
+ def commandSetBattleRank(session: Session, message: ChatMsg, contents: String): Unit = {
+ if (!setBattleRank(session, cliTokenization(contents), AvatarActor.SetBep)) {
+ sendResponse(
+ message.copy(messageType = UNK_229, contents = "@CMT_SETBATTLERANK_usage")
+ )
+ }
+ }
+
+ def commandSetCommandRank(session: Session, message: ChatMsg, contents: String): Unit = {
+ if (!setCommandRank(contents, session)) {
+ sendResponse(
+ message.copy(messageType = UNK_229, contents = "@CMT_SETCOMMANDRANK_usage")
+ )
+ }
+ }
+
+ def commandAddBattleExperience(message: ChatMsg, contents: String): Unit = {
+ contents.toIntOption match {
+ case Some(bep) => avatarActor ! AvatarActor.AwardBep(bep, ExperienceType.Normal)
+ case None =>
+ sendResponse(
+ message.copy(messageType = UNK_229, contents = "@CMT_ADDBATTLEEXPERIENCE_usage")
+ )
+ }
+ }
+
+ def commandAddCommandExperience(message: ChatMsg, contents: String): Unit = {
+ contents.toIntOption match {
+ case Some(cep) => avatarActor ! AvatarActor.AwardCep(cep)
+ case None =>
+ sendResponse(
+ message.copy(messageType = UNK_229, contents = "@CMT_ADDCOMMANDEXPERIENCE_usage")
+ )
+ }
+ }
+
+ def commandToggleHat(session: Session, message: ChatMsg, contents: String): Unit = {
+ val cosmetics = session.avatar.decoration.cosmetics.getOrElse(Set())
+ val nextCosmetics = contents match {
+ case "off" =>
+ cosmetics.diff(Set(Cosmetic.BrimmedCap, Cosmetic.Beret))
+ case _ =>
+ if (cosmetics.contains(Cosmetic.BrimmedCap)) {
+ cosmetics.diff(Set(Cosmetic.BrimmedCap)) + Cosmetic.Beret
+ } else if (cosmetics.contains(Cosmetic.Beret)) {
+ cosmetics.diff(Set(Cosmetic.BrimmedCap, Cosmetic.Beret))
+ } else {
+ cosmetics + Cosmetic.BrimmedCap
+ }
+ }
+ val on = nextCosmetics.contains(Cosmetic.BrimmedCap) || nextCosmetics.contains(Cosmetic.Beret)
+ avatarActor ! AvatarActor.SetCosmetics(nextCosmetics)
+ sendResponse(
+ message.copy(
+ messageType = UNK_229,
+ contents = s"@CMT_TOGGLE_HAT_${if (on) "on" else "off"}"
+ )
+ )
+ }
+
+ def commandToggleCosmetics(session: Session, message: ChatMsg, contents: String): Unit = {
+ val cosmetics = session.avatar.decoration.cosmetics.getOrElse(Set())
+ val cosmetic = message.messageType match {
+ case ChatMessageType.CMT_HIDE_HELMET => Cosmetic.NoHelmet
+ case ChatMessageType.CMT_TOGGLE_SHADES => Cosmetic.Sunglasses
+ case ChatMessageType.CMT_TOGGLE_EARPIECE => Cosmetic.Earpiece
+ case _ => null
+ }
+ val on = contents match {
+ case "on" => true
+ case "off" => false
+ case _ => !cosmetics.contains(cosmetic)
+ }
+ avatarActor ! AvatarActor.SetCosmetics(
+ if (on) cosmetics + cosmetic
+ else cosmetics.diff(Set(cosmetic))
+ )
+ sendResponse(
+ message.copy(
+ messageType = UNK_229,
+ contents = s"@${message.messageType.toString}_${if (on) "on" else "off"}"
+ )
+ )
+ }
+
+ def commandAddCertification(session: Session, message: ChatMsg, contents: String): Unit = {
+ val certs = cliTokenization(contents).map(name => Certification.values.find(_.name == name))
+ val result = if (certs.nonEmpty) {
+ if (certs.contains(None)) {
+ s"@AckErrorCertifications"
+ } else {
+ avatarActor ! AvatarActor.SetCertifications(session.avatar.certifications ++ certs.flatten)
+ s"@AckSuccessCertifications"
+ }
+ } else {
+ if (session.avatar.certifications.size < Certification.values.size) {
+ avatarActor ! AvatarActor.SetCertifications(Certification.values.toSet)
+ } else {
+ avatarActor ! AvatarActor.SetCertifications(Certification.values.filter(_.cost == 0).toSet)
+ }
+ s"@AckSuccessCertifications"
+ }
+ sendResponse(message.copy(messageType = UNK_229, contents = result))
+ }
+
+ def commandKick(session: Session, message: ChatMsg, contents: String): Unit = {
+ val inputs = cliTokenization(contents)
+ inputs.headOption match {
+ case Some(input) =>
+ val determination: Player => Boolean = input.toLongOption match {
+ case Some(id) => _.CharId == id
+ case _ => _.Name.equals(input)
+ }
+ session.zone.LivePlayers
+ .find(determination)
+ .orElse(session.zone.Corpses.find(determination)) match {
+ case Some(player) =>
+ inputs.lift(1).map(_.toLongOption) match {
+ case Some(Some(time)) =>
+ context.self ! SessionActor.Kick(player, Some(time))
+ case _ =>
+ context.self ! SessionActor.Kick(player)
+ }
+ sendResponse(message.copy(messageType = UNK_229, recipient = "Server", contents = "@kick_i"))
+ case None =>
+ sendResponse(message.copy(messageType = UNK_229, recipient = "Server", contents = "@kick_o"))
+ }
+ case None =>
+ sendResponse(message.copy(messageType = UNK_229, recipient = "Server", contents = "@kick_o"))
+ }
+ }
+
+ def commandIncomingSendAllIfOnline(session: Session, message: ChatMsg): Unit = {
+ if (AvatarActor.onlineIfNotIgnored(session.avatar, message.recipient)) {
+ sendResponse(message)
+ }
+ }
+
+ def commandIncomingSendToLocalIfOnline(session: Session, fromSession: Session, message: ChatMsg): Unit = {
+ if (
+ session.zone == fromSession.zone &&
+ Vector3.DistanceSquared(session.player.Position, fromSession.player.Position) < 625 &&
+ session.player.Faction == fromSession.player.Faction &&
+ AvatarActor.onlineIfNotIgnored(session.avatar, message.recipient)
+ ) {
+ sendResponse(message)
+ }
+ }
+
+ def commandIncomingVoice(session: Session, fromSession: Session, message: ChatMsg): Unit = {
+ if (
+ (session.zone == fromSession.zone || message.contents.startsWith("SH")) && /*tactical squad voice macro*/
+ Vector3.DistanceSquared(session.player.Position, fromSession.player.Position) < 1600
+ ) {
+ val name = fromSession.avatar.name
+ if (!session.avatar.people.ignored.exists { f => f.name.equals(name) } ||
+ {
+ val id = fromSession.avatar.id.toLong
+ val curr = System.currentTimeMillis()
+ ignoredEmoteCooldown.get(id) match {
+ case None =>
+ ignoredEmoteCooldown.put(id, curr + 15000L)
+ true
+ case Some(time) if time < curr =>
+ ignoredEmoteCooldown.put(id, curr + 15000L)
+ true
+ case _ =>
+ false
+ }}
+ ) {
+ sendResponse(message)
+ }
+ }
+ }
+
+ def commandIncomingSilence(session: Session, message: ChatMsg): Unit = {
+ val args = cliTokenization(message.contents)
+ val (name, time) = (args.headOption, 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) {
+ context.self ! SessionActor.SetSilenced(false)
+ sendResponse(
+ ChatMsg(ChatMessageType.UNK_229, wideContents=true, "", "@silence_off", None)
+ )
+ if (!silenceTimer.isCancelled) silenceTimer.cancel()
+ } else {
+ context.self ! SessionActor.SetSilenced(true)
+ sendResponse(
+ ChatMsg(ChatMessageType.UNK_229, wideContents=true, "", "@silence_on", None)
+ )
+ import scala.concurrent.ExecutionContext.Implicits.global
+ silenceTimer = context.system.scheduler.scheduleOnce(
+ time minutes,
+ new Runnable {
+ def run(): Unit = {
+ context.self ! SessionActor.SetSilenced(false)
+ sendResponse(
+ ChatMsg(ChatMessageType.UNK_229, wideContents=true, "", "@silence_timeout", None)
+ )
+ }
+ }
+ )
+ }
+ case (name, time) =>
+ log.warn(s"Bad silence args $name $time")
+ }
+ }
+
+
+
+ /**
+ * For a provided number of facility nanite transfer unit resource silos,
+ * charge the facility's silo with an expected amount of nanite transfer units.
+ * @see `Amenity`
+ * @see `ChatMsg`
+ * @see `ResourceSilo`
+ * @see `ResourceSilo.UpdateChargeLevel`
+ * @see `SessionActor.Command`
+ * @see `SessionActor.SendResponse`
+ * @param resources the optional number of resources to set to each silo;
+ * different values provide different resources as indicated below;
+ * an undefined value also has a condition
+ * @param silos where to deposit the resources
+ * @param debugContent something for log output context
+ */
+ private def setBaseResources(resources: Option[Int], silos: Iterable[Amenity], debugContent: String): Unit = {
+ if (silos.isEmpty) {
+ context.self ! SessionActor.SendResponse(
+ ChatMsg(UNK_229, wideContents=true, "Server", s"no targets for ntu found with parameters $debugContent", None)
+ )
+ }
+ resources match {
+ // x = n0% of maximum capacitance
+ case Some(value) if value > -1 && value < 11 =>
+ silos.collect {
+ case silo: ResourceSilo =>
+ silo.Actor ! ResourceSilo.UpdateChargeLevel(
+ value * silo.MaxNtuCapacitor * 0.1f - silo.NtuCapacitor
+ )
+ }
+ // capacitance set to x (where x > 10) exactly, within limits
+ case Some(value) =>
+ silos.collect {
+ case silo: ResourceSilo =>
+ silo.Actor ! ResourceSilo.UpdateChargeLevel(value - silo.NtuCapacitor)
+ }
+ case None =>
+ // x >= n0% of maximum capacitance and x <= maximum capacitance
+ val rand = new scala.util.Random
+ silos.collect {
+ case silo: ResourceSilo =>
+ val a = 7
+ val b = 10 - a
+ val tenth = silo.MaxNtuCapacitor * 0.1f
+ silo.Actor ! ResourceSilo.UpdateChargeLevel(
+ a * tenth + rand.nextFloat() * b * tenth - silo.NtuCapacitor
+ )
+ }
+ }
+ }
+
+ /**
+ * Create a medkit shortcut if there is no medkit shortcut on the hotbar.
+ * Bounce the packet to the client and the client will bounce it back to the server to continue the setup,
+ * or cancel / invalidate the shortcut creation.
+ * @see `Array::indexWhere`
+ * @see `CreateShortcutMessage`
+ * @see `net.psforever.objects.avatar.Shortcut`
+ * @see `net.psforever.packet.game.Shortcut.Medkit`
+ * @see `SessionActor.SendResponse`
+ * @param guid current player unique identifier for the target client
+ * @param shortcuts list of all existing shortcuts, used for early validation
+ */
+ private def medkitSanityTest(
+ guid: PlanetSideGUID,
+ shortcuts: Array[Option[AvatarShortcut]]
+ ): Unit = {
+ if (!shortcuts.exists {
+ case Some(a) => a.purpose == 0
+ case None => false
+ }) {
+ shortcuts.indexWhere(_.isEmpty) match {
+ case -1 => ()
+ case index =>
+ //new shortcut
+ sendResponse(CreateShortcutMessage(
+ guid,
+ index + 1,
+ Some(Shortcut.Medkit())
+ ))
+ }
+ }
+ }
+
+ /**
+ * Create all implant macro shortcuts for all implants whose shortcuts have been removed from the hotbar.
+ * Bounce the packet to the client and the client will bounce it back to the server to continue the setup,
+ * or cancel / invalidate the shortcut creation.
+ * @see `CreateShortcutMessage`
+ * @see `ImplantDefinition`
+ * @see `net.psforever.objects.avatar.Shortcut`
+ * @see `SessionActor.SendResponse`
+ * @param guid current player unique identifier for the target client
+ * @param haveImplants list of implants the player possesses
+ * @param shortcuts list of all existing shortcuts, used for early validation
+ */
+ private def implantSanityTest(
+ guid: PlanetSideGUID,
+ haveImplants: Iterable[ImplantDefinition],
+ shortcuts: Array[Option[AvatarShortcut]]
+ ): Unit = {
+ val haveImplantShortcuts = shortcuts.collect {
+ case Some(shortcut) if shortcut.purpose == 2 => shortcut.tile
+ }
+ var start: Int = 0
+ haveImplants.filterNot { imp => haveImplantShortcuts.contains(imp.Name) }
+ .foreach { implant =>
+ shortcuts.indexWhere(_.isEmpty, start) match {
+ case -1 => ()
+ case index =>
+ //new shortcut
+ start = index + 1
+ sendResponse(CreateShortcutMessage(
+ guid,
+ start,
+ Some(implant.implantType.shortcut)
+ ))
+ }
+ }
+ }
+
+ /**
+ * Create a text chat macro shortcut if it doesn't already exist.
+ * Bounce the packet to the client and the client will bounce it back to the server to continue the setup,
+ * or cancel / invalidate the shortcut creation.
+ * @see `Array::indexWhere`
+ * @see `CreateShortcutMessage`
+ * @see `net.psforever.objects.avatar.Shortcut`
+ * @see `net.psforever.packet.game.Shortcut.Macro`
+ * @see `SessionActor.SendResponse`
+ * @param guid current player unique identifier for the target client
+ * @param acronym three letters emblazoned on the shortcut icon
+ * @param msg the message published to text chat
+ * @param shortcuts a list of all existing shortcuts, used for early validation
+ */
+ private def macroSanityTest(
+ guid: PlanetSideGUID,
+ acronym: String,
+ msg: String,
+ shortcuts: Array[Option[AvatarShortcut]]
+ ): Unit = {
+ shortcuts.indexWhere(_.isEmpty) match {
+ case -1 => ()
+ case index =>
+ //new shortcut
+ sendResponse(CreateShortcutMessage(
+ guid,
+ index + 1,
+ Some(Shortcut.Macro(acronym, msg))
+ ))
+ }
+ }
+
+ private def setBattleRank(
+ session: Session,
+ params: Seq[String],
+ msgFunc: Long => AvatarActor.Command
+ ): Boolean = {
+ val (target, rank) = (params.headOption, params.lift(1)) match {
+ case (Some(target), Some(rank)) if target == session.avatar.name =>
+ rank.toIntOption match {
+ case Some(rank) => (None, BattleRank.withValueOpt(rank))
+ case None => (None, None)
+ }
+ case (Some("-h"), _) | (Some("-help"), _) =>
+ (None, Some(BattleRank.BR1))
+ case (Some(_), Some(_)) =>
+ // picking other targets is not supported for now
+ (None, None)
+ case (Some(rank), None) =>
+ rank.toIntOption match {
+ case Some(rank) => (None, BattleRank.withValueOpt(rank))
+ case None => (None, None)
+ }
+ case _ => (None, None)
+ }
+ (target, rank) match {
+ case (_, Some(rank)) if rank.value <= Config.app.game.maxBattleRank =>
+ context.self ! msgFunc(rank.experience)
+ true
+ case _ =>
+ false
+ }
+ }
+
+ private def setCommandRank(
+ contents: String,
+ session: Session
+ ): Boolean = {
+ val buffer = cliTokenization(contents)
+ val (target, rank) = (buffer.headOption, buffer.lift(1)) match {
+ case (Some(target), Some(rank)) if target == session.avatar.name =>
+ rank.toIntOption match {
+ case Some(rank) => (None, CommandRank.withValueOpt(rank))
+ case None => (None, None)
+ }
+ case (Some(_), Some(_)) =>
+ // picking other targets is not supported for now
+ (None, None)
+ case (Some(rank), None) =>
+ rank.toIntOption match {
+ case Some(rank) => (None, CommandRank.withValueOpt(rank))
+ case None => (None, None)
+ }
+ case _ => (None, None)
+ }
+ (target, rank) match {
+ case (_, Some(rank)) =>
+ context.self ! AvatarActor.SetCep(rank.experience)
+ true
+ case _ =>
+ false
+ }
+ }
+
+ private def captureBaseParamFacilities(
+ session: Session,
+ token: Option[String]
+ ): Option[Seq[Building]] = {
+ token.collect {
+ case "curr" =>
+ val list = captureBaseCurrSoi(session)
+ if (list.nonEmpty) {
+ Some(list.toSeq)
+ } else {
+ None
+ }
+ case "all" =>
+ val list = session.zone.Buildings.values.filter(_.CaptureTerminal.isDefined)
+ if (list.nonEmpty) {
+ Some(list.toSeq)
+ } else {
+ None
+ }
+ case name =>
+ val trueName = ZoneInfo
+ .values
+ .find(_.id.equals(session.zone.id))
+ .flatMap { info =>
+ info.aliases
+ .facilities
+ .collectFirst { case (key, internalName) if key.equalsIgnoreCase(name) => internalName }
+ }
+ .getOrElse(name)
+ session.zone.Buildings
+ .values
+ .find {
+ building => trueName.equalsIgnoreCase(building.Name) && building.CaptureTerminal.isDefined
+ }
+ .map(b => Seq(b))
+ }
+ .flatten
+ }
+
+ private def captureBaseCurrSoi(
+ session: Session
+ ): Iterable[Building] = {
+ val charId = session.player.CharId
+ session.zone.Buildings.values.filter { building =>
+ building.PlayersInSOI.exists(_.CharId == charId)
+ }
+ }
+
+ private def captureBaseParamFaction(
+ @unused session: Session,
+ token: Option[String]
+ ): Option[PlanetSideEmpire.Value] = {
+ token.collect {
+ case faction =>
+ faction.toLowerCase() match {
+ case "tr" => Some(PlanetSideEmpire.TR)
+ case "nc" => Some(PlanetSideEmpire.NC)
+ case "vs" => Some(PlanetSideEmpire.VS)
+ case "none" => Some(PlanetSideEmpire.NEUTRAL)
+ case "bo" => Some(PlanetSideEmpire.NEUTRAL)
+ case "neutral" => Some(PlanetSideEmpire.NEUTRAL)
+ case _ => None
+ }
+ }.flatten
+ }
+
+ private def captureBaseParamTimer(
+ @unused session: Session,
+ token: Option[String]
+ ): Option[Int] = {
+ token.flatMap(_.toIntOption)
+ }
+
+
+
+ def customCommandWhitetext(
+ session: Session,
+ content: Seq[String]
+ ): Boolean = {
+ chatService ! ChatService.Message(
+ session,
+ ChatMsg(UNK_227, wideContents=true, "", content.mkString(" "), None),
+ DefaultChannel
+ )
+ true
+ }
+
+ def customCommandLoc(
+ session: Session,
+ message: ChatMsg
+ ): Boolean = {
+ 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}"
+ sendResponse(message.copy(contents = loc))
+ true
+ }
+
+ def customCommandList(
+ session: Session,
+ params: Seq[String],
+ message: ChatMsg
+ ): Boolean = {
+ val zone = params.headOption match {
+ case Some("") | None =>
+ Some(session.zone)
+ case Some(id) =>
+ Zones.zones.find(_.id == id)
+ }
+ zone match {
+ case Some(inZone) =>
+ sendResponse(
+ ChatMsg(
+ CMT_GMOPEN,
+ message.wideContents,
+ "Server",
+ "\\#8Name (Faction) [ID] at PosX PosY PosZ",
+ message.note
+ )
+ )
+ (inZone.LivePlayers ++ inZone.Corpses)
+ .filter(_.CharId != session.player.CharId)
+ .sortBy(p => (p.Name, !p.isAlive))
+ .foreach(player => {
+ val color = if (!player.isAlive) "\\#7" else ""
+ sendResponse(
+ ChatMsg(
+ CMT_GMOPEN,
+ message.wideContents,
+ "Server",
+ s"$color${player.Name} (${player.Faction}) [${player.CharId}] at ${player.Position.x.toInt} ${player.Position.y.toInt} ${player.Position.z.toInt}",
+ message.note
+ )
+ )
+ })
+ case None =>
+ sendResponse(
+ ChatMsg(
+ CMT_GMOPEN,
+ message.wideContents,
+ "Server",
+ "Invalid zone ID",
+ message.note
+ )
+ )
+ }
+ true
+ }
+
+ def customCommandNtu(
+ session: Session,
+ params: Seq[String]
+ ): Boolean = {
+ val (facility, customNtuValue) = (params.headOption, params.lift(1)) match {
+ case (Some(x), Some(y)) if y.toIntOption.nonEmpty => (Some(x), Some(y.toInt))
+ case (Some(x), None) if x.toIntOption.nonEmpty => (None, Some(x.toInt))
+ case _ => (None, None)
+ }
+ val silos = (facility match {
+ case Some(cur) if cur.toLowerCase().startsWith("curr") =>
+ val position = session.player.Position
+ session.zone.Buildings.values
+ .filter { building =>
+ val soi2 = building.Definition.SOIRadius * building.Definition.SOIRadius
+ Vector3.DistanceSquared(building.Position, position) < soi2
+ }
+ case Some(all) if all.toLowerCase.startsWith("all") =>
+ session.zone.Buildings.values
+ case Some(x) =>
+ session.zone.Buildings.values.find(_.Name.equalsIgnoreCase(x)).toList
+ case _ =>
+ session.zone.Buildings.values
+ })
+ .flatMap { building =>
+ building.Amenities.filter(_.isInstanceOf[ResourceSilo])
+ }
+ setBaseResources(customNtuValue, silos, debugContent = s"$facility")
+ true
+ }
+
+ def customCommandZonerotate(
+ params: Seq[String]
+ ): Boolean = {
+ cluster ! InterstellarClusterService.CavernRotation(params.headOption match {
+ case Some("-list") | Some("-l") =>
+ CavernRotationService.ReportRotationOrder(context.self)
+ case _ =>
+ CavernRotationService.HurryNextRotation
+ })
+ true
+ }
+
+ def customCommandSuicide(
+ session: Session
+ ): Boolean = {
+ //this is like CMT_SUICIDE but it ignores checks and forces a suicide state
+ val tplayer = session.player
+ tplayer.Revive
+ tplayer.Actor ! Player.Die()
+ true
+ }
+
+ def customCommandGrenade(
+ session: Session,
+ log: Logger
+ ): Boolean = {
+ WorldSession.QuickSwapToAGrenade(session.player, DrawnSlot.Pistol1.id, log)
+ true
+ }
+
+ def customCommandMacro(
+ session: Session,
+ params: Seq[String]
+ ): Boolean = {
+ val avatar = session.avatar
+ (params.headOption, params.lift(1)) match {
+ case (Some(cmd), other) =>
+ cmd.toLowerCase() match {
+ case "medkit" =>
+ medkitSanityTest(session.player.GUID, avatar.shortcuts)
+ true
+
+ case "implants" =>
+ //implant shortcut sanity test
+ implantSanityTest(
+ session.player.GUID,
+ avatar.implants.collect {
+ case Some(implant) if implant.definition.implantType != ImplantType.None => implant.definition
+ },
+ avatar.shortcuts
+ )
+ true
+
+ case name
+ if ImplantType.values.exists { a => a.shortcut.tile.equals(name) } =>
+ avatar.implants.find {
+ case Some(implant) => implant.definition.Name.equalsIgnoreCase(name)
+ case None => false
+ } match {
+ case Some(Some(implant)) =>
+ //specific implant shortcut sanity test
+ implantSanityTest(session.player.GUID, Seq(implant.definition), avatar.shortcuts)
+ true
+ case _ if other.nonEmpty =>
+ //add macro?
+ macroSanityTest(session.player.GUID, name, params.drop(2).mkString(" "), avatar.shortcuts)
+ true
+ case _ =>
+ false
+ }
+
+ case name
+ if name.nonEmpty && other.nonEmpty =>
+ //add macro
+ macroSanityTest(session.player.GUID, name, params.drop(2).mkString(" "), avatar.shortcuts)
+ true
+
+ case _ =>
+ false
+ }
+ case _ =>
+ false
+ }
+ }
+
+ def customCommandProgress(
+ session: Session,
+ params: Seq[String]
+ ): Boolean = {
+ val ourRank = BattleRank.withExperience(session.avatar.bep).value
+ if (!session.account.gm &&
+ (ourRank <= Config.app.game.promotion.broadcastBattleRank ||
+ ourRank > Config.app.game.promotion.resetBattleRank && ourRank < Config.app.game.promotion.maxBattleRank + 1)) {
+ setBattleRank(session, params, AvatarActor.Progress)
+ true
+ } else {
+ setBattleRank(session, Seq("1"), AvatarActor.Progress)
+ false
+ }
+ }
+
+ def customCommandNearby(
+ session: Session
+ ): Boolean = {
+ val playerPos = session.player.Position.xy
+ val closest = session.zone
+ .Buildings
+ .values
+ .toSeq
+ .minByOption(base => Vector3.DistanceSquared(playerPos, base.Position.xy))
+ .map(base => s"${base.Name} - ${base.Definition.Name}")
+ sendResponse(
+ ChatMsg(CMT_GMOPEN, wideContents = false, "Server", s"closest facility: $closest", None)
+ )
+ true
+ }
+
+ def firstParam[T](
+ session: Session,
+ buffer: Iterable[String],
+ func: (Session, Option[String])=>Option[T]
+ ): (Option[T], Option[String], Iterable[String]) = {
+ val tokenOpt = buffer.headOption
+ val valueOpt = func(session, tokenOpt)
+ val outBuffer = if (valueOpt.nonEmpty) {
+ buffer.drop(1)
+ } else {
+ buffer
+ }
+ (valueOpt, tokenOpt, outBuffer)
+ }
+
+ def cliTokenization(str: String): List[String] = {
+ str.replaceAll("\\s+", " ").toLowerCase.trim.split("\\s").toList
+ }
+
+ def commandIncomingSend(message: ChatMsg): Unit = {
+ sendResponse(message)
+ }
+
+ def commandSend(session: Session, message: ChatMsg, toChannel: ChatChannel): Unit = {
+ chatService ! ChatService.Message(
+ session,
+ message,
+ toChannel
+ )
+ }
+
+ def commandSendToRecipient(session: Session, message: ChatMsg, toChannel: ChatChannel): Unit = {
+ chatService ! ChatService.Message(
+ session,
+ message.copy(recipient = session.player.Name),
+ toChannel
+ )
+ }
+
+ override protected[session] def stop(): Unit = {
+ silenceTimer.cancel()
+ chatService ! ChatService.LeaveAllChannels(chatServiceAdapter)
+ }
+}
diff --git a/src/main/scala/net/psforever/actors/session/support/GeneralOperations.scala b/src/main/scala/net/psforever/actors/session/support/GeneralOperations.scala
index 8a2342951..889646993 100644
--- a/src/main/scala/net/psforever/actors/session/support/GeneralOperations.scala
+++ b/src/main/scala/net/psforever/actors/session/support/GeneralOperations.scala
@@ -8,7 +8,7 @@ import scala.collection.mutable
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
//
-import net.psforever.actors.session.{AvatarActor, ChatActor, SessionActor}
+import net.psforever.actors.session.{AvatarActor, SessionActor}
import net.psforever.login.WorldSession._
import net.psforever.objects._
import net.psforever.objects.avatar._
@@ -25,7 +25,7 @@ import net.psforever.objects.vehicles._
import net.psforever.objects.vital._
import net.psforever.objects.zones._
import net.psforever.packet._
-import net.psforever.packet.game.{ActionCancelMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, ChangeShortcutBankMessage, CharacterRequestMessage, ConnectToWorldRequestMessage, CreateShortcutMessage, DeployObjectMessage, DisplayedAwardMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FriendsRequest, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, InvalidTerrainMessage, LootItemMessage, MoveItemMessage, ObjectDetectedMessage, ObjectHeldMessage, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, RequestDestroyMessage, SetChatFilterMessage, TargetingImplantRequest, TradeMessage, UnuseItemMessage, UseItemMessage, ZipLineMessage}
+import net.psforever.packet.game.{ActionCancelMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, ChangeShortcutBankMessage, CharacterRequestMessage, ConnectToWorldRequestMessage, CreateShortcutMessage, DeployObjectMessage, DisplayedAwardMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FriendsRequest, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, InvalidTerrainMessage, LootItemMessage, MoveItemMessage, ObjectDetectedMessage, ObjectHeldMessage, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, RequestDestroyMessage, TargetingImplantRequest, TradeMessage, UnuseItemMessage, UseItemMessage, ZipLineMessage}
import net.psforever.packet.game.PlanetsideAttributeEnum.PlanetsideAttributeEnum
import net.psforever.packet.game.objectcreate._
import net.psforever.packet.game._
@@ -48,10 +48,6 @@ trait GeneralFunctions extends CommonSessionInterfacingFunctionality {
def handlePlayerStateUpstream(pkt: PlayerStateMessageUpstream): Unit
- def handleChat(pkt: ChatMsg): Unit
-
- def handleChatFilter(pkt: SetChatFilterMessage): Unit
-
def handleVoiceHostRequest(pkt: VoiceHostRequest): Unit
def handleVoiceHostInfo(pkt: VoiceHostInfo): Unit
@@ -148,7 +144,6 @@ trait GeneralFunctions extends CommonSessionInterfacingFunctionality {
class GeneralOperations(
val sessionLogic: SessionData,
val avatarActor: typed.ActorRef[AvatarActor.Command],
- val chatActor: typed.ActorRef[ChatActor.Command],
implicit val context: ActorContext
) extends CommonSessionInterfacingFunctionality {
private[session] var progressBarValue: Option[Float] = None
diff --git a/src/main/scala/net/psforever/actors/session/support/PlayerMode.scala b/src/main/scala/net/psforever/actors/session/support/PlayerMode.scala
index bafccc852..6c5040057 100644
--- a/src/main/scala/net/psforever/actors/session/support/PlayerMode.scala
+++ b/src/main/scala/net/psforever/actors/session/support/PlayerMode.scala
@@ -7,6 +7,7 @@ import net.psforever.objects.Session
trait ModeLogic {
def avatarResponse: AvatarHandlerFunctions
+ def chat: ChatFunctions
def galaxy: GalaxyHandlerFunctions
def general: GeneralFunctions
def local: LocalHandlerFunctions
diff --git a/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala b/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala
index 64df47cd0..3e0e7f9d8 100644
--- a/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala
+++ b/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala
@@ -8,7 +8,7 @@ import net.psforever.objects.zones.exp
import scala.collection.mutable
//
-import net.psforever.actors.session.{AvatarActor, ChatActor}
+import net.psforever.actors.session.AvatarActor
import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent
import net.psforever.packet.game._
import net.psforever.services.avatar.AvatarResponse
@@ -24,7 +24,6 @@ trait AvatarHandlerFunctions extends CommonSessionInterfacingFunctionality {
class SessionAvatarHandlers(
val sessionLogic: SessionData,
val avatarActor: typed.ActorRef[AvatarActor.Command],
- chatActor: typed.ActorRef[ChatActor.Command],
implicit val context: ActorContext
) extends CommonSessionInterfacingFunctionality {
//TODO player characters only exist within a certain range of GUIDs for a given zone; this is overkill
diff --git a/src/main/scala/net/psforever/actors/session/support/SessionData.scala b/src/main/scala/net/psforever/actors/session/support/SessionData.scala
index 401bd7850..5c40651fb 100644
--- a/src/main/scala/net/psforever/actors/session/support/SessionData.scala
+++ b/src/main/scala/net/psforever/actors/session/support/SessionData.scala
@@ -4,12 +4,14 @@ package net.psforever.actors.session.support
import akka.actor.typed.receptionist.Receptionist
import akka.actor.typed.scaladsl.adapter._
import akka.actor.{ActorContext, ActorRef, typed}
+import net.psforever.services.chat.ChatService
+
import scala.collection.mutable
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
//
import net.psforever.actors.net.MiddlewareActor
-import net.psforever.actors.session.{AvatarActor, ChatActor}
+import net.psforever.actors.session.AvatarActor
import net.psforever.actors.zone.ZoneActor
import net.psforever.objects._
import net.psforever.objects.avatar._
@@ -60,7 +62,7 @@ object SessionData {
class SessionData(
val middlewareActor: typed.ActorRef[MiddlewareActor.Command],
implicit val context: ActorContext
- ) {
+ ) extends SessionSource {
/**
* Hardwire an implicit `sender` to be the same as `context.self` of the `SessionActor` actor class
* for which this support class was initialized.
@@ -74,7 +76,6 @@ class SessionData(
private[this] implicit val sender: ActorRef = context.self
private val avatarActor: typed.ActorRef[AvatarActor.Command] = context.spawnAnonymous(AvatarActor(context.self))
- private val chatActor: typed.ActorRef[ChatActor.Command] = context.spawnAnonymous(ChatActor(context.self, avatarActor))
private[session] val log = org.log4s.getLogger
private[session] var theSession: Session = Session()
@@ -83,6 +84,7 @@ class SessionData(
private[session] var galaxyService: ActorRef = Default.Actor
private[session] var squadService: ActorRef = Default.Actor
private[session] var cluster: typed.ActorRef[ICS.Command] = Default.typed.Actor
+ private[session] var chatService: typed.ActorRef[ChatService.Command] = Default.typed.Actor
private[session] var connectionState: Int = 25
private[session] var persistFunc: () => Unit = noPersistence
private[session] var persist: () => Unit = updatePersistenceOnly
@@ -92,13 +94,13 @@ class SessionData(
private var contextSafeEntity: PlanetSideGUID = PlanetSideGUID(0)
val general: GeneralOperations =
- new GeneralOperations(sessionLogic=this, avatarActor, chatActor, context)
+ new GeneralOperations(sessionLogic=this, avatarActor, context)
val shooting: WeaponAndProjectileOperations =
- new WeaponAndProjectileOperations(sessionLogic=this, avatarActor, chatActor, context)
+ new WeaponAndProjectileOperations(sessionLogic=this, avatarActor, context)
val vehicles: VehicleOperations =
new VehicleOperations(sessionLogic=this, avatarActor, context)
val avatarResponse: SessionAvatarHandlers =
- new SessionAvatarHandlers(sessionLogic=this, avatarActor, chatActor, context)
+ new SessionAvatarHandlers(sessionLogic=this, avatarActor, context)
val localResponse: SessionLocalHandlers =
new SessionLocalHandlers(sessionLogic=this, context)
val mountResponse: SessionMountHandlers =
@@ -109,16 +111,19 @@ class SessionData(
private var galaxyResponseOpt: Option[SessionGalaxyHandlers] = None
private var squadResponseOpt: Option[SessionSquadHandlers] = None
private var zoningOpt: Option[ZoningOperations] = None
+ private var chatOpt: Option[ChatOperations] = None
def vehicleResponseOperations: SessionVehicleHandlers = vehicleResponseOpt.orNull
def galaxyResponseHandlers: SessionGalaxyHandlers = galaxyResponseOpt.orNull
def squad: SessionSquadHandlers = squadResponseOpt.orNull
def zoning: ZoningOperations = zoningOpt.orNull
+ def chat: ChatOperations = chatOpt.orNull
ServiceManager.serviceManager ! Lookup("accountIntermediary")
ServiceManager.serviceManager ! Lookup("accountPersistence")
ServiceManager.serviceManager ! Lookup("galaxy")
ServiceManager.serviceManager ! Lookup("squad")
ServiceManager.receptionist ! Receptionist.Find(ICS.InterstellarClusterServiceKey, context.self)
+ ServiceManager.receptionist ! Receptionist.Find(ChatService.ChatServiceKey, context.self)
/**
* updated when an upstream packet arrives;
@@ -129,7 +134,6 @@ class SessionData(
def session: Session = theSession
def session_=(session: Session): Unit = {
- chatActor ! ChatActor.SetSession(session)
avatarActor ! AvatarActor.SetSession(session)
theSession = session
}
@@ -164,6 +168,11 @@ class SessionData(
case ICS.InterstellarClusterServiceKey.Listing(listings) =>
cluster = listings.head
buildDependentOperationsForZoning(galaxyService, cluster)
+ buildDependentOperationsForChat(chatService, cluster)
+ true
+ case ChatService.ChatServiceKey.Listing(listings) =>
+ chatService = listings.head
+ buildDependentOperationsForChat(chatService, cluster)
true
case _ =>
@@ -186,7 +195,13 @@ class SessionData(
def buildDependentOperationsForSquad(squadActor: ActorRef): Unit = {
if (squadResponseOpt.isEmpty && squadActor != Default.Actor) {
- squadResponseOpt = Some(new SessionSquadHandlers(sessionLogic=this, avatarActor, chatActor, squadActor, context))
+ squadResponseOpt = Some(new SessionSquadHandlers(sessionLogic=this, avatarActor, squadActor, context))
+ }
+ }
+
+ def buildDependentOperationsForChat(chatService: typed.ActorRef[ChatService.Command], clusterActor: typed.ActorRef[ICS.Command]): Unit = {
+ if (chatOpt.isEmpty && chatService != Default.typed.Actor && clusterActor != Default.typed.Actor) {
+ chatOpt = Some(new ChatOperations(sessionLogic=this, avatarActor, chatService, clusterActor, context))
}
}
@@ -196,7 +211,8 @@ class SessionData(
vehicleResponseOpt.nonEmpty &&
galaxyResponseOpt.nonEmpty &&
squadResponseOpt.nonEmpty &&
- zoningOpt.nonEmpty
+ zoningOpt.nonEmpty &&
+ chatOpt.nonEmpty
}
/* support functions */
@@ -363,11 +379,11 @@ class SessionData(
terminals.actionsToCancel()
if (session.flying) {
session = session.copy(flying = false)
- chatActor ! ChatActor.Message(ChatMsg(ChatMessageType.CMT_FLY, wideContents=false, "", "off", None))
+ chat.commandFly(contents = "off", recipient = "")
}
if (session.speed > 1) {
session = session.copy(speed = 1)
- chatActor ! ChatActor.Message(ChatMsg(ChatMessageType.CMT_SPEED, wideContents=false, "", "1.000", None))
+ chat.commandSpeed(ChatMsg(ChatMessageType.CMT_SPEED, "1.000"), contents = "1.000")
}
}
@@ -542,7 +558,6 @@ class SessionData(
def stop(): Unit = {
context.stop(avatarActor)
- context.stop(chatActor)
general.stop()
shooting.stop()
vehicles.stop()
@@ -554,6 +569,7 @@ class SessionData(
galaxyResponseOpt.foreach(_.stop())
squadResponseOpt.foreach(_.stop())
zoningOpt.foreach(_.stop())
+ chatOpt.foreach(_.stop())
continent.AvatarEvents ! Service.Leave()
continent.LocalEvents ! Service.Leave()
continent.VehicleEvents ! Service.Leave()
diff --git a/src/main/scala/net/psforever/actors/session/support/SessionSquadHandlers.scala b/src/main/scala/net/psforever/actors/session/support/SessionSquadHandlers.scala
index 891443561..470db3569 100644
--- a/src/main/scala/net/psforever/actors/session/support/SessionSquadHandlers.scala
+++ b/src/main/scala/net/psforever/actors/session/support/SessionSquadHandlers.scala
@@ -4,7 +4,7 @@ package net.psforever.actors.session.support
import akka.actor.{ActorContext, ActorRef, typed}
import scala.collection.mutable
//
-import net.psforever.actors.session.{AvatarActor, ChatActor}
+import net.psforever.actors.session.AvatarActor
import net.psforever.objects.teamwork.Squad
import net.psforever.objects.{Default, Player}
import net.psforever.packet.game._
@@ -39,7 +39,6 @@ object SessionSquadHandlers {
class SessionSquadHandlers(
val sessionLogic: SessionData,
val avatarActor: typed.ActorRef[AvatarActor.Command],
- val chatActor: typed.ActorRef[ChatActor.Command],
val squadService: ActorRef,
implicit val context: ActorContext
) extends CommonSessionInterfacingFunctionality {
diff --git a/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala b/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala
index 70412e93d..6f0b289f3 100644
--- a/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala
+++ b/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala
@@ -9,7 +9,7 @@ import net.psforever.objects.zones.exp.ToDatabase
import scala.collection.mutable
import scala.concurrent.duration._
//
-import net.psforever.actors.session.{AvatarActor, ChatActor}
+import net.psforever.actors.session.AvatarActor
import net.psforever.objects.avatar.scoring.EquipmentStat
import net.psforever.objects.ballistics.Projectile
import net.psforever.objects.equipment.EquipmentSize
@@ -63,7 +63,6 @@ trait WeaponAndProjectileFunctions extends CommonSessionInterfacingFunctionality
class WeaponAndProjectileOperations(
val sessionLogic: SessionData,
val avatarActor: typed.ActorRef[AvatarActor.Command],
- val chatActor: typed.ActorRef[ChatActor.Command],
implicit val context: ActorContext
) extends CommonSessionInterfacingFunctionality {
var shooting: mutable.Set[PlanetSideGUID] = mutable.Set.empty //ChangeFireStateMessage_Start
diff --git a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala
index 7128fb11d..3c494a8b2 100644
--- a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala
+++ b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala
@@ -6,6 +6,7 @@ import akka.actor.typed.scaladsl.adapter._
import akka.actor.{ActorContext, ActorRef, Cancellable, typed}
import akka.pattern.ask
import akka.util.Timeout
+import net.psforever.actors.session.spectator.SpectatorMode
import net.psforever.login.WorldSession
import net.psforever.objects.avatar.BattleRank
import net.psforever.objects.avatar.scoring.{CampaignStatistics, ScoreCard, SessionStatistics}
@@ -17,6 +18,7 @@ import net.psforever.objects.serverobject.turret.auto.AutomatedTurret
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource}
import net.psforever.objects.vital.{InGameHistory, IncarnationActivity, ReconstructionActivity, SpawningActivity}
import net.psforever.packet.game.{CampaignStatistic, ChangeFireStateMessage_Start, MailMessage, ObjectDetectedMessage, SessionStatistic}
+import net.psforever.services.chat.DefaultChannel
import scala.collection.mutable
import scala.concurrent.duration._
@@ -2164,6 +2166,7 @@ class ZoningOperations(
*/
def avatarLoginResponse(avatar: Avatar): Unit = {
session = session.copy(avatar = avatar)
+ sessionLogic.chat.JoinChannel(DefaultChannel)
Deployables.InitializeDeployableQuantities(avatar)
cluster ! ICS.FilterZones(_ => true, context.self)
}
@@ -3070,6 +3073,9 @@ class ZoningOperations(
}
})
}
+ if (!setAvatar && tplayer.spectator) {
+ context.self ! SessionActor.SetMode(SpectatorMode) //should reload spectator status
+ }
}
upstreamMessageCount = 0
setAvatar = true
diff --git a/src/main/scala/net/psforever/objects/Session.scala b/src/main/scala/net/psforever/objects/Session.scala
index 06b5d6d9a..8dbe39f9b 100644
--- a/src/main/scala/net/psforever/objects/Session.scala
+++ b/src/main/scala/net/psforever/objects/Session.scala
@@ -15,3 +15,7 @@ case class Session(
speed: Float = 1.0f,
flying: Boolean = false
)
+
+trait SessionSource {
+ def session: Session
+}
diff --git a/src/main/scala/net/psforever/services/chat/ChatChannel.scala b/src/main/scala/net/psforever/services/chat/ChatChannel.scala
new file mode 100644
index 000000000..94515f0a3
--- /dev/null
+++ b/src/main/scala/net/psforever/services/chat/ChatChannel.scala
@@ -0,0 +1,12 @@
+// Copyright (c) 2024 PSForever
+package net.psforever.services.chat
+
+import net.psforever.types.PlanetSideGUID
+
+trait ChatChannel
+
+case object DefaultChannel extends ChatChannel
+
+final case class SquadChannel(guid: PlanetSideGUID) extends ChatChannel
+
+case object SpectatorChannel extends ChatChannel
diff --git a/src/main/scala/net/psforever/services/chat/ChatService.scala b/src/main/scala/net/psforever/services/chat/ChatService.scala
index 0aa8ff2cf..8803c3667 100644
--- a/src/main/scala/net/psforever/services/chat/ChatService.scala
+++ b/src/main/scala/net/psforever/services/chat/ChatService.scala
@@ -4,9 +4,9 @@ package net.psforever.services.chat
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.objects.{Session, SessionSource}
import net.psforever.packet.game.ChatMsg
-import net.psforever.types.{ChatMessageType, PlanetSideEmpire, PlanetSideGUID}
+import net.psforever.types.{ChatMessageType, PlanetSideEmpire}
object ChatService {
val ChatServiceKey: ServiceKey[Command] = ServiceKey[ChatService.Command]("chatService")
@@ -19,20 +19,12 @@ object ChatService {
sealed trait Command
- final case class JoinChannel(actor: ActorRef[MessageResponse], session: Session, channel: ChatChannel) extends Command
+ final case class JoinChannel(actor: ActorRef[MessageResponse], sessionSource: SessionSource, 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
- }
-
}
class ChatService(context: ActorContext[ChatService.Command]) extends AbstractBehavior[ChatService.Command](context) {
@@ -63,9 +55,10 @@ class ChatService(context: ActorContext[ChatService.Command]) extends AbstractBe
case Message(session, message, channel) =>
(channel, message.messageType) match {
- case (ChatChannel.Squad(_), CMT_SQUAD) => ()
- case (ChatChannel.Squad(_), CMT_VOICE) if message.contents.startsWith("SH") => ()
- case (ChatChannel.Default(), messageType) if messageType != CMT_SQUAD => ()
+ case (SquadChannel(_), CMT_SQUAD) => ()
+ case (SquadChannel(_), CMT_VOICE) if message.contents.startsWith("SH") => ()
+ case (DefaultChannel, messageType) if messageType != CMT_SQUAD => ()
+ case (SpectatorChannel, messageType) if messageType != CMT_SQUAD => ()
case _ =>
log.error(s"invalid chat channel $channel for messageType ${message.messageType}")
return this
@@ -78,8 +71,8 @@ class ChatService(context: ActorContext[ChatService.Command]) extends AbstractBe
val recipientName = message.recipient
val recipientNameLower = recipientName.toLowerCase()
(
- subs.find(_.session.player.Name.toLowerCase().equals(playerNameLower)),
- subs.find(_.session.player.Name.toLowerCase().equals(recipientNameLower))
+ subs.find(_.sessionSource.session.player.Name.toLowerCase().equals(playerNameLower)),
+ subs.find(_.sessionSource.session.player.Name.toLowerCase().equals(recipientNameLower))
) match {
case (Some(JoinChannel(sender, _, _)), Some(JoinChannel(receiver, _, _))) =>
val replyType = if (mtype == CMT_TELL) { U_CMT_TELLFROM } else { U_CMT_GMTELLFROM }
@@ -122,14 +115,14 @@ class ChatService(context: ActorContext[ChatService.Command]) extends AbstractBe
case _ => (None, None, None)
}
- val sender = subs.find(_.session.player.Name == session.player.Name)
+ val sender = subs.find(_.sessionSource.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)
+ val recipient = subs.find(_.sessionSource.session.player.Name == name)
recipient match {
case Some(recipient) =>
- if (recipient.session.player.silenced) {
+ if (recipient.sessionSource.session.player.silenced) {
sender.actor ! MessageResponse(
session,
ChatMsg(UNK_229, wideContents = true, "", "@silence_disabled_ack", None),
@@ -167,7 +160,7 @@ class ChatService(context: ActorContext[ChatService.Command]) extends AbstractBe
case CMT_NOTE =>
subs
- .filter(_.session.player.Name == message.recipient)
+ .filter(_.sessionSource.session.player.Name == message.recipient)
.foreach(
_.actor ! MessageResponse(session, message.copy(recipient = session.player.Name), channel)
)
@@ -175,23 +168,23 @@ class ChatService(context: ActorContext[ChatService.Command]) extends AbstractBe
// faction commands
case CMT_OPEN | CMT_PLATOON | CMT_COMMAND =>
subs
- .filter(_.session.player.Faction == session.player.Faction)
+ .filter(_.sessionSource.session.player.Faction == session.player.Faction)
.foreach(
_.actor ! MessageResponse(session, message, channel)
)
case CMT_GMBROADCAST_NC =>
- subs.filter(_.session.player.Faction == PlanetSideEmpire.NC).foreach {
+ subs.filter(_.sessionSource.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 {
+ subs.filter(_.sessionSource.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 {
+ subs.filter(_.sessionSource.session.player.Faction == PlanetSideEmpire.VS).foreach {
case JoinChannel(actor, _, _) => actor ! MessageResponse(session, message, channel)
}