From 402259e33824ccc5ae69b790a2df7febb2667ab8 Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Thu, 28 Aug 2025 21:06:19 -0400 Subject: [PATCH] outfit checkpoint --- .../actors/session/SessionActor.scala | 11 +- .../actors/session/csr/GeneralLogic.scala | 17 +- .../actors/session/normal/ChatLogic.scala | 7 +- .../actors/session/normal/GeneralLogic.scala | 55 ++- .../session/spectator/GeneralLogic.scala | 8 +- .../session/support/ChatOperations.scala | 10 +- .../session/support/GeneralOperations.scala | 6 + .../session/support/OutfitInvites.scala | 34 ++ .../support/SessionOutfitHandlers.scala | 390 ++++++++++++++++++ .../scala/net/psforever/objects/Player.scala | 2 + .../converter/AvatarConverter.scala | 4 +- .../game/OutfitMembershipResponse.scala | 4 +- .../psforever/services/chat/ChatChannel.scala | 2 + .../psforever/services/chat/ChatService.scala | 4 + 14 files changed, 541 insertions(+), 13 deletions(-) create mode 100644 src/main/scala/net/psforever/actors/session/support/OutfitInvites.scala create mode 100644 src/main/scala/net/psforever/actors/session/support/SessionOutfitHandlers.scala diff --git a/src/main/scala/net/psforever/actors/session/SessionActor.scala b/src/main/scala/net/psforever/actors/session/SessionActor.scala index 25b5c377..21093f14 100644 --- a/src/main/scala/net/psforever/actors/session/SessionActor.scala +++ b/src/main/scala/net/psforever/actors/session/SessionActor.scala @@ -12,7 +12,7 @@ import net.psforever.objects.serverobject.mount.Mountable import net.psforever.objects.serverobject.terminals.{ProximityUnit, Terminal} import net.psforever.objects.zones.Zone import net.psforever.packet.PlanetSideGamePacket -import net.psforever.packet.game.{AIDamage, ActionCancelMessage, AvatarFirstTimeEventMessage, AvatarGrenadeStateMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BeginZoningMessage, BindPlayerMessage, BugReportMessage, ChangeAmmoMessage, ChangeFireModeMessage, ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestMessage, ChatMsg, ChildObjectStateMessage, ConnectToWorldRequestMessage, CreateShortcutMessage, DeployObjectMessage, DeployRequestMessage, DismountVehicleCargoMsg, DismountVehicleMsg, DisplayedAwardMessage, DropItemMessage, DroppodLaunchRequestMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FavoritesRequest, FrameVehicleStateMessage, FriendsRequest, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, HitMessage, InvalidTerrainMessage, ItemTransactionMessage, LashMessage, LongRangeProjectileInfoMessage, LootItemMessage, MountVehicleCargoMsg, MountVehicleMsg, MoveItemMessage, ObjectDetectedMessage, ObjectHeldMessage, OutfitRequest, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, ProjectileStateMessage, ProximityTerminalUseMessage, ReleaseAvatarRequestMessage, ReloadMessage, RequestDestroyMessage, SetChatFilterMessage, SpawnRequestMessage, SplashHitMessage, SquadDefinitionActionMessage, SquadMembershipRequest, SquadWaypointRequest, TargetingImplantRequest, TradeMessage, UnuseItemMessage, UplinkRequest, UseItemMessage, VehicleStateMessage, VehicleSubStateMessage, VoiceHostInfo, VoiceHostRequest, WarpgateRequest, WeaponDelayFireMessage, WeaponDryFireMessage, WeaponFireMessage, WeaponLazeTargetPositionMessage, ZipLineMessage} +import net.psforever.packet.game.{AIDamage, ActionCancelMessage, AvatarFirstTimeEventMessage, AvatarGrenadeStateMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BeginZoningMessage, BindPlayerMessage, BugReportMessage, ChangeAmmoMessage, ChangeFireModeMessage, ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestMessage, ChatMsg, ChildObjectStateMessage, ConnectToWorldRequestMessage, CreateShortcutMessage, DeployObjectMessage, DeployRequestMessage, DismountVehicleCargoMsg, DismountVehicleMsg, DisplayedAwardMessage, DropItemMessage, DroppodLaunchRequestMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FavoritesRequest, FrameVehicleStateMessage, FriendsRequest, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, HitMessage, InvalidTerrainMessage, ItemTransactionMessage, LashMessage, LongRangeProjectileInfoMessage, LootItemMessage, MountVehicleCargoMsg, MountVehicleMsg, MoveItemMessage, ObjectDetectedMessage, ObjectHeldMessage, OutfitMembershipRequest, OutfitMembershipResponse, OutfitRequest, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, ProjectileStateMessage, ProximityTerminalUseMessage, ReleaseAvatarRequestMessage, ReloadMessage, RequestDestroyMessage, SetChatFilterMessage, SpawnRequestMessage, SplashHitMessage, SquadDefinitionActionMessage, SquadMembershipRequest, SquadWaypointRequest, TargetingImplantRequest, TradeMessage, UnuseItemMessage, UplinkRequest, UseItemMessage, VehicleStateMessage, VehicleSubStateMessage, VoiceHostInfo, VoiceHostRequest, WarpgateRequest, WeaponDelayFireMessage, WeaponDryFireMessage, WeaponFireMessage, WeaponLazeTargetPositionMessage, ZipLineMessage} import net.psforever.services.{InterstellarClusterService => ICS} import net.psforever.services.CavernRotationService import net.psforever.services.CavernRotationService.SendCavernRotationUpdates @@ -610,7 +610,14 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case packet: HitHint => logic.general.handleHitHint(packet) - case _: OutfitRequest => () + case packet: OutfitRequest => + logic.general.handleOutfitRequest(packet) + + case packet: OutfitMembershipRequest => + logic.general.handleOutfitMembershipRequest(packet) + + case packet: OutfitMembershipResponse => + logic.general.handleOutfitMembershipResponse(packet) case pkt => data.log.warn(s"Unhandled GamePacket $pkt") diff --git a/src/main/scala/net/psforever/actors/session/csr/GeneralLogic.scala b/src/main/scala/net/psforever/actors/session/csr/GeneralLogic.scala index 2cd1782e..f32bdef9 100644 --- a/src/main/scala/net/psforever/actors/session/csr/GeneralLogic.scala +++ b/src/main/scala/net/psforever/actors/session/csr/GeneralLogic.scala @@ -30,11 +30,13 @@ import net.psforever.objects.vehicles.Utility import net.psforever.objects.vital.Vitality import net.psforever.objects.zones.{ZoneProjectile, Zoning} import net.psforever.packet.PlanetSideGamePacket -import net.psforever.packet.game.{ActionCancelMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, BugReportMessage, ChangeFireModeMessage, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestMessage, ChatMsg, CollisionIs, ConnectToWorldRequestMessage, CreateShortcutMessage, DeadState, DeployObjectMessage, DisplayedAwardMessage, DropItemMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FriendsRequest, GenericAction, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, InvalidTerrainMessage, LootItemMessage, MoveItemMessage, ObjectDetectedMessage, ObjectHeldMessage, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, RequestDestroyMessage, TargetingImplantRequest, TerrainCondition, TradeMessage, UnuseItemMessage, UseItemMessage, VoiceHostInfo, VoiceHostRequest, ZipLineMessage} +import net.psforever.packet.game.OutfitEventAction.{OutfitInfo, OutfitRankNames, Unk0, Unk1} +import net.psforever.packet.game.{ActionCancelMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, BugReportMessage, ChangeFireModeMessage, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestMessage, ChatMsg, CollisionIs, ConnectToWorldRequestMessage, CreateShortcutMessage, DeadState, DeployObjectMessage, DisplayedAwardMessage, DropItemMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FriendsRequest, GenericAction, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, InvalidTerrainMessage, LootItemMessage, MoveItemMessage, ObjectDetectedMessage, ObjectHeldMessage, OutfitEvent, OutfitMemberEvent, OutfitMembershipRequest, OutfitMembershipResponse, OutfitRequest, OutfitRequestAction, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, RequestDestroyMessage, TargetingImplantRequest, TerrainCondition, TradeMessage, UnuseItemMessage, UseItemMessage, VoiceHostInfo, VoiceHostRequest, ZipLineMessage} import net.psforever.services.RemoverActor import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.local.{LocalAction, LocalServiceMessage} import net.psforever.types.{CapacitorStateType, ChatMessageType, Cosmetic, ExoSuitType, PlanetSideEmpire, PlanetSideGUID, Vector3} +import scodec.bits.ByteVector import scala.util.Success @@ -665,6 +667,19 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex val HitHint(_, _) = pkt } + def handleOutfitMembershipRequest(pkt: OutfitMembershipRequest): Unit = {} + + def handleOutfitMembershipResponse(pkt: OutfitMembershipResponse): Unit = {} + + def handleOutfitRequest(pkt: OutfitRequest): Unit = { + pkt match { + case OutfitRequest(_, OutfitRequestAction.Unk3(true)) => + + case OutfitRequest(_, OutfitRequestAction.Unk3(false)) => + + case _ => + } + } /* messages */ def handleRenewCharSavedTimer(): Unit = { /* */ } diff --git a/src/main/scala/net/psforever/actors/session/normal/ChatLogic.scala b/src/main/scala/net/psforever/actors/session/normal/ChatLogic.scala index 0d0cd12d..55206318 100644 --- a/src/main/scala/net/psforever/actors/session/normal/ChatLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/ChatLogic.scala @@ -7,7 +7,7 @@ import net.psforever.actors.session.spectator.SpectatorMode import net.psforever.actors.session.support.{ChatFunctions, ChatOperations, SessionData} import net.psforever.objects.Session import net.psforever.packet.game.{ChatMsg, ServerType, SetChatFilterMessage} -import net.psforever.services.chat.{DefaultChannel, SquadChannel} +import net.psforever.services.chat.{DefaultChannel, OutfitChannel, SquadChannel} import net.psforever.types.ChatMessageType import net.psforever.types.ChatMessageType.{CMT_TOGGLESPECTATORMODE, CMT_TOGGLE_GM} import net.psforever.util.Config @@ -79,6 +79,9 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext case (CMT_SQUAD, _, _) => ops.commandSquad(session, message, SquadChannel(sessionLogic.squad.squad_guid)) + case (CMT_OUTFIT, _, _) => + ops.commandOutfit(session, message, OutfitChannel(sessionLogic.player.outfit_id)) + case (CMT_WHO | CMT_WHO_CSR | CMT_WHO_CR | CMT_WHO_PLATOONLEADERS | CMT_WHO_SQUADLEADERS | CMT_WHO_TEAMS, _, _) => ops.commandWho(session) @@ -100,7 +103,7 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext def handleIncomingMessage(message: ChatMsg, fromSession: Session): Unit = { import ChatMessageType._ message.messageType match { - case CMT_BROADCAST | CMT_SQUAD | CMT_PLATOON | CMT_COMMAND | CMT_NOTE => + case CMT_BROADCAST | CMT_SQUAD | CMT_PLATOON | CMT_COMMAND | CMT_NOTE | CMT_OUTFIT => ops.commandIncomingSendAllIfOnline(session, message) case CMT_OPEN => 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 608f06d0..ac21b6e2 100644 --- a/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala @@ -4,7 +4,7 @@ package net.psforever.actors.session.normal import akka.actor.typed.scaladsl.adapter._ import akka.actor.{ActorContext, ActorRef, typed} import net.psforever.actors.session.{AvatarActor, SessionActor} -import net.psforever.actors.session.support.{GeneralFunctions, GeneralOperations, SessionData} +import net.psforever.actors.session.support.{GeneralFunctions, GeneralOperations, SessionData, SessionOutfitHandlers} import net.psforever.objects.{Account, BoomerDeployable, BoomerTrigger, ConstructionItem, GlobalDefinitions, LivePlayerList, Player, SensorDeployable, ShieldGeneratorDeployable, SpecialEmp, TelepadDeployable, Tool, TrapDeployable, TurretDeployable, Vehicle} import net.psforever.objects.avatar.{Avatar, PlayerControl, SpecialCarry} import net.psforever.objects.ballistics.Projectile @@ -37,13 +37,16 @@ import net.psforever.objects.vital.etc.SuicideReason import net.psforever.objects.vital.interaction.DamageInteraction import net.psforever.objects.zones.{ZoneProjectile, Zoning} import net.psforever.packet.PlanetSideGamePacket -import net.psforever.packet.game.{ActionCancelMessage, ActionResultMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, BugReportMessage, ChangeFireModeMessage, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestAction, CharacterRequestMessage, ChatMsg, CollisionIs, ConnectToWorldRequestMessage, CreateShortcutMessage, DeadState, DeployObjectMessage, DisplayedAwardMessage, DropItemMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FriendsRequest, GenericAction, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, InvalidTerrainMessage, LootItemMessage, MoveItemMessage, ObjectDetectedMessage, ObjectHeldMessage, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, RequestDestroyMessage, TargetingImplantRequest, TerrainCondition, TradeMessage, UnuseItemMessage, UseItemMessage, VoiceHostInfo, VoiceHostRequest, ZipLineMessage} +import net.psforever.packet.game.OutfitEventAction.{OutfitInfo, OutfitRankNames, Unk2} +import net.psforever.packet.game.{ActionCancelMessage, ActionResultMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, BugReportMessage, ChangeFireModeMessage, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestAction, CharacterRequestMessage, ChatMsg, CollisionIs, ConnectToWorldRequestMessage, CreateShortcutMessage, DeadState, DeployObjectMessage, DisplayedAwardMessage, DropItemMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FriendsRequest, GenericAction, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, InvalidTerrainMessage, LootItemMessage, MoveItemMessage, ObjectDetectedMessage, ObjectHeldMessage, OutfitEvent, OutfitMembershipRequest, OutfitMembershipRequestAction, OutfitMembershipResponse, OutfitRequest, OutfitRequestAction, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, RequestDestroyMessage, TargetingImplantRequest, TerrainCondition, TradeMessage, UnuseItemMessage, UseItemMessage, VoiceHostInfo, VoiceHostRequest, ZipLineMessage} import net.psforever.services.account.{AccountPersistenceService, RetrieveAccountData} import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} +import net.psforever.services.chat.OutfitChannel import net.psforever.services.local.{LocalAction, LocalServiceMessage} import net.psforever.services.local.support.CaptureFlagManager import net.psforever.types.{CapacitorStateType, ChatMessageType, Cosmetic, ExoSuitType, ImplantType, PlanetSideEmpire, PlanetSideGUID, Vector3} import net.psforever.util.Config +import net.psforever.zones.Zones.zones import scala.concurrent.duration._ @@ -796,6 +799,54 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex val HitHint(_, _) = pkt } + def handleOutfitMembershipRequest(pkt: OutfitMembershipRequest): Unit = { + pkt match { + case OutfitMembershipRequest(_, OutfitMembershipRequestAction.Form(_, outfitName)) => + if (player.outfit_id == 0) { + SessionOutfitHandlers.HandleOutfitForm(outfitName, player, sessionLogic) + } + + case OutfitMembershipRequest(_, OutfitMembershipRequestAction.Invite(_, invitedName)) => + SessionOutfitHandlers.HandleOutfitInvite(zones, invitedName, player) + + case OutfitMembershipRequest(_, OutfitMembershipRequestAction.Kick(memberId, _)) => + SessionOutfitHandlers.HandleOutfitKick(zones, memberId, player) + + case OutfitMembershipRequest(_, OutfitMembershipRequestAction.SetRank(memberId, newRank, _)) => + SessionOutfitHandlers.HandleOutfitPromote(zones, memberId, newRank, player) + + case OutfitMembershipRequest(_, OutfitMembershipRequestAction.AcceptInvite(_)) => + SessionOutfitHandlers.HandleOutfitInviteAccept(player, sessionLogic) + + case _ => + } + } + + def handleOutfitMembershipResponse(pkt: OutfitMembershipResponse): Unit = {} + + def handleOutfitRequest(pkt: OutfitRequest): Unit = { + pkt match { + + case OutfitRequest(_, OutfitRequestAction.Ranks(List(r1, r2, r3, r4, r5, r6, r7, r8))) => + // update db + //sendResponse(OutfitEvent(6418, Unk2(OutfitInfo(player.outfit_name, 0, 0, 1, OutfitRankNames(r1.getOrElse(""), r2.getOrElse(""), r3.getOrElse(""), r4.getOrElse(""), r5.getOrElse(""), r6.getOrElse(""), r7.getOrElse(""), r8.getOrElse("")), "Welcome to the first PSForever Outfit!", 0, unk11=true, 0, 8888888, 0, 0, 0)))) + + case OutfitRequest(_, OutfitRequestAction.Motd(message)) => + // update db + //sendResponse(OutfitEvent(6418, Unk2(OutfitInfo(player.outfit_name, 0, 0, 1, OutfitRankNames("", "", "", "", "", "", "", ""), message, 0, unk11=true, 0, 8888888, 0, 0, 0)))) + + case OutfitRequest(_, OutfitRequestAction.Unk3(true)) => + SessionOutfitHandlers.HandleViewOutfitWindow(zones, player, player.outfit_id) + + case OutfitRequest(_, OutfitRequestAction.Unk3(false)) => + + case OutfitRequest(_, OutfitRequestAction.Unk4(true)) => + SessionOutfitHandlers.HandleGetOutfitList(player) + + case _ => + } + } + /* messages */ def handleRenewCharSavedTimer(): Unit = { 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 e1f6415f..8c332585 100644 --- a/src/main/scala/net/psforever/actors/session/spectator/GeneralLogic.scala +++ b/src/main/scala/net/psforever/actors/session/spectator/GeneralLogic.scala @@ -13,7 +13,7 @@ import net.psforever.objects.serverobject.doors.Door import net.psforever.objects.vehicles.Utility import net.psforever.objects.zones.ZoneProjectile import net.psforever.packet.PlanetSideGamePacket -import net.psforever.packet.game.{ActionCancelMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, BugReportMessage, ChangeFireModeMessage, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestMessage, ConnectToWorldRequestMessage, CreateShortcutMessage, DeployObjectMessage, DisplayedAwardMessage, DropItemMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FriendsRequest, GenericAction, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, ImplantAction, InvalidTerrainMessage, LootItemMessage, MoveItemMessage, ObjectDetectedMessage, ObjectHeldMessage, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, RequestDestroyMessage, TargetingImplantRequest, TradeMessage, UnuseItemMessage, UseItemMessage, VoiceHostInfo, VoiceHostRequest, ZipLineMessage} +import net.psforever.packet.game.{ActionCancelMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, BugReportMessage, ChangeFireModeMessage, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestMessage, ConnectToWorldRequestMessage, CreateShortcutMessage, DeployObjectMessage, DisplayedAwardMessage, DropItemMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FriendsRequest, GenericAction, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, ImplantAction, InvalidTerrainMessage, LootItemMessage, MoveItemMessage, ObjectDetectedMessage, ObjectHeldMessage, OutfitMembershipRequest, OutfitMembershipResponse, OutfitRequest, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, RequestDestroyMessage, TargetingImplantRequest, TradeMessage, UnuseItemMessage, UseItemMessage, VoiceHostInfo, VoiceHostRequest, ZipLineMessage} import net.psforever.services.account.AccountPersistenceService import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.types.{ExoSuitType, Vector3} @@ -375,6 +375,12 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex def handleHitHint(pkt: HitHint): Unit = { /* intentionally blank */ } + def handleOutfitMembershipRequest(pkt: OutfitMembershipRequest): Unit = {} + + def handleOutfitMembershipResponse(pkt: OutfitMembershipResponse): Unit = {} + + def handleOutfitRequest(pkt: OutfitRequest): Unit = {} + /* messages */ def handleRenewCharSavedTimer(): Unit = { /* intentionally blank */ } diff --git a/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala b/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala index 48a724db..bdaa6f2e 100644 --- a/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala @@ -14,7 +14,7 @@ import net.psforever.objects.LivePlayerList 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.chat.{DefaultChannel, OutfitChannel, SquadChannel} import net.psforever.services.local.{LocalAction, LocalServiceMessage} import net.psforever.services.teamwork.{SquadResponse, SquadService, SquadServiceResponse} import net.psforever.types.ChatMessageType.CMT_QUIT @@ -446,6 +446,14 @@ class ChatOperations( } } + def commandOutfit(session: Session, message: ChatMsg, toChannel: ChatChannel): Unit = { + channels.foreach { + case _/*channel*/: OutfitChannel => + commandSendToRecipient(session, message, toChannel) + case _ => () + } + } + def commandWho(session: Session): Unit = { val players = session.zone.Players val popTR = players.count(_.faction == PlanetSideEmpire.TR) 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 e72e57a5..9f5ce393 100644 --- a/src/main/scala/net/psforever/actors/session/support/GeneralOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/GeneralOperations.scala @@ -166,6 +166,12 @@ trait GeneralFunctions extends CommonSessionInterfacingFunctionality { def handleCanNotPutItemInSlot(msg: Containable.CanNotPutItemInSlot): Unit def handleReceiveDefaultMessage(default: Any, sender: ActorRef): Unit + + def handleOutfitMembershipRequest(pkt: OutfitMembershipRequest): Unit + + def handleOutfitMembershipResponse(pkt: OutfitMembershipResponse): Unit + + def handleOutfitRequest(pkt: OutfitRequest): Unit } class GeneralOperations( diff --git a/src/main/scala/net/psforever/actors/session/support/OutfitInvites.scala b/src/main/scala/net/psforever/actors/session/support/OutfitInvites.scala new file mode 100644 index 00000000..5465cee5 --- /dev/null +++ b/src/main/scala/net/psforever/actors/session/support/OutfitInvites.scala @@ -0,0 +1,34 @@ +package net.psforever.actors.session.support + +import net.psforever.objects.Player +import scala.collection.mutable + +case class OutfitInvite( + sentTo: Player, + sentFrom: Player, + timestamp: Long = System.currentTimeMillis() / 1000 + ) + +object OutfitInviteManager { + private val invites = mutable.Map[Long, OutfitInvite]() + private val ExpirationSeconds = 320 + + def addOutfitInvite(invite: OutfitInvite): Boolean = { + invites.get(invite.sentTo.CharId) match { + case Some(existing) if (System.currentTimeMillis() / 1000 - existing.timestamp) < ExpirationSeconds => + false // Reject new invite (previous one is still valid) + case _ => + invites(invite.sentTo.CharId) = invite + true + } + } + + def removeOutfitInvite(sentToId: Long): Unit = { + invites.remove(sentToId) + } + + def getOutfitInvite(sentToId: Long): Option[OutfitInvite] = invites.get(sentToId) + + def getAllOutfitInvites: List[OutfitInvite] = invites.values.toList +} + diff --git a/src/main/scala/net/psforever/actors/session/support/SessionOutfitHandlers.scala b/src/main/scala/net/psforever/actors/session/support/SessionOutfitHandlers.scala new file mode 100644 index 00000000..4b83e5a0 --- /dev/null +++ b/src/main/scala/net/psforever/actors/session/support/SessionOutfitHandlers.scala @@ -0,0 +1,390 @@ +// Copyright (c) 2025 PSForever +package net.psforever.actors.session.support + +import io.getquill.{ActionReturning, EntityQuery, Insert, PostgresJAsyncContext, Query, Quoted, SnakeCase} +import net.psforever.objects.avatar.PlayerControl +import net.psforever.objects.zones.Zone +import net.psforever.objects.Player +import net.psforever.packet.game.OutfitEventAction.{OutfitInfo, OutfitRankNames, Unk0, Unk1, Unk2} +import net.psforever.packet.game.OutfitMembershipResponse.PacketType.CreateResponse +import net.psforever.packet.game._ +import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} +import net.psforever.services.chat.OutfitChannel +import net.psforever.types.ChatMessageType +import net.psforever.util.Config + +import java.time.LocalDateTime +import scala.util.{Failure, Success} + +object SessionOutfitHandlers { + + case class Avatar(id: Long, name: String, faction_id: Int, last_login: java.time.LocalDateTime) + case class Outfit(id: Long, name: String, faction: Int, owner_id: Long, motd: Option[String], created: java.time.LocalDateTime, + rank0: Option[String], + rank1: Option[String], + rank2: Option[String], + rank3: Option[String], + rank4: Option[String], + rank5: Option[String], + rank6: Option[String], + rank7: Option[String]) + case class Outfitmember(id: Long, outfit_id: Long, avatar_id: Long, rank: Int) + case class Outfitpoint(id: Long, outfit_id: Long, avatar_id: Long, points: Long) + case class OutfitpointMv(outfit_id: Long, points: Long) + + val ctx = new PostgresJAsyncContext(SnakeCase, Config.config.getConfig("database")) + import ctx._ + + import scala.concurrent.ExecutionContext.Implicits.global + import scala.concurrent.Future + + def HandleOutfitForm(outfitName: String, player: Player, session: SessionData): Unit = { + val cleanedName = sanitizeOutfitName(outfitName) + + cleanedName match { + case Some(validName) => + ctx.run(findOutfitByName(validName)).flatMap { + case existing if existing.nonEmpty => + PlayerControl.sendResponse(player.Zone, player.Name, + ChatMsg(ChatMessageType.UNK_227, "@OutfitErrorNameAlreadyTaken")) + Future.successful(()) + + case _ => + createNewOutfit(validName, player.Faction.id, player.CharId).map { outfit => + val seconds: Long = + outfit.created.atZone(java.time.ZoneOffset.UTC).toInstant.toEpochMilli / 1000 + + PlayerControl.sendResponse(player.Zone, player.Name, + OutfitEvent(outfit.id, Unk2( + OutfitInfo( + outfit.name, 0, 0, 1, + OutfitRankNames("", "", "", "", "", "", "", ""), + "", + 14, unk11 = true, 0, seconds, 0, 0, 0)))) + + PlayerControl.sendResponse(player.Zone, player.Name, + OutfitMemberUpdate(outfit.id, player.CharId, 7, flag = true)) + + PlayerControl.sendResponse(player.Zone, player.Name, + ChatMsg(ChatMessageType.UNK_227, "@OutfitCreateSuccess")) + + PlayerControl.sendResponse(player.Zone, player.Name, + OutfitMembershipResponse(CreateResponse, 0, 0, player.CharId, 0, "", "", flag = true)) + + player.outfit_id = outfit.id + player.outfit_name = outfit.name + + session.chat.JoinChannel(OutfitChannel(player.outfit_id)) + } + .recover { case e => + e.printStackTrace() + PlayerControl.sendResponse(player.Zone, player.Name, + ChatMsg(ChatMessageType.UNK_227, "@OutfitCreateFailure")) + } + } + case None => + PlayerControl.sendResponse(player.Zone, player.Name, + ChatMsg(ChatMessageType.UNK_227, "@OutfitCreateFailure")) + } + } + + def HandleOutfitInvite(zones: Seq[Zone], invitedName: String, sentFrom: Player): Unit = { + findPlayerByNameForOutfitAction(zones, invitedName, sentFrom).foreach { invitedPlayer => + + PlayerControl.sendResponse(invitedPlayer.Zone, invitedPlayer.Name, + OutfitMembershipResponse(OutfitMembershipResponse.PacketType.Invite, 0, 0, + sentFrom.CharId, sentFrom.CharId, sentFrom.Name, sentFrom.outfit_name, flag = false)) + + PlayerControl.sendResponse(sentFrom.Zone, sentFrom.Name, + OutfitMembershipResponse(OutfitMembershipResponse.PacketType.Invite, 0, 0, + sentFrom.CharId, invitedPlayer.CharId, invitedPlayer.Name, sentFrom.outfit_name, flag = true)) + + val outfitInvite = OutfitInvite(invitedPlayer, sentFrom) + OutfitInviteManager.addOutfitInvite(outfitInvite) + } + } + + def HandleOutfitInviteAccept(invited: Player, session: SessionData): Unit = { + OutfitInviteManager.getOutfitInvite(invited.CharId) match { + case Some(outfitInvite) => + val outfitId = outfitInvite.sentFrom.outfit_id + + (for { + _ <- addMemberToOutfit(outfitId, invited.CharId) + outfitOpt <- ctx.run(getOutfitById(outfitId)).map(_.headOption) + memberCount <- ctx.run(getOutfitMemberCount(outfitId)) + points <- ctx.run(getOutfitPoints(outfitId)).map(_.headOption.map(_.points).getOrElse(0L)) + } yield (outfitOpt, memberCount, points)) + .map { + case (Some(outfit), memberCount, points) => + + PlayerControl.sendResponse(outfitInvite.sentFrom.Zone, outfitInvite.sentFrom.Name, + OutfitMembershipResponse( + OutfitMembershipResponse.PacketType.Unk2, 0, 0, + invited.CharId, outfitInvite.sentFrom.CharId, invited.Name, outfit.name, flag = false)) + + PlayerControl.sendResponse(invited.Zone, invited.Name, + OutfitMembershipResponse( + OutfitMembershipResponse.PacketType.Unk2, 0, 0, + invited.CharId, outfitInvite.sentFrom.CharId, invited.Name, outfit.name, flag = true)) + + PlayerControl.sendResponse(outfitInvite.sentFrom.Zone, outfitInvite.sentFrom.Name, + OutfitEvent(outfitId, OutfitEventAction.Unk5(memberCount))) + + PlayerControl.sendResponse(outfitInvite.sentFrom.Zone, outfitInvite.sentFrom.Name, + OutfitMemberEvent(outfitId, invited.CharId, + OutfitMemberEventAction.Unk0(invited.Name, 0, 0, 0, + OutfitMemberEventAction.PacketType.Padding, 0))) + + val seconds: Long = outfit.created.atZone(java.time.ZoneOffset.UTC).toInstant.toEpochMilli / 1000 + PlayerControl.sendResponse(invited.Zone, invited.Name, + OutfitEvent(outfitId, Unk0(OutfitInfo( + outfit.name, points, points, memberCount, + OutfitRankNames("", "", "", "", "", "", "", ""), + outfit.motd.getOrElse(""), + 14, unk11 = true, 0, seconds, 0, 0, 0)))) + + PlayerControl.sendResponse(invited.Zone, invited.Name, + OutfitMemberUpdate(outfit.id, invited.CharId, 0, flag=true)) + + OutfitInviteManager.removeOutfitInvite(invited.CharId) + + session.chat.JoinChannel(OutfitChannel(outfit.id)) + invited.outfit_id = outfit.id + invited.outfit_name = outfit.name + case (None, _, _) => + + PlayerControl.sendResponse(invited.Zone, invited.Name, + ChatMsg(ChatMessageType.UNK_227, "Failed to join outfit")) + } + .recover { case _ => + PlayerControl.sendResponse(invited.Zone, invited.Name, + ChatMsg(ChatMessageType.UNK_227, "Failed to join outfit")) + } + case None => + } + } + + def HandleOutfitKick(zones: Seq[Zone], kickedId: Long, kickedBy: Player): Unit = { + // if same id, player has left the outfit by their own choice + if (kickedId == kickedBy.CharId) { + // db stuff first + PlayerControl.sendResponse(kickedBy.Zone, kickedBy.Name, OutfitMemberEvent(kickedBy.outfit_id, kickedId, OutfitMemberEventAction.Unk1())) + } + else { + // db stuff first + // tell player they've been kicked (if online) + findPlayerByIdForOutfitAction(zones, kickedId, kickedBy).foreach { kicked => + PlayerControl.sendResponse(kicked.Zone, kicked.Name, OutfitMembershipResponse(OutfitMembershipResponse.PacketType.Kick, 0, 1, kickedBy.CharId, kicked.CharId, kicked.Name, kickedBy.Name, flag = false)) + kicked.Zone.AvatarEvents ! AvatarServiceMessage(kicked.Zone.id, AvatarAction.PlanetsideAttributeToAll(kicked.GUID, 39, 0)) + //kicked.Zone.AvatarEvents ! AvatarServiceMessage(kicked.Zone.id, AvatarAction.PlanetsideStringAttributeMessage(kicked.GUID, 0, "")) + kicked.outfit_id = 0 + kicked.outfit_name = "" + PlayerControl.sendResponse(kicked.Zone, kicked.Name, OutfitMemberEvent(kickedBy.outfit_id, kickedId, OutfitMemberEventAction.Unk1())) + + // move this out of foreach - db will provide kicked char details + PlayerControl.sendResponse(kickedBy.Zone, kickedBy.Name, OutfitMembershipResponse(OutfitMembershipResponse.PacketType.Kick, 0, 1, kickedBy.CharId, kicked.CharId, kicked.Name, "", flag = true)) + PlayerControl.sendResponse(kickedBy.Zone, kickedBy.Name, OutfitMemberEvent(kickedBy.outfit_id, kickedId, OutfitMemberEventAction.Unk1())) + // new number of outfit members? + PlayerControl.sendResponse(kickedBy.Zone, kickedBy.Name, OutfitEvent(kickedBy.outfit_id, OutfitEventAction.Unk5(34))) + } + } + } + + def HandleOutfitPromote(zones: Seq[Zone], promotedId: Long, newRank: Int, promoter: Player): Unit = { + // send to all online players in outfit + findPlayerByIdForOutfitAction(zones, promotedId, promoter).foreach { promoted => + PlayerControl.sendResponse(promoted.Zone, promoted.Name, OutfitMemberEvent(6418, promotedId, OutfitMemberEventAction.Unk0(promoted.Name, newRank, 1032432, 0, OutfitMemberEventAction.PacketType.Padding, 0))) + PlayerControl.sendResponse(promoter.Zone, promoter.Name, OutfitMemberEvent(6418, promotedId, OutfitMemberEventAction.Unk0(promoted.Name, newRank, 1032432, 0, OutfitMemberEventAction.PacketType.Padding, 0))) + } + } + + def HandleViewOutfitWindow(zones: Seq[Zone], player: Player, outfitId: Long): Unit = { + val outfitDetailsF = for { + outfitOpt <- ctx.run(getOutfitById(outfitId)).map(_.headOption) + memberCount <- ctx.run(query[Outfitmember].filter(_.outfit_id == lift(outfitId)).size) + pointsTotal <- ctx.run(querySchema[OutfitpointMv]("outfitpoint_mv").filter(_.outfit_id == lift(outfitId))) + } yield (outfitOpt, memberCount, pointsTotal.headOption.map(_.points).getOrElse(0L)) + + val membersF = ctx.run(getOutfitMembersWithDetails(outfitId)) + + for { + (outfitOpt, memberCount, totalPoints) <- outfitDetailsF + members <- membersF + } yield { + outfitOpt.foreach { outfit => + val seconds: Long = outfit.created.atZone(java.time.ZoneOffset.UTC).toInstant.toEpochMilli / 1000 + + PlayerControl.sendResponse(player.Zone, player.Name, + OutfitEvent(outfit.id, Unk0(OutfitInfo( + outfit.name, + totalPoints, + totalPoints, + memberCount, + OutfitRankNames("", "", "", "", "", "", "", ""), + outfit.motd.getOrElse(""), + 14, unk11 = true, 0, seconds, 0, 0, 0)))) + + members.foreach { case (avatarId, avatarName, points, rank, login) => + val lastLogin = findPlayerByIdForOutfitAction(zones, avatarId, player) match { + case Some(_) => 0L + case None if player.Name == avatarName => 0L + case None => (System.currentTimeMillis() - login.atZone(java.time.ZoneOffset.UTC).toInstant.toEpochMilli) / 1000 + } + PlayerControl.sendResponse(player.Zone, player.Name, + OutfitMemberEvent(outfit.id, avatarId, + OutfitMemberEventAction.Unk0( + avatarName, + rank, + points, + lastLogin, + OutfitMemberEventAction.PacketType.Padding, 0))) + } + PlayerControl.sendResponse(player.Zone, player.Name, + OutfitEvent(outfit.id, Unk1())) + } + } + } + + def HandleGetOutfitList(player: Player): Unit = { + val q = getOutfitsByEmpire(player.Faction.id) + val futureResult = ctx.run(q) + + futureResult.onComplete { + case Success(rows) => + rows.foreach { case (outfitId, points, name, leaderName, memberCount) => + PlayerControl.sendResponse(player.Zone, player.Name, + OutfitListEvent( + OutfitListEventAction.ListElementOutfit( + outfitId, + points, + memberCount, + name, + leaderName))) + } + + case Failure(_) => + PlayerControl.sendResponse(player.Zone, player.Name, + ChatMsg(ChatMessageType.UNK_227, "Outfit list failed to return") + ) + } + } + + /* supporting functions */ + + def sanitizeOutfitName(name: String): Option[String] = { + val cleaned = name + .replaceAll("""[^A-Za-z0-9\-="\;\[\]\(\)\. ]""", "") // Remove disallowed chars + .replaceAll(" +", " ") // Collapse multiple spaces to one + .trim // Remove leading/trailing spaces + if (cleaned.length >= 2 && cleaned.length <= 32) Some(cleaned) else None + } + + def findPlayerByNameForOutfitAction(zones: Iterable[Zone], targetName: String, inviter: Player): Option[Player] = { + zones + .flatMap(_.LivePlayers) + .find(p => + p.Name.equalsIgnoreCase(targetName) && p.Name != inviter.Name && + p.Faction == inviter.Faction && p.outfit_id == 0 + ) + } + + def findPlayerByIdForOutfitAction(zones: Iterable[Zone], targetId: Long, initiator: Player): Option[Player] = { + zones + .flatMap(_.LivePlayers) + .find(p => + p.CharId == targetId && p.Name != initiator.Name && + p.Faction == initiator.Faction && p.outfit_id == initiator.outfit_id + ) + } + + /* db actions */ + + def findOutfitByName(name: String): Quoted[EntityQuery[Outfit]] = quote { + query[Outfit].filter(outfit => lift(name).toLowerCase == outfit.name.toLowerCase) + } + + def insertNewOutfit(name: String, faction: Int, owner_id: Long): Quoted[ActionReturning[Outfit, Outfit]] = quote { + query[Outfit] + .insert(_.name -> lift(name), _.faction -> lift(faction), _.owner_id -> lift(owner_id)) + .returning(outfit => outfit) + } + + def insertOutfitMember(outfit_id: Long, avatar_id: Long, rank: Int): Quoted[Insert[Outfitmember]] = quote { + query[Outfitmember].insert( + _.outfit_id -> lift(outfit_id), + _.avatar_id -> lift(avatar_id), + _.rank -> lift(rank) + ) + } + + def insertOutfitPoint(outfit_id: Long, avatar_id: Long): Quoted[Insert[Outfitpoint]] = quote { + query[Outfitpoint].insert( + _.outfit_id -> lift(outfit_id), + _.avatar_id -> lift(avatar_id) + ) + } + + def createNewOutfit(name: String, faction: Int, owner_id: Long): Future[Outfit] = { + ctx.transaction { implicit ec => + for { + outfit <- ctx.run(insertNewOutfit(name, faction, owner_id)) + _ <- ctx.run(insertOutfitMember(outfit.id, owner_id, rank=7)) + _ <- ctx.run(insertOutfitPoint(outfit.id, owner_id)) + } yield outfit + } + } + + def addMemberToOutfit(outfit_id: Long, avatar_id: Long): Future[Unit] = { + ctx.transaction { implicit ec => + for { + _ <- ctx.run(insertOutfitMember(outfit_id, avatar_id, rank=0)) + _ <- ctx.run(insertOutfitPoint(outfit_id, avatar_id)) + } yield () + } + } + + def getOutfitById(id: Long): Quoted[EntityQuery[Outfit]] = quote { + query[Outfit].filter(_.id == lift(id)) + } + + def getOutfitMemberCount(id: Long): Quoted[Long] = quote { + query[Outfitmember].filter(_.outfit_id == lift(id)).size + } + + def getOutfitPoints(id: Long): Quoted[EntityQuery[OutfitpointMv]] = quote { + querySchema[OutfitpointMv]("outfitpoint_mv").filter(_.outfit_id == lift(id)) + } + + def getOutfitMembersWithDetails(outfitId: Long): Quoted[Query[(Long, String, Long, Int, LocalDateTime)]] = quote { + query[Outfitmember] + .filter(_.outfit_id == lift(outfitId)) + .join(query[Avatar]).on(_.avatar_id == _.id) + .leftJoin(query[Outfitpoint]).on { + case ((member, _), points) => + points.outfit_id == member.outfit_id && points.avatar_id == member.avatar_id + } + .map { + case ((member, avatar), pointsOpt) => + (member.avatar_id, avatar.name, pointsOpt.map(_.points).getOrElse(0L), member.rank, avatar.last_login) + } + } + + def getOutfitsByEmpire(playerEmpireId: Int): Quoted[Query[(Long, Long, String, String, Long)]] = quote { + query[Outfit] + .filter(_.faction == lift(playerEmpireId)) + .join(query[Avatar]).on((outfit, avatar) => outfit.owner_id == avatar.id) + .leftJoin( + query[Outfitmember] + .groupBy(_.outfit_id) + .map { case (oid, members) => (oid, members.size) } + ).on { case ((outfit, _), (oid, _)) => oid == outfit.id } + .leftJoin(querySchema[OutfitpointMv]("outfitpoint_mv")).on { + case (((outfit, _), _), points) => points.outfit_id == outfit.id + } + .map { + case (((outfit, leader), memberCounts), points) => + (outfit.id, points.map(_.points).getOrElse(0L), outfit.name, leader.name, memberCounts.map(_._2).getOrElse(0L)) + } + } +} diff --git a/src/main/scala/net/psforever/objects/Player.scala b/src/main/scala/net/psforever/objects/Player.scala index fd00fb54..d4847ad4 100644 --- a/src/main/scala/net/psforever/objects/Player.scala +++ b/src/main/scala/net/psforever/objects/Player.scala @@ -85,6 +85,8 @@ class Player(var avatar: Avatar) var silenced: Boolean = false var death_by: Int = 0 var lastShotSeq_time: Int = -1 + var outfit_name: String = "" + var outfit_id: Long = 0 /** From PlanetsideAttributeMessage */ var PlanetsideAttribute: Array[Long] = Array.ofDim(120) diff --git a/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala b/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala index 1d2f16fe..ea96ad0e 100644 --- a/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala +++ b/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala @@ -93,8 +93,8 @@ object AvatarConverter { 0 ) val ab: (Boolean, Int) => CharacterAppearanceB = CharacterAppearanceB( - 0L, - outfit_name = "", + obj.outfit_id, + obj.outfit_name, outfit_logo = 0, unk1 = false, obj.isBackpack, diff --git a/src/main/scala/net/psforever/packet/game/OutfitMembershipResponse.scala b/src/main/scala/net/psforever/packet/game/OutfitMembershipResponse.scala index 1608cbf9..ebde8270 100644 --- a/src/main/scala/net/psforever/packet/game/OutfitMembershipResponse.scala +++ b/src/main/scala/net/psforever/packet/game/OutfitMembershipResponse.scala @@ -31,11 +31,11 @@ object OutfitMembershipResponse extends Marshallable[OutfitMembershipResponse] { type Type = Value val CreateResponse: PacketType.Value = Value(0) - val Unk1: PacketType.Value = Value(1) // Info: Player has been invited / response to OutfitMembershipRequest Unk2 for that player + val Invite: PacketType.Value = Value(1) // Info: Player has been invited / response to OutfitMembershipRequest Unk2 for that player val Unk2: PacketType.Value = Value(2) // Invited / Accepted / Added val Unk3: PacketType.Value = Value(3) val Unk4: PacketType.Value = Value(4) - val Unk5: PacketType.Value = Value(5) + val Kick: PacketType.Value = Value(5) val Unk6: PacketType.Value = Value(6) // 6 and 7 seen as failed decodes, validity unknown val Unk7: PacketType.Value = Value(7) diff --git a/src/main/scala/net/psforever/services/chat/ChatChannel.scala b/src/main/scala/net/psforever/services/chat/ChatChannel.scala index 3fbea3e6..fab599fd 100644 --- a/src/main/scala/net/psforever/services/chat/ChatChannel.scala +++ b/src/main/scala/net/psforever/services/chat/ChatChannel.scala @@ -12,3 +12,5 @@ final case class SquadChannel(guid: PlanetSideGUID) extends ChatChannel case object SpectatorChannel extends ChatChannel case object CustomerServiceChannel extends ChatChannel + +final case class OutfitChannel(id: Long) 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 8803c366..53c105f9 100644 --- a/src/main/scala/net/psforever/services/chat/ChatService.scala +++ b/src/main/scala/net/psforever/services/chat/ChatService.scala @@ -57,6 +57,7 @@ class ChatService(context: ActorContext[ChatService.Command]) extends AbstractBe (channel, message.messageType) match { case (SquadChannel(_), CMT_SQUAD) => () case (SquadChannel(_), CMT_VOICE) if message.contents.startsWith("SH") => () + case (OutfitChannel(_), CMT_OUTFIT) => () case (DefaultChannel, messageType) if messageType != CMT_SQUAD => () case (SpectatorChannel, messageType) if messageType != CMT_SQUAD => () case _ => @@ -158,6 +159,9 @@ class ChatService(context: ActorContext[ChatService.Command]) extends AbstractBe case CMT_SQUAD => subs.foreach(_.actor ! MessageResponse(session, message, channel)) + case CMT_OUTFIT => + subs.foreach(_.actor ! MessageResponse(session, message, channel)) + case CMT_NOTE => subs .filter(_.sessionSource.session.player.Name == message.recipient)