From 2372a95040332135fa5bcbe29df042c3446baa18 Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Tue, 10 Sep 2024 20:38:30 -0400 Subject: [PATCH] experimental invitation management commands for squad leaders, mostly untested atm; messages for being denied squad admission --- .../actors/session/normal/ChatLogic.scala | 1 + .../session/normal/SquadHandlerLogic.scala | 3 + .../session/support/ChatOperations.scala | 30 ++ .../actors/session/support/SessionData.scala | 17 +- .../teamwork/SquadInvitationManager.scala | 333 +++++++++++++++--- .../services/teamwork/SquadService.scala | 123 ++++++- .../teamwork/SquadServiceResponse.scala | 2 + .../teamwork/SquadSubscriptionEntity.scala | 10 +- 8 files changed, 436 insertions(+), 83 deletions(-) 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 35c09b7f5..052a1ebb4 100644 --- a/src/main/scala/net/psforever/actors/session/normal/ChatLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/ChatLogic.scala @@ -138,6 +138,7 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext case "grenade" => ops.customCommandGrenade(session, log) case "macro" => ops.customCommandMacro(session, params) case "progress" => ops.customCommandProgress(session, params) + case "squad" => ops.customCommandSquad(params) case _ => // command was not handled sendResponse( 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 b80eb01cd..7ebfdde20 100644 --- a/src/main/scala/net/psforever/actors/session/normal/SquadHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/SquadHandlerLogic.scala @@ -349,6 +349,9 @@ class SquadHandlerLogic(val ops: SessionSquadHandlers, implicit val context: Act case SquadResponse.WaypointEvent(WaypointEventAction.Remove, char_id, waypoint_type, _, _, _) => sendResponse(SquadWaypointEvent.Remove(ops.squad_supplement_id, char_id, waypoint_type)) + case SquadResponse.SquadRelatedComment(comment) => + sendResponse(ChatMsg(ChatMessageType.UNK_227, comment)) + case _ => () } } 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 399fede66..5bfe20f74 100644 --- a/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala @@ -2,6 +2,8 @@ package net.psforever.actors.session.support import akka.actor.Cancellable +import net.psforever.objects.LivePlayerList +import akka.actor.{ActorRef => ClassicActorRef} import akka.actor.typed.ActorRef import akka.actor.{ActorContext, typed} import net.psforever.actors.session.spectator.SpectatorMode @@ -12,6 +14,7 @@ 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.services.teamwork.SquadService import net.psforever.types.ChatMessageType.CMT_QUIT import org.log4s.Logger @@ -54,6 +57,7 @@ class ChatOperations( val sessionLogic: SessionData, val avatarActor: typed.ActorRef[AvatarActor.Command], val chatService: typed.ActorRef[ChatService.Command], + val squadService: ClassicActorRef, val cluster: typed.ActorRef[InterstellarClusterService.Command], implicit val context: ActorContext ) extends CommonSessionInterfacingFunctionality { @@ -1284,6 +1288,32 @@ class ChatOperations( true } + def customCommandSquad(params: Seq[String]): Boolean = { + params match { + case "invites" :: _ => + squadService ! SquadService.ListAllCurrentInvites + + case "accept" :: names if names.contains("all") => + squadService ! SquadService.ChainAcceptance(player, player.CharId, Nil) + case "accept" :: names if names.nonEmpty => + val results = names.flatMap { name => + LivePlayerList.WorldPopulation { case (_, p) => p.name.equals(name) }.map(_.id.toLong) + } + squadService ! SquadService.ChainAcceptance(player, player.CharId, results) + + case "reject" :: names if names.contains("all") => + squadService ! SquadService.ChainRejection(player, player.CharId, Nil) + case "reject" :: names if names.nonEmpty => + val results = names.flatMap { name => + LivePlayerList.WorldPopulation { case (_, p) => p.name.equals(name) }.map(_.id.toLong) + } + squadService ! SquadService.ChainRejection(player, player.CharId, results) + + case _ => () + } + true + } + def firstParam[T]( session: Session, buffer: Iterable[String], 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 68fcf9f0d..44c7ed18c 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionData.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionData.scala @@ -165,15 +165,15 @@ class SessionData( case LookupResult("squad", endpoint) => squadService = endpoint buildDependentOperationsForSquad(endpoint) + buildDependentOperationsForChat(chatService, endpoint, cluster) true 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) + buildDependentOperationsForChat(chatService, squadService, cluster) true case _ => @@ -200,9 +200,16 @@ class SessionData( } } - 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)) + def buildDependentOperationsForChat( + chatService: typed.ActorRef[ChatService.Command], + squadService: ActorRef, + clusterActor: typed.ActorRef[ICS.Command] + ): Unit = { + if (chatOpt.isEmpty && + chatService != Default.typed.Actor && + squadService !=Default.Actor && + clusterActor != Default.typed.Actor) { + chatOpt = Some(new ChatOperations(sessionLogic=this, avatarActor, chatService, squadService, clusterActor, context)) } } diff --git a/src/main/scala/net/psforever/services/teamwork/SquadInvitationManager.scala b/src/main/scala/net/psforever/services/teamwork/SquadInvitationManager.scala index 9e58d2fa2..6622e1b8a 100644 --- a/src/main/scala/net/psforever/services/teamwork/SquadInvitationManager.scala +++ b/src/main/scala/net/psforever/services/teamwork/SquadInvitationManager.scala @@ -4,6 +4,8 @@ package net.psforever.services.teamwork import akka.actor.ActorRef import akka.pattern.ask import akka.util.Timeout + +import scala.annotation.unused import scala.collection.mutable import scala.concurrent.duration._ import scala.concurrent.ExecutionContext.Implicits.global @@ -119,14 +121,15 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { val leader = squad2.Leader.CharId Allowed(invitedPlayer, invitingPlayer) Allowed(leader, invitingPlayer) + lazy val preface = s"$invitingPlayer's invitation got reversed to $invitedPlayer's squad, but" if (squad2.Size == squad2.Capacity) { - log.debug(s"$invitingPlayer's invitation got reversed to $invitedPlayer's squad, but the squad has no available positions") + log.debug(s"$preface the squad has no available positions") } else if (Refused(invitingPlayer).contains(invitedPlayer)) { - log.debug(s"$invitingPlayer's invitation got reversed to $invitedPlayer's squad, but $invitedPlayer repeated a previous refusal to $invitingPlayer's invitation offer") + log.debug(s"$preface $invitedPlayer repeated a previous refusal to $invitingPlayer's invitation offer") } else if (Refused(invitingPlayer).contains(leader)) { - log.debug(s"$invitingPlayer's invitation got reversed to $invitedPlayer's squad, but $leader repeated a previous refusal to $invitingPlayer's invitation offer") + log.debug(s"$preface $leader repeated a previous refusal to $invitingPlayer's invitation offer") } else if (features.DeniedPlayers().contains(invitingPlayer)) { - log.debug(s"$invitingPlayer's invitation got reversed to $invitedPlayer's squad, but $invitingPlayer is denied the invitation") + log.debug(s"$preface $invitingPlayer is denied the invitation") } else { features.AllowedPlayers(invitedPlayer) AddInviteAndRespond( @@ -165,7 +168,7 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { val availableForJoiningSquad = notLimitedByEnrollmentInSquad(invitedPlayerSquadOpt, invitedPlayer) acceptedInvite match { case Some(RequestRole(petitioner, features, position)) - if canEnrollInSquad(features, petitioner.CharId) => + if SquadInvitationManager.canEnrollInSquad(features, petitioner.CharId) => //player requested to join a squad's specific position //invitedPlayer is actually the squad leader; petitioner is the actual "invitedPlayer" if (JoinSquad(petitioner, features, position)) { @@ -174,7 +177,7 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { } case Some(IndirectInvite(recruit, features)) - if canEnrollInSquad(features, recruit.CharId) => + if SquadInvitationManager.canEnrollInSquad(features, recruit.CharId) => //tplayer / invitedPlayer is actually the squad leader val recruitCharId = recruit.CharId HandleVacancyInvite(features, recruitCharId, invitedPlayer, recruit) match { @@ -188,7 +191,7 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { } case Some(VacancyInvite(invitingPlayer, _, features)) - if availableForJoiningSquad && canEnrollInSquad(features, invitedPlayer) => + if availableForJoiningSquad && SquadInvitationManager.canEnrollInSquad(features, invitedPlayer) => //accepted an invitation to join an existing squad HandleVacancyInvite(features, invitedPlayer, invitingPlayer, tplayer) match { case Some((_, line)) => @@ -204,7 +207,7 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { SquadMembershipAcceptInviteAction(invitingPlayer, tplayer, invitedPlayer) case Some(LookingForSquadRoleInvite(member, features, position)) - if availableForJoiningSquad && canEnrollInSquad(features, invitedPlayer) => + if availableForJoiningSquad && SquadInvitationManager.canEnrollInSquad(features, invitedPlayer) => val invitingPlayer = member.CharId features.ProxyInvites = features.ProxyInvites.filterNot { _ == invitedPlayer } if (JoinSquad(tplayer, features, position)) { @@ -215,7 +218,7 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { } case Some(ProximityInvite(member, features, position)) - if availableForJoiningSquad && canEnrollInSquad(features, invitedPlayer) => + if availableForJoiningSquad && SquadInvitationManager.canEnrollInSquad(features, invitedPlayer) => val invitingPlayer = member.CharId features.ProxyInvites = features.ProxyInvites.filterNot { _ == invitedPlayer } if (JoinSquad(tplayer, features, position)) { @@ -280,10 +283,6 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { } } - def canEnrollInSquad(features: SquadFeatures, charId: Long): Boolean = { - !features.Squad.Membership.exists { _.CharId == charId } - } - def SquadMembershipAcceptInviteAction(invitingPlayer: Player, player: Player, invitedPlayer: Long): Unit = { //originally, we were invited by someone into a new squad they would form val invitingPlayerCharId = invitingPlayer.CharId @@ -416,24 +415,75 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { def handleRejection( tplayer: Player, rejectingPlayer: Long, - squadsToLeaders: List[(PlanetSideGUID, Long)] + squadsToLeaders: List[(PlanetSideGUID, SquadFeatures)] ): Unit = { val rejectedBid = RemoveInvite(rejectingPlayer) - (rejectedBid match { + FormatRejection( + DoRejection(rejectedBid, tplayer, rejectingPlayer, squadsToLeaders), + tplayer + ) + NextInviteAndRespond(rejectingPlayer) + } + + def ParseRejection( + rejectedBid: Option[Invitation], + @unused tplayer: Player, + rejectingPlayer: Long, + squadsToLeaders: List[(PlanetSideGUID, SquadFeatures)] + ): (Option[Long], Option[Long]) = { + rejectedBid match { + case Some(SpontaneousInvite(leader)) => + //rejectingPlayer is the would-be squad member; the would-be squad leader sent the request and was rejected + val invitingPlayerCharId = leader.CharId + (Some(rejectingPlayer), Some(invitingPlayerCharId)) + + case Some(VacancyInvite(leader, _, _)) + /*if SquadInvitationManager.notLeaderOfThisSquad(squadsToLeaders, features.Squad.GUID, rejectingPlayer)*/ => + //rejectingPlayer is the would-be squad member; the squad leader sent the request and was rejected + (Some(rejectingPlayer), Some(leader)) + + case Some(ProximityInvite(_, _, _)) + /*if SquadInvitationManager.notLeaderOfThisSquad(squadsToLeaders, features.Squad.GUID, rejectingPlayer)*/ => + //rejectingPlayer is the would-be squad member; the squad leader sent the request and was rejected + (Some(rejectingPlayer), None) + + case Some(LookingForSquadRoleInvite(member, _, _)) + if member.CharId != rejectingPlayer => + val leaderCharId = member.CharId + //rejectingPlayer is the would-be squad member; the squad leader sent the request and was rejected + (Some(rejectingPlayer), Some(leaderCharId)) + + case Some(RequestRole(rejected, features, _)) + if SquadInvitationManager.notLeaderOfThisSquad(squadsToLeaders, features.Squad.GUID, rejected.CharId) => + //rejected is the would-be squad member; rejectingPlayer is the squad leader who rejected the request + (Some(rejectingPlayer), None) + + case _ => //TODO IndirectInvite, etc., but how to handle them? + (None, None) + } + } + + def DoRejection( + rejectedBid: Option[Invitation], + tplayer: Player, + rejectingPlayer: Long, + squadsToLeaders: List[(PlanetSideGUID, SquadFeatures)] + ): (Option[Long], Option[Long], String) = { + rejectedBid match { case Some(SpontaneousInvite(leader)) => //rejectingPlayer is the would-be squad member; the would-be squad leader sent the request and was rejected val invitingPlayerCharId = leader.CharId Refused(rejectingPlayer, invitingPlayerCharId) - (Some(rejectingPlayer), Some(invitingPlayerCharId)) + (Some(rejectingPlayer), Some(invitingPlayerCharId), "anonymous") - case Some(VacancyInvite(leader, _, _)) - /*if notLeaderOfThisSquad(squadsToLeaders, features.Squad.GUID, rejectingPlayer)*/ => + case Some(VacancyInvite(leader, _, features)) + /*if SquadInvitationManager.notLeaderOfThisSquad(squadsToLeaders, features.Squad.GUID, rejectingPlayer)*/ => //rejectingPlayer is the would-be squad member; the squad leader sent the request and was rejected Refused(rejectingPlayer, leader) - (Some(rejectingPlayer), Some(leader)) + (Some(rejectingPlayer), Some(leader), features.Squad.Task) case Some(ProximityInvite(_, features, position)) - /*if notLeaderOfThisSquad(squadsToLeaders, features.Squad.GUID, rejectingPlayer)*/ => + /*if SquadInvitationManager.notLeaderOfThisSquad(squadsToLeaders, features.Squad.GUID, rejectingPlayer)*/ => //rejectingPlayer is the would-be squad member; the squad leader sent the request and was rejected ReloadProximityInvite( tplayer.Zone.Players, @@ -441,52 +491,53 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { features, position ) - (Some(rejectingPlayer), None) + (Some(rejectingPlayer), None, features.Squad.Task) - case Some(LookingForSquadRoleInvite(member, guid, position)) + case Some(LookingForSquadRoleInvite(member, features, position)) if member.CharId != rejectingPlayer => val leaderCharId = member.CharId //rejectingPlayer is the would-be squad member; the squad leader sent the request and was rejected ReloadSearchForRoleInvite( LivePlayerList.WorldPopulation { _ => true }, rejectingPlayer, - guid, + features, position ) - (Some(rejectingPlayer), Some(leaderCharId)) + (Some(rejectingPlayer), Some(leaderCharId), features.Squad.Task) case Some(RequestRole(rejected, features, _)) - if notLeaderOfThisSquad(squadsToLeaders, features.Squad.GUID, rejected.CharId) => + if SquadInvitationManager.notLeaderOfThisSquad(squadsToLeaders, features.Squad.GUID, rejected.CharId) => //rejected is the would-be squad member; rejectingPlayer is the squad leader who rejected the request features.DeniedPlayers(rejected.CharId) - (Some(rejectingPlayer), None) + (Some(rejectingPlayer), None, features.Squad.Task) - case _ => ; //TODO IndirectInvite, etc., but how to handle them? - (None, None) - }) match { - case (Some(rejected), Some(invited)) => + case _ => //TODO IndirectInvite, etc., but how to handle them? + (None, None, "n|a") + } + } + + def FormatRejection( + rejectedPair: (Option[Long], Option[Long], String), + tplayer: Player + ): Unit = { + rejectedPair match { + case (Some(rejected), Some(inviter), squadName) => subs.Publish( rejected, - SquadResponse.Membership(SquadResponseType.Reject, 0, 0, rejected, Some(invited), "", unk5=true, Some(None)) + SquadResponse.Membership(SquadResponseType.Reject, 0, 0, rejected, Some(inviter), "", unk5=true, Some(None)) ) subs.Publish( - invited, - SquadResponse.Membership(SquadResponseType.Reject, 0, 0, invited, Some(rejected), tplayer.Name, unk5=false, Some(None)) + inviter, + SquadResponse.Membership(SquadResponseType.Reject, 0, 0, inviter, Some(rejected), tplayer.Name, unk5=false, Some(None)) ) - case (Some(rejected), None) => + subs.Publish(rejected, SquadResponse.SquadRelatedComment(s"Your request to join squad '$squadName' has been refused.")) + case (Some(rejected), None, squadName) => subs.Publish( rejected, SquadResponse.Membership(SquadResponseType.Reject, 0, 0, rejected, Some(rejected), "", unk5=true, Some(None)) ) - case _ => ; - } - NextInviteAndRespond(rejectingPlayer) - } - - def notLeaderOfThisSquad(squadsToLeaders: List[(PlanetSideGUID, Long)], guid: PlanetSideGUID, charId: Long): Boolean = { - squadsToLeaders.find { case (squadGuid, _) => squadGuid == guid } match { - case Some((_, leaderId)) => leaderId != charId - case None => false + subs.Publish(rejected, SquadResponse.SquadRelatedComment(s"Your request to join squad '$squadName' has been refused.")) + case _ => () } } @@ -611,8 +662,14 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { tplayer: Player, features: SquadFeatures ): Unit = { + SquadActionDefinitionAutoApproveInvitationRequests(tplayer.CharId, features) + } + + def SquadActionDefinitionAutoApproveInvitationRequests( + charId: Long, + features: SquadFeatures + ): Unit = { //allowed auto-approval - resolve the requests (only) - val charId = tplayer.CharId val (requests, others) = (invites.get(charId) match { case Some(invite) => invite +: queuedInvites.getOrElse(charId, Nil) @@ -911,6 +968,155 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { } } + def listCurrentInvitations(charId: Long): List[String] = { + ((invites.get(charId), queuedInvites.get(charId)) match { + case (Some(invite), Some(invites)) => + invite +: invites + case (Some(invite), None) => + List(invite) + case (None, Some(invites)) => + invites + case _ => + List() + }).collect { + case RequestRole(player, _, _) => player.Name + case IndirectInvite(player, features) if !features.Squad.Leader.Name.equals(player.Name) => player.Name + } + } + + def tryChainAcceptance( + inviter: Player, + charId: Long, + list: List[Long], + features: SquadFeatures + ): Unit = { + //filter queued invites + lazy val squadToLeader = List((features.Squad.GUID, features)) + lazy val squadName = features.Squad.Task + var foundPairs: List[(Player, Invitation)] = List() + val unmatchedInvites = queuedInvites + .getOrElse(charId, Nil) + .filter { + case invite @ RequestRole(invitee, _, _) + if list.contains(invitee.CharId) && !features.Squad.Leader.Name.equals(invitee.Name) => + foundPairs = foundPairs :+ (invitee, invite) + false + case invite @ IndirectInvite(invitee, _) + if list.contains(invitee.CharId) && !features.Squad.Leader.Name.equals(invitee.Name) => + foundPairs = foundPairs :+ (invitee, invite) + false + case _ => + true + } + //handle active invite + val clearedActiveInvite = invites + .get(charId) + .collect { + case invite @ RequestRole(invitee, _, _) + if list.contains(invitee.CharId) && !features.Squad.Leader.Name.equals(invitee.Name) => + SquadActionMembershipAcceptInvite(inviter, invitee.CharId, Some(invite), Some(features)) + invites.remove(charId) + true + case invite @ IndirectInvite(invitee, _) + if list.contains(invitee.CharId) && !features.Squad.Leader.Name.equals(invitee.Name) => + SquadActionMembershipAcceptInvite(inviter, invitee.CharId, Some(invite), Some(features)) + invites.remove(charId) + true + case _ => + false + } + //handle selected queued invites + val pairIterator = foundPairs.iterator + while (pairIterator.hasNext && features.Squad.Capacity < features.Squad.Size) { + val (player, invite) = pairIterator.next() + SquadActionMembershipAcceptInvite(inviter, player.CharId, Some(invite), Some(features)) + } + //evaluate final squad composition + if (features.Squad.Capacity < features.Squad.Size) { + //replace unfiltered invites + if (unmatchedInvites.isEmpty) { + queuedInvites.remove(charId) + } else { + queuedInvites.put(charId, unmatchedInvites) + } + //manage next invitation + clearedActiveInvite.collect { + case true => NextInviteAndRespond(charId) + } + } else { + //squad is full + previousInvites.remove(charId) + queuedInvites.remove(charId) + clearedActiveInvite.collect { + case true => invites.remove(charId) + } + unmatchedInvites.foreach { invite => + val (refusedId, _) = ParseRejection(Some(invite), inviter, inviter.CharId, squadToLeader) + subs.Publish(refusedId, SquadResponse.SquadRelatedComment(s"Your request to join squad '$squadName' has been refused.")) + } + NextInviteAndRespond(charId) + } + //clean up any incomplete selected invites + pairIterator.foreach { case (_, invite) => + val (refusedId, _) = ParseRejection(Some(invite), inviter, inviter.CharId, squadToLeader) + subs.Publish(refusedId, SquadResponse.SquadRelatedComment(s"Your request to join squad '$squadName' has been refused.")) + } + } + + def tryChainRejection( + inviter: Player, + charId: Long, + list: List[Long], + features: SquadFeatures): Unit = { + //handle queued invites + lazy val squadToLeader = List((features.Squad.GUID, features)) + lazy val squadName = features.Squad.Task + val unmatchedInvites = queuedInvites + .getOrElse(charId, Nil) + .filter { + case invite @ RequestRole(invitee, _, _) + if list.contains(invitee.CharId) && !features.Squad.Leader.Name.equals(invitee.Name) => + val (refusedId, _, _) = DoRejection(Some(invite), inviter, charId, squadToLeader) + subs.Publish(refusedId, SquadResponse.SquadRelatedComment(s"Your request to join squad '$squadName' has been refused.")) + false + case invite @ IndirectInvite(invitee, _) + if list.contains(invitee.CharId) && !features.Squad.Leader.Name.equals(invitee.Name) => + val (refusedId, _, _) = DoRejection(Some(invite), inviter, charId, squadToLeader) + subs.Publish(refusedId, SquadResponse.SquadRelatedComment(s"Your request to join squad '$squadName' has been refused.")) + false + case _ => + true + } + queuedInvites.put(charId, unmatchedInvites) + //handle active invite + invites + .get(charId) + .collect { + case invite @ RequestRole(player, features, _) + if list.contains(player.CharId) && !features.Squad.Leader.Name.equals(player.Name) => + val (refusedId, _, _) = DoRejection(Some(invite), inviter, charId, squadToLeader) + subs.Publish(refusedId, SquadResponse.SquadRelatedComment(s"Your request to join squad '$squadName' has been refused.")) + invites.remove(charId) + NextInviteAndRespond(charId) + case invite @ IndirectInvite(player, features) + if list.contains(player.CharId) && !features.Squad.Leader.Name.equals(player.Name) => + val (refusedId, _, _) = DoRejection(Some(invite), inviter, charId, squadToLeader) + subs.Publish(refusedId, SquadResponse.SquadRelatedComment(s"Your request to join squad '$squadName' has been refused.")) + invites.remove(charId) + NextInviteAndRespond(charId) + case _ => () + } + } + + def tryChainRejectionAll(charId: Long, features: SquadFeatures): Unit = { + val squadName = features.Squad.Task + CleanUpAllInvitesToSquad(features) + .filterNot(_ == charId) + .foreach { refusedId => + subs.Publish(refusedId, SquadResponse.SquadRelatedComment(s"Your request to join squad '$squadName' has been refused.")) + } + } + def ShiftInvitesToPromotedSquadLeader( sponsoringPlayer: Long, promotedPlayer: Long @@ -1695,18 +1901,18 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { * @see `VacancyInvite` * @param features the squad identifier */ - def CleanUpAllInvitesToSquad(features: SquadFeatures): Unit = { + def CleanUpAllInvitesToSquad(features: SquadFeatures): List[Long] = { val guid = features.Squad.GUID //clean up invites val activeInviteIds = { val keys = invites.keys.toSeq invites.values.zipWithIndex .collect { - case (VacancyInvite(_, _, _squad), index) if _squad.Squad.GUID == guid => index - case (IndirectInvite(_, _squad), index) if _squad.Squad.GUID == guid => index - case (LookingForSquadRoleInvite(_, _squad, _), index) if _squad.Squad.GUID == guid => index - case (ProximityInvite(_, _squad, _), index) if _squad.Squad.GUID == guid => index - case (RequestRole(_, _squad, _), index) if _squad.Squad.GUID == guid => index + case (VacancyInvite(_, _, f), index) if f.Squad.GUID == guid => index + case (IndirectInvite(_, f), index) if f.Squad.GUID == guid => index + case (LookingForSquadRoleInvite(_, f, _), index) if f.Squad.GUID == guid => index + case (ProximityInvite(_, f, _), index) if f.Squad.GUID == guid => index + case (RequestRole(_, f, _), index) if f.Squad.GUID == guid => index } .map { index => val key = keys(index) @@ -1723,12 +1929,12 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { case (queue, index) => val key = keys(index) val (targets, retained) = queue.partition { - case VacancyInvite(_, _, _squad) => _squad.Squad.GUID == guid - case IndirectInvite(_, _squad) => _squad.Squad.GUID == guid - case LookingForSquadRoleInvite(_, _squad, _) => _squad.Squad.GUID == guid - case ProximityInvite(_, _squad, _) => _squad.Squad.GUID == guid - case RequestRole(_, _squad, _) => _squad.Squad.GUID == guid - case _ => false + case VacancyInvite(_, _, f) => f.Squad.GUID == guid + case IndirectInvite(_, f) => f.Squad.GUID == guid + case LookingForSquadRoleInvite(_, f, _) => f.Squad.GUID == guid + case ProximityInvite(_, f, _) => f.Squad.GUID == guid + case RequestRole(_, f, _) => f.Squad.GUID == guid + case _ => false } if (retained.isEmpty) { queuedInvites.remove(key) @@ -1744,7 +1950,9 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { .flatten .toList } - CleanUpSquadFeatures(activeInviteIds ++ queuedInviteIds, features, position = -1) + val allInviteIds = (activeInviteIds ++ queuedInviteIds).distinct + CleanUpSquadFeatures(allInviteIds, features, position = -1) + allInviteIds } /** @@ -2151,4 +2359,15 @@ object SquadInvitationManager { } final case class FinishStartSquad(features: SquadFeatures) + + def canEnrollInSquad(features: SquadFeatures, charId: Long): Boolean = { + !features.Squad.Membership.exists { _.CharId == charId } + } + + def notLeaderOfThisSquad(squadsToLeaders: List[(PlanetSideGUID, SquadFeatures)], guid: PlanetSideGUID, charId: Long): Boolean = { + squadsToLeaders.find { case (squadGuid, _) => squadGuid == guid } match { + case Some((_, features)) => features.Squad.Leader.CharId != charId + case None => false + } + } } diff --git a/src/main/scala/net/psforever/services/teamwork/SquadService.scala b/src/main/scala/net/psforever/services/teamwork/SquadService.scala index 2dc80fd10..317e1aaef 100644 --- a/src/main/scala/net/psforever/services/teamwork/SquadService.scala +++ b/src/main/scala/net/psforever/services/teamwork/SquadService.scala @@ -2,6 +2,9 @@ package net.psforever.services.teamwork import akka.actor.{Actor, ActorRef, Terminated} +import net.psforever.actors.session.SessionActor +import net.psforever.packet.game.ChatMsg +import net.psforever.types.ChatMessageType import java.io.{PrintWriter, StringWriter} import scala.annotation.unused @@ -111,7 +114,7 @@ class SquadService extends Actor { * @return `true`, if the identifier is reset; `false`, otherwise */ def TryResetSquadId(): Boolean = { - if (squadFeatures.isEmpty) { + if (squadFeatures.isEmpty && LivePlayerList.WorldPopulation(_ => true).isEmpty) { sid = 1 true } else { @@ -199,7 +202,7 @@ class SquadService extends Actor { LeaveInGeneral(sender()) case Terminated(actorRef) => - TerminatedBy(actorRef) + LeaveInGeneral(actorRef) case message @ SquadServiceMessage(tplayer, zone, squad_action) => squad_action match { @@ -243,16 +246,39 @@ class SquadService extends Actor { case SquadService.ResendActiveInvite(charId) => invitations.resendActiveInvite(charId) + case SquadService.ListAllCurrentInvites(charId) => + ListCurrentInvitations(charId) + + case SquadService.ChainAcceptance(player, charId, list) => + ChainAcceptanceIntoSquad(player, charId, list) + + case SquadService.ChainRejection(player, charId, list) => + ChainRejectionFromSquad(player, charId, list) + case msg => log.warn(s"Unhandled message $msg from ${sender()}") } + /** + * Subscribe to a faction-wide channel. + * @param faction sub-channel name + * @param sender subscriber + */ def JoinByFaction(faction: String, sender: ActorRef): Unit = { val path = s"/$faction/Squad" log.trace(s"$sender has joined $path") subs.SquadEvents.subscribe(sender, path) } + /** + * Subscribe to a client-specific channel. + * The channel name is expected to be a `Long` number but is passed as a `String`. + * As is the case, the actual channel name may not parse into a proper long integer after being passed and + * there are failure cases. + * @param charId sub-channel name + * @param sender subscriber + * @see `SquadInvitationManager.handleJoin` + */ def JoinByCharacterId(charId: String, sender: ActorRef): Unit = { try { val longCharId = charId.toLong @@ -272,12 +298,23 @@ class SquadService extends Actor { } } + /** + * Unsubscribe from a faction-wide channel. + * @param faction sub-channel name + * @param sender subscriber + */ def LeaveByFaction(faction: String, sender: ActorRef): Unit = { val path = s"/$faction/Squad" log.trace(s"$sender has left $path") subs.SquadEvents.unsubscribe(sender, path) } + /** + * Unsubscribe from a client-specific channel. + * @param charId sub-channel name + * @param sender subscriber + * @see `LeaveService` + */ def LeaveByCharacterId(charId: String, sender: ActorRef): Unit = { try { LeaveService(charId.toLong, sender) @@ -292,23 +329,23 @@ class SquadService extends Actor { } } + /** + * Assuming a subscriber that matches previously subscribed data, + * completely unsubscribe and forget this entry. + * @param sender subscriber + * @see `LeaveService` + */ def LeaveInGeneral(sender: ActorRef): Unit = { - subs.UserEvents find { case (_, subscription) => subscription.path.equals(sender.path) } match { + context.unwatch(sender) + subs.UserEvents.find { + case (_, subscription) => (subscription eq sender) || subscription.path.equals(sender.path) + } match { case Some((to, _)) => LeaveService(to, sender) case _ => () } } - def TerminatedBy(requestee: ActorRef): Unit = { - context.unwatch(requestee) - subs.UserEvents find { case (_, subscription) => subscription eq requestee } match { - case Some((to, _)) => - LeaveService(to, requestee) - case _ => () - } - } - def performStartSquad(sender: ActorRef, player: Player): Unit = { val invitingPlayerCharId = player.CharId if (EnsureEmptySquad(invitingPlayerCharId)) { @@ -544,7 +581,7 @@ class SquadService extends Actor { invitations.handleRejection( tplayer, rejectingPlayer, - squadFeatures.map { case (guid, features) => (guid, features.Squad.Leader.CharId) }.toList + squadFeatures.map { case (guid, features) => (guid, features) }.toList ) } @@ -1084,9 +1121,14 @@ class SquadService extends Actor { } /** - * na + * Completely remove information about a former subscriber from the squad management service. * @param charId the player's unique character identifier number * @param sender the `ActorRef` associated with this character + * @see `DisbandSquad` + * @see `LeaveSquad` + * @see `SquadInvitationManager.handleLeave` + * @see `SquadSwitchboard.PanicLeaveSquad` + * @see `TryResetSquadId` */ def LeaveService(charId: Long, sender: ActorRef): Unit = { subs.MonitorSquadDetails.subtractOne(charId) @@ -1129,8 +1171,7 @@ class SquadService extends Actor { } subs.SquadEvents.unsubscribe(sender) //just to make certain searchData.remove(charId) - //todo turn this back on. See PR 1157 for why it was commented out. - //TryResetSquadId() + TryResetSquadId() } /** @@ -1285,6 +1326,50 @@ class SquadService extends Actor { } } } + + def ListCurrentInvitations(charId: Long) : Unit = { + GetLeadingSquad(charId, None) + .map { _ => + invitations.listCurrentInvitations(charId) + } + .collect { + case listOfInvites if listOfInvites.nonEmpty => + listOfInvites match { + case active :: queued if queued.nonEmpty => + subs.Publish(charId, SquadResponse.WantsSquadPosition(charId, s"$active, ${queued.mkString(", ")}")) + listOfInvites + case active :: _ => + subs.Publish(charId, SquadResponse.WantsSquadPosition(charId, active)) + listOfInvites + } + } + .orElse { + context.self ! SessionActor.SendResponse(ChatMsg(ChatMessageType.UNK_227, "You do not have any current invites to manage.")) + None + } + } + + def ChainAcceptanceIntoSquad(player: Player, charId: Long, listOfCharIds: List[Long]): Unit = { + GetLeadingSquad(charId, None) + .foreach { features => + if (listOfCharIds.nonEmpty) { + invitations.tryChainAcceptance(player, charId, listOfCharIds, features) + } else { + invitations.SquadActionDefinitionAutoApproveInvitationRequests(charId, features) + } + } + } + + def ChainRejectionFromSquad(player: Player, charId: Long, listOfCharIds: List[Long]): Unit = { + GetLeadingSquad(charId, None) + .foreach { features => + if (listOfCharIds.nonEmpty) { + invitations.tryChainRejection(player, charId, listOfCharIds, features) + } else { + invitations.tryChainRejectionAll(charId, features) + } + } + } } object SquadService { @@ -1294,6 +1379,12 @@ object SquadService { final case class ResendActiveInvite(charId: Long) + final case class ListAllCurrentInvites(charId: Long) + + final case class ChainAcceptance(player: Player, charId: Long, listOfCharIds: List[Long]) + + final case class ChainRejection(player: Player, charId: Long, listOfCharIds: List[Long]) + /** * A message to indicate that the squad list needs to update for the clients. * @param features the squad diff --git a/src/main/scala/net/psforever/services/teamwork/SquadServiceResponse.scala b/src/main/scala/net/psforever/services/teamwork/SquadServiceResponse.scala index 367127f45..8935627e0 100644 --- a/src/main/scala/net/psforever/services/teamwork/SquadServiceResponse.scala +++ b/src/main/scala/net/psforever/services/teamwork/SquadServiceResponse.scala @@ -73,4 +73,6 @@ object SquadResponse { unk2: Int, zoneNumber: Int ) extends Response + + final case class SquadRelatedComment(str: String) extends Response } diff --git a/src/main/scala/net/psforever/services/teamwork/SquadSubscriptionEntity.scala b/src/main/scala/net/psforever/services/teamwork/SquadSubscriptionEntity.scala index 427d6c85e..104bd5320 100644 --- a/src/main/scala/net/psforever/services/teamwork/SquadSubscriptionEntity.scala +++ b/src/main/scala/net/psforever/services/teamwork/SquadSubscriptionEntity.scala @@ -133,7 +133,7 @@ class SquadSubscriptionEntity { case str if str.matches("\\d+") => Publish(to.toLong, msg, excluded) case _ => - log.warn(s"Publish(String): subscriber information is an unhandled format - $to") + log.warn(s"Publish(String): subscriber information is an unhandled format - $to; message $msg dropped") } } @@ -148,7 +148,7 @@ class SquadSubscriptionEntity { case Some(user) => user ! SquadServiceResponse("", msg) case None => - log.warn(s"Publish(Long): subscriber information can not be found - $to") + log.warn(s"Publish(Long): subscriber information can not be found - $to; message $msg dropped") } } @@ -174,7 +174,7 @@ class SquadSubscriptionEntity { * @param msg a message that can be stored in a `SquadServiceResponse` object */ def Publish[ANY >: Any](to: ANY, msg: SquadResponse.Response): Unit = { - log.warn(s"Publish(Any): subscriber information is an unhandled format - $to") + log.warn(s"Publish(Any): subscriber information is an unhandled format - $to; message $msg dropped") } /** @@ -187,7 +187,7 @@ class SquadSubscriptionEntity { * @param excluded a group of character identifier numbers who should not receive the message */ def Publish[ANY >: Any](to: ANY, msg: SquadResponse.Response, excluded: Iterable[Long]): Unit = { - log.warn(s"Publish(Any): subscriber information is an unhandled format - $to") + log.warn(s"Publish(Any): subscriber information is an unhandled format - $to; message $msg dropped") } /* The following functions are related to common communications of squad information, mainly detail. */ @@ -211,7 +211,7 @@ class SquadSubscriptionEntity { } /** - * Dispatch an intial message entailing the strategic information and the composition of this squad. + * Dispatch an initial message entailing the strategic information and the composition of this squad. * The details of the squad will be updated in full and be sent to all indicated observers. * @see `SquadService.PublishFullDetails` * @param guid the unique squad identifier to be used when composing the details for this message