experimental invitation management commands for squad leaders, mostly untested atm; messages for being denied squad admission

This commit is contained in:
Fate-JH 2024-09-10 20:38:30 -04:00
parent d15e916f46
commit 2372a95040
8 changed files with 436 additions and 83 deletions

View file

@ -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(

View file

@ -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 _ => ()
}
}

View file

@ -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],

View file

@ -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))
}
}

View file

@ -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,11 +1929,11 @@ 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 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) {
@ -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
}
}
}

View file

@ -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

View file

@ -73,4 +73,6 @@ object SquadResponse {
unk2: Int,
zoneNumber: Int
) extends Response
final case class SquadRelatedComment(str: String) extends Response
}

View file

@ -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