From 8e7be33a15b7bb16b1d3b76843dbc948275b5b1d Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Sat, 4 May 2024 09:26:38 -0400 Subject: [PATCH] modified ChatActor to be replaced by a function-data logic pair, and modified ChatService to be able to accommodate the new chat channel; chat packet handling moved from general operations to the new chat operations --- .../psforever/actors/session/ChatActor.scala | 19 +- .../actors/session/SessionActor.scala | 4 +- .../actors/session/normal/ChatLogic.scala | 234 +++ .../actors/session/normal/GeneralLogic.scala | 19 +- .../actors/session/normal/NormalMode.scala | 18 +- .../session/normal/SquadHandlerLogic.scala | 12 +- .../actors/session/spectator/ChatLogic.scala | 143 ++ .../session/spectator/GeneralLogic.scala | 12 +- .../session/spectator/SpectatorMode.scala | 20 +- .../session/spectator/SquadHandlerLogic.scala | 10 +- .../session/support/ChatOperations.scala | 1292 +++++++++++++++++ .../session/support/GeneralOperations.scala | 9 +- .../actors/session/support/PlayerMode.scala | 1 + .../support/SessionAvatarHandlers.scala | 3 +- .../actors/session/support/SessionData.scala | 40 +- .../support/SessionSquadHandlers.scala | 3 +- .../WeaponAndProjectileOperations.scala | 3 +- .../session/support/ZoningOperations.scala | 6 + .../scala/net/psforever/objects/Session.scala | 4 + .../psforever/services/chat/ChatChannel.scala | 12 + .../psforever/services/chat/ChatService.scala | 41 +- 21 files changed, 1791 insertions(+), 114 deletions(-) create mode 100644 src/main/scala/net/psforever/actors/session/normal/ChatLogic.scala create mode 100644 src/main/scala/net/psforever/actors/session/spectator/ChatLogic.scala create mode 100644 src/main/scala/net/psforever/actors/session/support/ChatOperations.scala create mode 100644 src/main/scala/net/psforever/services/chat/ChatChannel.scala 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) }