diff --git a/src/main/scala/net/psforever/actors/session/normal/AvatarHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/normal/AvatarHandlerLogic.scala index eb430a45..e9936cda 100644 --- a/src/main/scala/net/psforever/actors/session/normal/AvatarHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/AvatarHandlerLogic.scala @@ -538,6 +538,7 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A continent.actor ! ZoneActor.RewardThisDeath(player) //player state changes + sessionLogic.zoning.spawn.avatarActive = false AvatarActor.updateToolDischargeFor(avatar) player.FreeHand.Equipment.foreach { item => DropEquipmentFromInventory(player)(item) 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 35c09b7f..052a1ebb 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/GeneralLogic.scala b/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala index 99cf5103..608f06d0 100644 --- a/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala @@ -100,6 +100,7 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex _, _ )= pkt + sessionLogic.zoning.spawn.tryQueuedActivity(vel) sessionLogic.persist() sessionLogic.turnCounterFunc(avatarGuid) sessionLogic.updateBlockMap(player, pos) 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 b80eb01c..8a0ae9f3 100644 --- a/src/main/scala/net/psforever/actors/session/normal/SquadHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/SquadHandlerLogic.scala @@ -2,20 +2,24 @@ package net.psforever.actors.session.normal import akka.actor.{ActorContext, ActorRef, typed} -import net.psforever.actors.session.support.SessionSquadHandlers.SquadUIElement import net.psforever.actors.session.AvatarActor -import net.psforever.actors.session.support.{SessionData, SessionSquadHandlers, SquadHandlerFunctions} +import net.psforever.actors.session.support.SessionSquadHandlers.{rethrowSquadServiceResponse, SquadUIElement} +import net.psforever.actors.session.support.{SessionData, SessionSquadHandlers, SpawnOperations, SquadHandlerFunctions} import net.psforever.objects.{Default, LivePlayerList} import net.psforever.objects.avatar.Avatar import net.psforever.packet.game.{CharacterKnowledgeInfo, CharacterKnowledgeMessage, ChatMsg, MemberEvent, PlanetsideAttributeMessage, ReplicationStreamMessage, SquadAction, SquadDefinitionActionMessage, SquadDetailDefinitionUpdateMessage, SquadListing, SquadMemberEvent, SquadMembershipRequest, SquadMembershipResponse, SquadState, SquadStateInfo, SquadWaypointEvent, SquadWaypointRequest, WaypointEvent, WaypointEventAction} import net.psforever.services.chat.SquadChannel -import net.psforever.services.teamwork.{SquadResponse, SquadServiceMessage, SquadAction => SquadServiceAction} +import net.psforever.services.teamwork.{SquadResponse, SquadServiceMessage, SquadServiceResponse, SquadAction => SquadServiceAction} import net.psforever.types.{ChatMessageType, PlanetSideGUID, SquadListDecoration, SquadResponseType, WaypointSubtype} object SquadHandlerLogic { def apply(ops: SessionSquadHandlers): SquadHandlerLogic = { new SquadHandlerLogic(ops, ops.context) } + + def rethrowSquadServiceResponse(response: SquadResponse.Response)(sessionLogic: SessionData): Unit = { + sessionLogic.context.self ! SquadServiceResponse("", response) + } } class SquadHandlerLogic(val ops: SessionSquadHandlers, implicit val context: ActorContext) extends SquadHandlerFunctions { @@ -28,17 +32,17 @@ class SquadHandlerLogic(val ops: SessionSquadHandlers, implicit val context: Act /* packet */ def handleSquadDefinitionAction(pkt: SquadDefinitionActionMessage): Unit = { - /*val SquadDefinitionActionMessage(u1, u2, action) = pkt - squadService ! SquadServiceMessage(player, continent, SquadServiceAction.Definition(u1, u2, action))*/ + val SquadDefinitionActionMessage(u1, u2, action) = pkt + squadService ! SquadServiceMessage(player, continent, SquadServiceAction.Definition(u1, u2, action)) } def handleSquadMemberRequest(pkt: SquadMembershipRequest): Unit = { - /*val SquadMembershipRequest(request_type, char_id, unk3, player_name, unk5) = pkt + val SquadMembershipRequest(request_type, char_id, unk3, player_name, unk5) = pkt squadService ! SquadServiceMessage( player, continent, SquadServiceAction.Membership(request_type, char_id, unk3, player_name, unk5) - )*/ + ) } def handleSquadWaypointRequest(pkt: SquadWaypointRequest): Unit = { @@ -59,6 +63,7 @@ class SquadHandlerLogic(val ops: SessionSquadHandlers, implicit val context: Act def handle(response: SquadResponse.Response, excluded: Iterable[Long]): Unit = { if (!excluded.exists(_ == avatar.id)) { response match { + /* these messages will never be queued for later */ case SquadResponse.ListSquadFavorite(line, task) => sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), line, SquadAction.ListSquadFavorite(task))) @@ -106,12 +111,69 @@ class SquadHandlerLogic(val ops: SessionSquadHandlers, implicit val context: Act case SquadResponse.Detail(guid, detail) => sendResponse(SquadDetailDefinitionUpdateMessage(guid, detail)) - case SquadResponse.IdentifyAsSquadLeader(squad_guid) => - sendResponse(SquadDefinitionActionMessage(squad_guid, 0, SquadAction.IdentifyAsSquadLeader())) - case SquadResponse.SetListSquad(squad_guid) => sendResponse(SquadDefinitionActionMessage(squad_guid, 0, SquadAction.SetListSquad())) + case SquadResponse.SquadSearchResults(results) => + //TODO positive squad search results message? + if(results.nonEmpty) { + results.foreach { guid => + sendResponse(SquadDefinitionActionMessage( + guid, + 0, + SquadAction.SquadListDecorator(SquadListDecoration.SearchResult)) + ) + } + } else { + sendResponse(SquadDefinitionActionMessage(player.GUID, 0, SquadAction.NoSquadSearchResults())) + } + sendResponse(SquadDefinitionActionMessage(player.GUID, 0, SquadAction.CancelSquadSearch())) + + case SquadResponse.UpdateMembers(_, positions) => + val pairedEntries = positions.collect { + case entry if ops.squadUI.contains(entry.char_id) => + (entry, ops.squadUI(entry.char_id)) + } + //prune entries + val updatedEntries = pairedEntries + .collect({ + case (entry, element) if entry.zone_number != element.zone => + //zone gets updated for these entries + sendResponse( + SquadMemberEvent.UpdateZone(ops.squad_supplement_id, entry.char_id, element.index, entry.zone_number) + ) + ops.squadUI(entry.char_id) = + SquadUIElement(element.name, element.outfit, element.index, entry.zone_number, entry.health, entry.armor, entry.pos) + entry + case (entry, element) + if entry.health != element.health || entry.armor != element.armor || entry.pos != element.position => + //other elements that need to be updated + ops.squadUI(entry.char_id) = + SquadUIElement(element.name, element.outfit, element.index, entry.zone_number, entry.health, entry.armor, entry.pos) + entry + }) + .filterNot(_.char_id == avatar.id) //we want to update our backend, but not our frontend + if (updatedEntries.nonEmpty) { + sendResponse( + SquadState( + PlanetSideGUID(ops.squad_supplement_id), + updatedEntries.map { entry => + SquadStateInfo(entry.char_id, entry.health, entry.armor, entry.pos) + } + ) + ) + } + + /* queue below messages for later if the initial conditions are inappropriate */ + case msg if !sessionLogic.zoning.spawn.startEnqueueSquadMessages => + sessionLogic.zoning.spawn.enqueueNewActivity( + SpawnOperations.ActivityQueuedTask(rethrowSquadServiceResponse(msg), 1) + ) + + /* these messages will be queued for later if initial conditions are inappropriate */ + case SquadResponse.IdentifyAsSquadLeader(squad_guid) => + sendResponse(SquadDefinitionActionMessage(squad_guid, 0, SquadAction.IdentifyAsSquadLeader())) + case SquadResponse.Membership(request_type, unk1, unk2, charId, opt_char_id, player_name, unk5, unk6) => val name = request_type match { case SquadResponseType.Invite if unk5 => @@ -270,59 +332,9 @@ class SquadHandlerLogic(val ops: SessionSquadHandlers, implicit val context: Act //the players have already been swapped in the backend object ops.PromoteSquadUIElements(squad, from_index) - case SquadResponse.UpdateMembers(_, positions) => - val pairedEntries = positions.collect { - case entry if ops.squadUI.contains(entry.char_id) => - (entry, ops.squadUI(entry.char_id)) - } - //prune entries - val updatedEntries = pairedEntries - .collect({ - case (entry, element) if entry.zone_number != element.zone => - //zone gets updated for these entries - sendResponse( - SquadMemberEvent.UpdateZone(ops.squad_supplement_id, entry.char_id, element.index, entry.zone_number) - ) - ops.squadUI(entry.char_id) = - SquadUIElement(element.name, element.outfit, element.index, entry.zone_number, entry.health, entry.armor, entry.pos) - entry - case (entry, element) - if entry.health != element.health || entry.armor != element.armor || entry.pos != element.position => - //other elements that need to be updated - ops.squadUI(entry.char_id) = - SquadUIElement(element.name, element.outfit, element.index, entry.zone_number, entry.health, entry.armor, entry.pos) - entry - }) - .filterNot(_.char_id == avatar.id) //we want to update our backend, but not our frontend - if (updatedEntries.nonEmpty) { - sendResponse( - SquadState( - PlanetSideGUID(ops.squad_supplement_id), - updatedEntries.map { entry => - SquadStateInfo(entry.char_id, entry.health, entry.armor, entry.pos) - } - ) - ) - } - case SquadResponse.CharacterKnowledge(charId, name, certs, u1, u2, zone) => sendResponse(CharacterKnowledgeMessage(charId, Some(CharacterKnowledgeInfo(name, certs, u1, u2, zone)))) - case SquadResponse.SquadSearchResults(_/*results*/) => - //TODO positive squad search results message? - // if(results.nonEmpty) { - // results.foreach { guid => - // sendResponse(SquadDefinitionActionMessage( - // guid, - // 0, - // SquadAction.SquadListDecorator(SquadListDecoration.SearchResult)) - // ) - // } - // } else { - // sendResponse(SquadDefinitionActionMessage(player.GUID, 0, SquadAction.NoSquadSearchResults())) - // } - // sendResponse(SquadDefinitionActionMessage(player.GUID, 0, SquadAction.CancelSquadSearch())) - case SquadResponse.InitWaypoints(char_id, waypoints) => waypoints.foreach { case (waypoint_type, info, unk) => @@ -349,6 +361,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, messageType) => + sendResponse(ChatMsg(messageType, comment)) + case _ => () } } diff --git a/src/main/scala/net/psforever/actors/session/normal/VehicleLogic.scala b/src/main/scala/net/psforever/actors/session/normal/VehicleLogic.scala index 5ed83c9b..77d3a99f 100644 --- a/src/main/scala/net/psforever/actors/session/normal/VehicleLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/VehicleLogic.scala @@ -44,6 +44,7 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex ops.GetVehicleAndSeat() match { case (Some(obj), Some(0)) => //we're driving the vehicle + sessionLogic.zoning.spawn.tryQueuedActivity(vel) sessionLogic.persist() sessionLogic.turnCounterFunc(player.GUID) sessionLogic.general.fallHeightTracker(pos.z) @@ -128,6 +129,7 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex ops.GetVehicleAndSeat() match { case (Some(obj), Some(0)) => //we're driving the vehicle + sessionLogic.zoning.spawn.tryQueuedActivity(vel) sessionLogic.persist() sessionLogic.turnCounterFunc(player.GUID) val (position, angle, velocity, notMountedState) = continent.GUID(obj.MountedIn) match { @@ -217,6 +219,7 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex case (None, None) | (_, None) | (Some(_: Vehicle), Some(0)) => () case _ => + sessionLogic.zoning.spawn.tryQueuedActivity() //todo conditionals? sessionLogic.persist() sessionLogic.turnCounterFunc(player.GUID) } diff --git a/src/main/scala/net/psforever/actors/session/spectator/AvatarHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/spectator/AvatarHandlerLogic.scala index e96e6cee..160bda39 100644 --- a/src/main/scala/net/psforever/actors/session/spectator/AvatarHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/spectator/AvatarHandlerLogic.scala @@ -450,6 +450,7 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A sessionLogic.zoning.CancelZoningProcess() //player state changes + sessionLogic.zoning.spawn.avatarActive = false AvatarActor.updateToolDischargeFor(avatar) player.FreeHand.Equipment.foreach { item => DropEquipmentFromInventory(player)(item) 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 869369fc..e1f6415f 100644 --- a/src/main/scala/net/psforever/actors/session/spectator/GeneralLogic.scala +++ b/src/main/scala/net/psforever/actors/session/spectator/GeneralLogic.scala @@ -56,6 +56,7 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex _, _ )= pkt + sessionLogic.zoning.spawn.tryQueuedActivity(vel) sessionLogic.persist() sessionLogic.turnCounterFunc(avatarGuid) sessionLogic.updateBlockMap(player, pos) diff --git a/src/main/scala/net/psforever/actors/session/spectator/SquadHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/spectator/SquadHandlerLogic.scala index fae3a681..b7321760 100644 --- a/src/main/scala/net/psforever/actors/session/spectator/SquadHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/spectator/SquadHandlerLogic.scala @@ -2,15 +2,15 @@ package net.psforever.actors.session.spectator import akka.actor.{ActorContext, typed} -import net.psforever.actors.session.support.SessionSquadHandlers.SquadUIElement import net.psforever.actors.session.AvatarActor -import net.psforever.actors.session.support.{SessionData, SessionSquadHandlers, SquadHandlerFunctions} +import net.psforever.actors.session.support.SessionSquadHandlers.{rethrowSquadServiceResponse, SquadUIElement} +import net.psforever.actors.session.support.{SessionData, SessionSquadHandlers, SpawnOperations, SquadHandlerFunctions} import net.psforever.objects.{Default, LivePlayerList} import net.psforever.objects.avatar.Avatar -import net.psforever.packet.game.{CharacterKnowledgeInfo, CharacterKnowledgeMessage, PlanetsideAttributeMessage, ReplicationStreamMessage, SquadAction, SquadDefinitionActionMessage, SquadDetailDefinitionUpdateMessage, SquadListing, SquadMemberEvent, SquadMembershipRequest, SquadMembershipResponse, SquadState, SquadStateInfo, SquadWaypointEvent, SquadWaypointRequest, WaypointEventAction} +import net.psforever.packet.game.{CharacterKnowledgeInfo, CharacterKnowledgeMessage, ChatMsg, PlanetsideAttributeMessage, ReplicationStreamMessage, SquadAction, SquadDefinitionActionMessage, SquadDetailDefinitionUpdateMessage, SquadListing, SquadMemberEvent, SquadMembershipRequest, SquadMembershipResponse, SquadState, SquadStateInfo, SquadWaypointEvent, SquadWaypointRequest, WaypointEventAction} import net.psforever.services.chat.SquadChannel import net.psforever.services.teamwork.SquadResponse -import net.psforever.types.{PlanetSideGUID, SquadListDecoration, SquadResponseType} +import net.psforever.types.{ChatMessageType, PlanetSideGUID, SquadListDecoration, SquadResponseType} object SquadHandlerLogic { def apply(ops: SessionSquadHandlers): SquadHandlerLogic = { @@ -77,6 +77,62 @@ class SquadHandlerLogic(val ops: SessionSquadHandlers, implicit val context: Act } sendResponse(SquadDefinitionActionMessage(guid, 0, SquadAction.SquadListDecorator(decoration))) + case SquadResponse.SquadSearchResults(results) => + //TODO positive squad search results message? + if(results.nonEmpty) { + results.foreach { guid => + sendResponse(SquadDefinitionActionMessage( + guid, + 0, + SquadAction.SquadListDecorator(SquadListDecoration.SearchResult)) + ) + } + } else { + sendResponse(SquadDefinitionActionMessage(player.GUID, 0, SquadAction.NoSquadSearchResults())) + } + sendResponse(SquadDefinitionActionMessage(player.GUID, 0, SquadAction.CancelSquadSearch())) + + case SquadResponse.UpdateMembers(_, positions) => + val pairedEntries = positions.collect { + case entry if ops.squadUI.contains(entry.char_id) => + (entry, ops.squadUI(entry.char_id)) + } + //prune entries + val updatedEntries = pairedEntries + .collect({ + case (entry, element) if entry.zone_number != element.zone => + //zone gets updated for these entries + sendResponse( + SquadMemberEvent.UpdateZone(ops.squad_supplement_id, entry.char_id, element.index, entry.zone_number) + ) + ops.squadUI(entry.char_id) = + SquadUIElement(element.name, element.outfit, element.index, entry.zone_number, entry.health, entry.armor, entry.pos) + entry + case (entry, element) + if entry.health != element.health || entry.armor != element.armor || entry.pos != element.position => + //other elements that need to be updated + ops.squadUI(entry.char_id) = + SquadUIElement(element.name, element.outfit, element.index, entry.zone_number, entry.health, entry.armor, entry.pos) + entry + }) + .filterNot(_.char_id == avatar.id) //we want to update our backend, but not our frontend + if (updatedEntries.nonEmpty) { + sendResponse( + SquadState( + PlanetSideGUID(ops.squad_supplement_id), + updatedEntries.map { entry => + SquadStateInfo(entry.char_id, entry.health, entry.armor, entry.pos) + } + ) + ) + } + + /* queue below messages for later if the initial conditions are inappropriate */ + case msg if !sessionLogic.zoning.spawn.startEnqueueSquadMessages => + sessionLogic.zoning.spawn.enqueueNewActivity( + SpawnOperations.ActivityQueuedTask(rethrowSquadServiceResponse(msg), 1) + ) + case SquadResponse.Detail(guid, detail) => sendResponse(SquadDetailDefinitionUpdateMessage(guid, detail)) @@ -132,47 +188,15 @@ class SquadHandlerLogic(val ops: SessionSquadHandlers, implicit val context: Act ) } - case SquadResponse.UpdateMembers(_, positions) => - val pairedEntries = positions.collect { - case entry if ops.squadUI.contains(entry.char_id) => - (entry, ops.squadUI(entry.char_id)) - } - //prune entries - val updatedEntries = pairedEntries - .collect({ - case (entry, element) if entry.zone_number != element.zone => - //zone gets updated for these entries - sendResponse( - SquadMemberEvent.UpdateZone(ops.squad_supplement_id, entry.char_id, element.index, entry.zone_number) - ) - ops.squadUI(entry.char_id) = - SquadUIElement(element.name, element.outfit, element.index, entry.zone_number, entry.health, entry.armor, entry.pos) - entry - case (entry, element) - if entry.health != element.health || entry.armor != element.armor || entry.pos != element.position => - //other elements that need to be updated - ops.squadUI(entry.char_id) = - SquadUIElement(element.name, element.outfit, element.index, entry.zone_number, entry.health, entry.armor, entry.pos) - entry - }) - .filterNot(_.char_id == avatar.id) //we want to update our backend, but not our frontend - if (updatedEntries.nonEmpty) { - sendResponse( - SquadState( - PlanetSideGUID(ops.squad_supplement_id), - updatedEntries.map { entry => - SquadStateInfo(entry.char_id, entry.health, entry.armor, entry.pos) - } - ) - ) - } - case SquadResponse.CharacterKnowledge(charId, name, certs, u1, u2, zone) => sendResponse(CharacterKnowledgeMessage(charId, Some(CharacterKnowledgeInfo(name, certs, u1, u2, zone)))) case SquadResponse.WaypointEvent(WaypointEventAction.Remove, char_id, waypoint_type, _, _, _) => sendResponse(SquadWaypointEvent.Remove(ops.squad_supplement_id, char_id, waypoint_type)) + case SquadResponse.SquadRelatedComment(comment, messageType) => + sendResponse(ChatMsg(messageType, 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 7827cd23..48a724db 100644 --- a/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala @@ -2,23 +2,30 @@ package net.psforever.actors.session.support import akka.actor.Cancellable +import akka.actor.{ActorRef => ClassicActorRef} import akka.actor.typed.ActorRef import akka.actor.{ActorContext, typed} +import akka.pattern.ask +import akka.util.Timeout import net.psforever.actors.session.spectator.SpectatorMode import net.psforever.actors.session.{AvatarActor, SessionActor} import net.psforever.actors.zone.ZoneActor +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.local.{LocalAction, LocalServiceMessage} +import net.psforever.services.teamwork.{SquadResponse, SquadService, SquadServiceResponse} import net.psforever.types.ChatMessageType.CMT_QUIT import org.log4s.Logger import java.util.concurrent.{Executors, TimeUnit} import scala.annotation.unused -import scala.collection.mutable +import scala.collection.{Seq, mutable} +import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration._ +import scala.util.Success // import net.psforever.actors.zone.BuildingActor import net.psforever.login.WorldSession @@ -54,6 +61,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 { @@ -74,6 +82,10 @@ class ChatOperations( import akka.actor.typed.scaladsl.adapter._ private val chatServiceAdapter: ActorRef[ChatService.MessageResponse] = context.self.toTyped[ChatService.MessageResponse] + private implicit lazy val timeout: Timeout = Timeout(2.seconds) + + private var invitationList: Array[String] = Array() + def JoinChannel(channel: ChatChannel): Unit = { chatService ! ChatService.JoinChannel(chatServiceAdapter, sessionLogic, channel) channels ++= List(channel) @@ -1284,6 +1296,47 @@ class ChatOperations( true } + def customCommandSquad(params: Seq[String]): Boolean = { + params match { + case "invites" :: _ => + invitationList = Array() + ask(squadService, SquadService.ListAllCurrentInvites) + .onComplete { + case Success(msg @ SquadServiceResponse(_, _, SquadResponse.WantsSquadPosition(_, str))) => + invitationList = str.replaceAll("/s","").split(",") + context.self.forward(msg) + case _ => () + } + + case "accept" :: names if names.contains("all") => + squadService ! SquadService.ChainAcceptance(player, player.CharId, Nil) + case "accept" :: names if names.nonEmpty => + //when passing indices to existing invite list, the indices are 1-based + val results = (names.flatMap(_.toIntOption.flatMap(i => invitationList.lift(i-1))) ++ names) + .distinct + .flatMap { name => + LivePlayerList.WorldPopulation { case (_, p) => p.name.equalsIgnoreCase(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 => + //when passing indices to existing invite list, the indices are 1-based + val results = (names.flatMap(_.toIntOption.flatMap(i => invitationList.lift(i-1))) ++ names) + .distinct + .flatMap { name => + LivePlayerList.WorldPopulation { case (_, p) => p.name.equalsIgnoreCase(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 68fcf9f0..44769b87 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)) } } @@ -547,6 +554,7 @@ class SessionData( if (avatar != null) { accountPersistence ! AccountPersistenceService.Logout(avatar.name) } + squad.cleanUpSquadCards() middlewareActor ! MiddlewareActor.Teardown() } diff --git a/src/main/scala/net/psforever/actors/session/support/SessionSquadHandlers.scala b/src/main/scala/net/psforever/actors/session/support/SessionSquadHandlers.scala index 1aaea60c..25c43274 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionSquadHandlers.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionSquadHandlers.scala @@ -2,6 +2,8 @@ package net.psforever.actors.session.support import akka.actor.{ActorContext, ActorRef, typed} +import net.psforever.services.teamwork.SquadServiceResponse + import scala.collection.mutable // import net.psforever.actors.session.AvatarActor @@ -36,6 +38,9 @@ object SessionSquadHandlers { armor: Int, position: Vector3 ) + def rethrowSquadServiceResponse(response: SquadResponse.Response)(sessionLogic: SessionData): Unit = { + sessionLogic.context.self ! SquadServiceResponse("", response) + } } class SessionSquadHandlers( @@ -84,6 +89,7 @@ class SessionSquadHandlers( sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(18))) squadService ! SquadServiceMessage(player, continent, SquadServiceAction.InitSquadList()) squadService ! SquadServiceMessage(player, continent, SquadServiceAction.InitCharId()) + cleanUpSquadCards() squadSetup = RespawnSquadSetup } @@ -345,4 +351,12 @@ class SessionSquadHandlers( ) } } + + def cleanUpSquadCards(): Unit = { + squadUI.foreach { case (id, card) => + sendResponse(SquadMemberEvent.Remove(squad_supplement_id, id, card.index)) + } + squadUI.clear() + squad_supplement_id = 0 + } } diff --git a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala index 7f9aa731..68b5d140 100644 --- a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala @@ -6,10 +6,12 @@ import akka.actor.typed.scaladsl.adapter._ import akka.actor.{ActorContext, ActorRef, Cancellable, typed} import akka.pattern.ask import akka.util.Timeout +import net.psforever.actors.session.support.SpawnOperations.ActivityQueuedTask import net.psforever.login.WorldSession import net.psforever.objects.avatar.{BattleRank, DeployableToolbox} import net.psforever.objects.avatar.scoring.{CampaignStatistics, ScoreCard, SessionStatistics} import net.psforever.objects.definition.converter.OCM +import net.psforever.objects.entity.WorldEntity import net.psforever.objects.inventory.InventoryItem import net.psforever.objects.serverobject.interior.Sidedness import net.psforever.objects.serverobject.mount.Seat @@ -22,7 +24,6 @@ import net.psforever.packet.game.GenericAction.FirstPersonViewWithEffect import net.psforever.packet.game.{CampaignStatistic, ChangeFireStateMessage_Start, CloudInfo, GenericActionMessage, GenericObjectActionEnum, HackState7, MailMessage, ObjectDetectedMessage, SessionStatistic, StormInfo, TriggeredSound, WeatherMessage} import net.psforever.services.chat.DefaultChannel -import scala.collection.mutable import scala.concurrent.duration._ import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future @@ -84,8 +85,8 @@ object ZoningOperations { private final val zoningCountdownMessages: Seq[Int] = Seq(5, 10, 20) - def reportProgressionSystem(sessionActor: ActorRef): Unit = { - sessionActor ! SessionActor.SendResponse( + def reportProgressionSystem(logic: SessionData): Unit = { + logic.context.self ! SessionActor.SendResponse( MailMessage( "High Command", "Progress versus Promotion", @@ -186,6 +187,14 @@ object ZoningOperations { } } +object SpawnOperations { + final case class ActivityQueuedTask(task: SessionData => Unit, delayBeforeNext: Int, repeat: Int = 0) + + def sendEventMessage(msg: ChatMsg)(sessionLogic: SessionData): Unit = { + sessionLogic.sendResponse(msg) + } +} + class ZoningOperations( val sessionLogic: SessionData, avatarActor: typed.ActorRef[AvatarActor.Command], @@ -672,11 +681,15 @@ class ZoningOperations( zoningType = Zoning.Method.Login response match { case Some((zone, spawnPoint)) => - spawn.loginChatMessage.addOne("@login_reposition_to_friendly_facility") //Your previous location was held by the enemy. You have been moved to the nearest friendly facility. + spawn.enqueueNewActivity(ActivityQueuedTask( + SpawnOperations.sendEventMessage(ChatMsg(ChatMessageType.CMT_QUIT, "@login_reposition_to_friendly_facility")), 20) + ) val (pos, ori) = spawnPoint.SpecificPoint(player) spawn.LoadZonePhysicalSpawnPoint(zone.id, pos, ori, respawnTime = 0 seconds, Some(spawnPoint)) case _ => - spawn.loginChatMessage.addOne("@login_reposition_to_sanctuary") //Your previous location was held by the enemy. As there were no operational friendly facilities on that continent, you have been brought back to your Sanctuary. + spawn.enqueueNewActivity(ActivityQueuedTask( + SpawnOperations.sendEventMessage(ChatMsg(ChatMessageType.CMT_QUIT, "@login_reposition_to_sanctuary")), 20) + ) RequestSanctuaryZoneSpawn(player, player.Zone.Number) } @@ -1865,7 +1878,6 @@ class ZoningOperations( class SpawnOperations() { private[session] var deadState: DeadState.Value = DeadState.Dead - private[session] var loginChatMessage: mutable.ListBuffer[String] = new mutable.ListBuffer[String]() private[session] var amsSpawnPoints: List[SpawnPoint] = Nil private[session] var noSpawnPointHere: Boolean = false private[session] var setupAvatarFunc: () => Unit = AvatarCreate @@ -1888,9 +1900,14 @@ class ZoningOperations( private[session] var drawDeloyableIcon: PlanetSideGameObject with Deployable => Unit = RedrawDeployableIcons private[session] var populateAvatarAwardRibbonsFunc: (Int, Long) => Unit = setupAvatarAwardMessageDelivery private[session] var setAvatar: Boolean = false + private[session] var avatarActive: Boolean = false private[session] var reviveTimer: Cancellable = Default.Cancellable private[session] var respawnTimer: Cancellable = Default.Cancellable + private var queuedActivities: Seq[SpawnOperations.ActivityQueuedTask] = Seq() + private val initialActivityDelay: Int = 4 + private var nextActivityDelay: Int = 0 + private var statisticsPacketFunc: () => Unit = loginAvatarStatisticsFields /* packets */ @@ -1899,6 +1916,7 @@ class ZoningOperations( val ReleaseAvatarRequestMessage() = pkt log.info(s"${player.Name} on ${continent.id} has released") reviveTimer.cancel() + avatarActive = false GoToDeploymentMap() HandleReleaseAvatar(player, continent) } @@ -1986,16 +2004,19 @@ class ZoningOperations( } val noFriendlyPlayersInZone = friendlyPlayersInZone == 0 if (inZone.map.cavern) { - loginChatMessage.addOne("@reset_sanctuary_locked") - //You have been returned to the sanctuary because the location you logged out is not available. + enqueueNewActivity(ActivityQueuedTask( + SpawnOperations.sendEventMessage(ChatMsg(ChatMessageType.CMT_QUIT, "@reset_sanctuary_locked")), 20) + ) //You have been returned to the sanctuary because the location you logged out is not available. player.Zone = Zone.Nowhere } else if (ourBuildings.isEmpty && (amsSpawnPoints.isEmpty || noFriendlyPlayersInZone)) { - loginChatMessage.addOne("@reset_sanctuary_locked") - //You have been returned to the sanctuary because the location you logged out is not available. + enqueueNewActivity(ActivityQueuedTask( + SpawnOperations.sendEventMessage(ChatMsg(ChatMessageType.CMT_QUIT, "@reset_sanctuary_locked")), 20) + ) //You have been returned to the sanctuary because the location you logged out is not available. player.Zone = Zone.Nowhere } else if (friendlyPlayersInZone > 137 || playersInZone.size > 413) { - loginChatMessage.addOne("@reset_sanctuary_full") - //You have been returned to the sanctuary because the zone you logged out on is full. + enqueueNewActivity(ActivityQueuedTask( + SpawnOperations.sendEventMessage(ChatMsg(ChatMessageType.CMT_QUIT, "@reset_sanctuary_full")), 20) + ) //You have been returned to the sanctuary because the zone you logged out on is full. player.Zone = Zone.Nowhere } else { val inBuildingSOI = buildings.filter { b => @@ -2012,8 +2033,9 @@ class ZoningOperations( } } else { if (noFriendlyPlayersInZone) { - loginChatMessage.addOne("@reset_sanctuary_inactive") - //You have been returned to the sanctuary because the location you logged out is not available. + enqueueNewActivity(ActivityQueuedTask( + SpawnOperations.sendEventMessage(ChatMsg(ChatMessageType.CMT_QUIT, "@reset_sanctuary_inactive")), 20) + ) //You have been returned to the sanctuary because the location you logged out is not available. player.Zone = Zone.Nowhere } } @@ -2021,8 +2043,9 @@ class ZoningOperations( } } else { //player is dead; go back to sanctuary - loginChatMessage.addOne("@reset_sanctuary_inactive") - //You have been returned to the sanctuary because the location you logged out is not available. + enqueueNewActivity(ActivityQueuedTask( + SpawnOperations.sendEventMessage(ChatMsg(ChatMessageType.CMT_QUIT, "@reset_sanctuary_inactive")), 20) + ) //You have been returned to the sanctuary because the location you logged out is not available. player.Zone = Zone.Nowhere } @@ -2120,11 +2143,15 @@ class ZoningOperations( zoningType = Zoning.Method.Login response match { case Some((zone, spawnPoint)) => - loginChatMessage.addOne("@login_reposition_to_friendly_facility") //Your previous location was held by the enemy. You have been moved to the nearest friendly facility. + enqueueNewActivity(ActivityQueuedTask( + SpawnOperations.sendEventMessage(ChatMsg(ChatMessageType.CMT_QUIT, "@login_reposition_to_friendly_facility")), 20) + ) //Your previous location was held by the enemy. You have been moved to the nearest friendly facility. val (pos, ori) = spawnPoint.SpecificPoint(player) LoadZonePhysicalSpawnPoint(zone.id, pos, ori, respawnTime = 0 seconds, Some(spawnPoint)) case _ => - loginChatMessage.addOne("@login_reposition_to_sanctuary") //Your previous location was held by the enemy. As there were no operational friendly facilities on that continent, you have been brought back to your Sanctuary. + enqueueNewActivity(ActivityQueuedTask( + SpawnOperations.sendEventMessage(ChatMsg(ChatMessageType.CMT_QUIT, "@login_reposition_to_sanctuary")), 20) + ) //Your previous location was held by the enemy. As there were no operational friendly facilities on that continent, you have been brought back to your Sanctuary. RequestSanctuaryZoneSpawn(player, player.Zone.Number) } @@ -2926,6 +2953,7 @@ class ZoningOperations( respawnTimer.cancel() reviveTimer.cancel() deadState = DeadState.RespawnTime + avatarActive = false sendResponse( AvatarDeadStateMessage( DeadState.RespawnTime, @@ -3299,7 +3327,7 @@ class ZoningOperations( tavatar.scorecard.CurrentLife.prior.isEmpty && /* no revives */ tplayer.History.size == 1 /* did nothing but come into existence */ ) { - ZoningOperations.reportProgressionSystem(context.self) + enqueueNewActivity(ActivityQueuedTask(ZoningOperations.reportProgressionSystem, 2)) } } avatarActor ! AvatarActor.RefreshPurchaseTimes() @@ -3561,8 +3589,6 @@ class ZoningOperations( */ def TurnCounterLogin(guid: PlanetSideGUID): Unit = { NormalTurnCounter(guid) - loginChatMessage.foreach { msg => sendResponse(ChatMsg(zoningChatMessageType, wideContents=false, "", msg, None)) } - loginChatMessage.clear() CancelZoningProcess() sessionLogic.turnCounterFunc = NormalTurnCounter } @@ -3773,6 +3799,7 @@ class ZoningOperations( nextSpawnPoint = Some(obj) //set fallback zoningStatus = Zoning.Status.Deconstructing player.allowInteraction = false + avatarActive = false if (player.death_by == 0) { player.death_by = 1 } @@ -3791,6 +3818,7 @@ class ZoningOperations( zoningStatus = Zoning.Status.None player.death_by = math.min(player.death_by, 0) player.allowInteraction = true + avatarActive = true nextSpawnPoint.foreach { tube => sendResponse(PlayerStateShiftMessage(ShiftState(0, tube.Position, tube.Orientation.z))) nextSpawnPoint = None @@ -3815,7 +3843,7 @@ class ZoningOperations( private def usingSpawnTubeAnimation(): Unit = { getSpawnTubeOwner - .collect { case (sp, owner @ Some(_)) => (sp, owner) } + .collect { case (sp, owner@Some(_)) => (sp, owner) } .collect { case (sp, Some(_: Vehicle)) => ZoningOperations.usingVehicleSpawnTubeAnimation(sp.Zone, sp.WhichSide, sp.Faction, player.Position, sp.Orientation, List(player.Name)) @@ -3823,6 +3851,84 @@ class ZoningOperations( ZoningOperations.usingFacilitySpawnTubeAnimation(sp.Zone, sp.WhichSide, sp.Faction, player.Position, sp.Orientation, List(player.Name)) } } + + def startEnqueueSquadMessages: Boolean = { + sessionLogic.zoning.zoneReload && sessionLogic.zoning.spawn.setAvatar && player.isAlive + } + + def enqueueNewActivity(newTasking: SpawnOperations.ActivityQueuedTask): Unit = { + if (avatarActive && queuedActivities.isEmpty) { + nextActivityDelay = initialActivityDelay + } + queuedActivities = queuedActivities :+ newTasking + } + + def tryQueuedActivity(pos1: Vector3, pos2: Vector3, distanceSquared: Float = 1f) : Unit = { + if (!avatarActive) { + if (Vector3.DistanceSquared(pos1, pos2) > distanceSquared) { + startExecutingQueuedActivity() + } + } else { + countDownUntilQueuedActivity() + } + } + + def tryQueuedActivity(vel: Option[Vector3]) : Unit = { + if (!avatarActive) { + if (WorldEntity.isMoving(vel)) { + startExecutingQueuedActivity() + } + } else { + countDownUntilQueuedActivity() + } + } + + def tryQueuedActivity() : Unit = { + if (!avatarActive) { + startExecutingQueuedActivity() + } else { + countDownUntilQueuedActivity() + } + } + + def addDelayBeforeNextQueuedActivity(delay: Int): Unit = { + if (nextActivityDelay == 0) { + nextActivityDelay = initialActivityDelay + } else if (queuedActivities.nonEmpty) { + val lastActivity = queuedActivities.last + if (lastActivity.delayBeforeNext < delay) { + queuedActivities = queuedActivities.dropRight(1) :+ lastActivity.copy(delayBeforeNext = delay) + } + } + } + + private def startExecutingQueuedActivity(): Unit = { + avatarActive = startEnqueueSquadMessages + if (nextActivityDelay == 0) { + nextActivityDelay = initialActivityDelay + } + } + + private def countDownUntilQueuedActivity(): Unit = { + if (nextActivityDelay > 0) { + nextActivityDelay -= 1 + } else if (queuedActivities.nonEmpty) { + val task :: rest = queuedActivities + queuedActivities = if (task.repeat > 0) { + task.copy(repeat = task.repeat - 1) +: rest //positive: repeat immediately + } else if (task.repeat < 0) { + rest :+ task.copy(repeat = task.repeat + 1) //negative: repeat after all other tasks have been completed + } else { + rest + } + nextActivityDelay = task.delayBeforeNext + task.task(sessionLogic) + } + } + + def clearAllQueuedActivity(): Unit = { + queuedActivities = Seq() + } } def doorsThatShouldBeClosedOrBeOpenedByRange( diff --git a/src/main/scala/net/psforever/services/teamwork/SquadInvitationManager.scala b/src/main/scala/net/psforever/services/teamwork/SquadInvitationManager.scala index 9e58d2fa..eb7dd322 100644 --- a/src/main/scala/net/psforever/services/teamwork/SquadInvitationManager.scala +++ b/src/main/scala/net/psforever/services/teamwork/SquadInvitationManager.scala @@ -4,16 +4,25 @@ package net.psforever.services.teamwork import akka.actor.ActorRef import akka.pattern.ask import akka.util.Timeout + import scala.collection.mutable import scala.concurrent.duration._ -import scala.concurrent.ExecutionContext.Implicits.global -import scala.util.Success +import scala.concurrent.Future // -import net.psforever.objects.{LivePlayerList, Player} +import net.psforever.objects.Player import net.psforever.objects.avatar.Avatar import net.psforever.objects.teamwork.{Member, SquadFeatures} import net.psforever.objects.zones.Zone -import net.psforever.packet.game.{SquadDetail, SquadPositionDetail, SquadPositionEntry, SquadAction => SquadRequestAction} +import net.psforever.packet.game.{SquadDetail, SquadPositionDetail, SquadPositionEntry} +import net.psforever.services.teamwork.invitations.{ + IndirectInvite, + Invitation, + InvitationToCreateASquad, + InvitationToJoinSquad, + LookingForSquadRoleInvite, + ProximityInvite, + RequestToJoinSquadRole +} import net.psforever.types.{PlanetSideGUID, SquadResponseType} class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { @@ -47,7 +56,7 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { If the player submits an invitation request for that squad, the current squad leader is cleared from the blocked list. When a squad leader refuses an invitation by a player, - that player will not be able to send further invitations (field on sqaud's features). + that player will not be able to send further invitations (field on squad's features). If the squad leader sends an invitation request for that player, the current player is cleared from the blocked list. */ @@ -56,7 +65,7 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { * key - a unique character identifier number; * value - a list of unique character identifier numbers; squad leaders or once-squad leaders */ - private val refused: mutable.LongMap[List[Long]] = mutable.LongMap[List[Long]]() + private val refusedPlayers: mutable.LongMap[List[Long]] = mutable.LongMap[List[Long]]() private[this] val log = org.log4s.getLogger @@ -64,72 +73,81 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { invites.clear() queuedInvites.clear() previousInvites.clear() - refused.clear() + refusedPlayers.clear() } + /* whenever a new player joins */ + def handleJoin(charId: Long): Unit = { - refused.put(charId, List[Long]()) + refusedPlayers.put(charId, List[Long]()) } - def createRequestRole(player: Player, features: SquadFeatures, position: Int): Unit = { + /* create invitations */ + + def createRequestToJoinSquadRole(player: Player, features: SquadFeatures, position: Int): Unit = { //we could join directly but we need permission from the squad leader first val charId = features.Squad.Leader.CharId - val requestRole = RequestRole(player, features, position) + val requestRole = RequestToJoinSquadRole(player, features, position) if (features.AutoApproveInvitationRequests) { - SquadActionMembershipAcceptInvite( - player, - charId, - Some(requestRole), - None - ) - } else { + requestRole.handleAcceptance(manager = this, player, charId, None) + } else if (addInvite(charId, requestRole).contains(requestRole)) { //circumvent tests in AddInviteAndRespond - InviteResponseTemplate(indirectInviteResp)( - requestRole, - AddInvite(charId, requestRole), + requestRole.handleInvitation(altIndirectInviteResp)( + manager = this, charId, - invitingPlayer = 0L, //we ourselves technically are ... + invitingPlayer = 0L, /* we ourselves technically are ... */ player.Name ) } } - def createVacancyInvite(player: Player, invitedPlayer: Long, features: SquadFeatures): Unit = { + def createInvitationToJoinSquad(player: Player, invitedPlayer: Long, features: SquadFeatures): Unit = { val invitingPlayer = player.CharId val squad = features.Squad - Allowed(invitedPlayer, invitingPlayer) + allowed(invitedPlayer, invitingPlayer) if (squad.Size == squad.Capacity) { log.debug(s"$invitingPlayer tried to invite $invitedPlayer to a squad without available positions") - } else if (Refused(invitingPlayer).contains(invitedPlayer)) { + } else if (refused(invitingPlayer).contains(invitedPlayer)) { log.debug(s"$invitedPlayer repeated a previous refusal to $invitingPlayer's invitation offer") } else { features.AllowedPlayers(invitingPlayer) - AddInviteAndRespond( + addInviteAndRespond( invitedPlayer, - VacancyInvite(invitingPlayer, player.Name, features), + InvitationToJoinSquad(invitingPlayer, player.Name, features), invitingPlayer, player.Name ) } } + def createPermissionToRedirectInvite(player: Player, invitingPlayer: Long, features: SquadFeatures): Unit = { + val leader = features.Squad.Leader.CharId + addInviteAndRespond( + leader, + IndirectInvite(player, features), + invitingPlayer, + player.Name + ) + } + def createIndirectInvite(player: Player, invitingPlayer: Long, features: SquadFeatures): Unit = { val invitedPlayer = player.CharId - val squad2 = features.Squad - val leader = squad2.Leader.CharId - Allowed(invitedPlayer, invitingPlayer) - Allowed(leader, invitingPlayer) - if (squad2.Size == squad2.Capacity) { - log.debug(s"$invitingPlayer's invitation got reversed to $invitedPlayer's squad, but 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") - } 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") + val squad = features.Squad + val leader = squad.Leader.CharId + allowed(invitedPlayer, invitingPlayer) + allowed(leader, invitingPlayer) + lazy val preface = s"$invitingPlayer's invitation got reversed to $invitedPlayer's squad, but" + if (squad.Size == squad.Capacity) { + log.debug(s"$preface the squad has no available positions") + } else if (refused(invitingPlayer).contains(invitedPlayer)) { + log.debug(s"$preface $invitedPlayer repeated a previous refusal to $invitingPlayer's invitation offer") + } else if (refused(invitingPlayer).contains(leader)) { + 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( + addInviteAndRespond( leader, IndirectInvite(player, features), invitingPlayer, @@ -138,198 +156,25 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { } } - def createSpontaneousInvite(player: Player, invitedPlayer: Long): Unit = { + def createInvitationToCreateASquad(player: Player, invitedPlayer: Long): Unit = { //neither the invited player nor the inviting player belong to any squad val invitingPlayer = player.CharId - Allowed(invitedPlayer, invitingPlayer) - if (Refused(invitingPlayer).contains(invitedPlayer)) { + allowed(invitedPlayer, invitingPlayer) + if (refused(invitingPlayer).contains(invitedPlayer)) { log.debug(s"$invitedPlayer repeated a previous refusal to $invitingPlayer's invitation offer") - } else if (Refused(invitedPlayer).contains(invitingPlayer)) { + } else if (refused(invitedPlayer).contains(invitingPlayer)) { log.debug(s"$invitingPlayer repeated a previous refusal to $invitedPlayer's invitation offer") } else { - AddInviteAndRespond( + addInviteAndRespond( invitedPlayer, - SpontaneousInvite(player), + InvitationToCreateASquad(player), invitingPlayer, player.Name ) } } - def SquadActionMembershipAcceptInvite( - tplayer: Player, - invitedPlayer: Long, - acceptedInvite: Option[Invitation], - invitedPlayerSquadOpt: Option[SquadFeatures] - ): Unit = { - val availableForJoiningSquad = notLimitedByEnrollmentInSquad(invitedPlayerSquadOpt, invitedPlayer) - acceptedInvite match { - case Some(RequestRole(petitioner, features, position)) - if 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)) { - DeliverAcceptanceMessages(invitedPlayer, petitioner.CharId, petitioner.Name) - CleanUpInvitesForSquadAndPosition(features, position) - } - - case Some(IndirectInvite(recruit, features)) - if canEnrollInSquad(features, recruit.CharId) => - //tplayer / invitedPlayer is actually the squad leader - val recruitCharId = recruit.CharId - HandleVacancyInvite(features, recruitCharId, invitedPlayer, recruit) match { - case Some((_, line)) => - DeliverAcceptanceMessages(invitedPlayer, recruitCharId, recruit.Name) - JoinSquad(recruit, features, line) - CleanUpAllInvitesWithPlayer(recruitCharId) - CleanUpInvitesForSquadAndPosition(features, line) - //TODO since we are the squad leader, we do not want to brush off our queued squad invite tasks - case _ => ; - } - - case Some(VacancyInvite(invitingPlayer, _, features)) - if availableForJoiningSquad && canEnrollInSquad(features, invitedPlayer) => - //accepted an invitation to join an existing squad - HandleVacancyInvite(features, invitedPlayer, invitingPlayer, tplayer) match { - case Some((_, line)) => - DeliverAcceptanceMessages(invitingPlayer, invitedPlayer, tplayer.Name) - JoinSquad(tplayer, features, line) - CleanUpQueuedInvites(invitedPlayer) - CleanUpInvitesForSquadAndPosition(features, line) - case _ => ; - } - - case Some(SpontaneousInvite(invitingPlayer)) - if availableForJoiningSquad => - SquadMembershipAcceptInviteAction(invitingPlayer, tplayer, invitedPlayer) - - case Some(LookingForSquadRoleInvite(member, features, position)) - if availableForJoiningSquad && canEnrollInSquad(features, invitedPlayer) => - val invitingPlayer = member.CharId - features.ProxyInvites = features.ProxyInvites.filterNot { _ == invitedPlayer } - if (JoinSquad(tplayer, features, position)) { - //join this squad - DeliverAcceptanceMessages(invitingPlayer, invitedPlayer, tplayer.Name) - CleanUpQueuedInvites(tplayer.CharId) - CleanUpInvitesForSquadAndPosition(features, position) - } - - case Some(ProximityInvite(member, features, position)) - if availableForJoiningSquad && canEnrollInSquad(features, invitedPlayer) => - val invitingPlayer = member.CharId - features.ProxyInvites = features.ProxyInvites.filterNot { _ == invitedPlayer } - if (JoinSquad(tplayer, features, position)) { - //join this squad - DeliverAcceptanceMessages(invitingPlayer, invitedPlayer, tplayer.Name) - CleanUpAllInvitesWithPlayer(invitedPlayer) - val squad = features.Squad - if (squad.Size == squad.Capacity) { - //all available squad positions filled; terminate all remaining invitations - CleanUpAllInvitesToSquad(features) - } - } else { - ReloadProximityInvite(tplayer.Zone.Players, invitedPlayer, features, position) //TODO ? - } - - case _ => - //the invite either timed-out or was withdrawn or is now invalid - (previousInvites.get(invitedPlayer) match { - case Some(SpontaneousInvite(leader)) => (leader.CharId, leader.Name) - case Some(VacancyInvite(charId, name, _)) => (charId, name) - case Some(ProximityInvite(member, _, _)) => (member.CharId, member.Name) - case Some(LookingForSquadRoleInvite(member, _, _)) => (member.CharId, member.Name) - case _ => (0L, "") - }) match { - case (0L, "") => ; - case (charId, name) => - subs.Publish( - charId, - SquadResponse.Membership(SquadResponseType.Cancel, 0, 0, charId, Some(0L), name, unk5 = false, Some(None)) - ) - } - } - } - - def DeliverAcceptanceMessages( - squadLeader: Long, - joiningPlayer: Long, - joiningPlayerName: String - ): Unit = { - val msg = SquadResponse.Membership( - SquadResponseType.Accept, - 0, - 0, - joiningPlayer, - Some(squadLeader), - joiningPlayerName, - unk5 = false, - Some(None) - ) - subs.Publish(squadLeader, msg) - subs.Publish(joiningPlayer, msg.copy(unk5 = true)) - } - - def notLimitedByEnrollmentInSquad(squadOpt: Option[SquadFeatures], charId: Long): Boolean = { - squadOpt match { - case Some(features) if features.Squad.Membership.exists { _.CharId == charId } => - EnsureEmptySquad(features) - case Some(_) => - false - case None => - true - } - } - - 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 - if (invitingPlayerCharId != invitedPlayer) { - //generate a new squad, with invitingPlayer as the leader - val result = ask(parent, SquadService.PerformStartSquad(invitingPlayer)) - result.onComplete { - case Success(FinishStartSquad(features)) => - HandleVacancyInvite(features, player.CharId, invitingPlayerCharId, player) match { - case Some((_, line)) => - subs.Publish( - invitedPlayer, - SquadResponse.Membership( - SquadResponseType.Accept, - 0, - 0, - invitedPlayer, - Some(invitingPlayerCharId), - "", - unk5=true, - Some(None) - ) - ) - subs.Publish( - invitingPlayerCharId, - SquadResponse.Membership( - SquadResponseType.Accept, - 0, - 0, - invitingPlayerCharId, - Some(invitedPlayer), - player.Name, - unk5=false, - Some(None) - ) - ) - JoinSquad(player, features, line) - CleanUpQueuedInvites(player.CharId) - case _ => ; - } - case _ => ; - } - } - } - - def handleProximityInvite(zone: Zone, invitingPlayer: Long, features: SquadFeatures): Unit = { + def createProximityInvite(zone: Zone, invitingPlayer: Long, features: SquadFeatures): Unit = { val squad = features.Squad val sguid = squad.GUID val origSearchForRole = features.SearchForRole @@ -352,7 +197,9 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { newRecruitment = newRecruitment :+ key squad.Membership.zipWithIndex.filterNot { case (_, index) => index == position } case None => - CleanUpQueuedInvitesForSquadAndPosition(features, position) + val comment = s"An invitation to squad '${features.Squad.Task}' was filled by a different player." + cleanUpQueuedInvitesForSquadAndPosition(features.Squad.GUID, position) + .foreach { id => subs.Publish(id, SquadResponse.SquadRelatedComment(comment)) } squad.Membership.zipWithIndex } case _ => @@ -362,7 +209,8 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { .collect { case (member, index) if member.CharId == 0 && squad.Availability(index) => (member, index) } .sortBy({ _._1.Requirements.foldLeft(0)(_ + _.value) })(Ordering.Int.reverse) //find recruits - val players = zone.LivePlayers.map { _.avatar } + val faction = squad.Faction + val players = zone.Players.filter(_.faction == faction) if (positionsToRecruitFor.nonEmpty && players.nonEmpty) { //does this do anything? subs.Publish( @@ -378,19 +226,19 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { Some(None) ) ) - positionsToRecruitFor.foreach { case (_, position) => - FindSoldiersWithinScopeAndInvite( - squad.Leader, - features, - position, - players, - features.ProxyInvites ++ newRecruitment, - ProximityEnvelope - ) match { - case None => ; - case Some(id) => + positionsToRecruitFor + .foreach { case (_, position) => + findSoldiersWithinScopeAndInvite( + squad.Leader, + features, + position, + players, + features.ProxyInvites ++ newRecruitment, + proximityEnvelope + ) + .collect { id => newRecruitment = newRecruitment :+ id - } + } } } if (newRecruitment.isEmpty) { @@ -400,97 +248,39 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { //if searching for a position originally, convert the active invite to proximity invite, or remove it val key = newRecruitment.head (origSearchForRole, invites.get(key)) match { - case (Some(-1), _) => ; + case (Some(-1), _) => () case (Some(position), Some(LookingForSquadRoleInvite(member, _, _))) => - invites(key) = ProximityInvite(member, features, position) - case _ => ; + invites.put(key, ProximityInvite(member, features, position)) + case _ => () } } } - def handleAcceptance(player: Player, charId: Long, squadOpt: Option[SquadFeatures]): Unit = { - SquadActionMembershipAcceptInvite(player, charId, RemoveInvite(charId), squadOpt) - NextInviteAndRespond(charId) - } + /* invitation reloads */ - def handleRejection( - tplayer: Player, - rejectingPlayer: Long, - squadsToLeaders: List[(PlanetSideGUID, Long)] - ): Unit = { - val rejectedBid = RemoveInvite(rejectingPlayer) - (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)) - - case Some(VacancyInvite(leader, _, _)) - /*if 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)) - - case Some(ProximityInvite(_, features, position)) - /*if 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, - rejectingPlayer, - features, - position - ) - (Some(rejectingPlayer), None) - - case Some(LookingForSquadRoleInvite(member, guid, 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, - position - ) - (Some(rejectingPlayer), Some(leaderCharId)) - - case Some(RequestRole(rejected, features, _)) - if 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) - - case _ => ; //TODO IndirectInvite, etc., but how to handle them? - (None, None) - }) match { - case (Some(rejected), Some(invited)) => - subs.Publish( - rejected, - SquadResponse.Membership(SquadResponseType.Reject, 0, 0, rejected, Some(invited), "", unk5=true, Some(None)) - ) - subs.Publish( - invited, - SquadResponse.Membership(SquadResponseType.Reject, 0, 0, invited, Some(rejected), tplayer.Name, unk5=false, Some(None)) - ) - case (Some(rejected), None) => - 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 + 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 RequestToJoinSquadRole(player, _, _) => player.Name + case IndirectInvite(player, features) if !features.Squad.Leader.Name.equals(player.Name) => player.Name } } - def ReloadSearchForRoleInvite( + def reloadActiveInvite(charId: Long): Unit = { + invites + .get(charId) + .foreach(respondToInvite(charId, _)) + } + + def reloadSearchForRoleInvite( scope: List[Avatar], rejectingPlayer: Long, features: SquadFeatures, @@ -498,20 +288,23 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { ) : Unit = { //rejectingPlayer is the would-be squad member; the squad leader rejected the request val squadLeader = features.Squad.Leader - Refused(rejectingPlayer, squadLeader.CharId) + refused(rejectingPlayer, squadLeader.CharId) features.ProxyInvites = features.ProxyInvites.filterNot { _ == rejectingPlayer } - FindSoldiersWithinScopeAndInvite( + findSoldiersWithinScopeAndInvite( squadLeader, features, position, scope, features.ProxyInvites, - LookingForSquadRoleEnvelope + lookingForSquadRoleEnvelope ) match { case None => if (features.SearchForRole.contains(position) && features.ProxyInvites.isEmpty) { features.SearchForRole = None - //TODO message the squadLeader.CharId to indicate that there are no more candidates for this position + subs.Publish( + squadLeader.CharId, + SquadResponse.SquadRelatedComment("Exhausted all possible candidates to fill the open squad position.") + ) } case Some(id) => features.ProxyInvites = features.ProxyInvites :+ id @@ -521,7 +314,7 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { } } - def ReloadProximityInvite( + def reloadProximityInvite( scope: List[Avatar], rejectingPlayer: Long, features: SquadFeatures, @@ -529,113 +322,386 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { ): Unit = { //rejectingPlayer is the would-be squad member; the squad leader rejected the request val squadLeader = features.Squad.Leader - Refused(rejectingPlayer, squadLeader.CharId) + refused(rejectingPlayer, squadLeader.CharId) features.ProxyInvites = features.ProxyInvites.filterNot { _ == rejectingPlayer } - FindSoldiersWithinScopeAndInvite( + findSoldiersWithinScopeAndInvite( squadLeader, features, position, scope, features.ProxyInvites, - ProximityEnvelope - ) match { - case None => + proximityEnvelope + ) + .collect { id => + features.ProxyInvites = features.ProxyInvites :+ id + id + } + .orElse { if (features.SearchForRole.contains(-1) && features.ProxyInvites.isEmpty) { features.SearchForRole = None - //TODO message the squadLeader.CharId to indicate that there are no more candidates for this position + subs.Publish( + squadLeader.CharId, + SquadResponse.SquadRelatedComment("Exhausted all possible local candidates to fill the open squad positions.") + ) } - case Some(id) => - features.ProxyInvites = features.ProxyInvites :+ id + None + } + } + + /* acceptance */ + + def handleAcceptance(player: Player, charId: Long, squadOpt: Option[SquadFeatures]): Unit = { + removeInvite(charId) + .map { invite => + invite.handleAcceptance(manager = this, player, charId, squadOpt) + invite + } + .orElse { + //the invite either timed-out or was withdrawn or is now invalid + //originally, only InvitationToCreateASquad, InvitationToJoinSquad, ProximityInvite, LookingForSquadRoleInvite + previousInvites + .get(charId) + .map { invite => (invite.inviterCharId, invite.inviterName) } match { + case Some((0L, "")) => () + case Some((charId, name)) => + subs.Publish( + charId, + SquadResponse.Membership(SquadResponseType.Cancel, charId, Some(0L), name, unk5 = false) + ) + } + None + } + nextInviteAndRespond(charId) + } + + def tryChainAcceptance( + inviter: Player, + charId: Long, + list: List[Long], + features: SquadFeatures + ): Unit = { + //filter queued invites + lazy val squadName = features.Squad.Task + var foundPairs: List[(Player, Invitation)] = List() + val unmatchedInvites = queuedInvites + .getOrElse(charId, Nil) + .filter { + case invite @ RequestToJoinSquadRole(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 @ RequestToJoinSquadRole(invitee, _, _) + if list.contains(invitee.CharId) && !features.Squad.Leader.Name.equals(invitee.Name) => + invite.handleAcceptance(manager = this, inviter, invitee.CharId, Some(features)) + invites.remove(charId) + true + case invite @ IndirectInvite(invitee, _) + if list.contains(invitee.CharId) && !features.Squad.Leader.Name.equals(invitee.Name) => + invite.handleAcceptance(manager = this, inviter, invitee.CharId, 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() + invite.handleAcceptance(manager = this, inviter, player.CharId, 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 { _ => + subs.Publish(inviter.CharId, SquadResponse.SquadRelatedComment(s"Your request to join squad '$squadName' has been refused.")) + } + nextInviteAndRespond(charId) + } + //clean up any incomplete selected invites + pairIterator.foreach { case (_, _) => + subs.Publish(inviter.CharId, SquadResponse.SquadRelatedComment(s"Your request to join squad '$squadName' has been refused.")) } } + def acceptanceMessages( + squadLeader: Long, + joiningPlayer: Long, + joiningPlayerName: String + ): Unit = { + val msg = SquadResponse.Membership(SquadResponseType.Accept, joiningPlayer, Some(squadLeader), joiningPlayerName, unk5 = false) + subs.Publish(squadLeader, msg) + subs.Publish(joiningPlayer, msg.copy(unk5 = true)) + } + + /* rejection */ + + def handleRejection( + tplayer: Player, + rejectingPlayer: Long, + squadsToLeaders: List[(PlanetSideGUID, SquadFeatures)] + ): Unit = { + removeInvite(rejectingPlayer) + .foreach { invite => + invite.handleRejection(manager = this, tplayer, rejectingPlayer, squadsToLeaders) + invite + } + nextInviteAndRespond(rejectingPlayer) + } + + def tryChainRejection( + inviter: Player, + charId: Long, + list: List[Long], + features: SquadFeatures): Unit = { + //handle queued invites + lazy val squadName = features.Squad.Task + val unmatchedInvites = queuedInvites + .getOrElse(charId, Nil) + .filter { + case invite @ RequestToJoinSquadRole(invitee, _, _) + if list.contains(invitee.CharId) && !features.Squad.Leader.Name.equals(invitee.Name) => + invite.doRejection(manager = this, inviter, charId) + subs.Publish(charId, 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) => + invite.doRejection(manager = this, inviter, charId) + subs.Publish(charId, 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 @ RequestToJoinSquadRole(player, features, _) + if list.contains(player.CharId) && !features.Squad.Leader.Name.equals(player.Name) => + invite.doRejection(manager = this, inviter, charId) + subs.Publish(charId, 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) => + invite.doRejection(manager = this, inviter, charId) + subs.Publish(charId, 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 comment = s"Your request to join squad '${features.Squad.Task}' has been refused." + cleanUpAllInvitesForSquad(features.Squad.GUID) + .map(_._1) + .filterNot(_ == charId) + .foreach { refusedId => + subs.Publish(refusedId, SquadResponse.SquadRelatedComment(comment)) + } + } + + def rejectionMessage(rejectingPlayer: Long): Unit = { + subs.Publish( + rejectingPlayer, + SquadResponse.Membership(SquadResponseType.Reject, rejectingPlayer, Some(rejectingPlayer), "", unk5 = true) + ) + } + + def rejectionMessages( + rejectingPlayer: Long, + leaderCharId: Long, + name: String + ): Unit = { + subs.Publish( + rejectingPlayer, + SquadResponse.Membership(SquadResponseType.Reject, rejectingPlayer, Some(leaderCharId), "", unk5 = true) + ) + subs.Publish( + leaderCharId, + SquadResponse.Membership(SquadResponseType.Reject, leaderCharId, Some(rejectingPlayer), name, unk5 = false) + ) + } + + /* other special actions */ + def handleDisbanding(features: SquadFeatures): Unit = { - CleanUpAllInvitesToSquad(features) + cleanUpAllInvitesForSquad(features.Squad.GUID) } - def handleCancelling(cancellingPlayer: Long): Unit = { - //get rid of SpontaneousInvite objects and VacancyInvite objects - invites.collect { - case (id, invite: SpontaneousInvite) if invite.InviterCharId == cancellingPlayer => RemoveInvite(id) - case (id, invite: VacancyInvite) if invite.InviterCharId == cancellingPlayer => RemoveInvite(id) - case (id, invite: LookingForSquadRoleInvite) if invite.InviterCharId == cancellingPlayer => RemoveInvite(id) - } - queuedInvites.foreach { - case (id: Long, inviteList) => - val inList = inviteList.filterNot { - case invite: SpontaneousInvite if invite.InviterCharId == cancellingPlayer => true - case invite: VacancyInvite if invite.InviterCharId == cancellingPlayer => true - case invite: LookingForSquadRoleInvite if invite.InviterCharId == cancellingPlayer => true - case _ => false + def handleCancelling( + cancellingPlayer: Long, + player: Player, + featureOpt: Option[SquadFeatures] + ): Unit = { + featureOpt + .collect { + case features if features.SearchForRole.contains(-1L) => + //cancel proximity invites + features.SearchForRole = None + features.ProxyInvites = Nil + val queuedButCancelled = cleanUpQueuedProximityInvitesForPlayer(cancellingPlayer) + val activeButCancelled = cleanUpActiveProximityInvitesForPlayer(cancellingPlayer) + activeButCancelled.collect { case (id, invites) => + invites.foreach(_.handleCancel(manager = this, player, id)) + } + if (queuedButCancelled.nonEmpty || activeButCancelled.nonEmpty) { + subs.Publish( + cancellingPlayer, + SquadResponse.SquadRelatedComment("You have cancelled proximity invitations for your squad recruitment.") + ) + } + true + case features if features.SearchForRole.nonEmpty => + //cancel search for role + cancelSelectRoleForYourself(player, features) + subs.Publish( + cancellingPlayer, + SquadResponse.SquadRelatedComment("You have cancelled search for role invitations for your squad recruitment.") + ) + true + } + .orElse { + //todo cancel any request to join squad role or invitation to create a squad + None + } + .orElse { + //we have nothing special to cancel; search everything and see what we have dipped our feet in + val queuedButCancelled = cleanUpQueuedInvitesForPlayer(cancellingPlayer) + val activeButCancelled = cleanUpActiveInvitesForPlayer(cancellingPlayer) + activeButCancelled.collect { case (id, invites) => + invites.foreach(_.handleCancel(manager = this, player, id)) } - if (inList.isEmpty) { - queuedInvites.remove(id) - } else { - queuedInvites(id) = inList + if (queuedButCancelled.nonEmpty || activeButCancelled.nonEmpty) { + subs.Publish( + cancellingPlayer, + SquadResponse.SquadRelatedComment("You have cancelled some invitations and/or squad requests.") + ) } - } - //get rid of ProximityInvite objects - CleanUpAllProximityInvites(cancellingPlayer) + None + } } + def handleClosingSquad(features: SquadFeatures): Unit = { + cleanUpAllInvitesForSquad(features.Squad.GUID) + } + + def handleCleanup(charId: Long): Unit = { + cleanUpAllInvitesForPlayer(charId) + } + + def handleLeave(charId: Long): Unit = { + refusedPlayers.remove(charId) + cleanUpAllInvitesForPlayer(charId) + } + + /* other special actions, promotion */ + def handlePromotion( - sponsoringPlayer: Long, - promotedPlayer: Long, - ): Unit = { - ShiftInvitesToPromotedSquadLeader(sponsoringPlayer, promotedPlayer) + sponsoringPlayer: Long, + promotedPlayer: Long, + ): Unit = { + shiftInvitesToPromotedSquadLeader(sponsoringPlayer, promotedPlayer) } - def handleDefinitionAction( - player: Player, - action: SquadRequestAction, - features: SquadFeatures - ): Unit = { - import SquadRequestAction._ - action match { - //the following actions cause changes with the squad composition or with invitations - case AutoApproveInvitationRequests(_) => - SquadActionDefinitionAutoApproveInvitationRequests(player, features) - case FindLfsSoldiersForRole(position) => - SquadActionDefinitionFindLfsSoldiersForRole(player, position, features) - case CancelFind() => - SquadActionDefinitionCancelFind(None) - case SelectRoleForYourself(position) => - SquadActionDefinitionSelectRoleForYourselfAsInvite(player, features, position) - case _: CancelSelectRoleForYourself => - SquadActionDefinitionCancelSelectRoleForYourself(player, features) - case _ => ; + def shiftInvitesToPromotedSquadLeader( + sponsoringPlayer: Long, + promotedPlayer: Long + ): Unit = { + val leaderInvite = invites.remove(sponsoringPlayer) + val leaderQueuedInvites = queuedInvites.remove(sponsoringPlayer).toList.flatten + val (invitesToConvert, invitesToAppend) = (invites.remove(promotedPlayer).orElse(previousInvites.get(promotedPlayer)), leaderInvite) match { + case (Some(activePromotedInvite), Some(outLeaderInvite)) => + //the promoted player has an active invite; queue these + val promotedQueuedInvites = queuedInvites.remove(promotedPlayer).toList.flatten + nextInvite(promotedPlayer) + nextInvite(sponsoringPlayer) + (activePromotedInvite +: (outLeaderInvite +: leaderQueuedInvites), promotedQueuedInvites) + + case (Some(activePromotedInvite), None) => + //the promoted player has an active invite; queue these + val promotedQueuedInvites = queuedInvites.remove(promotedPlayer).toList.flatten + nextInvite(promotedPlayer) + (activePromotedInvite :: leaderQueuedInvites, promotedQueuedInvites) + + case (None, Some(outLeaderInvite)) => + //no active invite for the promoted player, but the leader had an active invite; trade the queued invites + nextInvite(sponsoringPlayer) + (outLeaderInvite +: leaderQueuedInvites, queuedInvites.remove(promotedPlayer).toList.flatten) + + case (None, None) => + //no active invites for anyone; assign the first queued invite from the promoting player, if available, and queue the rest + (leaderQueuedInvites, queuedInvites.remove(promotedPlayer).toList.flatten) + } + //move over promoted invites + invitesToConvert ++ invitesToAppend match { + case Nil => () + case x :: Nil => + addInviteAndRespond(promotedPlayer, x, x.inviterCharId, x.inviterName) + case x :: xs => + addInviteAndRespond(promotedPlayer, x, x.inviterCharId, x.inviterName) + queuedInvites.put(promotedPlayer, xs) } } - def SquadActionDefinitionAutoApproveInvitationRequests( - tplayer: Player, - features: SquadFeatures - ): Unit = { + /* squad definition features */ + + def autoApproveInvitationRequests( + 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) case None => queuedInvites.getOrElse(charId, Nil) }) - .partition({ case _: RequestRole => true; case _ => false }) + .partition({ case _: RequestToJoinSquadRole => true; case _ => false }) invites.remove(charId) queuedInvites.remove(charId) previousInvites.remove(charId) - //RequestRole invitations that still have to be handled + //RequestToJoinSquadRole invitations that still have to be handled val squad = features.Squad var remainingRequests = requests.collect { - case request: RequestRole => (request, request.player) + case request: RequestToJoinSquadRole => (request, request.requestee) } var unfulfilled = List[Player]() //give roles to people who requested specific positions (1 to 9).foreach { position => val (discovered, remainder) = remainingRequests.partition { - case (request: RequestRole, _) => request.position == position + case (request: RequestToJoinSquadRole, _) => request.position == position case _ => false } unfulfilled ++= (discovered - .find { case (_, player) => JoinSquad(player, features, position) } match { + .find { case (_, player) => joinSquad(player, features, position) } match { case Some((_, player)) => remainingRequests = remainder.filterNot { case (_, p) => p.CharId == player.CharId } discovered.filterNot { case (_, p) => p.CharId == player.CharId } @@ -647,89 +713,92 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { //fill any incomplete role by trying to match all sorts of unfulfilled invitations var otherInvites = unfulfilled ++ others.collect { - case invite: SpontaneousInvite => invite.player - case invite: IndirectInvite => invite.player + case invite: InvitationToCreateASquad => invite.futureSquadLeader + case invite: IndirectInvite => invite.originalRequester } .distinctBy { _.CharId } (1 to 9).foreach { position => if (squad.Availability(position)) { otherInvites.zipWithIndex.find { case (invitedPlayer, _) => - JoinSquad(invitedPlayer, features, position) + joinSquad(invitedPlayer, features, position) } match { case Some((_, index)) => otherInvites = otherInvites.take(index) ++ otherInvites.drop(index+1) - case None => ; + case None => () } } } //cleanup searches by squad leader features.SearchForRole match { - case Some(-1) => CleanUpAllProximityInvites(charId) - case Some(_) => SquadActionDefinitionCancelFind(Some(features)) - case None => ; + case Some(-1) => cleanUpAllProximityInvitesForPlayer(charId) + case Some(_) => cancelFind(Some(features)) + case None => () } } - def SquadActionDefinitionFindLfsSoldiersForRole( - tplayer: Player, - position: Int, - features: SquadFeatures - ): Unit = { + def findLfsSoldiersForRole( + tplayer: Player, + features: SquadFeatures, + position: Int + ): List[(Long, List[Invitation])] = { val squad = features.Squad val sguid = squad.GUID - (features.SearchForRole match { - case None => - Some(Nil) - case Some(-1) => + val list = features + .SearchForRole + .collect { + case -1 => //a proximity invitation has not yet cleared; nothing will be gained by trying to invite for a specific role log.debug("FindLfsSoldiersForRole: waiting for proximity invitations to clear") None - case Some(pos) if pos == position => + case pos if pos == position => //already recruiting for this specific position in the squad? do nothing log.debug("FindLfsSoldiersForRole: already recruiting for this position; client-server mismatch?") None - case Some(pos) => + case pos => //some other role is undergoing recruitment; cancel and redirect efforts for new position + val comment = s"An invitation to join squad '${features.Squad.Task}' has been rescinded." features.SearchForRole = None - CleanUpQueuedInvitesForSquadAndPosition(features, pos) - Some( - invites.filter { - case (_, LookingForSquadRoleInvite(_, _features, role)) => _features.Squad.GUID == sguid && role == pos - case _ => false - }.keys.toList - ) - }) match { - case None => - features.SearchForRole = None - case Some(list) => - //this will update the role entry in the GUI to visually indicate being searched for; only one will be displayed at a time - subs.Publish( - tplayer.CharId, - SquadResponse.Detail( - sguid, - SquadDetail().Members( - List(SquadPositionEntry(position, SquadPositionDetail().CharId(char_id = 0L).Name(name = ""))) - ) - ) - ) - //search! - FindSoldiersWithinScopeAndInvite( - squad.Leader, - features, - position, - LivePlayerList.WorldPopulation { _ => true }, - list, - LookingForSquadRoleEnvelope - ) match { - case None => ; - case Some(id) => - features.ProxyInvites = List(id) - features.SearchForRole = position + val retiredQueuedInvitations = cleanUpQueuedInvitesForSquadAndPosition(features.Squad.GUID, pos) + val retiredInvites = invites.collect { case (id, invite) if invite.appliesToSquadAndPosition(sguid, pos) => (id, List(invite)) } + retiredInvites.foreach { case (id, _) => + invites.remove(id) + nextInviteAndRespond(id) } + SquadInvitationManager.moveListElementsToMap(retiredQueuedInvitations, retiredInvites) + retiredInvites.foreach { case (id, _) => + subs.Publish(id, SquadResponse.SquadRelatedComment(comment)) + } + Some(retiredInvites.toList) + } + .flatten + .getOrElse(Nil) + //this will update the role entry in the GUI to visually indicate being searched for; only one will be displayed at a time + subs.Publish( + tplayer.CharId, + SquadResponse.Detail( + sguid, + SquadDetail().Members( + List(SquadPositionEntry(position, SquadPositionDetail().CharId(char_id = 0L).Name(name = ""))) + ) + ) + ) + //search! + val faction = squad.Faction + findSoldiersWithinScopeAndInvite( + squad.Leader, + features, + position, + tplayer.Zone.Players.filter(_.faction == faction), + Nil, + lookingForSquadRoleEnvelope + ).collect { id => + features.ProxyInvites = List(id) + features.SearchForRole = position } + list } - def SquadActionDefinitionCancelFind(lSquadOpt: Option[SquadFeatures]): Unit = { + def cancelFind(lSquadOpt: Option[SquadFeatures]): Unit = { lSquadOpt match { case Some(features) => features.SearchForRole match { @@ -745,7 +814,7 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { } .keys .foreach { charId => - RemoveInvite(charId) + removeInvite(charId) } //remove queued invites queuedInvites.foreach { @@ -761,90 +830,77 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { } //remove yet-to-be invitedPlayers features.ProxyInvites = Nil - case _ => ; + case _ => () } - case _ => ; + case _ => () } } - /** the following action can be performed by an unaffiliated player */ - def SquadActionDefinitionSelectRoleForYourselfAsInvite( - tplayer: Player, - features: SquadFeatures, - position: Int - ): Unit = { + /* the following action can be performed by an unaffiliated player */ + def selectRoleForYourselfAsInvite( + tplayer: Player, + features: SquadFeatures, + position: Int + ): Unit = { //not a member of any squad, but we might become a member of this one val squad = features.Squad if (squad.isAvailable(position, tplayer.avatar.certifications)) { //we could join directly but we need permission from the squad leader first if (features.AutoApproveInvitationRequests) { - SquadActionMembershipAcceptInvite( - tplayer, - squad.Leader.CharId, - Some(RequestRole(tplayer, features, position)), - None - ) + invitations.RequestToJoinSquadRole(tplayer, features, position).handleAcceptance(manager = this, tplayer, squad.Leader.CharId, None) } else { //circumvent tests in AddInviteAndRespond - val requestRole = RequestRole(tplayer, features, position) val charId = squad.Leader.CharId - InviteResponseTemplate(indirectInviteResp)( - requestRole, - AddInvite(charId, requestRole), - charId, - invitingPlayer = 0L, //we ourselves technically are ... - tplayer.Name - ) + val requestRole = invitations.RequestToJoinSquadRole(tplayer, features, position) + if (addInvite(charId, requestRole).contains(requestRole)) { + requestRole.handleInvitation(indirectInviteResp)( + manager = this, + charId, + invitingPlayer = 0L, //we ourselves technically are ... + tplayer.Name + ) + } } } } - /** the following action can be performed by anyone who has tried to join a squad */ - def SquadActionDefinitionCancelSelectRoleForYourself( - tplayer: Player, - features: SquadFeatures - ): Unit = { + /* the following action can be performed by anyone who has tried to join a squad */ + def cancelSelectRoleForYourself( + tplayer: Player, + features: SquadFeatures + ): Unit = { val cancellingPlayer = tplayer.CharId //assumption: a player who is cancelling will rarely end up with their invite queued val squad = features.Squad val leaderCharId = squad.Leader.CharId - //clean up any active RequestRole invite entry where we are the player who wants to join the leader's squad + //clean up any active RequestToJoinSquadRole invite entry where we are the player who wants to join the leader's squad ((invites.get(leaderCharId) match { case out @ Some(entry) - if entry.isInstanceOf[RequestRole] && - entry.asInstanceOf[RequestRole].player.CharId == cancellingPlayer => + if entry.isInstanceOf[RequestToJoinSquadRole] && + entry.asInstanceOf[RequestToJoinSquadRole].requestee.CharId == cancellingPlayer => out case _ => None }) match { - case Some(entry: RequestRole) => - RemoveInvite(leaderCharId) + case Some(entry: RequestToJoinSquadRole) => + removeInvite(leaderCharId) subs.Publish( leaderCharId, - SquadResponse.Membership( - SquadResponseType.Cancel, - 0, - 0, - cancellingPlayer, - None, - entry.player.Name, - unk5=false, - Some(None) - ) + SquadResponse.Membership(SquadResponseType.Cancel, cancellingPlayer, None, entry.requestee.Name, unk5 = false ) ) - NextInviteAndRespond(leaderCharId) + nextInviteAndRespond(leaderCharId) Some(true) case _ => None }).orElse( - //look for a queued RequestRole entry where we are the player who wants to join the leader's squad + //look for a queued RequestToJoinSquadRole entry where we are the player who wants to join the leader's squad (queuedInvites.get(leaderCharId) match { case Some(_list) => ( _list, _list.indexWhere { entry => - entry.isInstanceOf[RequestRole] && - entry.asInstanceOf[RequestRole].player.CharId == cancellingPlayer + entry.isInstanceOf[RequestToJoinSquadRole] && + entry.asInstanceOf[RequestToJoinSquadRole].requestee.CharId == cancellingPlayer } ) case None => @@ -853,143 +909,199 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { case (_, -1) => None //no change case (list, _) if list.size == 1 => - val entry = list.head.asInstanceOf[RequestRole] + val entry = list.head.asInstanceOf[RequestToJoinSquadRole] subs.Publish( leaderCharId, - SquadResponse.Membership( - SquadResponseType.Cancel, - 0, - 0, - cancellingPlayer, - None, - entry.player.Name, - unk5=false, - Some(None) - ) + SquadResponse.Membership(SquadResponseType.Cancel, cancellingPlayer, None, entry.requestee.Name, unk5 = false) ) queuedInvites.remove(leaderCharId) Some(true) case (list, index) => - val entry = list(index).asInstanceOf[RequestRole] + val entry = list(index).asInstanceOf[RequestToJoinSquadRole] subs.Publish( leaderCharId, - SquadResponse.Membership( - SquadResponseType.Cancel, - 0, - 0, - cancellingPlayer, - None, - entry.player.Name, - unk5=false, - Some(None) - ) + SquadResponse.Membership(SquadResponseType.Cancel, cancellingPlayer, None, entry.requestee.Name, unk5 = false) ) - queuedInvites(leaderCharId) = list.take(index) ++ list.drop(index + 1) + queuedInvites.put(leaderCharId, list.take(index) ++ list.drop(index + 1)) Some(true) } ) } - def handleClosingSquad(features: SquadFeatures): Unit = { - CleanUpAllInvitesToSquad(features) + /* squad interaction */ + + def askToCreateANewSquad(invitingPlayer: Player): Future[Any] = { + //originally, we were invited by someone into a new squad they would form + //generate a new squad, with invitingPlayer as the leader + ask(parent, SquadService.PerformStartSquad(invitingPlayer)) } - def handleCleanup(charId: Long): Unit = { - CleanUpAllInvitesWithPlayer(charId) - } - - def handleLeave(charId: Long): Unit = { - refused.remove(charId) - CleanUpAllInvitesWithPlayer(charId) - } - - def resendActiveInvite(charId: Long): Unit = { - invites.get(charId) match { - case Some(invite) => - RespondToInvite(charId, invite) - case None => ; - } - } - - def ShiftInvitesToPromotedSquadLeader( - sponsoringPlayer: Long, - promotedPlayer: Long - ): Unit = { - val leaderInvite = invites.remove(sponsoringPlayer) - val leaderQueuedInvites = queuedInvites.remove(sponsoringPlayer).toList.flatten - val (invitesToConvert, invitesToAppend) = (invites.remove(promotedPlayer).orElse(previousInvites.get(promotedPlayer)), leaderInvite) match { - case (Some(activePromotedInvite), Some(outLeaderInvite)) => - //the promoted player has an active invite; queue these - val promotedQueuedInvites = queuedInvites.remove(promotedPlayer).toList.flatten - NextInvite(promotedPlayer) - NextInvite(sponsoringPlayer) - (activePromotedInvite +: (outLeaderInvite +: leaderQueuedInvites), promotedQueuedInvites) - - case (Some(activePromotedInvite), None) => - //the promoted player has an active invite; queue these - val promotedQueuedInvites = queuedInvites.remove(promotedPlayer).toList.flatten - NextInvite(promotedPlayer) - (activePromotedInvite :: leaderQueuedInvites, promotedQueuedInvites) - - case (None, Some(outLeaderInvite)) => - //no active invite for the promoted player, but the leader had an active invite; trade the queued invites - NextInvite(sponsoringPlayer) - (outLeaderInvite +: leaderQueuedInvites, queuedInvites.remove(promotedPlayer).toList.flatten) - - case (None, None) => - //no active invites for anyone; assign the first queued invite from the promoting player, if available, and queue the rest - (leaderQueuedInvites, queuedInvites.remove(promotedPlayer).toList.flatten) - } - moveOverPromotedInvites(promotedPlayer, invitesToConvert, invitesToAppend) - } - - def moveOverPromotedInvites( - targetPlayer: Long, - convertableInvites: List[Invitation], - otherInvitations: List[Invitation] - ): Unit = { - convertableInvites ++ otherInvitations match { - case Nil => ; - case x :: Nil => - AddInviteAndRespond(targetPlayer, x, x.InviterCharId, x.InviterName) - case x :: xs => - AddInviteAndRespond(targetPlayer, x, x.InviterCharId, x.InviterName) - queuedInvites += targetPlayer -> xs + def notLimitedByEnrollmentInSquad(squadOpt: Option[SquadFeatures], charId: Long): Boolean = { + squadOpt match { + case Some(features) if features.Squad.Membership.exists { _.CharId == charId } => + ensureEmptySquad(features) + case Some(_) => + false + case None => + true } } /** - * Enqueue a newly-submitted invitation object - * either as the active position or into the inactive positions - * and dispatch a response for any invitation object that is discovered. - * Implementation of a workflow. - * - * @see `AddInvite` - * @see `indirectInviteResp` - * @param invitedPlayer the unique character identifier for the player being invited; - * in actuality, represents the player who will address the invitation object - * @param targetInvite a comparison invitation object - * @param invitingPlayer the unique character identifier for the player who invited the former - * @param name a name to be used in message composition + * Determine whether a player is sufficiently unemployed + * and has no grand delusions of being a squad leader. + * @see `CloseSquad` + * @param features an optional squad + * @return `true`, if the target player possesses no squad or the squad is nonexistent; + * `false`, otherwise */ - def AddInviteAndRespond( + def ensureEmptySquad(features: Option[SquadFeatures]): Boolean = { + features match { + case Some(squad) => ensureEmptySquad(squad) + case None => true + } + } + + /** + * Determine whether a player is sufficiently unemployed + * and has no grand delusions of being a squad leader. + * @see `CloseSquad` + * @param features the squad + * @return `true`, if the target player possesses no squad or a squad that is suitably nonexistent; + * `false`, otherwise + */ + def ensureEmptySquad(features: SquadFeatures): Boolean = { + val ensuredEmpty = features.Squad.Size <= 1 + if (ensuredEmpty) { + cleanUpAllInvitesForSquad(features.Squad.GUID) + } + ensuredEmpty + } + + /** + * Behaviors and exchanges necessary to complete the fulfilled recruitment process for the squad role.
+ *
+ * This operation is fairly safe to call whenever a player is to be inducted into a squad. + * The aforementioned player must have a callback retained in `subs.UserEvents` + * and conditions imposed by both the role and the player must be satisfied. + * @see `CleanUpAllInvitesForPlayer` + * @see `Squad.isAvailable` + * @see `Squad.Switchboard` + * @see `SquadSubscriptionEntity.MonitorSquadDetails` + * @see `SquadSubscriptionEntity.Publish` + * @see `SquadSubscriptionEntity.Join` + * @see `SquadSubscriptionEntity.UserEvents` + * @param player the new squad member; + * this player is NOT the squad leader + * @param features the squad the player is joining + * @param position the squad member role that the player will be filling + * @return `true`, if the player joined the squad in some capacity; + * `false`, if the player did not join the squad or is already a squad member + */ + def joinSquad(player: Player, features: SquadFeatures, position: Int): Boolean = { + cleanUpAllInvitesForPlayer(player.CharId) + parent ! SquadService.PerformJoinSquad(player, features, position) + true + } + + /* refusal and allowance */ + + /** + * This player has been refused to join squads by these players, or to form squads with these players. + * @param charId the player who refused other players + * @return the list of other players who have refused this player + */ + def refused(charId: Long): List[Long] = refusedPlayers.getOrElse(charId, Nil) + + /** + * This player has been refused to join squads by this squad leaders, or to form squads with this other player. + * @param charId the player who is being refused + * @param refusedCharId the player who refused + * @return the list of other players who have refused this player + */ + def refused(charId: Long, refusedCharId: Long): List[Long] = { + if (charId != refusedCharId) { + refused(charId, List(refusedCharId)) + } else { + Nil + } + } + + /** + * This player has been refused to join squads by these squad leaders, or to form squads with these other players. + * @param charId the player who is being refused + * @param list the players who refused + * @return the list of other players who have refused this player + */ + def refused(charId: Long, list: List[Long]): List[Long] = { + refusedPlayers.get(charId) match { + case Some(refusedList) => + refusedPlayers.put(charId, list ++ refusedList) + refused(charId) + case None => + Nil + } + } + + /** + * This player was previously refused to join squads by this squad leaders, or to form squads with this other player. + * They are now allowed. + * @param charId the player who is being refused + * @param permittedCharId the player who was previously refused + * @return the list of other players who have refused this player + */ + def allowed(charId: Long, permittedCharId: Long): List[Long] = { + if (charId != permittedCharId) { + allowed(charId, List(permittedCharId)) + } else { + Nil + } + } + + /** + * This player has been refused to join squads by these squad leaders, or to form squads with these other players. + * They are now allowed. + * @param charId the player who is being refused + * @param list the players who was previously refused + * @return the list of other players who have refused this player + */ + def allowed(charId: Long, list: List[Long]): List[Long] = { + refusedPlayers.get(charId) match { + case Some(refusedList) => + refusedPlayers.put(charId, refusedList.filterNot(list.contains)) + refused(charId) + case None => + Nil + } + } + + /* enqueue invite */ + + /** + * Enqueue a newly-submitted invitation object + * either as the active position or into the inactive positions + * and dispatch a response for any invitation object that is discovered. + * Implementation of a workflow. + * @see `AddInvite` + * @see `indirectInviteResp` + * @param invitedPlayer the unique character identifier for the player being invited; + * in actuality, represents the player who will address the invitation object + * @param targetInvite a comparison invitation object + * @param invitingPlayer the unique character identifier for the player who invited the former + * @param name a name to be used in message composition + */ + def addInviteAndRespond( invitedPlayer: Long, targetInvite: Invitation, invitingPlayer: Long, name: String, autoApprove: Boolean = false ): Unit = { - val (player, approval) = targetInvite match { - case IndirectInvite(_player, _) => (_player, autoApprove) - case RequestRole(_player, _, _) => (_player, autoApprove) - case _ => (null, false) - } - if (approval) { - SquadActionMembershipAcceptInvite(player, invitingPlayer, Some(targetInvite), None) - } else { - InviteResponseTemplate(indirectInviteResp)( - targetInvite, - AddInvite(invitedPlayer, targetInvite), + if (targetInvite.canBeAutoApproved && autoApprove) { + targetInvite.handleAcceptance(manager = this, targetInvite.getPlayer, invitingPlayer, None) + } else if (addInvite(invitedPlayer, targetInvite).contains(targetInvite)) { + targetInvite.handleInvitation(indirectInviteResp)( + manager = this, invitedPlayer, invitingPlayer, name @@ -998,36 +1110,29 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { } /** - * Enqueue a newly-submitted invitation object - * either as the active position or into the inactive positions - * and dispatch a response for any invitation object that is discovered. - * Implementation of a workflow. - * - * @see `AddInvite` - * @see `altIndirectInviteResp` - * @param targetInvite a comparison invitation object - * @param invitedPlayer the unique character identifier for the player being invited - * @param invitingPlayer the unique character identifier for the player who invited the former - * @param name a name to be used in message composition - */ - def AltAddInviteAndRespond( + * Enqueue a newly-submitted invitation object + * either as the active position or into the inactive positions + * and dispatch a response for any invitation object that is discovered. + * Implementation of a workflow. + * @see `AddInvite` + * @see `altIndirectInviteResp` + * @param targetInvite a comparison invitation object + * @param invitedPlayer the unique character identifier for the player being invited + * @param invitingPlayer the unique character identifier for the player who invited the former + * @param name a name to be used in message composition + */ + def altAddInviteAndRespond( invitedPlayer: Long, targetInvite: Invitation, invitingPlayer: Long, name: String, autoApprove: Boolean = false ): Unit = { - val (player, approval) = targetInvite match { - case IndirectInvite(_player, _) => (_player, autoApprove) - case RequestRole(_player, _, _) => (_player, autoApprove) - case _ => (null, false) - } - if (approval) { - SquadActionMembershipAcceptInvite(player, invitingPlayer, Some(targetInvite), None) - } else { - InviteResponseTemplate(altIndirectInviteResp)( - targetInvite, - AddInvite(invitedPlayer, targetInvite), + if (targetInvite.canBeAutoApproved && autoApprove) { + targetInvite.handleAcceptance(manager = this, targetInvite.getPlayer, invitingPlayer, None) + } else if (addInvite(invitedPlayer, targetInvite).contains(targetInvite)) { + targetInvite.handleInvitation(altIndirectInviteResp)( + manager = this, invitedPlayer, invitingPlayer, name @@ -1036,20 +1141,19 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { } /** - * Component method used for the response behavior for processing the invitation object as an `IndirectInvite` object. - * - * @see `HandleRequestRole` - * @param invite the original invitation object that started this process - * @param player the target of the response and invitation - * @param invitedPlayer the unique character identifier for the player being invited; - * in actuality, represents the player who will address the invitation object; - * not useful here - * @param invitingPlayer the unique character identifier for the player who invited the former; - * not useful here - * @param name a name to be used in message composition; - * not useful here - * @return na - */ + * Component method used for the response behavior for processing the invitation object as an `IndirectInvite` object. + * @see `SquadInvitationManager.HandleRequestRole` + * @param invite the original invitation object that started this process + * @param player the target of the response and invitation + * @param invitedPlayer the unique character identifier for the player being invited; + * in actuality, represents the player who will address the invitation object; + * not useful here + * @param invitingPlayer the unique character identifier for the player who invited the former; + * not useful here + * @param name a name to be used in message composition; + * not useful here + * @return na + */ def indirectInviteResp( invite: IndirectInvite, player: Player, @@ -1057,21 +1161,20 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { invitingPlayer: Long, name: String ): Boolean = { - HandleRequestRole(player, invite) + SquadInvitationManager.handleRequestRole(manager = this, player, invite) } /** - * Component method used for the response behavior for processing the invitation object as an `IndirectInvite` object. - * - * @see `HandleRequestRole` - * @param invite the original invitation object that started this process - * @param player the target of the response and invitation - * @param invitedPlayer the unique character identifier for the player being invited - * in actuality, represents the player who will address the invitation object - * @param invitingPlayer the unique character identifier for the player who invited the former - * @param name a name to be used in message composition - * @return na - */ + * Component method used for the response behavior for processing the invitation object as an `IndirectInvite` object. + * @see `SquadInvitationManager.HandleRequestRole` + * @param invite the original invitation object that started this process + * @param player the target of the response and invitation + * @param invitedPlayer the unique character identifier for the player being invited + * in actuality, represents the player who will address the invitation object + * @param invitingPlayer the unique character identifier for the player who invited the former + * @param name a name to be used in message composition + * @return na + */ def altIndirectInviteResp( invite: IndirectInvite, player: Player, @@ -1081,196 +1184,60 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { ): Boolean = { subs.Publish( invitingPlayer, - SquadResponse.Membership( - SquadResponseType.Accept, - 0, - 0, - invitingPlayer, - Some(invitedPlayer), - player.Name, - unk5 = false, - Some(None) - ) + SquadResponse.Membership(SquadResponseType.Accept, invitingPlayer, Some(invitedPlayer), player.Name, unk5 = false) ) - HandleRequestRole(player, invite) + SquadInvitationManager.handleRequestRole(manager = this, player, invite) } /** - * A branched response for processing (new) invitation objects that have been submitted to the system.
- *
- * A comparison is performed between the original invitation object and an invitation object - * that represents the potential modification or redirection of the current active invitation obect. - * Any further action is only performed when an "is equal" comparison is `true`. - * When passing, the system publishes up to two messages - * to users that would anticipate being informed of squad join activity. - * - * @param indirectInviteFunc the method that cans the responding behavior should an `IndirectInvite` object being consumed - * @param targetInvite a comparison invitation object; - * represents the unmodified, unadjusted invite - * @param actualInvite a comparaison invitation object; - * proper use of this field should be the output of another process upon the following `actualInvite` - * @param invitedPlayer the unique character identifier for the player being invited - * in actuality, represents the player who will address the invitation object - * @param invitingPlayer the unique character identifier for the player who invited the former - * @param name a name to be used in message composition - */ - def InviteResponseTemplate(indirectInviteFunc: (IndirectInvite, Player, Long, Long, String) => Boolean)( - targetInvite: Invitation, - actualInvite: Option[Invitation], - invitedPlayer: Long, - invitingPlayer: Long, - name: String - ): Unit = { - if (actualInvite.contains(targetInvite)) { - //immediately respond - targetInvite match { - case VacancyInvite(charId, _name, _) => - subs.Publish( - invitedPlayer, - SquadResponse.Membership( - SquadResponseType.Invite, - 0, - 0, - charId, - Some(invitedPlayer), - _name, - unk5 = false, - Some(None) - ) - ) - subs.Publish( - charId, - SquadResponse.Membership( - SquadResponseType.Invite, - 0, - 0, - invitedPlayer, - Some(charId), - _name, - unk5 = true, - Some(None) - ) - ) - - case _bid@IndirectInvite(player, _) => - indirectInviteFunc(_bid, player, invitedPlayer, invitingPlayer, name) - - case _bid@SpontaneousInvite(player) => - val bidInvitingPlayer = _bid.InviterCharId - subs.Publish( - invitedPlayer, - SquadResponse.Membership( - SquadResponseType.Invite, - 0, - 0, - bidInvitingPlayer, - Some(invitedPlayer), - player.Name, - unk5 = false, - Some(None) - ) - ) - subs.Publish( - bidInvitingPlayer, - SquadResponse.Membership( - SquadResponseType.Invite, - 0, - 0, - invitedPlayer, - Some(bidInvitingPlayer), - player.Name, - unk5 = true, - Some(None) - ) - ) - - case _bid@RequestRole(player, _, _) => - HandleRequestRole(player, _bid) - - case LookingForSquadRoleInvite(member, _, _) => - subs.Publish( - invitedPlayer, - SquadResponse.Membership( - SquadResponseType.Invite, - 0, - 0, - invitedPlayer, - Some(member.CharId), - member.Name, - unk5 = false, - Some(None) - ) - ) - - case ProximityInvite(member, _, _) => - subs.Publish( - invitedPlayer, - SquadResponse.Membership( - SquadResponseType.Invite, - 0, - 0, - invitedPlayer, - Some(member.CharId), - member.Name, - unk5 = false, - Some(None) - ) - ) - - case _ => - log.warn(s"AddInviteAndRespond: can not parse discovered unhandled invitation type - $targetInvite") - } - } - } - - /** - * Assign a provided invitation object to either the active or inactive position for a player.
- *
- * The determination for the active position is whether or not something is currently in the active position - * or whether some mechanism tried to shift invitation object into the active position - * but found nothing to shift. - * If an invitation object originating from the reported player already exists, - * a new one is not appended to the inactive queue. - * This method should always be used as the entry point for the active and inactive invitation options - * or as a part of the entry point for the aforesaid options. - * - * @see `AddInviteAndRespond` - * @see `AltAddInviteAndRespond` - * @param invitedPlayer the unique character identifier for the player being invited; - * in actuality, represents the player who will address the invitation object - * @param invite the "new" invitation envelop object - * @return an optional invite; - * the invitation object in the active invite position; - * `None`, if it is not added to either the active option or inactive position - */ - def AddInvite(invitedPlayer: Long, invite: Invitation): Option[Invitation] = { + * Assign a provided invitation object to either the active or inactive position for a player.
+ *
+ * The determination for the active position is whether or not something is currently in the active position + * or whether some mechanism tried to shift invitation object into the active position + * but found nothing to shift. + * If an invitation object originating from the reported player already exists, + * a new one is not appended to the inactive queue. + * This method should always be used as the entry point for the active and inactive invitation options + * or as a part of the entry point for the aforesaid options. + * + * @see `AddInviteAndRespond` + * @see `AltAddInviteAndRespond` + * @param invitedPlayer the unique character identifier for the player being invited; + * in actuality, represents the player who will address the invitation object + * @param invite the "new" invitation envelop object + * @return an optional invite; + * the invitation object in the active invite position; + * `None`, if it is not added to either the active option or inactive position + */ + def addInvite(invitedPlayer: Long, invite: Invitation): Option[Invitation] = { invites.get(invitedPlayer).orElse(previousInvites.get(invitedPlayer)) match { case Some(_bid) => //the new invite may not interact with the active invite; add to queued invites queuedInvites.get(invitedPlayer) match { case Some(bidList) => //ensure that new invite does not interact with the queue's invites by invitingPlayer info - val inviteInviterCharId = invite.InviterCharId + val inviteInviterCharId = invite.inviterCharId if ( - _bid.InviterCharId != inviteInviterCharId && !bidList.exists { eachBid => - eachBid.InviterCharId == inviteInviterCharId + _bid.inviterCharId != inviteInviterCharId && !bidList.exists { eachBid => + eachBid.inviterCharId == inviteInviterCharId } ) { - queuedInvites(invitedPlayer) = invite match { - case _: RequestRole => - //RequestRole is to be expedited - val (normals, others) = bidList.partition(_.isInstanceOf[RequestRole]) + val restoredInvites = invite match { + case _: RequestToJoinSquadRole => + //RequestToJoinSquadRole is to be expedited + val (normals, others) = bidList.partition(_.isInstanceOf[RequestToJoinSquadRole]) (normals :+ invite) ++ others case _ => bidList :+ invite } + queuedInvites.put(invitedPlayer, restoredInvites) Some(_bid) } else { None } case None => - if (_bid.InviterCharId != invite.InviterCharId) { - queuedInvites(invitedPlayer) = List(invite) + if (_bid.inviterCharId != invite.inviterCharId) { + queuedInvites.put(invitedPlayer, List(invite)) Some(_bid) } else { None @@ -1278,137 +1245,35 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { } case None => - invites(invitedPlayer) = invite + invites.put(invitedPlayer, invite) Some(invite) } } /** - * Select the next invitation object to be shifted into the active position.
- *
- * The determination for the active position is whether or not something is currently in the active position - * or whether some mechanism tried to shift invitation object into the active position - * but found nothing to shift. - * After handling of the previous invitation object has completed or finished, - * the temporary block on adding new invitations is removed - * and any queued inactive invitation on the head of the inactive queue is shifted into the active position. - * @see `NextInviteAndRespond` - * @param invitedPlayer the unique character identifier for the player being invited; - * in actuality, represents the player who will address the invitation object - * @return an optional invite; - * the invitation object in the active invite position; - * `None`, if not shifted into the active position - */ - def NextInvite(invitedPlayer: Long): Option[Invitation] = { - previousInvites.remove(invitedPlayer) - invites.get(invitedPlayer) match { - case None => - queuedInvites.get(invitedPlayer) match { - case Some(list) => - list match { - case Nil => - None - case x :: Nil => - invites(invitedPlayer) = x - queuedInvites.remove(invitedPlayer) - Some(x) - case x :: xs => - invites(invitedPlayer) = x - queuedInvites(invitedPlayer) = xs - Some(x) - } - - case None => - None - } - case Some(_) => - None - } - } - - /** - * Select the next invitation object to be shifted into the active position - * and dispatch a response for any invitation object that is discovered. - * @see `NextInvite` - * @see `RespondToInvite` - * @param invitedPlayer the unique character identifier for the player being invited; - * in actuality, represents the player who will address the invitation object - */ - def NextInviteAndRespond(invitedPlayer: Long): Unit = { - NextInvite(invitedPlayer) match { - case Some(invite) => - RespondToInvite(invitedPlayer, invite) - case None => ; - } - } - - /** - * Compose the response to an invitation. - * Use standard handling methods for `IndirectInvite` invitation envelops. - * @see `InviteResponseTemplate` - * @param invitedPlayer the unique character identifier for the player being invited; - * in actuality, represents the player who will address the invitation object - * @param invite the invitation envelope used to recover information about the action being taken - */ - def RespondToInvite(invitedPlayer: Long, invite: Invitation): Unit = { - InviteResponseTemplate(indirectInviteResp)( - invite, - Some(invite), - invitedPlayer, - invite.InviterCharId, - invite.InviterName - ) - } - - /** - * Remove any invitation object from the active position. - * Flag the temporary field to indicate that the active position, while technically available, - * should not yet have a new invitation object shifted into it yet. - * This is the "proper" way to demote invitation objects from the active position - * whether or not they are to be handled - * except in cases of manipulative cleanup. - * @see `NextInvite` - * @see `NextInviteAndRespond` - * @param invitedPlayer the unique character identifier for the player being invited; - * in actuality, represents the player who will address the invitation object - * @return an optional invite; - * the invitation object formerly in the active invite position; - * `None`, if no invitation was in the active position - */ - def RemoveInvite(invitedPlayer: Long): Option[Invitation] = { - invites.remove(invitedPlayer) match { - case out @ Some(invite) => - previousInvites += invitedPlayer -> invite - out - case None => - None - } - } - - /** - * Resolve an invitation to a general, not guaranteed, position in someone else's squad.
- *
- * Originally, the instigating type of invitation object was a "`VacancyInvite`" - * which indicated a type of undirected invitation extended from the squad leader to another player - * but the resolution is generalized enough to suffice for a number of invitation objects. - * First, an actual position is determined; - * then, the squad is tested for recruitment conditions, - * including whether the person who solicited the would-be member is still the squad leader. - * If the recruitment is manual and the squad leader is not the same as the recruiting player, - * then the real squad leader is sent an indirect query regarding the player's eligibility. - * These `IndirectInvite` invitation objects also are handled by calls to `HandleVacancyInvite`. - * @see `AltAddInviteAndRespond` - * @see `IndirectInvite` - * @see `SquadFeatures::AutoApproveInvitationRequests` - * @see `VacancyInvite` - * @param features the squad - * @param invitedPlayer the unique character identifier for the player being invited - * @param invitingPlayer the unique character identifier for the player who invited the former - * @param recruit the player being invited - * @return the squad object and a role position index, if properly invited; - * `None`, otherwise - */ - def HandleVacancyInvite( + * Resolve an invitation to a general, not guaranteed, position in someone else's squad.
+ *
+ * Originally, the instigating type of invitation object was a "`InvitationToJoinSquad`" + * which indicated a type of undirected invitation extended from the squad leader to another player + * but the resolution is generalized enough to suffice for a number of invitation objects. + * First, an actual position is determined; + * then, the squad is tested for recruitment conditions, + * including whether the person who solicited the would-be member is still the squad leader. + * If the recruitment is manual and the squad leader is not the same as the recruiting player, + * then the real squad leader is sent an indirect query regarding the player's eligibility. + * These `IndirectInvite` invitation objects also are handled by calls to `HandleVacancyInvite`. + * @see `AltAddInviteAndRespond` + * @see `IndirectInvite` + * @see `SquadFeatures::AutoApproveInvitationRequests` + * @see `InvitationToJoinSquad` + * @param features the squad + * @param invitedPlayer the unique character identifier for the player being invited + * @param invitingPlayer the unique character identifier for the player who invited the former + * @param recruit the player being invited + * @return the squad object and a role position index, if properly invited; + * `None`, otherwise + */ + def handleVacancyInvite( features: SquadFeatures, invitedPlayer: Long, invitingPlayer: Long, @@ -1416,15 +1281,15 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { ): Option[(SquadFeatures, Int)] = { //accepted an invitation to join an existing squad val squad = features.Squad - squad.Membership.zipWithIndex.find({ - case (_, index) => - squad.isAvailable(index, recruit.avatar.certifications) - }) match { + squad + .Membership + .zipWithIndex + .find { case (_, index) => squad.isAvailable(index, recruit.avatar.certifications) } match { case Some((_, line)) => //position in squad found if (!features.AutoApproveInvitationRequests && squad.Leader.CharId != invitingPlayer) { //the inviting player was not the squad leader and this decision should be bounced off the squad leader - AltAddInviteAndRespond( + altAddInviteAndRespond( squad.Leader.CharId, IndirectInvite(recruit, features), invitingPlayer, @@ -1441,571 +1306,108 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { } /** - * An overloaded entry point to the functionality for handling one player requesting a specific squad role. - * - * @param player the player who wants to join the squad - * @param bid a specific kind of `Invitation` object - * @return `true`, if the player is not denied the possibility of joining the squad; - * `false`, otherwise, of it the squad does not exist - */ - def HandleRequestRole(player: Player, bid: RequestRole): Boolean = { - HandleRequestRole(player, bid.features, bid) - } - - /** - * An overloaded entry point to the functionality for handling indirection when messaging the squad leader about an invite. - * - * @param player the player who wants to join the squad - * @param bid a specific kind of `Invitation` object - * @return `true`, if the player is not denied the possibility of joining the squad; - * `false`, otherwise, of it the squad does not exist - */ - def HandleRequestRole(player: Player, bid: IndirectInvite): Boolean = { - HandleRequestRole(player, bid.features, bid) - } - - /** - * The functionality for handling indirection - * for handling one player requesting a specific squad role - * or when messaging the squad leader about an invite.
- *
- * At this point in the squad join process, the only consent required is that of the squad leader. - * An automatic consent flag exists on the squad; - * but, if that is not set, then the squad leader must be asked whether or not to accept or to reject the recruit. - * If the squad leader changes in the middle or the latter half of the process, - * the invitation may still fail even if the old squad leader accepts. - * If the squad leader changes in the middle of the latter half of the process, - * the inquiry might be posed again of the new squad leader, of whether to accept or to reject the recruit. - * - * @param player the player who wants to join the squad - * @param features the squad - * @param bid the `Invitation` object that was the target of this request - * @return `true`, if the player is not denied the possibility of joining the squad; - * `false`, otherwise, of it the squad does not exist - */ - def HandleRequestRole(player: Player, features: SquadFeatures, bid: Invitation): Boolean = { - val leaderCharId = features.Squad.Leader.CharId - subs.Publish(leaderCharId, SquadResponse.WantsSquadPosition(leaderCharId, player.Name)) - true - } - - /** - * Determine whether a player is sufficiently unemployed - * and has no grand delusions of being a squad leader. - * @see `CloseSquad` - * @param features an optional squad - * @return `true`, if the target player possesses no squad or the squad is nonexistent; - * `false`, otherwise - */ - def EnsureEmptySquad(features: Option[SquadFeatures]): Boolean = { - features match { - case Some(squad) => EnsureEmptySquad(squad) - case None => true - } - } - - /** - * Determine whether a player is sufficiently unemployed - * and has no grand delusions of being a squad leader. - * @see `CloseSquad` - * @param features the squad - * @return `true`, if the target player possesses no squad or a squad that is suitably nonexistent; - * `false`, otherwise - */ - def EnsureEmptySquad(features: SquadFeatures): Boolean = { - val ensuredEmpty = features.Squad.Size <= 1 - if (ensuredEmpty) { - CleanUpAllInvitesToSquad(features) - } - ensuredEmpty - } - - /** - * Behaviors and exchanges necessary to complete the fulfilled recruitment process for the squad role.
- *
- * This operation is fairly safe to call whenever a player is to be inducted into a squad. - * The aforementioned player must have a callback retained in `subs.UserEvents` - * and conditions imposed by both the role and the player must be satisfied. - * @see `CleanUpAllInvitesWithPlayer` - * @see `Squad.isAvailable` - * @see `Squad.Switchboard` - * @see `SquadSubscriptionEntity.MonitorSquadDetails` - * @see `SquadSubscriptionEntity.Publish` - * @see `SquadSubscriptionEntity.Join` - * @see `SquadSubscriptionEntity.UserEvents` - * @param player the new squad member; - * this player is NOT the squad leader - * @param features the squad the player is joining - * @param position the squad member role that the player will be filling - * @return `true`, if the player joined the squad in some capacity; - * `false`, if the player did not join the squad or is already a squad member - */ - def JoinSquad(player: Player, features: SquadFeatures, position: Int): Boolean = { - CleanUpAllInvitesWithPlayer(player.CharId) - parent ! SquadService.PerformJoinSquad(player, features, position) - true - } - - /** - * This player has been refused to join squads by these players, or to form squads with these players. - * @param charId the player who refused other players - * @return the list of other players who have refused this player - */ - def Refused(charId: Long): List[Long] = refused.getOrElse(charId, Nil) - - /** - * This player has been refused to join squads by this squad leaders, or to form squads with this other player. - * @param charId the player who is being refused - * @param refusedCharId the player who refused - * @return the list of other players who have refused this player - */ - def Refused(charId: Long, refusedCharId: Long): List[Long] = { - if (charId != refusedCharId) { - Refused(charId, List(refusedCharId)) - } else { - Nil - } - } - - /** - * This player has been refused to join squads by these squad leaders, or to form squads with these other players. - * @param charId the player who is being refused - * @param list the players who refused - * @return the list of other players who have refused this player - */ - def Refused(charId: Long, list: List[Long]): List[Long] = { - refused.get(charId) match { - case Some(refusedList) => - refused(charId) = list ++ refusedList - Refused(charId) + * Select the next invitation object to be shifted into the active position.
+ *
+ * The determination for the active position is whether or not something is currently in the active position + * or whether some mechanism tried to shift invitation object into the active position + * but found nothing to shift. + * After handling of the previous invitation object has completed or finished, + * the temporary block on adding new invitations is removed + * and any queued inactive invitation on the head of the inactive queue is shifted into the active position. + * @see `NextInviteAndRespond` + * @param invitedPlayer the unique character identifier for the player being invited; + * in actuality, represents the player who will address the invitation object + * @return an optional invite; + * the invitation object in the active invite position; + * `None`, if not shifted into the active position + */ + def nextInvite(invitedPlayer: Long): Option[Invitation] = { + previousInvites.remove(invitedPlayer) + invites.get(invitedPlayer) match { case None => - Nil - } - } - - /** - * This player has been refused to join squads by this squad leaders, or to form squads with this other player. - * They are now allowed. - * @param charId the player who is being refused - * @param permittedCharId the player who was previously refused - * @return the list of other players who have refused this player - */ - def Allowed(charId: Long, permittedCharId: Long): List[Long] = { - if (charId != permittedCharId) { - Allowed(charId, List(permittedCharId)) - } else { - Nil - } - } - - /** - * This player has been refused to join squads by these squad leaders, or to form squads with these other players. - * They are now allowed. - * @param charId the player who is being refused - * @param list the players who was previously refused - * @return the list of other players who have refused this player - */ - def Allowed(charId: Long, list: List[Long]): List[Long] = { - refused.get(charId) match { - case Some(refusedList) => - refused(charId) = refusedList.filterNot(list.contains) - Refused(charId) - case None => - Nil - } - } - - /** - * Remove all inactive invites associated with this player. - * @param charId the unique character identifier for the player being invited; - * in actuality, represents the player who will address the invitation object - * @return a list of the removed inactive invitation objects - */ - def CleanUpQueuedInvites(charId: Long): Unit = { - val allSquadGuids = queuedInvites.remove(charId) match { - case Some(bidList) => - bidList.collect { - case VacancyInvite(_, _, guid) => guid - case IndirectInvite(_, guid) => guid - case LookingForSquadRoleInvite(_, guid, _) => guid - case ProximityInvite(_, guid, _) => guid - case RequestRole(_, guid, _) => guid - } - case None => - Nil - } - val list = List(charId) - allSquadGuids.foreach { CleanUpSquadFeatures(list, _, position = -1) } - } - - def CleanUpSquadFeatures(removed: List[Long], features: SquadFeatures, position: Int): Unit = { - features.ProxyInvites = features.ProxyInvites.filterNot(removed.contains) - if (features.ProxyInvites.isEmpty) { - features.SearchForRole = None - } - } - - /** - * Remove all invitation objects - * that are related to the particular squad and the particular role in the squad. - * Specifically used to safely disarm obsolete invitation objects related to the specific criteria. - * Affects only certain invitation object types - * including "player requesting role" and "leader requesting recruiting role". - * @see `RemoveActiveInvitesForSquadAndPosition` - * @see `RemoveQueuedInvitesForSquadAndPosition` - * @param features the squad - * @param position the role position index - */ - def CleanUpInvitesForSquadAndPosition(features: SquadFeatures, position: Int): Unit = { - val guid = features.Squad.GUID - CleanUpSquadFeatures( - RemoveActiveInvitesForSquadAndPosition(guid, position) ++ RemoveQueuedInvitesForSquadAndPosition(guid, position), - features, - position - ) - } - - /** - * Remove all inactive invitation objects that are related to the particular squad and the particular role in the squad. - * Specifically used to safely disarm obsolete invitation objects by specific criteria. - * Affects only certain invitation object types. - * @see `RequestRole` - * @see `LookingForSquadRoleInvite` - * @see `CleanUpInvitesForSquadAndPosition` - * @param features the squa - * @param position the role position index - */ - def CleanUpQueuedInvitesForSquadAndPosition(features: SquadFeatures, position: Int): Unit = { - CleanUpSquadFeatures( - RemoveQueuedInvitesForSquadAndPosition(features.Squad.GUID, position), - features, - position - ) - } - - /** - * Remove all invitation objects that are related to the particular squad. - * Specifically used to safely disarm obsolete invitation objects by specific criteria. - * Affects all invitation object types and all data structures that deal with the squad. - * @see `RequestRole` - * @see `IndirectInvite` - * @see `LookingForSquadRoleInvite` - * @see `ProximityInvite` - * @see `RemoveInvite` - * @see `VacancyInvite` - * @param features the squad identifier - */ - def CleanUpAllInvitesToSquad(features: SquadFeatures): Unit = { - 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 - } - .map { index => - val key = keys(index) - RemoveInvite(key) - key - } - .toList - } - //tidy the queued invitations - val queuedInviteIds = { - val keys = queuedInvites.keys.toSeq - queuedInvites.values.zipWithIndex - .collect { - 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 + queuedInvites.get(invitedPlayer) match { + case Some(list) => + list match { + case Nil => + None + case x :: Nil => + invites.put(invitedPlayer, x) + queuedInvites.remove(invitedPlayer) + Some(x) + case x :: xs => + invites.put(invitedPlayer, x) + queuedInvites(invitedPlayer) = xs + Some(x) } - if (retained.isEmpty) { - queuedInvites.remove(key) - } else { - queuedInvites += key -> retained - } - if (targets.nonEmpty) { - Some(key) - } else { - None - } - } - .flatten - .toList - } - CleanUpSquadFeatures(activeInviteIds ++ queuedInviteIds, features, position = -1) - } - /** - * Remove all active and inactive invitation objects that are related to the particular player. - * Specifically used to safely disarm obsolete invitation objects by specific criteria. - * Affects all invitation object types and all data structures that deal with the player. - * @see `RequestRole` - * @see `IndirectInvite` - * @see `LookingForSquadRoleInvite` - * @see `RemoveInvite` - * @see `CleanUpAllProximityInvites` - * @see `VacancyInvite` - * @param charId the player's unique identifier number - */ - def CleanUpAllInvitesWithPlayer(charId: Long): Unit = { - //clean up our active invitation - val charIdInviteSquadGuid = RemoveInvite(charId) match { - case Some(VacancyInvite(_, _, guid)) => Some(guid) - case Some(IndirectInvite(_, guid)) => Some(guid) - case Some(LookingForSquadRoleInvite(_, guid, _)) => Some(guid) - case Some(ProximityInvite(_, guid, _)) => Some(guid) - case Some(RequestRole(_, guid, _)) => Some(guid) - case _ => None - } - //clean up invites - val (activeInviteIds, activeSquadGuids) = { - val keys = invites.keys.toSeq - invites.values.zipWithIndex - .collect { - case (SpontaneousInvite(player), index) if player.CharId == charId => (index, None) - case (VacancyInvite(player, _, guid), index) if player == charId => (index, Some(guid)) - case (IndirectInvite(player, guid), index) if player.CharId == charId => (index, Some(guid)) - case (LookingForSquadRoleInvite(member, guid, _), index) if member.CharId == charId => (index, Some(guid)) - case (ProximityInvite(member, guid, _), index) if member.CharId == charId => (index, Some(guid)) - case (RequestRole(player, guid, _), index) if player.CharId == charId => (index, Some(guid)) - } - .map { case (index, guid) => - val key = keys(index) - RemoveInvite(key) - (key, guid) - } - .unzip - } - //tidy the queued invitations - val (queuedInviteIds, queuedSquadGuids) = { - val keys = queuedInvites.keys.toSeq - queuedInvites.values.zipWithIndex - .collect { - case (queue, index) => - val key = keys(index) - val (targets, retained) = if(key != charId) { - queue.partition { - case SpontaneousInvite(player) => player.CharId == charId - case VacancyInvite(player, _, _) => player == charId - case IndirectInvite(player, _) => player.CharId == charId - case LookingForSquadRoleInvite(member, _, _) => member.CharId == charId - case ProximityInvite(member, _, _) => member.CharId == charId - case RequestRole(player, _, _) => player.CharId == charId - case _ => false - } - } else { - (queue, Nil) - } - if (retained.isEmpty) { - queuedInvites.remove(key) - } else { - queuedInvites += key -> retained - } - if (targets.nonEmpty) { - Some(( - key, - targets.collect { - case VacancyInvite(_, _, guid) => guid - case IndirectInvite(_, guid) => guid - case LookingForSquadRoleInvite(_, guid, _) => guid - case ProximityInvite(_, guid, _) => guid - case RequestRole(_, guid, _) => guid - } - )) - } else { - None - } - } - .flatten - .toList - .unzip - } - val allInvites = (activeInviteIds ++ queuedInviteIds).toList.distinct - ((activeSquadGuids.toSeq :+ charIdInviteSquadGuid) ++ queuedSquadGuids) - .flatten - .distinct - .foreach { guid => CleanUpSquadFeatures(allInvites, guid, position = -1) } - } - - /** - * Remove all active and inactive proximity squad invites. - * This is related to recruitment from the perspective of the recruiter. - * @param charId the player - */ - def CleanUpAllProximityInvites(charId: Long): Unit = { - //clean up invites - val (activeInviteIds, activeSquadGuids) = { - val keys = invites.keys.toSeq - invites.values.zipWithIndex - .collect { case (ProximityInvite(member, guid, _), index) if member.CharId == charId => (index, Some(guid)) } - .map { case (index, guid) => - val key = keys(index) - RemoveInvite(key) - (key, guid) - } - .unzip - } - //tidy the queued invitations - val (queuedInviteIds, queuedSquadGuids) = { - val keys = queuedInvites.keys.toSeq - queuedInvites.values.zipWithIndex - .collect { - case (queue, index) => - val key = keys(index) - val (targets, retained) = queue.partition { - case ProximityInvite(member, _, _) => member.CharId == charId - case _ => false - } - if (retained.isEmpty) { - queuedInvites.remove(key) - } else { - queuedInvites += key -> retained - } - if (targets.nonEmpty) { - Some((key, targets.collect { case ProximityInvite(_, guid, _) => guid } )) - } else { - None - } - } - .flatten - .toList - .unzip - } - val allInvites = (activeInviteIds ++ queuedInviteIds).toList.distinct - (activeSquadGuids.toSeq ++ queuedSquadGuids) - .flatten - .distinct - .foreach { guid => CleanUpSquadFeatures(allInvites, guid, position = -1) } - } - - /** - * Remove all active and inactive proximity squad invites for a specific squad. - * @param features the squad - */ - def CleanUpProximityInvites(features: SquadFeatures): Unit = { - val squadGuid = features.Squad.GUID - //clean up invites - val activeInviteIds = { - val keys = invites.keys.toSeq - invites.values.zipWithIndex - .collect { - case (ProximityInvite(_, _squad, _), index) if _squad.Squad.GUID == squadGuid => index - } - .map { index => - val key = keys(index) - RemoveInvite(key) - key - } - } - //tidy the queued invitations - val queuedInviteIds = { - val keys = queuedInvites.keys.toSeq - queuedInvites.values.zipWithIndex - .collect { - case (queue, index) => - val key = keys(index) - val (targets, retained) = queue.partition { - case ProximityInvite(_, _squad, _) => _squad.Squad.GUID == squadGuid - case _ => false - } - if (retained.isEmpty) { - queuedInvites.remove(key) - } else { - queuedInvites += key -> retained - } - if (targets.nonEmpty) { - keys.lift(index) - } else { - None - } - } - .flatten - .toList - } - CleanUpSquadFeatures((activeInviteIds ++ queuedInviteIds).toList.distinct, features, position = -1) - } - - /** - * Remove all active invitation objects - * that are related to the particular squad and the particular role in the squad. - * Specifically used to safely disarm obsolete invitation objects related to the specific criteria. - * Affects only certain invitation object types - * including "player requesting role" and "leader requesting recruiting role". - * @see `RequestRole` - * @see `LookingForSquadRoleInvite` - * @see `ProximityInvite` - * @see `RemoveInvite` - * @param guid the squad identifier - * @param position the role position index - * @return the character ids of all players whose invites were removed - */ - def RemoveActiveInvitesForSquadAndPosition(guid: PlanetSideGUID, position: Int): List[Long] = { - val keys = invites.keys.toSeq - invites.values.zipWithIndex - .collect { - case (LookingForSquadRoleInvite(_, _squad, pos), index) if _squad.Squad.GUID == guid && pos == position => index - case (ProximityInvite(_, _squad, pos), index) if _squad.Squad.GUID == guid && pos == position => index - case (RequestRole(_, _squad, pos), index) if _squad.Squad.GUID == guid && pos == position => index - } - .map { index => - val key = keys(index) - RemoveInvite(key) - key - } - .toList - } - - /** - * Remove all inactive invitation objects that are related to the particular squad and the particular role in the squad. - * Specifically used to safely disarm obsolete invitation objects by specific criteria. - * Affects only certain invitation object types. - * @see `RequestRole` - * @see `LookingForSquadRoleInvite` - * @see `CleanUpInvitesForSquadAndPosition` - * @param guid the squad identifier - * @param position the role position index - * @return the character ids of all players whose invites were removed - */ - def RemoveQueuedInvitesForSquadAndPosition(guid: PlanetSideGUID, position: Int): List[Long] = { - val keys = queuedInvites.keys.toSeq - queuedInvites.values.zipWithIndex - .collect { - case (queue, index) => - val key = keys(index) - val (targets, retained) = queue.partition { - case LookingForSquadRoleInvite(_, _squad, pos) => _squad.Squad.GUID == guid && pos == position - case ProximityInvite(_, _squad, pos) => _squad.Squad.GUID == guid && pos == position - case RequestRole(_, _squad, pos) => _squad.Squad.GUID == guid && pos == position - case _ => false - } - if (retained.isEmpty) { - queuedInvites.remove(key) - } else { - queuedInvites += key -> retained - } - if (targets.nonEmpty) { - Some(key) - } else { + case None => None - } - } - .flatten - .toList + } + case Some(_) => + None + } } - def FindSoldiersWithinScopeAndInvite( + /** + * Select the next invitation object to be shifted into the active position + * and dispatch a response for any invitation object that is discovered. + * @see `NextInvite` + * @see `RespondToInvite` + * @param invitedPlayer the unique character identifier for the player being invited; + * in actuality, represents the player who will address the invitation object + */ + def nextInviteAndRespond(invitedPlayer: Long): Unit = { + nextInvite(invitedPlayer) + .collect { invite => + respondToInvite(invitedPlayer, invite) + invite + } + } + + /** + * Compose the response to an invitation. + * Use standard handling methods for `IndirectInvite` invitation envelops. + * @param invitedPlayer the unique character identifier for the player being invited; + * in actuality, represents the player who will address the invitation object + * @param invite the invitation envelope used to recover information about the action being taken + */ + def respondToInvite(invitedPlayer: Long, invite: Invitation): Unit = { + invite.handleInvitation(indirectInviteResp)( + manager = this, + invitedPlayer, + invite.inviterCharId, + invite.inviterName + ) + } + + /** + * Remove any invitation object from the active position. + * Flag the temporary field to indicate that the active position, while technically available, + * should not yet have a new invitation object shifted into it yet. + * This is the "proper" way to demote invitation objects from the active position + * whether or not they are to be handled + * except in cases of manipulative cleanup. + * @see `NextInvite` + * @see `NextInviteAndRespond` + * @param invitedPlayer the unique character identifier for the player being invited; + * in actuality, represents the player who will address the invitation object + * @return an optional invite; + * the invitation object formerly in the active invite position; + * `None`, if no invitation was in the active position + */ + def removeInvite(invitedPlayer: Long): Option[Invitation] = { + invites.remove(invitedPlayer) match { + case out @ Some(invite) => + previousInvites.put(invitedPlayer, invite) + out + case None => + None + } + } + + /* search */ + + def findSoldiersWithinScopeAndInvite( invitingPlayer: Member, features: SquadFeatures, position: Int, @@ -2018,137 +1420,451 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { val squad = features.Squad val faction = squad.Faction val squadLeader = squad.Leader.CharId - val deniedAndExcluded = features.DeniedPlayers() ++ excluded + val deniedAndExcluded = (features.DeniedPlayers() ++ excluded) :+ squadLeader val requirementsToMeet = squad.Membership(position).Requirements //find a player who is of the same faction as the squad, is LFS, and is eligible for the squad position scope .find { avatar => val charId = avatar.id - faction == avatar.faction && - avatar.lookingForSquad && + avatar.lookingForSquad && + avatar.faction == faction && + charId != squadLeader && + charId != invitingPlayerCharId && !deniedAndExcluded.contains(charId) && - !refused(charId).contains(squadLeader) && - requirementsToMeet.intersect(avatar.certifications) == requirementsToMeet && - charId != invitingPlayerCharId //don't send invite to yourself. can cause issues if rejected - } match { - case None => - None - case Some(invitedPlayer) => + !refusedPlayers(charId).contains(squadLeader) && + requirementsToMeet.intersect(avatar.certifications) == requirementsToMeet + } + .collect { invitedPlayer => //add invitation for position in squad - val invite = invitationEnvelopFunc(invitingPlayer, features, position) val id = invitedPlayer.id - AddInviteAndRespond(id, invite, invitingPlayerCharId, invitingPlayerName) - Some(id) + addInviteAndRespond( + id, + invitationEnvelopFunc(invitingPlayer, features, position), + invitingPlayerCharId, + invitingPlayerName + ) + id } } + + /* invite clean-up */ + + private def removeActiveInvites(invitationFilteringRule: (Long, Invitation) => Boolean): List[(Long, List[Invitation])] = { + invites + .collect { + case out @ (id, invite) if invitationFilteringRule(id, invite) => out + } + .map { case (id, invite) => + removeInvite(id) + nextInviteAndRespond(id) + (id, List(invite)) + } + .toList + } + + /** + * Remove all active invitation objects that are related to the particular player. + * Specifically used to safely disarm obsolete invitation objects by specific criteria. + * Affects the active invitation list and any squads related to that player. + * @param charId the player's unique identifier number + * @return a list of the removed inactive invitation objects + */ + def cleanUpActiveInvitesForPlayer(charId: Long): List[(Long, List[Invitation])] = { + SquadInvitationManager.cleanUpInvites(removeActiveInvitesForPlayer(charId), List(charId)) + } + + /** + * Remove all active invitation objects that are related to the particular player. + * Specifically used to safely disarm obsolete invitation objects by specific criteria. + * Affects the active invitation list. + * @param charId the player's unique identifier number + * @return a list of the removed inactive invitation objects + */ + private def removeActiveInvitesForPlayer(charId: Long): List[(Long, List[Invitation])] = { + removeActiveInvites({ (id: Long, invite: Invitation) => id == charId || invite.appliesToPlayer(charId) }) + } + + /** + * Remove all inactive invitation objects that are related to the particular player. + * Specifically used to safely disarm obsolete invitation objects by specific criteria. + * Affects the queued invitation list and any squads related to that player. + * @param charId the player's unique identifier number + * @return a list of the removed inactive invitation objects + */ + def cleanUpQueuedInvitesForPlayer(charId: Long): List[(Long, List[Invitation])] = { + SquadInvitationManager.cleanUpInvites(removeQueuedInvitesForPlayer(charId), List(charId)) + } + + /** + * Remove all inactive invitation objects that are related to the particular player. + * Specifically used to safely disarm obsolete invitation objects by specific criteria. + * Affects the inactive invitation list. + * @param charId the player's unique identifier number + * @return a list of the removed inactive invitation objects + */ + private def removeQueuedInvitesForPlayer(charId: Long): List[(Long, List[Invitation])] = { + SquadInvitationManager.removeQueuedInvites( + queuedInvites, + { invite: Invitation => invite.appliesToPlayer(charId) } + ) + } + + /** + * Remove all invitation objects that are related to the particular player. + * Specifically used to safely disarm obsolete invitation objects by specific criteria. + * Affects the active invitation list and the inactive invitation list and any squads related to that player. + * @param charId the player's unique identifier number + * @return a list of the removed inactive invitation objects + */ + def cleanUpAllInvitesForPlayer(charId: Long): List[(Long, List[Invitation])] = { + SquadInvitationManager.cleanUpAllInvites( + removeQueuedInvitesForPlayer(charId), + removeActiveInvitesForPlayer(charId) + ) + } + + /** + * Remove all active invitation objects that are related to the particular squad. + * Specifically used to safely disarm obsolete invitation objects by specific criteria. + * @param guid squad identifier + */ + def cleanUpActiveInvitesForSquad(guid: PlanetSideGUID): List[(Long, List[Invitation])] = { + SquadInvitationManager.cleanUpInvites(removeActiveInvitesForSquad(guid), Nil) + } + + private def removeActiveInvitesForSquad(guid: PlanetSideGUID): List[(Long, List[Invitation])] = { + removeActiveInvites({ (_: Long, invite: Invitation) => invite.appliesToSquad(guid) }) + } + + /** + * Remove all queued invitation objects that are related to the particular squad. + * Specifically used to safely disarm obsolete invitation objects by specific criteria. + * @param guid squad identifier + */ + def cleanUpQueuedInvitesForSquad(guid: PlanetSideGUID): List[(Long, List[Invitation])] = { + SquadInvitationManager.cleanUpInvites(removeQueuedInvitesForSquad(guid), Nil) + } + + private def removeQueuedInvitesForSquad(guid: PlanetSideGUID): List[(Long, List[Invitation])] = { + SquadInvitationManager.removeQueuedInvites( + queuedInvites, + { invite: Invitation => invite.appliesToSquad(guid) } + ) + } + + /** + * Remove all invitation objects that are related to the particular squad. + * Specifically used to safely disarm obsolete invitation objects by specific criteria. + * Affects all invitation object types and all data structures that deal with the squad. + * @param guid squad identifier + */ + def cleanUpAllInvitesForSquad(guid: PlanetSideGUID): List[(Long, List[Invitation])] = { + SquadInvitationManager.cleanUpAllInvites( + removeQueuedInvitesForSquad(guid), + removeActiveInvitesForSquad(guid) + ) + } + + /** + * Remove all active invitation objects + * that are related to the particular squad and the particular role in the squad. + * Specifically used to safely disarm obsolete invitation objects related to the specific criteria. + * Affects only certain invitation object types + * including "player requesting role" and "leader requesting recruiting role". + * @param guid the squad identifier + * @param position the role position index + * @return the character ids of all players whose invites were removed + */ + def cleanUpActiveInvitesForSquadAndPosition(guid: PlanetSideGUID, position: Int): List[(Long, List[Invitation])] = { + SquadInvitationManager.cleanUpInvites(removeActiveInvitesForSquadAndPosition(guid, position), Nil) + } + + private def removeActiveInvitesForSquadAndPosition(guid: PlanetSideGUID, position: Int): List[(Long, List[Invitation])] = { + removeActiveInvites({ (_: Long, invite: Invitation) => invite.appliesToSquadAndPosition(guid, position) }) + } + + /** + * Remove all inactive invitation objects that are related to the particular squad and the particular role in the squad. + * Specifically used to safely disarm obsolete invitation objects by specific criteria. + * Affects only certain invitation object types. + * @param guid the squad identifier + * @param position the role position index + * @return the character ids of all players whose invites were removed + */ + def cleanUpQueuedInvitesForSquadAndPosition(guid: PlanetSideGUID, position: Int): List[(Long, List[Invitation])] = { + SquadInvitationManager.cleanUpInvites(removeQueuedInvitesForSquadAndPosition(guid, position), Nil) + } + + private def removeQueuedInvitesForSquadAndPosition(guid: PlanetSideGUID, position: Int): List[(Long, List[Invitation])] = { + SquadInvitationManager.removeQueuedInvites( + queuedInvites, + { invite: Invitation => invite.appliesToSquadAndPosition(guid, position) } + ) + } + + /** + * Remove all invitation objects + * that are related to the particular squad and the particular role in the squad. + * Specifically used to safely disarm obsolete invitation objects related to the specific criteria. + * Affects only certain invitation object types + * including "player requesting role" and "leader requesting recruiting role". + * @see `RemoveActiveInvitesForSquadAndPosition` + * @see `RemoveQueuedInvitesForSquadAndPosition` + * @param guid squad identifier + * @param position the role position index + */ + def cleanUpAllInvitesForSquadAndPosition(guid: PlanetSideGUID, position: Int): List[(Long, List[Invitation])] = { + SquadInvitationManager.cleanUpAllInvites( + removeQueuedInvitesForSquadAndPosition(guid, position), + removeActiveInvitesForSquadAndPosition(guid, position) + ) + } + + def cleanUpActiveProximityInvitesForPlayer(charId: Long): List[(Long, List[Invitation])] = { + SquadInvitationManager.cleanUpInvites(removeActiveProximityInvitesForPlayer(charId), List(charId)) + } + + private def removeActiveProximityInvitesForPlayer(charId: Long): List[(Long, List[Invitation])] = { + removeActiveInvites({ + (_: Long, invite: Invitation) => invite match { + case invite: ProximityInvite => invite.appliesToPlayer(charId) + case _ => false + } + }) + } + + def cleanUpQueuedProximityInvitesForPlayer(charId: Long): List[(Long, List[Invitation])] = { + SquadInvitationManager.cleanUpInvites(removeQueuedProximityInvitesForPlayer(charId), Nil) + } + + private def removeQueuedProximityInvitesForPlayer(charId: Long): List[(Long, List[Invitation])] = { + SquadInvitationManager.removeQueuedInvites( + queuedInvites, + { + case invite: ProximityInvite => invite.appliesToPlayer(charId) + case _ => false + } + ) + } + + /** + * Remove all active and inactive proximity squad invites. + * This is related to recruitment from the perspective of the recruiter. + * @param charId the player + */ + def cleanUpAllProximityInvitesForPlayer(charId: Long): List[(Long, List[Invitation])] = { + SquadInvitationManager.cleanUpAllInvites( + removeQueuedProximityInvitesForPlayer(charId), + removeActiveProximityInvitesForPlayer(charId) + ) + } + + private def removeActiveProximityInvitesForSquad(guid: PlanetSideGUID): List[(Long, List[Invitation])] = { + removeActiveInvites({ + (_: Long, invite: Invitation) => invite match { + case invite: ProximityInvite => invite.appliesToSquad(guid) + case _ => false + } + }) + } + + private def removeQueuedProximityInvitesForSquad(guid: PlanetSideGUID): List[(Long, List[Invitation])] = { + SquadInvitationManager.removeQueuedInvites( + queuedInvites, + { + case invite: ProximityInvite => invite.appliesToSquad(guid) + case _ => false + } + ) + } + + /** + * Remove all active and inactive proximity squad invites for a specific squad. + * @param guid squad identifier + */ + def cleanUpAllProximityInvitesForSquad(guid: PlanetSideGUID): List[(Long, List[Invitation])] = { + SquadInvitationManager.cleanUpAllInvites( + removeQueuedProximityInvitesForSquad(guid), + removeActiveProximityInvitesForSquad(guid) + ) + } + + def publish(to: Long, msg: SquadResponse.Response): Unit = { + subs.Publish(to, msg) + } } object SquadInvitationManager { final case class Join(charId: Long) - /** - * The base of all objects that exist for the purpose of communicating invitation from one player to the next. - * @param char_id the inviting player's unique identifier number - * @param name the inviting player's name - */ - sealed abstract class Invitation(char_id: Long, name: String) { - def InviterCharId: Long = char_id - def InviterName: String = name - } - - /** - * Utilized when one player attempts to join an existing squad in a specific role. - * Accessed by the joining player from the squad detail window. - * This invitation is handled by the squad leader. - * @param player the player who requested the role - * @param features the squad with the role - * @param position the index of the role - */ - private case class RequestRole(player: Player, features: SquadFeatures, position: Int) - extends Invitation(player.CharId, player.Name) - - /** - * Utilized when one squad member issues an invite for some other player. - * Accessed by an existing squad member using the "Invite" menu option on another player. - * This invitation is handled by the player who would join the squad. - * @param char_id the unique character identifier of the player who sent the invite - * @param name the name the player who sent the invite - * @param features the squad - */ - private case class VacancyInvite(char_id: Long, name: String, features: SquadFeatures) - extends Invitation(char_id, name) - - /** - * Utilized to redirect an (accepted) invitation request to the proper squad leader. - * No direct action causes this message. - * Depending on the situation, either the squad leader or the player who would join the squad handle this invitation. - * @param player the player who would be joining the squad; - * may or may not have actually requested it in the first place - * @param features the squad - */ - private case class IndirectInvite(player: Player, features: SquadFeatures) - extends Invitation(player.CharId, player.Name) - - /** - * Utilized in conjunction with an external queuing data structure - * to search for and submit requests to other players - * for the purposes of fill out unoccupied squad roles. - * This invitation is handled by the player who would be joining the squad. - * @param leader the squad leader - * @param features the squad - * @param position the index of a role - */ - private case class ProximityInvite(leader: Member, features: SquadFeatures, position: Int) - extends Invitation(leader.CharId, leader.Name) - - /** - * Utilized in conjunction with an external queuing data structure - * to search for and submit requests to other players - * for the purposes of fill out an unoccupied squad role. - * This invitation is handled by the player who would be joining the squad. - * @param leader the squad leader - * @param features the squad with the role - * @param position the index of the role - */ - private case class LookingForSquadRoleInvite(leader: Member, features: SquadFeatures, position: Int) - extends Invitation(leader.CharId, leader.Name) - - /** - * Utilized when one player issues an invite for some other player for a squad that does not yet exist. - * This invitation is handled by the player who would be joining the squad. - * @param player the player who wishes to become the leader of a squad - */ - private case class SpontaneousInvite(player: Player) extends Invitation(player.CharId, player.Name) - - /** - * na - * @param invitingPlayer na - * @param features na - * @param position na - * @return na - */ - private def ProximityEnvelope( - invitingPlayer: Member, - features: SquadFeatures, - position: Int - ): Invitation = { - ProximityInvite(invitingPlayer, features, position) - } - - /** - * na - * @param invitingPlayer na - * @param features na - * @param position na - * @return na - */ - private def LookingForSquadRoleEnvelope( - invitingPlayer: Member, - features: SquadFeatures, - position: Int - ): Invitation = { - LookingForSquadRoleInvite(invitingPlayer, features, position) - } - final case class FinishStartSquad(features: SquadFeatures) + + /** + * na + * @param invitingPlayer na + * @param features na + * @param position na + * @return na + */ + private def proximityEnvelope( + invitingPlayer: Member, + features: SquadFeatures, + position: Int + ): Invitation = { + invitations.ProximityInvite(invitingPlayer, features, position) + } + + /** + * na + * @param invitingPlayer na + * @param features na + * @param position na + * @return na + */ + private def lookingForSquadRoleEnvelope( + invitingPlayer: Member, + features: SquadFeatures, + position: Int + ): Invitation = { + invitations.LookingForSquadRoleInvite(invitingPlayer, features, position) + } + + /** + * Overloaded entry point to functionality for handling indirection + * for handling one player requesting a specific squad role + * or when messaging the squad leader about an invite. + * @param player the player who wants to join the squad + * @param bid a specific kind of `Invitation` object + * @return `true`, if the player is not denied the possibility of joining the squad; + * `false`, otherwise, of it the squad does not exist + */ + def handleRequestRole(manager: SquadInvitationManager, player: Player, bid: Invitation): Boolean = { + bid.getOptionalSquad.exists(handleRequestRole(manager, player, _, bid)) + } + + /** + * The functionality for handling indirection + * for handling one player requesting a specific squad role + * or when messaging the squad leader about an invite.
+ *
+ * At this point in the squad join process, the only consent required is that of the squad leader. + * An automatic consent flag exists on the squad; + * but, if that is not set, then the squad leader must be asked whether or not to accept or to reject the recruit. + * If the squad leader changes in the middle or the latter half of the process, + * the invitation may still fail even if the old squad leader accepts. + * If the squad leader changes in the middle of the latter half of the process, + * the inquiry might be posed again of the new squad leader, of whether to accept or to reject the recruit. + * + * @param player the player who wants to join the squad + * @param features the squad + * @param bid the `Invitation` object that was the target of this request + * @return `true`, if the player is not denied the possibility of joining the squad; + * `false`, otherwise, of it the squad does not exist + */ + def handleRequestRole(manager: SquadInvitationManager, player: Player, features: SquadFeatures, bid: Invitation): Boolean = { + val leaderCharId = features.Squad.Leader.CharId + manager.publish(leaderCharId, SquadResponse.WantsSquadPosition(leaderCharId, player.Name)) + true + } + + 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 + } + } + + private def moveListElementsToMap[T](fromList: List[(Long, List[T])], toMap: mutable.LongMap[List[T]]): Unit = { + fromList.foreach { case (id, listElements) => + toMap.get(id) match { + case None => + toMap.put(id, listElements) + case Some(mapElements) => + toMap.put(id, listElements ++ mapElements) + } + } + } + + private def cleanUpSquadFeatures(removed: List[Long], features: SquadFeatures, position: Int): Unit = { + features.ProxyInvites = features.ProxyInvites.filterNot(removed.contains) + if (features.ProxyInvites.isEmpty) { + features.SearchForRole = None + } + } + + /** + * Remove all active invitation objects that are related to the particular player. + * Specifically used to safely disarm obsolete invitation objects by specific criteria. + * Affects the active invitation list and any squads related to that player. + * @param proposedRemovalIds list of unique character identifiers to be eliminated from squad information; + * if empty, the unique character identifiers from the other parameter will be used instead + * @return a list of the removed inactive invitation objects + */ + private def cleanUpInvites( + list: List[(Long, List[Invitation])], + proposedRemovalIds: List[Long] + ): List[(Long, List[Invitation])] = { + val (idList, invites) = list.unzip + val removalList = proposedRemovalIds + .collectFirst(_ => proposedRemovalIds) + .getOrElse(idList) + invites + .flatten + .flatMap(_.getOptionalSquad) + .distinctBy(_.Squad.GUID) + .foreach(cleanUpSquadFeatures(removalList, _, position = -1)) + list + } + + /** + * Remove all invitation objects that are related to the particular squad. + * Specifically used to safely disarm obsolete invitation objects by specific criteria. + * Affects all invitation object types and all data structures that deal with the squad. + */ + private def cleanUpAllInvites( + queuedList: List[(Long, List[Invitation])], + activeList: List[(Long, List[Invitation])] + ): List[(Long, List[Invitation])] = { + val activeInvitesMap = mutable.LongMap.from(activeList) + val (removalList, featureList) = { + val (ids, inviteLists) = (activeInvitesMap ++ queuedList).unzip + ( + ids.toList, + inviteLists + .flatMap { invites => + invites.flatMap(_.getOptionalSquad) + } + ) + } + moveListElementsToMap(queuedList, activeInvitesMap) + featureList + .toSeq + .distinctBy(_.Squad.GUID) + .foreach { features => + cleanUpSquadFeatures(removalList, features, position = -1) + } + activeInvitesMap.toList + } + + private def removeQueuedInvites( + inviteMap: mutable.LongMap[List[Invitation]], + partitionRule: Invitation => Boolean + ): List[(Long, List[Invitation])] = { + inviteMap + .toList + .flatMap { + case (id, invites) => + val (found, retained) = invites.partition(partitionRule) + if (retained.nonEmpty) { + inviteMap.put(id, retained) + } else { + inviteMap.remove(id) + } + found.collectFirst { _ => (id, found) } + } + } } diff --git a/src/main/scala/net/psforever/services/teamwork/SquadService.scala b/src/main/scala/net/psforever/services/teamwork/SquadService.scala index 5a1edb56..a3d731a6 100644 --- a/src/main/scala/net/psforever/services/teamwork/SquadService.scala +++ b/src/main/scala/net/psforever/services/teamwork/SquadService.scala @@ -1,20 +1,23 @@ -// Copyright (c) 2019-2022 PSForever +// Copyright (c) 2019-2024 PSForever package net.psforever.services.teamwork import akka.actor.{Actor, ActorRef, Terminated} import java.io.{PrintWriter, StringWriter} +import scala.annotation.unused import scala.collection.concurrent.TrieMap import scala.collection.mutable // +import net.psforever.actors.session.SessionActor import net.psforever.objects.{LivePlayerList, Player} import net.psforever.objects.teamwork.{Member, Squad, SquadFeatures} import net.psforever.objects.avatar.{Avatar, Certification} import net.psforever.objects.definition.converter.StatConverter import net.psforever.objects.zones.Zone +import net.psforever.packet.game.ChatMsg import net.psforever.packet.game.SquadAction._ import net.psforever.packet.game.{PlanetSideZoneID, SquadDetail, SquadInfo, SquadPositionDetail, SquadPositionEntry, SquadAction => SquadRequestAction} import net.psforever.services.Service -import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, SquadRequestType, SquadResponseType} +import net.psforever.types.{ChatMessageType, PlanetSideEmpire, PlanetSideGUID, SquadRequestType, SquadResponseType} class SquadService extends Actor { import SquadService._ @@ -70,8 +73,6 @@ class SquadService extends Actor { private def info(msg: String): Unit = log.info(msg) - private def debug(msg: String): Unit = log.debug(msg) - override def postStop(): Unit = { //squads and members (users) squadFeatures.foreach { @@ -109,7 +110,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 { @@ -138,13 +139,11 @@ class SquadService extends Actor { * @param charId the potential member identifier * @return the discovered squad, or `None` */ - def GetParticipatingSquad(charId: Long): Option[SquadFeatures] = - memberToSquad.get(charId) match { - case Some(id) => - squadFeatures.get(id) - case None => - None - } + def GetParticipatingSquad(charId: Long): Option[SquadFeatures] = { + memberToSquad + .get(charId) + .flatMap(GetSquad) + } /** * If this player is a member of any squad, discover that squad. @@ -166,28 +165,25 @@ class SquadService extends Actor { * the expectation is that the provided squad is a known participating squad * @return the discovered squad, or `None` */ - def GetLeadingSquad(charId: Long, opt: Option[SquadFeatures]): Option[SquadFeatures] = - opt.orElse(GetParticipatingSquad(charId)) match { - case Some(features) => - if (features.Squad.Leader.CharId == charId) { - Some(features) - } else { - None - } - case _ => - None - } + def GetLeadingSquad(charId: Long, opt: Option[SquadFeatures]): Option[SquadFeatures] = { + opt + .orElse(GetParticipatingSquad(charId)) + .collect { + case features if features.Squad.Leader.CharId == charId => + features + } + } def receive: Receive = { //subscribe to a faction's channel - necessary to receive updates about listed squads - case Service.Join(faction) if "TRNCVS".indexOf(faction) > -1 => + case Service.Join(faction) if SquadService.FactionWordSalad.indexOf(faction) > -1 => JoinByFaction(faction, sender()) //subscribe to the player's personal channel - necessary for future and previous squad information case Service.Join(char_id) => JoinByCharacterId(char_id, sender()) - case Service.Leave(Some(faction)) if "TRNCVS".indexOf(faction) > -1 => + case Service.Leave(Some(faction)) if SquadService.FactionWordSalad.indexOf(faction) > -1 => LeaveByFaction(faction, sender()) case Service.Leave(Some(char_id)) => @@ -197,7 +193,7 @@ class SquadService extends Actor { LeaveInGeneral(sender()) case Terminated(actorRef) => - TerminatedBy(actorRef) + LeaveInGeneral(actorRef) case message @ SquadServiceMessage(tplayer, zone, squad_action) => squad_action match { @@ -239,120 +235,21 @@ class SquadService extends Actor { UpdateSquadListWhenListed(features, changes) case SquadService.ResendActiveInvite(charId) => - invitations.resendActiveInvite(charId) + invitations.reloadActiveInvite(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()}") } - def JoinByFaction(faction: String, sender: ActorRef): Unit = { - val path = s"/$faction/Squad" - log.trace(s"$sender has joined $path") - subs.SquadEvents.subscribe(sender, path) - } - - def JoinByCharacterId(charId: String, sender: ActorRef): Unit = { - try { - val longCharId = charId.toLong - val path = s"/$charId/Squad" - log.trace(s"$sender has joined $path") - context.watch(sender) - subs.UserEvents += longCharId -> sender - invitations.handleJoin(longCharId) - } catch { - case _: ClassCastException => - log.warn(s"Service.Join: tried $charId as a unique character identifier, but it could not be casted") - case e: Exception => - log.error(s"Service.Join: unexpected exception using $charId as data - ${e.getLocalizedMessage}") - val sw = new StringWriter - e.printStackTrace(new PrintWriter(sw)) - log.error(sw.toString) - } - } - - def LeaveByFaction(faction: String, sender: ActorRef): Unit = { - val path = s"/$faction/Squad" - log.trace(s"$sender has left $path") - subs.SquadEvents.unsubscribe(sender, path) - } - - def LeaveByCharacterId(charId: String, sender: ActorRef): Unit = { - try { - LeaveService(charId.toLong, sender) - } catch { - case _: ClassCastException => - log.warn(s"Service.Leave: tried $charId as a unique character identifier, but it could not be casted") - case e: Exception => - log.error(s"Service.Leave: unexpected exception using $charId as data - ${e.getLocalizedMessage}") - val sw = new StringWriter - e.printStackTrace(new PrintWriter(sw)) - log.error(sw.toString) - } - } - - def LeaveInGeneral(sender: ActorRef): Unit = { - subs.UserEvents find { case (_, subscription) => 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)) { - GetParticipatingSquad(player) match { - case Some(participating) => - //invitingPlayer became part of a squad while invited player was answering the original summons - Some(participating) - case _ => - //generate a new squad, with invitingPlayer as the leader - val features = StartSquad(player) - val squad = features.Squad - squad.Task = s"${player.Name}'s Squad" - subs.Publish(invitingPlayerCharId, SquadResponse.IdentifyAsSquadLeader(squad.GUID)) - sender.tell(SquadInvitationManager.FinishStartSquad(features), self) - Some(features) - } - } - } - - def SquadActionInitSquadList( - tplayer: Player, - sender: ActorRef - ): Unit = { - //send initial squad catalog - val faction = tplayer.Faction - val squads = PublishedLists(faction) - subs.Publish(sender, SquadResponse.InitList(squads)) - squads.foreach { squad => - val guid = squad.squad_guid.get - subs.Publish(tplayer.CharId, SquadResponse.SquadDecoration(guid, squadFeatures(guid).Squad)) - } - } - - def SquadActionInitCharId(tplayer: Player): Unit = { - val charId = tplayer.CharId - GetParticipatingSquad(charId) match { - case None => ; - case Some(features) => - features.Switchboard ! SquadSwitchboard.Join(tplayer, 0, sender()) - } - } - - def SquadServiceReloadSquadDecoration(faction: PlanetSideEmpire.Value, to: Long): Unit = { - ApplySquadDecorationToEntriesForUser(faction, to) - } - def SquadActionMembership(tplayer: Player, zone: Zone, action: Any): Unit = { action match { case SquadAction.Membership(SquadRequestType.Invite, invitingPlayer, Some(_invitedPlayer), invitedName, _) => @@ -374,238 +271,17 @@ class SquadService extends Actor { SquadActionMembershipDisband(char_id) case SquadAction.Membership(SquadRequestType.Cancel, cancellingPlayer, _, _, _) => - SquadActionMembershipCancel(cancellingPlayer) + SquadActionMembershipCancel(cancellingPlayer, tplayer) - case SquadAction.Membership(SquadRequestType.Promote, promotingPlayer, Some(_promotedPlayer), promotedName, _) => - //SquadActionMembershipPromote(promotingPlayer, _promotedPlayer, promotedName, SquadServiceMessage(tplayer, zone, action), sender()) + case SquadAction.Membership(SquadRequestType.Promote, _, _, _, _) => () + // case SquadAction.Membership(SquadRequestType.Promote, promotingPlayer, Some(_promotedPlayer), promotedName, _) => + // SquadActionMembershipPromote(promotingPlayer, _promotedPlayer, promotedName, SquadServiceMessage(tplayer, zone, action), sender()) case SquadAction.Membership(event, _, _, _, _) => - debug(s"SquadAction.Membership: $event is not yet supported") + info(s"SquadAction.Membership: $event is not yet supported") - case _ => ; - } - } - - def SquadActionMembershipInvite( - tplayer: Player, - invitingPlayer: Long, - _invitedPlayer: Long, - invitedName: String - ): Unit = { - //this is just busy work; for actual joining operations, see SquadRequestType.Accept - (if (invitedName.nonEmpty) { - //validate player with name exists - LivePlayerList - .WorldPopulation({ case (_, a: Avatar) => a.name.equalsIgnoreCase(invitedName) && a.faction == tplayer.Faction }) - .headOption match { - case Some(a) => subs.UserEvents.keys.find(_ == a.id) - case None => None - } - } else { - //validate player with id exists - LivePlayerList - .WorldPopulation({ case (_, a: Avatar) => a.id == _invitedPlayer && a.faction == tplayer.Faction }) - .headOption match { - case Some(_) => Some(_invitedPlayer) - case None => None - } - }) match { - case Some(invitedPlayer) if invitingPlayer != invitedPlayer => - (GetParticipatingSquad(invitingPlayer), GetParticipatingSquad(invitedPlayer)) match { - case (Some(features1), Some(features2)) - if features1.Squad.GUID == features2.Squad.GUID => - //both players are in the same squad; no need to do anything - - case (Some(invitersFeatures), Some(invitedFeatures)) if { - val squad1 = invitersFeatures.Squad - val squad2 = invitedFeatures.Squad - squad1.Leader.CharId == invitingPlayer && squad2.Leader.CharId == invitedPlayer && - squad1.Size > 1 && squad2.Size > 1 } => - //we might do some platoon chicanery with this case later - //TODO platoons - - case (Some(invitersFeatures), Some(invitedFeatures)) - if invitedFeatures.Squad.Size == 1 => - //both players belong to squads, but the invitedPlayer's squad (invitedFeatures) is underutilized - //treat the same as "the classic situation" using invitersFeatures - invitations.createVacancyInvite(tplayer, invitedPlayer, invitersFeatures) - - case (Some(invitersFeatures), Some(invitedFeatures)) - if invitersFeatures.Squad.Size == 1 => - //both players belong to squads, but the invitingPlayer's squad is underutilized by comparison - //treat the same as "indirection ..." using squad2 - invitations.createIndirectInvite(tplayer, invitedPlayer, invitedFeatures) - - case (Some(features), None) => - //the classic situation - invitations.createVacancyInvite(tplayer, invitedPlayer, features) - - case (None, Some(features)) => - //indirection; we're trying to invite ourselves to someone else's squad - invitations.createIndirectInvite(tplayer, invitedPlayer, features) - - case (None, None) => - //neither the invited player nor the inviting player belong to any squad - invitations.createSpontaneousInvite(tplayer, invitedPlayer) - - case _ => ; - } - case _ => ; - } - } - - def SquadActionMembershipProximityInvite(zone: Zone, invitingPlayer: Long): Unit = { - GetLeadingSquad(invitingPlayer, None) match { - case Some(features) => - invitations.handleProximityInvite(zone, invitingPlayer, features) - case _ => ; - } - } - - def SquadActionMembershipAccept(tplayer: Player, invitedPlayer: Long): Unit = { - invitations.handleAcceptance(tplayer, invitedPlayer, GetParticipatingSquad(tplayer)) - } - - def SquadActionMembershipLeave(tplayer: Player, actingPlayer: Long, _leavingPlayer: Option[Long], name: String): Unit = { - GetParticipatingSquad(actingPlayer) match { - case Some(features) => - val squad = features.Squad - val leader = squad.Leader.CharId - (if (name.nonEmpty) { - //validate player with name - LivePlayerList - .WorldPopulation({ case (_, a: Avatar) => a.name.equalsIgnoreCase(name) }) - .headOption match { - case Some(a) => subs.UserEvents.keys.find(_ == a.id) - case None => None - } - } else { - //validate player with id - _leavingPlayer match { - case Some(id) => subs.UserEvents.keys.find(_ == id) - case None => None - } - }) match { - case _ @ Some(leavingPlayer) - if GetParticipatingSquad(leavingPlayer).contains(features) => //kicked player must be in the same squad - if (actingPlayer == leader) { - if (leavingPlayer == leader || squad.Size == 2) { - //squad leader is leaving his own squad, so it will be disbanded - //OR squad is only composed of two people, so it will be closed-out when one of them leaves - DisbandSquad(features) - } else { - //kicked by the squad leader - subs.Publish( - leavingPlayer, - SquadResponse.Membership( - SquadResponseType.Leave, - 0, - 0, - leavingPlayer, - Some(leader), - tplayer.Name, - unk5=false, - Some(None) - ) - ) - subs.Publish( - leader, - SquadResponse.Membership( - SquadResponseType.Leave, - 0, - 0, - leader, - Some(leavingPlayer), - "", - unk5=true, - Some(None) - ) - ) - LeaveSquad(leavingPlayer, features) - } - } else if (leavingPlayer == actingPlayer) { - if (squad.Size == 2) { - //squad is only composed of two people, so it will be closed-out when one of them leaves - DisbandSquad(features) - } else { - //leaving the squad of own accord - LeaveSquad(actingPlayer, features) - } - } - - case _ => ; - } - case _ => ; - } - } - - def SquadActionMembershipReject(tplayer: Player, rejectingPlayer: Long): Unit = { - invitations.handleRejection( - tplayer, - rejectingPlayer, - squadFeatures.map { case (guid, features) => (guid, features.Squad.Leader.CharId) }.toList - ) - } - - def SquadActionMembershipDisband(charId: Long): Unit = { - GetLeadingSquad(charId, None) match { - case Some(features) => - DisbandSquad(features) - case None => ; - } - } - - def SquadActionMembershipCancel(cancellingPlayer: Long): Unit = { - //get rid of SpontaneousInvite objects and VacancyInvite objects - invitations.handleCancelling(cancellingPlayer) - } - - def SquadActionMembershipPromote( - sponsoringPlayer: Long, - promotionCandidatePlayer: Long, - promotionCandidateName: String, - msg: SquadServiceMessage, - ref: ActorRef - ): Unit = { - val promotedPlayer: Long = subs.UserEvents.keys.find(_ == promotionCandidatePlayer).orElse({ - LivePlayerList - .WorldPopulation({ case (_, a: Avatar) => a.name.equalsIgnoreCase(promotionCandidateName) }) - .headOption match { - case Some(a) => Some(a.id) - case None => None - } - }) match { - case Some(player: Long) => player - case _ => -1L - } - //sponsorPlayer should be squad leader - (GetLeadingSquad(sponsoringPlayer, None), GetParticipatingSquad(promotedPlayer)) match { - case (Some(features), Some(features2)) if features.Squad.GUID == features2.Squad.GUID => - SquadActionMembershipPromote(sponsoringPlayer, promotedPlayer, features, msg, ref) - case _ => ; - } - } - - def SquadActionMembershipPromote( - sponsoringPlayer: Long, - promotedPlayer: Long, - features: SquadFeatures, - msg: SquadServiceMessage, - ref: ActorRef - ): Unit = { - features.Switchboard.tell(msg, ref) - invitations.handlePromotion(sponsoringPlayer, promotedPlayer) - } - - def SquadActionWaypoint( - message: SquadServiceMessage, - tplayer: Player - ): Unit = { - GetParticipatingSquad(tplayer) match { - case Some(features) => - features.Switchboard.tell(message, sender()) - case None => - log.warn(s"Unsupported squad waypoint behavior: $message") + case msg => + log.warn(s"Unhandled message $msg from ${sender()}") } } @@ -628,68 +304,51 @@ class SquadService extends Actor { case _: StopListSquad => GetLeadingSquad(tplayer, None) //the following actions cause changes with the squad composition or with invitations case AutoApproveInvitationRequests(_) => - GetOrCreateSquadOnlyIfLeader(tplayer) match { - case out @ Some(features) => - invitations.handleDefinitionAction(tplayer, action, features) - out - case None => - None - } + GetOrCreateSquadOnlyIfLeader(tplayer) + .foreach(features => invitations.autoApproveInvitationRequests(tplayer.CharId, features)) + None case CloseSquadMemberPosition(position) => - GetOrCreateSquadOnlyIfLeader(tplayer) match { - case out @ Some(features) - if features.Squad.Membership(position).CharId > 0 => - val squad = features.Squad - LeaveSquad(squad.Membership(position).CharId, features) - out - case _ => - None - } - case FindLfsSoldiersForRole(_) => - GetLeadingSquad(tplayer, None) match { - case Some(features) => - invitations.handleDefinitionAction(tplayer, action, features) - case _ => ; - } + GetOrCreateSquadOnlyIfLeader(tplayer) + .collect { + case features if features.Squad.Membership(position).CharId > 0 => + LeaveSquad(features.Squad.Membership(position).CharId, features) + features + } + case FindLfsSoldiersForRole(position) => + GetLeadingSquad(tplayer, None) + .foreach(features => invitations.findLfsSoldiersForRole(tplayer, features, position)) None case CancelFind() => - GetLeadingSquad(tplayer, None) match { - case Some(features) => - invitations.handleDefinitionAction(tplayer, action, features) - case _ => ; - } + GetLeadingSquad(tplayer, None) + .foreach(features => invitations.cancelFind(Some(features))) None - case SelectRoleForYourself(_) => + case SelectRoleForYourself(position) => GetParticipatingSquad(tplayer) match { case out @ Some(features) => if (features.Squad.GUID == guid) { out } else { //this isn't the squad we're looking for by GUID; as a precaution, reload all of the published squad list + val charId = tplayer.CharId val faction = tplayer.Faction - subs.Publish(faction, SquadResponse.InitList(PublishedLists(tplayer.Faction))) + searchData.remove(charId) + subs.Publish(charId, SquadResponse.InitList(PublishedLists(faction))) None } case _ => - GetSquad(guid) match { - case Some(features) => - invitations.handleDefinitionAction(tplayer, action, features) - case _ => ; - } + GetSquad(guid) + .foreach(features => invitations.selectRoleForYourselfAsInvite(tplayer, features, position)) None } case _: CancelSelectRoleForYourself => - GetSquad(guid) match { - case Some(features) => - invitations.handleDefinitionAction(tplayer, action, features) - case _ => ; - } + GetSquad(guid) + .foreach(features => invitations.cancelSelectRoleForYourself(tplayer, features)) None case search: SearchForSquadsWithParticularRole => -// SquadActionDefinitionSearchForSquadsWithParticularRole(tplayer, search) + SquadActionDefinitionSearchForSquadsWithParticularRole(tplayer, search) None case _: CancelSquadSearch => -// SquadActionDefinitionCancelSquadSearch(tplayer.CharId) + SquadActionDefinitionCancelSquadSearch(tplayer.CharId) None case _: DisplaySquad => GetSquad(guid) match { @@ -704,21 +363,344 @@ class SquadService extends Actor { None case _ => GetSquad(guid) - }) match { - case Some(features) => features.Switchboard.tell(message, sender()) - case None => ; + }) + .foreach(features => features.Switchboard.tell(message, 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 + val path = s"/$charId/Squad" + log.trace(s"$sender has joined $path") + context.watch(sender) + subs.UserEvents += longCharId -> sender + invitations.handleJoin(longCharId) + } catch { + case _: ClassCastException => + log.warn(s"Service.Join: tried $charId as a unique character identifier, but it could not be casted") + case e: Exception => + log.error(s"Service.Join: unexpected exception using $charId as data - ${e.getLocalizedMessage}") + val sw = new StringWriter + e.printStackTrace(new PrintWriter(sw)) + log.error(sw.toString) } } + /** + * 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) + } catch { + case _: ClassCastException => + log.warn(s"Service.Leave: tried $charId as a unique character identifier, but it could not be casted") + case e: Exception => + log.error(s"Service.Leave: unexpected exception using $charId as data - ${e.getLocalizedMessage}") + val sw = new StringWriter + e.printStackTrace(new PrintWriter(sw)) + log.error(sw.toString) + } + } + + /** + * Assuming a subscriber that matches previously subscribed data, + * completely unsubscribe and forget this entry. + * @param sender subscriber + * @see `LeaveService` + */ + def LeaveInGeneral(sender: ActorRef): Unit = { + context.unwatch(sender) + subs + .UserEvents + .find { case (_, subscription) => (subscription eq sender) || subscription.path.equals(sender.path) } + .foreach { case (to, _) => LeaveService(to, sender) } + } + + def performStartSquad(sender: ActorRef, player: Player): Option[SquadFeatures] = { + val invitingPlayerCharId = player.CharId + if (EnsureEmptySquad(invitingPlayerCharId)) { + GetParticipatingSquad(player) + .orElse { + //generate a new squad, with invitingPlayer as the leader + val features = StartSquad(player) + val squad = features.Squad + squad.Task = s"${player.Name}'s Squad" + subs.Publish(invitingPlayerCharId, SquadResponse.IdentifyAsSquadLeader(squad.GUID)) + sender.tell(SquadInvitationManager.FinishStartSquad(features), self) + Some(features) + } + } else { + None + } + } + + def SquadActionInitSquadList( + tplayer: Player, + sender: ActorRef + ): Unit = { + //send initial squad catalog + val faction = tplayer.Faction + val squads = PublishedLists(faction) + subs.Publish(sender, SquadResponse.InitList(squads)) + squads.foreach { squad => + val guid = squad.squad_guid.get + subs.Publish(tplayer.CharId, SquadResponse.SquadDecoration(guid, squadFeatures(guid).Squad)) + } + } + + def SquadActionInitCharId(tplayer: Player): Unit = { + val charId = tplayer.CharId + GetParticipatingSquad(charId) + .foreach(features => features.Switchboard.tell(SquadSwitchboard.Join(tplayer, 0, sender()), self)) + } + + def SquadServiceReloadSquadDecoration(faction: PlanetSideEmpire.Value, to: Long): Unit = { + ApplySquadDecorationToEntriesForUser(faction, to) + } + + def SquadActionMembershipInvite( + tplayer: Player, + invitingPlayer: Long, + _invitedPlayer: Long, + invitedName: String + ): Unit = { + //this is just busy work; for actual joining operations, see SquadRequestType.Accept + (if (invitedName.nonEmpty) { + //validate player with name exists + LivePlayerList + .WorldPopulation({ case (_, a: Avatar) => a.name.equalsIgnoreCase(invitedName) && a.faction == tplayer.Faction }) + } else { + //validate player with id exists + LivePlayerList + .WorldPopulation({ case (_, a: Avatar) => a.id == _invitedPlayer && a.faction == tplayer.Faction }) + }) + .headOption + .collectFirst { + //important: squads must know about the person too + a => subs.UserEvents.keys.find(_ == a.id) + } + .flatten + .collect { + case invitedPlayer if invitingPlayer != invitedPlayer => + (GetParticipatingSquad(invitingPlayer), GetParticipatingSquad(invitedPlayer)) match { + case (Some(features1), Some(features2)) + if features1.Squad.GUID == features2.Squad.GUID => + //both players are in the same squad; no need to do anything + + case (Some(invitersFeatures), Some(invitedFeatures)) if { + val squad1 = invitersFeatures.Squad + val squad2 = invitedFeatures.Squad + squad1.Leader.CharId == invitingPlayer && squad2.Leader.CharId == invitedPlayer && + squad1.Size > 1 && squad2.Size > 1 } => + //we might do some platoon chicanery with this case later + //TODO platoons + + case (Some(invitersFeatures), Some(invitedFeatures)) + if invitedFeatures.Squad.Size == 1 => + //both players belong to squads, but the invitedPlayer's squad (invitedFeatures) is underutilized + //treat the same as "the classic situation" using invitersFeatures + invitations.createInvitationToJoinSquad(tplayer, invitedPlayer, invitersFeatures) + + case (Some(invitersFeatures), Some(invitedFeatures)) + if invitersFeatures.Squad.Size == 1 => + //both players belong to squads, but the invitingPlayer's squad is underutilized by comparison + //treat the same as "indirection ..." using squad2 + invitations.createPermissionToRedirectInvite(tplayer, invitedPlayer, invitedFeatures) + + case (Some(features), None) => + //the classic situation + invitations.createInvitationToJoinSquad(tplayer, invitedPlayer, features) + + case (None, Some(features)) => + //indirection; we're trying to invite ourselves to someone else's squad + invitations.createPermissionToRedirectInvite(tplayer, invitedPlayer, features) + + case (None, None) => + //neither the invited player nor the inviting player belong to any squad + invitations.createInvitationToCreateASquad(tplayer, invitedPlayer) + + case _ => () + } + } + } + + def SquadActionMembershipProximityInvite(zone: Zone, invitingPlayer: Long): Unit = { + GetLeadingSquad(invitingPlayer, None) + .foreach(features => invitations.createProximityInvite(zone, invitingPlayer, features)) + } + + def SquadActionMembershipAccept(tplayer: Player, invitedPlayer: Long): Unit = { + invitations.handleAcceptance(tplayer, invitedPlayer, GetParticipatingSquad(tplayer)) + } + + def SquadActionMembershipLeave(tplayer: Player, actingPlayer: Long, _leavingPlayer: Option[Long], name: String): Unit = { + GetParticipatingSquad(actingPlayer) + .foreach { features => + val squad = features.Squad + val leader = squad.Leader.CharId + (if (name.nonEmpty) { + //validate player with name + LivePlayerList + .WorldPopulation({ case (_, a: Avatar) => a.name.equalsIgnoreCase(name) }) + .headOption match { + case Some(a) => subs.UserEvents.keys.find(_ == a.id) + case None => None + } + } else { + //validate player with id + _leavingPlayer match { + case Some(id) => subs.UserEvents.keys.find(_ == id) + case None => None + } + }) + .collect { case leavingPlayer + if GetParticipatingSquad(leavingPlayer).contains(features) => //kicked player must be in the same squad + if (actingPlayer == leader) { + if (leavingPlayer == leader || squad.Size == 2) { + //squad leader is leaving his own squad, so it will be disbanded + //OR squad is only composed of two people, so it will be closed-out when one of them leaves + DisbandSquad(features) + } else { + //kicked by the squad leader + subs.Publish( + leavingPlayer, + SquadResponse.Membership(SquadResponseType.Leave, leavingPlayer, Some(leader), tplayer.Name, unk5 = false) + ) + subs.Publish( + leader, + SquadResponse.Membership( SquadResponseType.Leave, leader, Some(leavingPlayer), "", unk5 = true) + ) + LeaveSquad(leavingPlayer, features) + } + } else if (leavingPlayer == actingPlayer) { + if (squad.Size == 2) { + //squad is only composed of two people, so it will be closed-out when one of them leaves + DisbandSquad(features) + } else { + //leaving the squad of own accord + LeaveSquad(actingPlayer, features) + } + } + } + } + } + + def SquadActionMembershipReject(tplayer: Player, rejectingPlayer: Long): Unit = { + invitations.handleRejection( + tplayer, + rejectingPlayer, + squadFeatures.map { case (guid, features) => (guid, features) }.toList + ) + } + + def SquadActionMembershipDisband(charId: Long): Unit = { + GetLeadingSquad(charId, None) + .foreach(features => DisbandSquad(features)) + } + + def SquadActionMembershipCancel(cancellingPlayer: Long, player: Player): Unit = { + //get rid of SpontaneousInvite objects and VacancyInvite objects + invitations.handleCancelling( + cancellingPlayer, + player, + GetLeadingSquad(cancellingPlayer, None) + ) + } + + def SquadActionMembershipPromote( + sponsoringPlayer: Long, + promotionCandidatePlayer: Long, + promotionCandidateName: String, + msg: SquadServiceMessage, + ref: ActorRef + ): Unit = { + val promotedPlayer: Long = subs + .UserEvents + .keys + .find(_ == promotionCandidatePlayer) + .orElse { + LivePlayerList + .WorldPopulation({ case (_, a: Avatar) => a.name.equalsIgnoreCase(promotionCandidateName) }) + .headOption + .map(_.id.toLong) + } + .getOrElse(-1L) + //sponsorPlayer should be squad leader + (GetLeadingSquad(sponsoringPlayer, None), GetParticipatingSquad(promotedPlayer)) match { + case (Some(features), Some(features2)) if features.Squad.GUID == features2.Squad.GUID => + SquadActionMembershipPromote(sponsoringPlayer, promotedPlayer, features, msg, ref) + case _ => () + } + } + + def SquadActionMembershipPromote( + sponsoringPlayer: Long, + promotedPlayer: Long, + features: SquadFeatures, + msg: SquadServiceMessage, + ref: ActorRef + ): Unit = { + features.Switchboard.tell(msg, ref) + invitations.handlePromotion(sponsoringPlayer, promotedPlayer) + } + + def SquadActionWaypoint( + message: SquadServiceMessage, + tplayer: Player + ): Unit = { + GetParticipatingSquad(tplayer) + .collect { features => + features.Switchboard.tell(message, sender()) + features + } + .orElse { + log.warn(s"Unsupported squad waypoint behavior: $message") + None + } + } + def SquadActionUpdate( message: SquadServiceMessage, char_id: Long, replyTo: ActorRef, ): Unit = { - GetParticipatingSquad(char_id) match { - case Some(features) => features.Switchboard.tell(message, replyTo) - case None => ; - } + GetParticipatingSquad(char_id) + .foreach(features => features.Switchboard.tell(message, replyTo)) } def GetOrCreateSquadOnlyIfLeader(player: Player): Option[SquadFeatures] = { @@ -738,73 +720,75 @@ class SquadService extends Actor { criteria: SearchForSquadsWithParticularRole ): Unit = { val charId = tplayer.CharId - searchData.get(charId) match { - case Some(_) => ; - //already searching, so do nothing(?) - case None => + searchData + .get(charId) + .orElse { val data = SquadService.SearchCriteria(tplayer.Faction, criteria) searchData.put(charId, data) SquadActionDefinitionSearchForSquadsUsingCriteria(charId, data) - } + None + } + //if already searching, do nothing } private def SquadActionDefinitionSearchForSquadsUsingCriteria( charId: Long, criteria: SquadService.SearchCriteria ): Unit = { - subs.Publish( - charId, - SquadResponse.SquadSearchResults(SearchForSquadsResults(criteria)) - ) + subs.Publish(charId, SquadResponse.InitList(PublishedLists(criteria.faction))) + subs.Publish(charId, SquadResponse.SquadSearchResults(SearchForSquadsResults(criteria))) } private def SearchForSquadsResults(criteria: SquadService.SearchCriteria): List[PlanetSideGUID] = { - publishedLists.get(criteria.faction) match { - case Some(squads) if squads.nonEmpty => - squads.flatMap { guid => SearchForSquadsResults(criteria, guid) }.toList - case _ => - Nil - } + publishedLists + .get(criteria.faction) + .collect { + case squads if squads.nonEmpty => + squads.flatMap { guid => SearchForSquadsResults(criteria, guid) }.toList + } + .getOrElse(Nil) } def SquadActionDefinitionCancelSquadSearch(charId: Long): Unit = { - searchData.remove(charId) match { - case None => ; - case Some(data) => - SearchForSquadsResults(data).foreach { guid => - subs.Publish(charId, SquadResponse.SquadDecoration(guid, squadFeatures(guid).Squad)) - } - } + searchData + .remove(charId) + .map(SearchForSquadsResults) + .getOrElse(Nil) + .foreach { guid => + subs.Publish(charId, SquadResponse.SquadDecoration(guid, squadFeatures(guid).Squad)) + } } private def SearchForSquadsResults( criteria: SearchCriteria, guid: PlanetSideGUID ): Option[PlanetSideGUID] = { - val squad = squadFeatures(guid).Squad - val positions = if (criteria.mode == SquadRequestAction.SearchMode.AnyPositions) { - //includes occupied positions and closed positions that retain assignment information - squad.Membership - } else { - squad.Membership.zipWithIndex.filter { case (_, b) => squad.Availability(b) }.map { _._1 } - } - if ( - positions.nonEmpty && - (criteria.zoneId == 0 || criteria.zoneId == squad.ZoneId) && - (criteria.role.isEmpty || positions.exists(_.Role.equalsIgnoreCase(criteria.role))) && - (criteria.requirements.isEmpty || positions.exists { p => - val results = p.Requirements.intersect(criteria.requirements) - if (criteria.mode == SquadRequestAction.SearchMode.SomeCertifications) { - results.size > 1 - } else { - results == criteria.requirements - } - }) - ) { - Some(guid) - } else { - None - } + squadFeatures + .get(guid) + .map { features => + val squad = features.Squad + val positions = if (criteria.mode == SquadRequestAction.SearchMode.AnyPositions) { + //includes occupied positions and closed positions that retain assignment information + squad.Membership + } else { + squad.Membership.zipWithIndex.filter { case (_, b) => squad.Availability(b) }.map { _._1 } + } + (squad, positions) + } + .collect { + case (squad, positions) if positions.nonEmpty && + (criteria.zoneId == 0 || criteria.zoneId == squad.ZoneId) && + (criteria.role.isEmpty || positions.exists(_.Role.equalsIgnoreCase(criteria.role))) && + (criteria.requirements.isEmpty || positions.exists { p => + val results = p.Requirements.intersect(criteria.requirements) + if (criteria.mode == SquadRequestAction.SearchMode.SomeCertifications) { + results.size > 1 + } else { + results == criteria.requirements + } + }) => + guid + } } /** the following action can be performed by anyone */ @@ -831,15 +815,15 @@ class SquadService extends Actor { log.warn(s"${tplayer.Name} has a potential squad issue; might be exchanging information $reason") } - def CleanUpSquadFeatures(removed: List[Long], guid: PlanetSideGUID, position: Int): Unit = { - GetSquad(guid) match { - case Some(features) => - features.ProxyInvites = features.ProxyInvites.filterNot(removed.contains) - if (features.ProxyInvites.isEmpty) { + def CleanUpSquadFeatures(removed: List[Long], guid: PlanetSideGUID, @unused position: Int): Unit = { + GetSquad(guid) + .collect { + case features if features.ProxyInvites.isEmpty => + features.ProxyInvites = features.ProxyInvites.filterNot(removed.contains) features.SearchForRole = None - } - case None => ; - } + case features => + features.ProxyInvites = features.ProxyInvites.filterNot(removed.contains) + } } /** @@ -905,18 +889,19 @@ class SquadService extends Actor { def JoinSquad(player: Player, features: SquadFeatures, position: Int): Boolean = { val charId = player.CharId val squad = features.Squad - subs.UserEvents.get(charId) match { - case Some(events) - if squad.isAvailable(position, player.avatar.certifications) && - EnsureEmptySquad(charId) => - memberToSquad(charId) = squad.GUID - subs.MonitorSquadDetails.subtractOne(charId) - invitations.handleCleanup(charId) - features.Switchboard ! SquadSwitchboard.Join(player, position, events) - true - case _ => - false - } + subs + .UserEvents + .get(charId) + .collect { + case events + if squad.isAvailable(position, player.avatar.certifications) && EnsureEmptySquad(charId) => + memberToSquad(charId) = squad.GUID + subs.MonitorSquadDetails.subtractOne(charId) + invitations.handleCleanup(charId) + features.Switchboard ! SquadSwitchboard.Join(player, position, events) + true + } + .getOrElse(false) } /** @@ -928,16 +913,16 @@ class SquadService extends Actor { * `false`, otherwise */ def EnsureEmptySquad(charId: Long): Boolean = { - GetParticipatingSquad(charId) match { - case None => - true - case Some(features) if features.Squad.Size == 1 => - CloseSquad(features.Squad) - true - case _ => - log.warn("EnsureEmptySquad: the invited player is already a member of a squad and can not join a second one") - false - } + GetParticipatingSquad(charId) + .collect { + case features if features.Squad.Size == 1 => + CloseSquad(features.Squad) + true + case _ => + log.warn("EnsureEmptySquad: the invited player is already a member of a squad and can not join a second one") + false + } + .getOrElse(true) } /** @@ -952,15 +937,15 @@ class SquadService extends Actor { def LeaveSquad(charId: Long, features: SquadFeatures): Boolean = { val squad = features.Squad val membership = squad.Membership.zipWithIndex - membership.find { case (_member, _) => _member.CharId == charId } match { - case Some(_) if squad.Leader.CharId != charId => + membership + .find { case (_member, _) => _member.CharId == charId } + .collect { case _ if squad.Leader.CharId != charId => memberToSquad.remove(charId) subs.MonitorSquadDetails.subtractOne(charId) features.Switchboard ! SquadSwitchboard.Leave(charId) true - case _ => - false - } + } + .getOrElse(false) } /** @@ -969,7 +954,7 @@ class SquadService extends Actor { * will still leave the squad, but will not attempt to send feedback to the said unreachable client. * If the player is in the process of unsubscribing from the service, * the no-messaging pathway is useful to avoid accumulating dead letters. - * @see `CleanUpAllInvitesToSquad` + * @see `CleanUpAllInvitesForSquad` * @see `SquadDetail` * @see `SquadSubscriptionEntity.Publish` * @see `TryResetSquadId` @@ -1039,7 +1024,7 @@ class SquadService extends Actor { ) //the squad is being disbanded, the squad events channel is also going away; use cached character ids info(s"Squad #${squad.GUID.guid} has been disbanded.") - subs.Publish(leader, SquadResponse.Membership(SquadResponseType.Disband, 0, 0, leader, None, "", unk5=true, Some(None))) + subs.Publish(leader, SquadResponse.Membership(SquadResponseType.Disband, leader, None, "", unk5=true)) } /** @@ -1067,7 +1052,7 @@ class SquadService extends Actor { (membership.filterNot(_ == leader) ++ subs.PublishToMonitorTargets(squad.GUID, Nil)) .toSet .foreach { charId : Long => - subs.Publish(charId, SquadResponse.Membership(SquadResponseType.Disband, 0, 0, charId, None, "", unk5=false, Some(None))) + subs.Publish(charId, SquadResponse.Membership(SquadResponseType.Disband, charId, None, "", unk5=false)) } } @@ -1081,9 +1066,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) @@ -1098,7 +1088,7 @@ class SquadService extends Actor { subs.UserEvents.remove(charId) match { case Some(events) => subs.SquadEvents.unsubscribe(events, s"/${features.ToChannel}/Squad") - case _ => ; + case _ => () } if (size > 2) { GetLeadingSquad(charId, pSquadOpt) match { @@ -1126,8 +1116,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() } /** @@ -1282,15 +1271,66 @@ 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.autoApproveInvitationRequests(charId, features) + } + } + } + + def ChainRejectionFromSquad(player: Player, charId: Long, listOfCharIds: List[Long]): Unit = { + GetLeadingSquad(charId, None) + .collect { + case features if listOfCharIds.nonEmpty => + invitations.tryChainRejection(player, charId, listOfCharIds, features) + case features => + invitations.tryChainRejectionAll(charId, features) + } + } } object SquadService { + final private val FactionWordSalad: String = "TRNCVS" + final case class PerformStartSquad(player: Player) final case class PerformJoinSquad(player: Player, features: SquadFeatures, position: Int) 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 367127f4..bd275329 100644 --- a/src/main/scala/net/psforever/services/teamwork/SquadServiceResponse.scala +++ b/src/main/scala/net/psforever/services/teamwork/SquadServiceResponse.scala @@ -5,7 +5,7 @@ import akka.actor.ActorRef import net.psforever.objects.avatar.Certification import net.psforever.objects.teamwork.Squad import net.psforever.packet.game.{SquadDetail, SquadInfo, WaypointEventAction, WaypointInfo} -import net.psforever.types.{PlanetSideGUID, SquadResponseType, SquadWaypoint} +import net.psforever.types.{ChatMessageType, PlanetSideGUID, SquadResponseType, SquadWaypoint} import net.psforever.services.GenericEventBusMsg final case class SquadServiceResponse(channel: String, exclude: Iterable[Long], response: SquadResponse.Response) @@ -41,6 +41,16 @@ object SquadResponse { unk5: Boolean, unk6: Option[Option[String]] ) extends Response //see SquadMembershipResponse + object Membership { + def apply( + requestType: SquadResponseType.Value, + unk3: Long, + unk4: Option[Long], + playerName: String, + unk5: Boolean + ): Membership = new Membership(requestType, unk1 = 0, unk2 = 0, unk3, unk4, playerName, unk5, Some(None)) + } + final case class WantsSquadPosition(leader_char_id: Long, bid_name: String) extends Response final case class Join(squad: Squad, positionsToUpdate: List[Int], channel: String, ref: ActorRef) extends Response final case class Leave(squad: Squad, positionsToUpdate: List[(Long, Int)]) extends Response @@ -73,4 +83,6 @@ object SquadResponse { unk2: Int, zoneNumber: Int ) extends Response + + final case class SquadRelatedComment(str: String, messageType: ChatMessageType = ChatMessageType.UNK_227) 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 427d6c85..104bd532 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 diff --git a/src/main/scala/net/psforever/services/teamwork/invitations/IndirectInvite.scala b/src/main/scala/net/psforever/services/teamwork/invitations/IndirectInvite.scala new file mode 100644 index 00000000..cb90cb78 --- /dev/null +++ b/src/main/scala/net/psforever/services/teamwork/invitations/IndirectInvite.scala @@ -0,0 +1,163 @@ +// Copyright (c) 2024 PSForever +package net.psforever.services.teamwork.invitations + +import net.psforever.objects.Player +import net.psforever.objects.teamwork.SquadFeatures +import net.psforever.services.teamwork.{SquadInvitationManager, SquadResponse} +import net.psforever.types.PlanetSideGUID + +import scala.annotation.unused + +/** + * Utilized to redirect an (accepted) invitation request to the proper squad leader. + * An anticipated result of clarifying permission to request invitation + * to a squad belonging to some player who is not the squad leader. + * No direct action causes this message. + * This invitation is handled by the squad leader. + * @param originalRequester player who would be joining the squad; + * also the player who invited the player who will become the squad leader + * @param features squad + */ +final case class IndirectInvite(originalRequester: Player, features: SquadFeatures) + extends Invitation(originalRequester.CharId, originalRequester.Name) { + def handleInvitation(indirectInviteFunc: (IndirectInvite, Player, Long, Long, String) => Boolean)( + manager: SquadInvitationManager, + invitedPlayer: Long, + invitingPlayer: Long, + otherName: String + ): Unit = { + indirectInviteFunc(this, originalRequester, invitedPlayer, invitingPlayer, otherName) + } + + def handleAcceptance( + manager: SquadInvitationManager, + @unused player: Player, + invitedPlayer: Long, + @unused invitedPlayerSquadOpt: Option[SquadFeatures] + ): Unit = { + //tplayer / invitedPlayer is actually the squad leader + if (SquadInvitationManager.canEnrollInSquad(features, originalRequester.CharId)) { + val leaderCharId = player.CharId + val invitedPlayer = originalRequester.CharId + manager + .handleVacancyInvite(features, invitedPlayer, invitedPlayer, originalRequester) + .collect { + case (_, position) if manager.joinSquad(originalRequester, features, position) => + manager.acceptanceMessages(invitedPlayer, invitedPlayer, originalRequester.Name) + //clean up invitations specifically for this squad and this position + val cleanedUpActiveInvitesForSquadAndPosition = manager.cleanUpActiveInvitesForSquadAndPosition(features.Squad.GUID, position) + cleanedUpActiveInvitesForSquadAndPosition.collect { case (id, _) => + manager.publish( + id, + SquadResponse.SquadRelatedComment(s"An invitation to join a squad has ended.") + ) + } + val cleanedUpQueuedInvites = manager.cleanUpQueuedInvitesForSquadAndPosition(features.Squad.GUID, position) + if (features.Squad.Capacity == features.Squad.Size) { + val cleanedUpActiveInvites = manager.cleanUpActiveInvitesForSquad(features.Squad.GUID) + cleanedUpActiveInvites.collect { case (id, invites) => + invites.foreach(_.handleCancel(manager, player, id)) + manager.publish( + id, + SquadResponse.SquadRelatedComment(s"An invitation to join a squad has ended.") + ) + } + (manager.cleanUpQueuedInvitesForSquad(features.Squad.GUID) ++ cleanedUpActiveInvites ++ cleanedUpQueuedInvites).collectFirst { case _ => + manager.publish( + leaderCharId, + SquadResponse.SquadRelatedComment(s"You had invitations that were cancelled due to this action.") + ) + } + } else if (cleanedUpQueuedInvites.nonEmpty) { + manager.publish( + leaderCharId, + SquadResponse.SquadRelatedComment(s"You had invitations that were cancelled due to this action.") + ) + } + features + } + .orElse { + manager.publish( + leaderCharId, + SquadResponse.SquadRelatedComment(s"Your invitation to ${player.Name} was accepted, but failed.") + ) + manager.publish( + invitedPlayer, + SquadResponse.SquadRelatedComment(s"You have failed to joined the squad '${features.Squad.Task}'.") + ) + None + } + } else { + + } + } + + def handleRejection( + manager: SquadInvitationManager, + player: Player, + rejectingPlayer: Long, + @unused squadsToLeaders: List[(PlanetSideGUID, SquadFeatures)] + ): Unit = { + doRejection(manager, player, rejectingPlayer) + manager.publish( + originalRequester.CharId, + SquadResponse.SquadRelatedComment(s"Your request to join the squad has been refused.") + ) + manager.publish( + rejectingPlayer, + SquadResponse.SquadRelatedComment(s"You refused ${originalRequester.Name}'s request to join this squad.") + ) + } + + def doRejection( + manager: SquadInvitationManager, + player: Player, + rejectingPlayer: Long + ): Unit = { + features.DeniedPlayers(originalRequester.CharId) + } + + def handleCancel( + manager: SquadInvitationManager, + player: Player, + handlingPlayer: Long + ): Unit = { + val invitingPlayer = originalRequester.CharId + val invitingPlayerName = originalRequester.Name + val actingPlayer = player.CharId + val leaderCharId = features.Squad.Leader.CharId + val leaderName = features.Squad.Leader.Name + if (actingPlayer == handlingPlayer) { + manager.publish( + invitingPlayer, + SquadResponse.SquadRelatedComment(s"You were declined admission to a squad.") + ) + } else if (actingPlayer == invitingPlayer) { + manager.publish( + leaderCharId, + SquadResponse.SquadRelatedComment(s"$invitingPlayerName has rescinded the offer to join the squad.") + ) + } else { + manager.publish( + leaderCharId, + SquadResponse.SquadRelatedComment(s"The request from $invitingPlayerName to join the squad is no longer valid.") + ) + manager.publish( + invitingPlayer, + SquadResponse.SquadRelatedComment(s"The offer to $leaderName to join the squad is no longer valid.") + ) + } + } + + def canBeAutoApproved: Boolean = true + + def getOptionalSquad: Option[SquadFeatures] = Some(features) + + def getPlayer: Player = originalRequester + + def appliesToPlayer(playerCharId: Long): Boolean = playerCharId == originalRequester.CharId + + def appliesToSquad(guid: PlanetSideGUID): Boolean = features.Squad.GUID == guid + + def appliesToSquadAndPosition(guid: PlanetSideGUID, squadPosition: Int): Boolean = false +} diff --git a/src/main/scala/net/psforever/services/teamwork/invitations/Invitation.scala b/src/main/scala/net/psforever/services/teamwork/invitations/Invitation.scala new file mode 100644 index 00000000..0d6d2fa3 --- /dev/null +++ b/src/main/scala/net/psforever/services/teamwork/invitations/Invitation.scala @@ -0,0 +1,88 @@ +// Copyright (c) 2024 PSForever +package net.psforever.services.teamwork.invitations + +import net.psforever.objects.Player +import net.psforever.objects.teamwork.SquadFeatures +import net.psforever.services.teamwork.SquadInvitationManager +import net.psforever.types.PlanetSideGUID + +/** + * The base of all objects that exist for the purpose of communicating invitation from one player to the next. + * @param charId inviting player's unique identifier number + * @param name inviting player's name + */ +abstract class Invitation(charId: Long, name: String) { + def inviterCharId: Long = charId + def inviterName: String = name + + /** + * A branched response for processing (new) invitation objects that have been submitted to the system.
+ *
+ * A comparison is performed between the original invitation object and an invitation object + * that represents the potential modification or redirection of the current active invitation obect. + * Any further action is only performed when an "is equal" comparison is `true`. + * When passing, the system publishes up to two messages + * to users that would anticipate being informed of squad join activity. + * @param indirectInviteFunc the method that cans the responding behavior should an `IndirectInvite` object being consumed + * @param invitedPlayer the unique character identifier for the player being invited; + * in actuality, represents the player who will address the invitation object + * @param invitingPlayer the unique character identifier for the player who invited the former + * @param otherName a name to be used in message composition + */ + def handleInvitation(indirectInviteFunc: (IndirectInvite, Player, Long, Long, String) => Boolean)( + manager: SquadInvitationManager, + invitedPlayer: Long, + invitingPlayer: Long, + otherName: String + ): Unit + + def handleAcceptance( + manager: SquadInvitationManager, + player: Player, + invitedPlayer: Long, + invitedPlayerSquadOpt: Option[SquadFeatures] + ): Unit + + def handleRejection( + manager: SquadInvitationManager, + player: Player, + rejectingPlayer: Long, + squadsToLeaders: List[(PlanetSideGUID, SquadFeatures)] + ): Unit + + def doRejection( + manager: SquadInvitationManager, + player: Player, + rejectingPlayer: Long + ): Unit + + /** + * na + * + * @param manager subscription package + * @param handlingPlayer player who was intended to handle this invitation + * @param player player who caused cleanup action + */ + def handleCancel( + manager: SquadInvitationManager, + player: Player, + handlingPlayer: Long + ): Unit + + def canBeAutoApproved: Boolean + + def getOptionalSquad: Option[SquadFeatures] + + /** + * na + * @return active player entity associated with this invite; + * can be `null` as some invitations do not retain such character data + */ + def getPlayer: Player + + def appliesToPlayer(playerCharId: Long): Boolean + + def appliesToSquad(guid: PlanetSideGUID): Boolean + + def appliesToSquadAndPosition(guid: PlanetSideGUID, squadPosition: Int): Boolean +} diff --git a/src/main/scala/net/psforever/services/teamwork/invitations/InvitationToCreateASquad.scala b/src/main/scala/net/psforever/services/teamwork/invitations/InvitationToCreateASquad.scala new file mode 100644 index 00000000..efbdd908 --- /dev/null +++ b/src/main/scala/net/psforever/services/teamwork/invitations/InvitationToCreateASquad.scala @@ -0,0 +1,170 @@ +// Copyright (c) 2024 PSForever +package net.psforever.services.teamwork.invitations + +import net.psforever.objects.Player +import net.psforever.objects.teamwork.SquadFeatures +import net.psforever.services.teamwork.SquadInvitationManager.FinishStartSquad +import net.psforever.services.teamwork.{SquadInvitationManager, SquadResponse} +import net.psforever.types.{PlanetSideGUID, SquadResponseType} + +import scala.annotation.unused +import scala.util.Success + +/** + * Utilized when one player issues an invite for some other player for a squad that does not yet exist. + * This invitation is handled by the player who would be joining the squad. + * + * @param futureSquadLeader player who wishes to become the leader of a squad + */ +final case class InvitationToCreateASquad(futureSquadLeader: Player) + extends Invitation(futureSquadLeader.CharId, futureSquadLeader.Name) { + def handleInvitation(indirectInviteFunc: (IndirectInvite, Player, Long, Long, String) => Boolean)( + manager: SquadInvitationManager, + invitedPlayer: Long, + invitingPlayer: Long, + otherName: String + ): Unit = { + manager.publish( + invitedPlayer, + SquadResponse.Membership(SquadResponseType.Invite, inviterCharId, Some(invitedPlayer), futureSquadLeader.Name, unk5 = false) + ) + manager.publish( + inviterCharId, + SquadResponse.Membership(SquadResponseType.Invite, invitedPlayer, Some(inviterCharId), futureSquadLeader.Name, unk5 = true) + ) + } + + def handleAcceptance( + manager: SquadInvitationManager, + player: Player, + invitedPlayer: Long, + invitedPlayerSquadOpt: Option[SquadFeatures] + ): Unit = { + if (manager.notLimitedByEnrollmentInSquad(invitedPlayerSquadOpt, invitedPlayer)) { + //accepted an invitation to join an existing squad + import scala.concurrent.ExecutionContext.Implicits.global + val leaderCharId = futureSquadLeader.CharId + manager + .askToCreateANewSquad(futureSquadLeader) + .onComplete { + case Success(FinishStartSquad(features)) => + manager + .handleVacancyInvite(features, invitedPlayer, leaderCharId, player) + .collect { + case (_, line) if manager.joinSquad(player, features, line) => + manager.publish( + leaderCharId, + SquadResponse.Membership(SquadResponseType.Accept, invitedPlayer, Some(leaderCharId), "", unk5 = false) + ) + manager.publish( + invitedPlayer, + SquadResponse.Membership(SquadResponseType.Accept, leaderCharId, Some(invitedPlayer), player.Name, unk5 = true) + ) + //all invitations involving the invited person must be cancelled due to the nature of this acceptance + manager.cleanUpQueuedInvitesForPlayer(invitedPlayer) + val cleanedUpActiveInvites = manager.cleanUpAllInvitesForPlayer(invitedPlayer) + cleanedUpActiveInvites.collect { case (id, invites) => + invites.foreach(_.handleCancel(manager, player, id)) + manager.publish( + id, + SquadResponse.SquadRelatedComment(s"An invitation involving ${futureSquadLeader.Name} has ended.") + ) + } + features + } + .orElse { + manager.publish( + leaderCharId, + SquadResponse.SquadRelatedComment(s"Though a squad has been created, a member could not join it.") + ) + manager.publish( + invitedPlayer, + SquadResponse.SquadRelatedComment(s"You could not join ${futureSquadLeader.Name} squad.") + ) + None + } + //since a squad was created, currently operated by the leader, all invitations related to the leader have changed + manager.cleanUpAllInvitesForPlayer(leaderCharId).collectFirst { _ => + manager.publish( + leaderCharId, + SquadResponse.SquadRelatedComment(s"You had invitations that were cancelled due to this action.") + ) + } + case _ => + org.log4s.getLogger("InvitationToCreateASquad").error("could not create a squad when requested") + } + } + } + + def handleRejection( + manager: SquadInvitationManager, + player: Player, + rejectingPlayer: Long, + @unused squadsToLeaders: List[(PlanetSideGUID, SquadFeatures)] + ): Unit = { + //rejectingPlayer is the would-be squad member; the would-be squad leader sent the request and was rejected + val invitingPlayerCharId = futureSquadLeader.CharId + doRejection(manager, player, rejectingPlayer) + manager.publish( + rejectingPlayer, + SquadResponse.Membership(SquadResponseType.Reject, rejectingPlayer, Some(invitingPlayerCharId), "", unk5 = true) + ) + manager.publish( + invitingPlayerCharId, + SquadResponse.Membership(SquadResponseType.Reject, invitingPlayerCharId, Some(rejectingPlayer), player.Name, unk5 = false) + ) + manager.publish( + rejectingPlayer, + SquadResponse.SquadRelatedComment(s"Your request to form a squad has been refused.") + ) + } + + def doRejection( + manager: SquadInvitationManager, + player: Player, + rejectingPlayer: Long + ): Unit = { + manager.refused(rejectingPlayer, futureSquadLeader.CharId) + } + + def handleCancel( + manager: SquadInvitationManager, + player: Player, + handlingPlayer: Long + ): Unit = { + val actingPlayer = player.CharId + val leaderCharId = futureSquadLeader.CharId + if (actingPlayer == handlingPlayer) { + manager.publish( + leaderCharId, + SquadResponse.SquadRelatedComment(s"${player.Name} has declined joining into a squad with you, or the offer is no longer valid.") + ) + } else if (actingPlayer == leaderCharId) { + manager.publish( + handlingPlayer, + SquadResponse.SquadRelatedComment(s"${futureSquadLeader.Name} has decided not to join into a squad with you, or the offer is no longer valid.") + ) + } else { + manager.publish( + leaderCharId, + SquadResponse.SquadRelatedComment(s"The offer to ${player.Name} to join into a squad with you is no longer valid.") + ) + manager.publish( + handlingPlayer, + SquadResponse.SquadRelatedComment(s"The offer from ${futureSquadLeader.Name} join into a squad with you is no longer valid.") + ) + } + } + + def canBeAutoApproved: Boolean = false + + def getOptionalSquad: Option[SquadFeatures] = None + + def getPlayer: Player = futureSquadLeader + + def appliesToPlayer(playerCharId: Long): Boolean = playerCharId == futureSquadLeader.CharId + + def appliesToSquad(guid: PlanetSideGUID): Boolean = false + + def appliesToSquadAndPosition(guid: PlanetSideGUID, squadPosition: Int): Boolean = false +} diff --git a/src/main/scala/net/psforever/services/teamwork/invitations/InvitationToJoinSquad.scala b/src/main/scala/net/psforever/services/teamwork/invitations/InvitationToJoinSquad.scala new file mode 100644 index 00000000..8f43482d --- /dev/null +++ b/src/main/scala/net/psforever/services/teamwork/invitations/InvitationToJoinSquad.scala @@ -0,0 +1,167 @@ +// Copyright (c) 2024 PSForever +package net.psforever.services.teamwork.invitations + +import net.psforever.objects.Player +import net.psforever.objects.teamwork.SquadFeatures +import net.psforever.services.teamwork.{SquadInvitationManager, SquadResponse} +import net.psforever.types.{PlanetSideGUID, SquadResponseType} + +import scala.annotation.unused + +/** + * Utilized when one squad member issues an invite for some other player. + * Accessed by an existing squad member using the "Invite" menu option on another player. + * This invitation is handled by the player who would join the squad. + * + * @param charId unique character identifier of the player who sent the invite + * @param name name the player who sent the invite + * @param features the squad + */ +final case class InvitationToJoinSquad(charId: Long, name: String, features: SquadFeatures) + extends Invitation(charId, name) { + def handleInvitation(indirectInviteFunc: (IndirectInvite, Player, Long, Long, String) => Boolean)( + manager: SquadInvitationManager, + invitedPlayer: Long, + invitingPlayer: Long, + otherName: String + ): Unit = { + manager.publish( + invitedPlayer, + SquadResponse.Membership(SquadResponseType.Invite, charId, Some(invitedPlayer), name, unk5 = false) + ) + manager.publish( + charId, + SquadResponse.Membership(SquadResponseType.Invite, invitedPlayer, Some(charId), name, unk5 = true) + ) + } + + def handleAcceptance( + manager: SquadInvitationManager, + player: Player, + invitedPlayer: Long, + @unused invitedPlayerSquadOpt: Option[SquadFeatures] + ): Unit = { + if ( + manager.notLimitedByEnrollmentInSquad(invitedPlayerSquadOpt, invitedPlayer) && + SquadInvitationManager.canEnrollInSquad(features, invitedPlayer) + ) { + //accepted an invitation to join an existing squad + val leaderCharId = charId + manager + .handleVacancyInvite(features, invitedPlayer, charId, player) + .collect { + case (_, line) if manager.joinSquad(player, features, line) => + //manager.acceptanceMessages(charId, invitedPlayer, player.Name) + manager.publish( + leaderCharId, + SquadResponse.SquadRelatedComment(s"Your invitation to ${player.Name} was accepted.") + ) + manager.publish( + invitedPlayer, + SquadResponse.SquadRelatedComment(s"You have joined the squad '${features.Squad.Task}'.") + ) + //all invitations involving the invited person must be cancelled due to the nature of this acceptance + manager.cleanUpQueuedInvitesForPlayer(invitedPlayer).collect { case (id, _) => + manager.publish( + id, + SquadResponse.SquadRelatedComment(s"An invitation involving ${player.Name} has ended.") + ) + } + if (features.Squad.Capacity == features.Squad.Size) { + val cleanedUpActiveInvites = manager.cleanUpActiveInvitesForSquad(features.Squad.GUID) + cleanedUpActiveInvites.collect { case (id, invites) => + invites.foreach(_.handleCancel(manager, player, id)) + manager.publish( + id, + SquadResponse.SquadRelatedComment(s"An invitation to join a squad has ended.") + ) + } + (manager.cleanUpQueuedInvitesForSquad(features.Squad.GUID) ++ cleanedUpActiveInvites).collectFirst { case _ => + manager.publish( + leaderCharId, + SquadResponse.SquadRelatedComment(s"You had invitations that were cancelled due to this action.") + ) + } + } + features + } + .orElse { + manager.publish( + leaderCharId, + SquadResponse.SquadRelatedComment(s"Your invitation to ${player.Name} was accepted, but failed.") + ) + manager.publish( + invitedPlayer, + SquadResponse.SquadRelatedComment(s"You have failed to joined the squad '${features.Squad.Task}'.") + ) + None + } + } + } + + def handleRejection( + manager: SquadInvitationManager, + player: Player, + rejectingPlayer: Long, + @unused squadsToLeaders: List[(PlanetSideGUID, SquadFeatures)] + ): Unit = { + /*if SquadInvitationManager.notLeaderOfThisSquad(squadsToLeaders, features.Squad.GUID, rejectingPlayer)*/ + //rejectingPlayer is the would-be squad member; the squad leader sent the request and was rejected + doRejection(manager, player, rejectingPlayer) + manager.rejectionMessages(rejectingPlayer, charId, player.Name) + manager.publish( + rejectingPlayer, + SquadResponse.SquadRelatedComment(s"Your request to join squad '${features.Squad.Task}' has been refused.") + ) + } + + def doRejection( + manager: SquadInvitationManager, + player: Player, + rejectingPlayer: Long + ): Unit = { + manager.refused(rejectingPlayer, charId) + } + + def handleCancel( + manager: SquadInvitationManager, + player: Player, + handlingPlayer: Long + ): Unit = { + val actingPlayer = player.CharId + val leaderCharId = features.Squad.Leader.CharId + val leaderName = features.Squad.Leader.Name + if (actingPlayer == handlingPlayer) { + manager.publish( + leaderCharId, + SquadResponse.SquadRelatedComment(s"${player.Name} has declined to join the squad.") + ) + } else if (actingPlayer == leaderCharId) { + manager.publish( + handlingPlayer, + SquadResponse.SquadRelatedComment(s"$leaderName has rescinded the offer to join the squad.") + ) + } else { + manager.publish( + leaderCharId, + SquadResponse.SquadRelatedComment(s"The offer to ${player.Name} to join the squad is no longer valid.") + ) + manager.publish( + handlingPlayer, + SquadResponse.SquadRelatedComment(s"The offer from $leaderName to join the squad is no longer valid.") + ) + } + } + + def canBeAutoApproved: Boolean = false + + def getOptionalSquad: Option[SquadFeatures] = Some(features) + + def getPlayer: Player = null + + def appliesToPlayer(playerCharId: Long): Boolean = playerCharId == charId + + def appliesToSquad(guid: PlanetSideGUID): Boolean = features.Squad.GUID == guid + + def appliesToSquadAndPosition(guid: PlanetSideGUID, squadPosition: Int): Boolean = false +} diff --git a/src/main/scala/net/psforever/services/teamwork/invitations/LookingForSquadRoleInvite.scala b/src/main/scala/net/psforever/services/teamwork/invitations/LookingForSquadRoleInvite.scala new file mode 100644 index 00000000..7994efb4 --- /dev/null +++ b/src/main/scala/net/psforever/services/teamwork/invitations/LookingForSquadRoleInvite.scala @@ -0,0 +1,173 @@ +// Copyright (c) 2024 PSForever +package net.psforever.services.teamwork.invitations + +import net.psforever.objects.teamwork.{Member, SquadFeatures} +import net.psforever.objects.Player +import net.psforever.services.teamwork.{SquadInvitationManager, SquadResponse} +import net.psforever.types.{PlanetSideGUID, SquadResponseType} + +import scala.annotation.unused + +/** + * Utilized in conjunction with an external queuing data structure + * to search for and submit requests to other players + * for the purposes of fill out an unoccupied squad role. + * This invitation is handled by the player who would be joining the squad. + * + * @param squadLeader squad leader + * @param features squad with the role + * @param position index of the role + */ +final case class LookingForSquadRoleInvite(squadLeader: Member, features: SquadFeatures, position: Int) + extends Invitation(squadLeader.CharId, squadLeader.Name) { + def handleInvitation(indirectInviteFunc: (IndirectInvite, Player, Long, Long, String) => Boolean)( + manager: SquadInvitationManager, + invitedPlayer: Long, + invitingPlayer: Long, + otherName: String + ): Unit = { + manager.publish( + invitedPlayer, + SquadResponse.Membership(SquadResponseType.Invite, invitedPlayer, Some(squadLeader.CharId), squadLeader.Name, unk5 = false) + ) + } + + def handleAcceptance( + manager: SquadInvitationManager, + player: Player, + invitedPlayer: Long, + @unused invitedPlayerSquadOpt: Option[SquadFeatures] + ): Unit = { + val leaderCharId = squadLeader.CharId + if ( + manager.notLimitedByEnrollmentInSquad(invitedPlayerSquadOpt, invitedPlayer) && + SquadInvitationManager.canEnrollInSquad(features, invitedPlayer) && + manager.joinSquad(player, features, position) + ) { + //join this squad + //manager.acceptanceMessages(invitedPlayer, requestee.CharId, requestee.Name) + val msg = SquadResponse.Membership(SquadResponseType.Accept, invitedPlayer, Some(leaderCharId), player.Name, unk5 = false) + manager.publish(leaderCharId, msg) + manager.publish(invitedPlayer, msg.copy(unk5 = true)) +// manager.publish( +// invitedPlayer, +// SquadResponse.SquadRelatedComment(s"You have accepted ${squadLeader.Name}'s request to join a squad.") +// ) +// manager.publish( +// leaderCharId, +// SquadResponse.SquadRelatedComment(s"${player.Name} has agreed to joined your squad.") +// ) + //clean up invitations specifically for this squad and this position + val cleanedUpActiveInvitesForSquadAndPosition = manager.cleanUpActiveInvitesForSquadAndPosition(features.Squad.GUID, position) + cleanedUpActiveInvitesForSquadAndPosition.collect { case (id, _) => + manager.publish( + id, + SquadResponse.SquadRelatedComment(s"An invitation to join a squad has ended.") + ) + } + val cleanedUpQueuedInvites = manager.cleanUpQueuedInvitesForSquadAndPosition(features.Squad.GUID, position) + if (features.Squad.Capacity == features.Squad.Size) { + val cleanedUpActiveInvites = manager.cleanUpActiveInvitesForSquad(features.Squad.GUID) + cleanedUpActiveInvites.collect { case (id, invites) => + invites.foreach(_.handleCancel(manager, player, id)) + manager.publish( + id, + SquadResponse.SquadRelatedComment(s"An invitation to join a squad has ended.") + ) + } + (manager.cleanUpQueuedInvitesForSquad(features.Squad.GUID) ++ cleanedUpActiveInvites ++ cleanedUpQueuedInvites).collectFirst { case _ => + manager.publish( + leaderCharId, + SquadResponse.SquadRelatedComment(s"You had invitations that were cancelled due to this action.") + ) + } + } else if (cleanedUpQueuedInvites.nonEmpty) { + manager.publish( + leaderCharId, + SquadResponse.SquadRelatedComment(s"You had invitations that were cancelled due to this action.") + ) + } + } else { + manager.publish( + invitedPlayer, + SquadResponse.SquadRelatedComment(s"Your accepted an invitation to squad '${features.Squad.Task}', but it failed.") + ) + manager.publish( + leaderCharId, + SquadResponse.SquadRelatedComment(s"An accepted request to join your squad has failed.") + ) + } + } + + def handleRejection( + manager: SquadInvitationManager, + player: Player, + rejectingPlayer: Long, + @unused squadsToLeaders: List[(PlanetSideGUID, SquadFeatures)] + ): Unit = { + val leaderCharId = squadLeader.CharId + //rejectingPlayer is the would-be squad member; the squad leader sent the request and was rejected + doRejection(manager, player, rejectingPlayer) + manager.rejectionMessages(rejectingPlayer, leaderCharId, player.Name) + manager.publish( + rejectingPlayer, + SquadResponse.SquadRelatedComment(s"Your request to join squad '${features.Squad.Task}' has been refused.") + ) + } + + def doRejection( + manager: SquadInvitationManager, + player: Player, + rejectingPlayer: Long + ): Unit = { + val faction = player.Faction + manager.reloadSearchForRoleInvite( + player.Zone.Players.filter(_.faction == faction), + rejectingPlayer, + features, + position + ) + } + + def handleCancel( + manager: SquadInvitationManager, + player: Player, + handlingPlayer: Long + ): Unit = { + val actingPlayer = player.CharId + val leaderCharId = features.Squad.Leader.CharId + val leaderName = features.Squad.Leader.Name + if (actingPlayer == handlingPlayer) { + manager.publish( + leaderCharId, + SquadResponse.SquadRelatedComment(s"${player.Name} has declined to join the squad.") + ) + } else if (actingPlayer == leaderCharId) { + manager.publish( + handlingPlayer, + SquadResponse.SquadRelatedComment(s"$leaderName has rescinded the offer to join the squad.") + ) + } else { + manager.publish( + leaderCharId, + SquadResponse.SquadRelatedComment(s"The offer to ${player.Name} to join the squad is no longer valid.") + ) + manager.publish( + handlingPlayer, + SquadResponse.SquadRelatedComment(s"The offer from $leaderName to join the squad is no longer valid.") + ) + } + } + + def canBeAutoApproved: Boolean = false + + def getOptionalSquad: Option[SquadFeatures] = Some(features) + + def getPlayer: Player = null + + def appliesToPlayer(playerCharId: Long): Boolean = playerCharId == squadLeader.CharId + + def appliesToSquad(guid: PlanetSideGUID): Boolean = features.Squad.GUID == guid + + def appliesToSquadAndPosition(guid: PlanetSideGUID, squadPosition: Int): Boolean = appliesToSquad(guid) && position == squadPosition +} diff --git a/src/main/scala/net/psforever/services/teamwork/invitations/PermissionToReverseInvitationToSquad.scala b/src/main/scala/net/psforever/services/teamwork/invitations/PermissionToReverseInvitationToSquad.scala new file mode 100644 index 00000000..f12a9cdd --- /dev/null +++ b/src/main/scala/net/psforever/services/teamwork/invitations/PermissionToReverseInvitationToSquad.scala @@ -0,0 +1,90 @@ +// Copyright (c) 2024 PSForever +package net.psforever.services.teamwork.invitations + +import net.psforever.objects.Player +import net.psforever.objects.teamwork.SquadFeatures +import net.psforever.services.teamwork.{SquadInvitationManager, SquadResponse} +import net.psforever.types.PlanetSideGUID + +/** + * When requesting to that some other player join a newly-formed squad, + * but that player is actually the member of a squad already, + * this offer is extended to convert the invitation request into a different invitation request. + * The "different invitation" will be asking the leader of the other player's squad if our player can join it. + * Only technically an "invitation" in that sense, just for the purposes of handling it. + * This "invitation" is handled by the player who tried to initiate the original invitation to the other player. + * @param initialRequest player who would be joining the squad + * @param invitedPlayer player who would be joining the squad (unique character id) + * @param invitedPlayerSquad squad + */ +case class PermissionToReverseInvitationToSquad(initialRequest: Player, invitedPlayer: Long, invitedPlayerSquad: SquadFeatures) + extends Invitation(initialRequest.CharId, initialRequest.Name) { + + def handleInvitation(indirectInviteFunc: (IndirectInvite, Player, Long, Long, String) => Boolean)( + manager: SquadInvitationManager, + invitedPlayer: Long, + invitingPlayer: Long, + otherName: String + ): Unit = { + manager.publish( + invitingPlayer, + SquadResponse.SquadRelatedComment(s"\\#6 The player you tried to invite already belongs to a squad.") + ) + manager.publish( + invitingPlayer, + SquadResponse.SquadRelatedComment(s"\\#6Would you like to try join that squad? (respond with \\#3/accept\\#6 or \\#3/cancel\\#6)") + ) + } + + def handleAcceptance( + manager: SquadInvitationManager, + player: Player, + invitedPlayer: Long, + invitedPlayerSquadOpt: Option[SquadFeatures] + ): Unit = { + manager.createIndirectInvite(player, invitedPlayer, invitedPlayerSquad) //should put it at the front of the list + } + + def handleRejection( + manager: SquadInvitationManager, + player: Player, + rejectingPlayer: Long, + squadsToLeaders: List[(PlanetSideGUID, SquadFeatures)] + ): Unit = { + /* wordless rejection */ + } + + def doRejection( + manager: SquadInvitationManager, + player: Player, + rejectingPlayer: Long + ): Unit = { + /* wordless rejection */ + } + + def handleCancel( + manager: SquadInvitationManager, + player: Player, + handlingPlayer: Long + ): Unit = { + val actingPlayer = player.CharId + if (actingPlayer != handlingPlayer) { + manager.publish( + handlingPlayer, + SquadResponse.SquadRelatedComment(s"A question regarding squad invitations no longer matters.") + ) + } + } + + def canBeAutoApproved: Boolean = false + + def getOptionalSquad: Option[SquadFeatures] = Some(invitedPlayerSquad) + + def getPlayer: Player = initialRequest + + def appliesToPlayer(playerCharId: Long): Boolean = invitedPlayer == playerCharId + + def appliesToSquad(guid: PlanetSideGUID): Boolean = invitedPlayerSquad.Squad.GUID == guid + + def appliesToSquadAndPosition(guid: PlanetSideGUID, squadPosition: Int): Boolean = false +} diff --git a/src/main/scala/net/psforever/services/teamwork/invitations/ProximityInvite.scala b/src/main/scala/net/psforever/services/teamwork/invitations/ProximityInvite.scala new file mode 100644 index 00000000..658a6b36 --- /dev/null +++ b/src/main/scala/net/psforever/services/teamwork/invitations/ProximityInvite.scala @@ -0,0 +1,149 @@ +// Copyright (c) 2024 PSForever +package net.psforever.services.teamwork.invitations + +import net.psforever.objects.Player +import net.psforever.objects.teamwork.{Member, SquadFeatures} +import net.psforever.services.teamwork.{SquadInvitationManager, SquadResponse} +import net.psforever.types.{PlanetSideGUID, SquadResponseType} + +import scala.annotation.unused + +/** + * Utilized in conjunction with an external queuing data structure + * to search for and submit requests to other players + * for the purposes of fill out unoccupied squad roles. + * This invitation is handled by the player who would be joining the squad. + * + * @param squadLeader squad leader + * @param features squad + * @param position index of a role + */ +final case class ProximityInvite(squadLeader: Member, features: SquadFeatures, position: Int) + extends Invitation(squadLeader.CharId, squadLeader.Name) { + def handleInvitation(indirectInviteFunc: (IndirectInvite, Player, Long, Long, String) => Boolean)( + manager: SquadInvitationManager, + invitedPlayer: Long, + invitingPlayer: Long, + otherName: String + ): Unit = { + manager.publish( + invitedPlayer, + SquadResponse.Membership( + SquadResponseType.Invite, + invitedPlayer, + Some(squadLeader.CharId), + squadLeader.Name, + unk5 = false + ) + ) + } + + def handleAcceptance( + manager: SquadInvitationManager, + player: Player, + invitedPlayer: Long, + invitedPlayerSquadOpt: Option[SquadFeatures] + ): Unit = { + val leaderCharId = squadLeader.CharId + //this cleanup activity always happens + features.ProxyInvites = features.ProxyInvites.filterNot { _ == invitedPlayer } + if ( + manager.notLimitedByEnrollmentInSquad(invitedPlayerSquadOpt, invitedPlayer) && + SquadInvitationManager.canEnrollInSquad(features, invitedPlayer) && + manager.joinSquad(player, features, position) + ) { + //join this squad + //manager.acceptanceMessages(invitingPlayer, invitedPlayer, player.Name) + val msg = SquadResponse.Membership(SquadResponseType.Accept, invitedPlayer, Some(leaderCharId), player.Name, unk5 = false) + manager.publish(leaderCharId, msg) + manager.publish(invitedPlayer, msg.copy(unk5 = true)) + //clean up invitations specifically for this squad and this position + val cleanedUpQueuedInvites = manager.cleanUpQueuedInvitesForSquadAndPosition(features.Squad.GUID, position) + if (features.Squad.Capacity == features.Squad.Size) { + val cleanedUpActiveInvites = manager.cleanUpActiveInvitesForSquad(features.Squad.GUID) + cleanedUpActiveInvites.collect { case (id, invites) => + invites.foreach(_.handleCancel(manager, player, id)) + manager.publish( + id, + SquadResponse.SquadRelatedComment(s"An invitation to join a squad has ended.") + ) + } + (manager.cleanUpQueuedInvitesForSquad(features.Squad.GUID) ++ cleanedUpActiveInvites ++ cleanedUpQueuedInvites).collectFirst { case _ => + manager.publish( + leaderCharId, + SquadResponse.SquadRelatedComment(s"You had invitations that were cancelled due to this action.") + ) + } + } else if (cleanedUpQueuedInvites.nonEmpty) { + manager.publish( + leaderCharId, + SquadResponse.SquadRelatedComment(s"You had invitations that were cancelled due to this action.") + ) + } + } else { + //if able to attempt to accept this proximity invite, recruitment is still ongoing + manager.reloadProximityInvite(player.Zone.Players, invitedPlayer, features, position) + } + } + + def handleRejection( + manager: SquadInvitationManager, + player: Player, + rejectingPlayer: Long, + @unused squadsToLeaders: List[(PlanetSideGUID, SquadFeatures)] + ): Unit = { + /*if SquadInvitationManager.notLeaderOfThisSquad(squadsToLeaders, features.Squad.GUID, rejectingPlayer)*/ + //rejectingPlayer is the would-be squad member; the squad leader sent the request and was rejected + doRejection(manager, player, rejectingPlayer) + manager.rejectionMessage(rejectingPlayer) + manager.publish( + rejectingPlayer, + SquadResponse.SquadRelatedComment(s"Your request to join squad '${features.Squad.Task}' has been refused.") + ) + } + + def doRejection( + manager: SquadInvitationManager, + player: Player, + rejectingPlayer: Long + ): Unit = { + manager.reloadProximityInvite(player.Zone.Players, rejectingPlayer, features, position) + } + + def handleCancel( + manager: SquadInvitationManager, + player: Player, + handlingPlayer: Long + ): Unit = { + val actingPlayer = player.CharId + val leaderCharId = squadLeader.CharId + if (actingPlayer == handlingPlayer) { + manager.publish( + handlingPlayer, + SquadResponse.SquadRelatedComment(s"You have declined an offer to join a squad.") + ) + } else if (actingPlayer == leaderCharId) { + manager.publish( + handlingPlayer, + SquadResponse.SquadRelatedComment(s"The offer to join a squad has been cancelled.") + ) + } else { + manager.publish( + handlingPlayer, + SquadResponse.SquadRelatedComment(s"The offer to join into a squad is no longer valid.") + ) + } + } + + def canBeAutoApproved: Boolean = false + + def getOptionalSquad: Option[SquadFeatures] = Some(features) + + def getPlayer: Player = null + + def appliesToPlayer(playerCharId: Long): Boolean = playerCharId == squadLeader.CharId + + def appliesToSquad(guid: PlanetSideGUID): Boolean = features.Squad.GUID == guid + + def appliesToSquadAndPosition(guid: PlanetSideGUID, squadPosition: Int): Boolean = appliesToSquad(guid) && position == squadPosition +} diff --git a/src/main/scala/net/psforever/services/teamwork/invitations/RequestToJoinSquadRole.scala b/src/main/scala/net/psforever/services/teamwork/invitations/RequestToJoinSquadRole.scala new file mode 100644 index 00000000..598633b5 --- /dev/null +++ b/src/main/scala/net/psforever/services/teamwork/invitations/RequestToJoinSquadRole.scala @@ -0,0 +1,164 @@ +// Copyright (c) 2024 PSForever +package net.psforever.services.teamwork.invitations + +import net.psforever.objects.Player +import net.psforever.objects.teamwork.SquadFeatures +import net.psforever.services.teamwork.{SquadInvitationManager, SquadResponse} +import net.psforever.types.PlanetSideGUID + +import scala.annotation.unused + +/** + * Utilized when one player attempts to join an existing squad in a specific role. + * Accessed by the joining player from the squad detail window. + * This invitation is handled by the squad leader. + * + * @param requestee player who requested the role + * @param features squad with the role + * @param position index of the role + */ +final case class RequestToJoinSquadRole(requestee: Player, features: SquadFeatures, position: Int) + extends Invitation(requestee.CharId, requestee.Name) { + def handleInvitation(indirectInviteFunc: (IndirectInvite, Player, Long, Long, String) => Boolean)( + manager: SquadInvitationManager, + invitedPlayer: Long, + invitingPlayer: Long, + otherName: String + ): Unit = { + SquadInvitationManager.handleRequestRole(manager, requestee, bid = this) + } + + def handleAcceptance( + manager: SquadInvitationManager, + @unused player: Player, + invitedPlayer: Long, + @unused invitedPlayerSquadOpt: Option[SquadFeatures] + ): Unit = { + //player requested to join a squad's specific position + //invitedPlayer is actually the squad leader; petitioner is the actual "invitedPlayer" + val leaderCharId = player.CharId + val requestingPlayer = requestee.CharId + if ( + SquadInvitationManager.canEnrollInSquad(features, requestee.CharId) && + manager.joinSquad(requestee, features, position) + ) { + //manager.acceptanceMessages(invitedPlayer, requestee.CharId, requestee.Name) + manager.publish( + leaderCharId, + SquadResponse.SquadRelatedComment(s"You have accepted ${requestee.Name}'s request to join your squad.") + ) + manager.publish( + requestingPlayer, + SquadResponse.SquadRelatedComment(s"You have joined the squad '${features.Squad.Task}'.") + ) + //clean up invitations specifically for this squad and this position + val cleanedUpActiveInvitesForSquadAndPosition = manager.cleanUpActiveInvitesForSquadAndPosition(features.Squad.GUID, position) + cleanedUpActiveInvitesForSquadAndPosition.collect { case (id, invites) => + invites.foreach(_.handleCancel(manager, player, id)) + manager.publish( + id, + SquadResponse.SquadRelatedComment(s"An invitation to join a squad has ended.") + ) + } + val cleanedUpQueuedInvites = manager.cleanUpQueuedInvitesForSquadAndPosition(features.Squad.GUID, position) + if (features.Squad.Capacity == features.Squad.Size) { + val cleanedUpActiveInvites = manager.cleanUpActiveInvitesForSquad(features.Squad.GUID) + cleanedUpActiveInvites.collect { case (id, _) => + manager.publish( + id, + SquadResponse.SquadRelatedComment(s"An invitation to join a squad has ended.") + ) + } + (manager.cleanUpQueuedInvitesForSquad(features.Squad.GUID) ++ cleanedUpActiveInvites ++ cleanedUpQueuedInvites).collectFirst { case _ => + manager.publish( + leaderCharId, + SquadResponse.SquadRelatedComment(s"You had invitations that were cancelled due to this action.") + ) + } + } else if (cleanedUpQueuedInvites.nonEmpty) { + manager.publish( + leaderCharId, + SquadResponse.SquadRelatedComment(s"You had invitations that were cancelled due to this action.") + ) + } + } else { + manager.publish( + requestingPlayer, + SquadResponse.SquadRelatedComment(s"Your invitation to squad '${features.Squad.Task}' was accepted, but failed.") + ) + manager.publish( + leaderCharId, + SquadResponse.SquadRelatedComment(s"An accepted request to join your squad has failed.") + ) + } + } + + def handleRejection( + manager: SquadInvitationManager, + @unused player: Player, + rejectingPlayer: Long, + @unused squadsToLeaders: List[(PlanetSideGUID, SquadFeatures)] + ): Unit = { + if (SquadInvitationManager.notLeaderOfThisSquad(squadsToLeaders, features.Squad.GUID, requestee.CharId)) { + //rejected is the would-be squad member; rejectingPlayer is the squad leader who rejected the request + doRejection(manager, player, rejectingPlayer) + manager.rejectionMessage(rejectingPlayer) + manager.publish( + rejectingPlayer, + SquadResponse.SquadRelatedComment(s"Your request to join squad '${features.Squad.Task}' has been refused.") + ) + } + } + + def doRejection( + manager: SquadInvitationManager, + player: Player, + rejectingPlayer: Long + ): Unit = { + features.DeniedPlayers(requestee.CharId) + } + + def handleCancel( + manager: SquadInvitationManager, + player: Player, + handlingPlayer: Long + ): Unit = { + val invitingPlayer = requestee.CharId + val invitingPlayerName = requestee.Name + val actingPlayer = player.CharId + val leaderCharId = features.Squad.Leader.CharId + val leaderName = features.Squad.Leader.Name + if (actingPlayer == handlingPlayer) { + manager.publish( + invitingPlayer, + SquadResponse.SquadRelatedComment(s"You were declined admission to a squad.") + ) + } else if (actingPlayer == invitingPlayer) { + manager.publish( + leaderCharId, + SquadResponse.SquadRelatedComment(s"$invitingPlayerName has rescinded the offer to join the squad.") + ) + } else { + manager.publish( + leaderCharId, + SquadResponse.SquadRelatedComment(s"The request from $invitingPlayerName to join the squad is no longer valid.") + ) + manager.publish( + invitingPlayer, + SquadResponse.SquadRelatedComment(s"The offer to $leaderName to join the squad is no longer valid.") + ) + } + } + + def canBeAutoApproved: Boolean = true + + def getOptionalSquad: Option[SquadFeatures] = Some(features) + + def getPlayer: Player = requestee + + def appliesToPlayer(playerCharId: Long): Boolean = playerCharId == requestee.CharId + + def appliesToSquad(guid: PlanetSideGUID): Boolean = features.Squad.GUID == guid + + def appliesToSquadAndPosition(guid: PlanetSideGUID, squadPosition: Int): Boolean = appliesToSquad(guid) && position == squadPosition +}