diff --git a/common/src/main/scala/net/psforever/objects/Avatar.scala b/common/src/main/scala/net/psforever/objects/Avatar.scala index 0416b6a9..4b689187 100644 --- a/common/src/main/scala/net/psforever/objects/Avatar.scala +++ b/common/src/main/scala/net/psforever/objects/Avatar.scala @@ -48,13 +48,8 @@ class Avatar(private val char_id : Long, val name : String, val faction : Planet private val deployables : DeployableToolbox = new DeployableToolbox /** * Looking For Squad:
- * Used to indicate both the marque that appears underneath a player's nameplate and an actual player state
- * This `Avatar`-specific "Looking for Squad" variable - * is used to indicate the active state of the LFS marque in the game - * and will change to `false` if the player is the member of a squad. - * A client-local version of "Looking for Squad" will maintain the real state of LFS - * once the player has joined a squad. - * The client-local version will restore the `Avatar`-local variable upon leaving the squad. + * Indicates both a player state and the text on the marquee under the player nameplate. + * Should only be valid when the player is not in a squad. */ private var lfs : Boolean = false diff --git a/common/src/main/scala/net/psforever/objects/teamwork/Squad.scala b/common/src/main/scala/net/psforever/objects/teamwork/Squad.scala index 5f653dcd..3ff63035 100644 --- a/common/src/main/scala/net/psforever/objects/teamwork/Squad.scala +++ b/common/src/main/scala/net/psforever/objects/teamwork/Squad.scala @@ -11,11 +11,7 @@ class Squad(squadId : PlanetSideGUID, alignment : PlanetSideEmpire.Value) extend private var zoneId : Option[Int] = None private var task : String = "" private val membership : Array[Member] = Array.fill[Member](10)(new Member) - private val availability : Array[Boolean] = Array.fill[Boolean](10)(true) - private var listed : Boolean = false - private var leaderPositionIndex : Int = 0 - private var autoApproveInvitationRequests : Boolean = false - private var locationFollowsSquadLead : Boolean = false + private val availability : Array[Boolean] = Array.fill[Boolean](10)(elem = true) override def GUID_=(d : PlanetSideGUID) : PlanetSideGUID = GUID @@ -23,14 +19,7 @@ class Squad(squadId : PlanetSideGUID, alignment : PlanetSideEmpire.Value) extend def CustomZoneId : Boolean = zoneId.isDefined - def ZoneId : Int = zoneId.getOrElse({ - membership.lift(leaderPositionIndex) match { - case Some(leader) => - leader.ZoneId - case _ => - 0 - } - }) + def ZoneId : Int = zoneId.getOrElse(membership(0).ZoneId) def ZoneId_=(id : Int) : Int = { ZoneId_=(Some(id)) @@ -48,42 +37,12 @@ class Squad(squadId : PlanetSideGUID, alignment : PlanetSideEmpire.Value) extend Task } - def Listed : Boolean = listed - - def Listed_=(announce : Boolean) : Boolean = { - listed = announce - Listed - } - - def LocationFollowsSquadLead : Boolean = locationFollowsSquadLead - - def LocationFollowsSquadLead_=(follow : Boolean) : Boolean = { - locationFollowsSquadLead = follow - LocationFollowsSquadLead - } - - def AutoApproveInvitationRequests : Boolean = autoApproveInvitationRequests - - def AutoApproveInvitationRequests_=(autoApprove : Boolean) : Boolean = { - autoApproveInvitationRequests = autoApprove - AutoApproveInvitationRequests - } - def Membership : Array[Member] = membership def Availability : Array[Boolean] = availability - def LeaderPositionIndex : Int = leaderPositionIndex - - def LeaderPositionIndex_=(position : Int) : Int = { - if(availability.lift(position).contains(true)) { - leaderPositionIndex = position - } - LeaderPositionIndex - } - def Leader : Member = { - membership(leaderPositionIndex) match { + membership(0) match { case member if !member.Name.equals("") => member case _ => @@ -102,9 +61,7 @@ object Squad { override def ZoneId_=(id : Int) : Int = 0 override def ZoneId_=(id : Option[Int]) : Int = 0 override def Task_=(assignment : String) : String = "" - override def Listed_=(announce : Boolean) : Boolean = false override def Membership : Array[Member] = Array.empty[Member] override def Availability : Array[Boolean] = Array.fill[Boolean](10)(false) - override def LeaderPositionIndex_=(position : Int) : Int = 0 } } diff --git a/common/src/main/scala/net/psforever/objects/teamwork/SquadFeatures.scala b/common/src/main/scala/net/psforever/objects/teamwork/SquadFeatures.scala new file mode 100644 index 00000000..a374f0ab --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/teamwork/SquadFeatures.scala @@ -0,0 +1,154 @@ +// Copyright (c) 2019 PSForever +package net.psforever.objects.teamwork + +import akka.actor.{Actor, ActorContext, ActorRef, Cancellable, Props} +import net.psforever.objects.DefaultCancellable +import services.teamwork.SquadService.WaypointData +import services.teamwork.SquadSwitchboard + +class SquadFeatures(val Squad : Squad) { + /** + * `initialAssociation` per squad is similar to "Does this squad want to recruit members?" + * The squad does not have to be flagged. + * Dispatches an `AssociateWithSquad` `SDAM` to the squad leader and ??? + * and then a `SDDUM` that includes at least the squad owner name and char id. + * Dispatched only once when a squad is first listed + * or when the squad leader searches for recruits by proximity or for certain roles or by invite + * or when a spontaneous squad forms, + * whichever happens first. + * Additionally, the packets are also sent when the check is made when the continent is changed (or set). + */ + private var initialAssociation : Boolean = true + /** + * na + */ + private var switchboard : ActorRef = ActorRef.noSender + /** + * Waypoint data. + * The first four slots are used for squad waypoints. + * The fifth slot is used for the squad leader experience waypoint.
+ *
+ * All of the waypoints constantly exist as long as the squad to which they are attached exists. + * They are merely "activated" and "deactivated." + * When "activated," the waypoint knows on which continent to appear and where on the map and in the game world to be positioned. + * Waypoints manifest in the game world as a far-off beam of light that extends into the sky + * and whose ground contact utilizes a downwards pulsating arrow. + * On the continental map and deployment map, they appear as a diamond, with a differentiating number where applicable. + * The squad leader experience rally, for example, does not have a number like the preceding four waypoints. + * @see `Start` + */ + private var waypoints : Array[WaypointData] = Array[WaypointData]() + /** + * The particular position being recruited right at the moment. + * When `None`. no highlighted searches have been indicated. + * When a positive integer or 0, indicates distributed `LookingForSquadRoleInvite` messages as recorded by `proxyInvites`. + * Only one position may bne actively recruited at a time in this case. + * When -1, indicates distributed `ProximityIvite` messages as recorded by `proxyInvites`. + * Previous efforts may or may not be forgotten if there is a switch between the two modes. + */ + private var searchForRole : Option[Int] = None + /** + * Handle persistent data related to `ProximityInvite` and `LookingForSquadRoleInvite` messages + */ + private var proxyInvites : List[Long] = Nil + + private var requestInvitePrompt : Cancellable = DefaultCancellable.obj + /** + * These useres rejected invitation to this squad. + * For the purposes of wide-searches for membership + * such as Looking For Squad checks and proximity invitation, + * the unique character identifier numbers in this list are skipped. + * Direct invitation requests from the non sqad member should remain functional. + */ + private var refusedPlayers : List[Long] = Nil + private var autoApproveInvitationRequests : Boolean = true + private var locationFollowsSquadLead : Boolean = true + + private var listed : Boolean = false + + private lazy val channel : String = s"${Squad.Faction}-Squad${Squad.GUID.guid}" + + def Start(implicit context : ActorContext) : SquadFeatures = { + switchboard = context.actorOf(Props[SquadSwitchboard], s"squad${Squad.GUID.guid}") + waypoints = Array.fill[WaypointData](5)(new WaypointData()) + this + } + + def Stop : SquadFeatures = { + switchboard ! akka.actor.PoisonPill + switchboard = Actor.noSender + waypoints = Array.empty + requestInvitePrompt.cancel + this + } + + def InitialAssociation : Boolean = initialAssociation + + def InitialAssociation_=(assoc : Boolean) : Boolean = { + initialAssociation = assoc + InitialAssociation + } + + def Switchboard : ActorRef = switchboard + + def Waypoints : Array[WaypointData] = waypoints + + def SearchForRole : Option[Int] = searchForRole + + def SearchForRole_=(role : Int) : Option[Int] = SearchForRole_=(Some(role)) + + def SearchForRole_=(role : Option[Int]) : Option[Int] = { + searchForRole = role + SearchForRole + } + + def ProxyInvites : List[Long] = proxyInvites + + def ProxyInvites_=(list : List[Long]) : List[Long] = { + proxyInvites = list + ProxyInvites + } + + def Refuse : List[Long] = refusedPlayers + + def Refuse_=(charId : Long) : List[Long] = { + Refuse_=(List(charId)) + } + + def Refuse_=(list : List[Long]) : List[Long] = { + refusedPlayers = list ++ refusedPlayers + Refuse + } + + def LocationFollowsSquadLead : Boolean = locationFollowsSquadLead + + def LocationFollowsSquadLead_=(follow : Boolean) : Boolean = { + locationFollowsSquadLead = follow + LocationFollowsSquadLead + } + + def AutoApproveInvitationRequests : Boolean = autoApproveInvitationRequests + + def AutoApproveInvitationRequests_=(autoApprove : Boolean) : Boolean = { + autoApproveInvitationRequests = autoApprove + AutoApproveInvitationRequests + } + + def Listed : Boolean = listed + + def Listed_=(announce : Boolean) : Boolean = { + listed = announce + Listed + } + + def ToChannel : String = channel + + def Prompt : Cancellable = requestInvitePrompt + + def Prompt_=(callback: Cancellable) : Cancellable = { + if(requestInvitePrompt.isCancelled) { + requestInvitePrompt = callback + } + Prompt + } +} \ No newline at end of file diff --git a/common/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala b/common/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala index eec5c312..ac4b49ff 100644 --- a/common/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala @@ -118,7 +118,10 @@ import scodec.codecs._ * `27 - PA_JAMMED - plays jammed buzzing sound`
* `28 - PA_IMPLANT_ACTIVE - Plays implant sounds. Valid values seem to be up to 20.`
* `29 - PA_VAPORIZED - Visible ?! That's not the cloaked effect, Maybe for spectator mode ?. Value is 0 to visible, 1 to invisible.`
- * `31 - Info under avatar name : 0 = LFS, 1 = Looking For Squad Members`
+ * `31 - Looking for Squad info (marquee and ui):
+ * ` - 0 is LFS`
+ * ` - 1 is LFSM (Looking for Squad Members)`
+ * ` - n is the supplemental squad identifier number; same as "LFS;" for the leader, sets "LFSM" after the first manual flagging`
* `32 - Info under avatar name : 0 = Looking For Squad Members, 1 = LFS`
* `35 - BR. Value is the BR`
* `36 - CR. Value is the CR`
diff --git a/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala b/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala index 6fd99e69..eab055a9 100644 --- a/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala @@ -360,6 +360,7 @@ object SquadAction{ *     `17` - Set List Squad (ui)
*     `18` - UNKNOWN
*     `26` - Reset All
+ *     `32` - UNKNOWN
*     `35` - Cancel Squad Search
*     `39` - No Squad Search Results
*     `41` - Cancel Find
diff --git a/common/src/main/scala/services/teamwork/SquadAction.scala b/common/src/main/scala/services/teamwork/SquadAction.scala index 5b3ed90e..a0034da5 100644 --- a/common/src/main/scala/services/teamwork/SquadAction.scala +++ b/common/src/main/scala/services/teamwork/SquadAction.scala @@ -3,11 +3,14 @@ package services.teamwork import net.psforever.objects.zones.Zone import net.psforever.packet.game._ -import net.psforever.types.{SquadRequestType, Vector3} +import net.psforever.types.{PlanetSideEmpire, SquadRequestType, Vector3} object SquadAction { trait Action + final case class InitSquadList() extends Action + final case class InitCharId() extends Action + final case class Definition(guid : PlanetSideGUID, line : Int, action : SquadAction) extends Action final case class Membership(request_type : SquadRequestType.Value, unk2 : Long, unk3 : Option[Long], player_name : String, unk5 : Option[Option[String]]) extends Action final case class Waypoint(event_type : WaypointEventAction.Value, waypoint_type : Int, unk : Option[Long], waypoint_info : Option[WaypointInfo]) extends Action diff --git a/common/src/main/scala/services/teamwork/SquadResponse.scala b/common/src/main/scala/services/teamwork/SquadResponse.scala index 817d27e5..5860145c 100644 --- a/common/src/main/scala/services/teamwork/SquadResponse.scala +++ b/common/src/main/scala/services/teamwork/SquadResponse.scala @@ -20,8 +20,8 @@ object SquadResponse { final case class Membership(request_type : SquadResponseType.Value, unk1 : Int, unk2 : Int, unk3 : Long, unk4 : Option[Long], player_name : String, unk5 : Boolean, unk6 : Option[Option[String]]) extends Response //see SquadMembershipResponse final case class Invite(from_char_id : Long, to_char_id : Long, name : String) extends Response - final case class WantsSquadPosition(bid_name : String) extends Response - final case class Join(squad : Squad, positionsToUpdate : List[Int]) extends Response + final case class WantsSquadPosition(leader_char_id : Long, bid_name : String) extends Response + final case class Join(squad : Squad, positionsToUpdate : List[Int], channel : String) extends Response final case class Leave(squad : Squad, positionsToUpdate : List[(Long, Int)]) extends Response final case class UpdateMembers(squad : Squad, update_info : List[SquadAction.Update]) extends Response final case class AssignMember(squad : Squad, from_index : Int, to_index : Int) extends Response diff --git a/common/src/main/scala/services/teamwork/SquadService.scala b/common/src/main/scala/services/teamwork/SquadService.scala index 2c22886c..b837e7a1 100644 --- a/common/src/main/scala/services/teamwork/SquadService.scala +++ b/common/src/main/scala/services/teamwork/SquadService.scala @@ -1,11 +1,11 @@ // Copyright (c) 2019 PSForever package services.teamwork -import akka.actor.{Actor, ActorContext, ActorRef, Props} -import net.psforever.objects.Player +import akka.actor.{Actor, ActorRef, Terminated} +import net.psforever.objects.{Avatar, LivePlayerList, Player} import net.psforever.objects.definition.converter.StatConverter import net.psforever.objects.loadouts.SquadLoadout -import net.psforever.objects.teamwork.{Member, Squad} +import net.psforever.objects.teamwork.{Member, Squad, SquadFeatures} import net.psforever.objects.zones.Zone import net.psforever.packet.game._ import net.psforever.types._ @@ -15,8 +15,6 @@ import scala.collection.concurrent.TrieMap import scala.collection.mutable import scala.collection.mutable.ListBuffer -//import scala.concurrent.duration._ - class SquadService extends Actor { import SquadService._ @@ -26,31 +24,120 @@ class SquadService extends Actor { * A squad of `PlanetSideGUID(0)` indicates both a nonexistent squad and the default no-squad for clients. */ private var sid : Int = 1 - - private var memberToSquad : mutable.LongMap[Squad] = mutable.LongMap[Squad]() - private val invites : mutable.LongMap[Invitation] = mutable.LongMap[Invitation]() - private val queuedInvites : mutable.LongMap[List[Invitation]] = mutable.LongMap[List[Invitation]]() /** - * A placeholder for an absent active invite that has not (yet) been accepted or rejected, equal to the then-current active invite. - * Created when removing an active invite. - * Checked when trying to add a new invite (if found, the invite is queued). - * Cleared when the next queued invite becomes active. + * All squads.
+ * key - squad unique number; value - the squad wrapped around its attributes object */ - private val previousInvites : mutable.LongMap[Invitation] = mutable.LongMap[Invitation]() - - private var squadFeatures : TrieMap[PlanetSideGUID, SquadService.SquadFeatures] = new TrieMap[PlanetSideGUID, SquadService.SquadFeatures]() - private val publishedLists : TrieMap[PlanetSideEmpire.Value, ListBuffer[SquadInfo]] = TrieMap[PlanetSideEmpire.Value, ListBuffer[SquadInfo]]( + private var squadFeatures : TrieMap[PlanetSideGUID, SquadFeatures] = new TrieMap[PlanetSideGUID, SquadFeatures]() + /** + * The list of squads that each of the factions see for the purposes of keeping track of changes to the list. + * These squads are considered public "listed" squads - + * all the players of a certain faction can see them in the squad list + * and may have limited interaction with their squad definition windows.
+ * key - squad unique number; value - the squad's unique identifier number + */ + private val publishedLists : TrieMap[PlanetSideEmpire.Value, ListBuffer[PlanetSideGUID]] = TrieMap[PlanetSideEmpire.Value, ListBuffer[PlanetSideGUID]]( PlanetSideEmpire.TR -> ListBuffer.empty, PlanetSideEmpire.NC -> ListBuffer.empty, PlanetSideEmpire.VS -> ListBuffer.empty ) + /** + * key - a unique character identifier number; value - the squad to which this player is a member + */ + private var memberToSquad : mutable.LongMap[Squad] = mutable.LongMap[Squad]() + /** + * key - a unique character identifier number; value - the active invitation object + */ + private val invites : mutable.LongMap[Invitation] = mutable.LongMap[Invitation]() + /** + * key - a unique character identifier number; value - a list of inactive invitation objects waiting to be resolved + */ + private val queuedInvites : mutable.LongMap[List[Invitation]] = mutable.LongMap[List[Invitation]]() + /** + * The gien player has refused participation into this other player's sqaud.
+ * key - a unique character identifier number; value - a list of unique character identifier numbers + */ + private val refused : mutable.LongMap[List[Long]] = mutable.LongMap[List[Long]]() + /** + * Players who are interested in updated details regarding a certain squad though they may not be a member of the squad.
+ * key - unique character identifier number; value - a squad identifier number + */ + private val continueToMonitorDetails : mutable.LongMap[PlanetSideGUID] = mutable.LongMap[PlanetSideGUID]() + /** + * A placeholder for an absent active invite that has not (yet) been accepted or rejected, equal to the then-current active invite. + * Created when removing an active invite. + * Checked when trying to add a new invite (if found, the invite is queued). + * Cleared when the next queued invite becomes active.
+ * key - unique character identifier number; value, unique character identifier number + */ + private val previousInvites : mutable.LongMap[Invitation] = mutable.LongMap[Invitation]() + + /** + * This is a formal `ActorEventBus` object that is reserved for faction-wide messages and squad-specific messages. + * When the user joins the `SquadService` with a `Service.Join` message + * that includes a confirmed faction affiliation identifier, + * the origin `ActorRef` is added as a subscription. + * Squad channels are produced when a squad is created, + * and are subscribed to as users join the squad, + * and unsubscribed from as users leave the squad.
+ * key - a `PlanetSideEmpire` value; value - `ActorRef` reference
+ * key - a consistent squad channel name; value - `ActorRef` reference + * @see `CloseSquad` + * @see `JoinSquad` + * @see `LeaveSquad` + * @see `Service.Join` + * @see `Service.Leave` + */ + private val SquadEvents = new GenericEventBus[SquadServiceResponse] + /** + * This collection contains the message-sending contact reference for individuals who may become squad members. + * When the user joins the `SquadService` with a `Service.Join` message + * that includes their unique character identifier, + * the origin `ActorRef` is added as a subscription. + * It is maintained until they disconnect entirely. + * The subscription is anticipated to belong to an instance of `WorldSessionActor`.
+ * key - unique character identifier number; value - `ActorRef` reference for that character + * @see `Service.Join` + */ + private val UserEvents : mutable.LongMap[ActorRef] = mutable.LongMap[ActorRef]() + private [this] val log = org.log4s.getLogger + + private def debug(msg : String) : Unit = { + log.info(msg) + } override def preStart : Unit = { log.info("Starting...") } + override def postStop() : Unit = { + //invitations + invites.clear() + queuedInvites.clear() + previousInvites.clear() + refused.clear() + continueToMonitorDetails.clear() + //squads and members (users) + squadFeatures.foreach { case(_, features) => + CloseSquad(features.Squad) + } + memberToSquad.clear() + publishedLists.clear() + UserEvents.foreach { case(_, actor) => + SquadEvents.unsubscribe(actor) + } + UserEvents.clear() + } + + /** + * Produce the next available unique squad identifier. + * The first number is always 1. + * The greatest possible identifier is 65535 (an unsigned 16-bit integer) + * before it wraps back around to 1. + * @return the current squad unique identifier number + */ def GetNextSquadId() : PlanetSideGUID = { val out = sid val j = sid + 1 @@ -63,6 +150,10 @@ class SquadService extends Actor { PlanetSideGUID(out) } + /** + * Set the unique squad identifier back to the start (1) if no squads are active. + * @return `true`, if the identifier is reset; `false`, otherwise + */ def TryResetSquadId() : Boolean = { if(squadFeatures.isEmpty) { sid = 1 @@ -73,233 +164,377 @@ class SquadService extends Actor { } } - def GetSquad(id : PlanetSideGUID) : Option[Squad] = { - squadFeatures.get(id) match { - case Some(features) => Some(features.Squad) - case None => None - } + /** + * If a squad exists for an identifier, return that squad. + * @param id the squad unique identifier number + * @return the discovered squad, or `None` + */ + def GetSquad(id : PlanetSideGUID) : Option[Squad] = squadFeatures.get(id) match { + case Some(features) => Some(features.Squad) + case None => None } - def GetParticipatingSquad(player : Player) : Option[Squad] = { - GetParticipatingSquad(player.CharId) + /** + * If this player is a member of any squad, discover that squad. + * @param player the potential member + * @return the discovered squad, or `None` + */ + def GetParticipatingSquad(player : Player) : Option[Squad] = GetParticipatingSquad(player.CharId) + + /** + * If the player associated with this unique character identifier number is a member of any squad, discover that squad. + * @param charId the potential member identifier + * @return the discovered squad, or `None` + */ + def GetParticipatingSquad(charId : Long) : Option[Squad] = memberToSquad.get(charId) match { + case opt @ Some(_) => + opt + case None => + None } - def GetParticipatingSquad(charId : Long) : Option[Squad] = { - memberToSquad.get(charId) match { - case opt @ Some(_) => - opt - case None => + /** + * If this player is a member of any squad, discover that squad. + * @see `GetParticipatingSquad` + * @see `Squad::Leader` + * @param player the potential member + * @param opt an optional squad to check; + * the expectation is that the provided squad is a known participating squad + * @return the discovered squad, or `None` + */ + def GetLeadingSquad(player : Player, opt : Option[Squad]) : Option[Squad] = GetLeadingSquad(player.CharId, opt) + + /** + * If the player associated with this unique character identifier number is the leader of any squad, discover that squad. + * @see `GetParticipatingSquad` + * @see `Squad->Leader` + * @param charId the potential member identifier + * @param opt an optional squad to check; + * the expectation is that the provided squad is a known participating squad + * @return the discovered squad, or `None` + */ + def GetLeadingSquad(charId : Long, opt : Option[Squad]) : Option[Squad] = opt.orElse(GetParticipatingSquad(charId)) match { + case Some(squad) => + if(squad.Leader.CharId == charId) { + Some(squad) + } + else { None - } + } + case _ => + None } - def GetLeadingSquad(player : Player, opt : Option[Squad]) : Option[Squad] = { - val charId = player.CharId - opt match { - case Some(squad) => - if(squad.Leader.CharId == charId) { - Some(squad) - } - else { - None - } - - case None => - memberToSquad.get(charId) match { - case Some(squad) if squad.Leader.CharId == charId => - Some(squad) - case _ => - None - } - } + /** + * Overloaded message-sending operation. + * The `Actor` version wraps around the expected `!` functionality. + * @param to an `ActorRef` which to send the message + * @param msg a message that can be stored in a `SquadServiceResponse` object + */ + def Publish(to : ActorRef, msg : SquadResponse.Response) : Unit = { + Publish(to, msg, Nil) } - - def GetLeadingSquad(charId : Long, opt : Option[Squad]) : Option[Squad] = { - opt.orElse(memberToSquad.get(charId)) match { - case Some(squad) => - if(squad.Leader.CharId == charId) { - Some(squad) - } - else { - None - } + /** + * Overloaded message-sending operation. + * The `Actor` version wraps around the expected `!` functionality. + * @param to an `ActorRef` which to send the message + * @param msg a message that can be stored in a `SquadServiceResponse` object + * @param excluded a group of character identifier numbers who should not receive the message + * (resolved at destination) + */ + def Publish(to : ActorRef, msg : SquadResponse.Response, excluded : Iterable[Long]) : Unit = { + to ! SquadServiceResponse("", excluded, msg) + } + /** + * Overloaded message-sending operation. + * Always publishes on the `SquadEvents` object. + * @param to a faction affiliation used as the channel for the message + * @param msg a message that can be stored in a `SquadServiceResponse` object + */ + def Publish(to : PlanetSideEmpire.Type, msg : SquadResponse.Response) : Unit = { + Publish(to, msg, Nil) + } + /** + * Overloaded message-sending operation. + * Always publishes on the `SquadEvents` object. + * @param to a faction affiliation used as the channel for the message + * @param msg a message that can be stored in a `SquadServiceResponse` object + * @param excluded a group of character identifier numbers who should not receive the message + * (resolved at destination) + */ + def Publish(to : PlanetSideEmpire.Type, msg : SquadResponse.Response, excluded : Iterable[Long]) : Unit = { + SquadEvents.publish(SquadServiceResponse(s"/$to/Squad", excluded, msg)) + } + /** + * Overloaded message-sending operation. + * Strings come in three accepted patterns. + * The first resolves into a faction name, as determined by `PlanetSideEmpire` when transformed into a string. + * The second resolves into a squad's dedicated channel, a name that is formulaic. + * The third resolves as a unique character identifier number. + * @param to a string used as the channel for the message + * @param msg a message that can be stored in a `SquadServiceResponse` object + */ + def Publish(to : String, msg : SquadResponse.Response) : Unit = { + Publish(to, msg, Nil) + } + /** + * Overloaded message-sending operation. + * Strings come in three accepted patterns. + * The first resolves into a faction name, as determined by `PlanetSideEmpire` when transformed into a string. + * The second resolves into a squad's dedicated channel, a name that is formulaic. + * The third resolves as a unique character identifier number. + * @param to a string used as the channel for the message + * @param msg a message that can be stored in a `SquadServiceResponse` object + * @param excluded a group of character identifier numbers who should not receive the message + * (resolved at destination, usually) + */ + def Publish(to : String, msg : SquadResponse.Response, excluded : Iterable[Long]) : Unit = { + to match { + case str if "TRNCVS".indexOf(str) > -1 || str.matches("(TR|NC|VS)-Squad\\d+") => + SquadEvents.publish(SquadServiceResponse(s"/$str/Squad", excluded, msg)) + case str if str.matches("//d+") => + Publish(to.toLong, msg, excluded) case _ => - None + log.error(s"Publish(String): subscriber information is an unhandled format - $to") } } - - def CreateSquad(player : Player) : Squad = { - val faction = player.Faction - val name = player.Name - val squad = new Squad(GetNextSquadId(), faction) - val leadPosition = squad.Membership(squad.LeaderPositionIndex) - leadPosition.Name = name - leadPosition.CharId = player.CharId - leadPosition.Health = player.Health - leadPosition.Armor = player.Armor - leadPosition.Position = player.Position - leadPosition.ZoneId = 1 - log.info(s"$name-$faction has created a new squad") - squad + /** + * Overloaded message-sending operation. + * Always publishes on the `ActorRef` objects retained by the `UserEvents` object. + * @param to a unique character identifier used as the channel for the message + * @param msg a message that can be stored in a `SquadServiceResponse` object + */ + def Publish(to : Long, msg : SquadResponse.Response) : Unit = { + UserEvents.get(to) match { + case Some(user) => + user ! SquadServiceResponse("", msg) + case None => + log.error(s"Publish(Long): subscriber information can not be found - $to") + } } - - def StartSquad(squad : Squad) : Squad = { - squadFeatures += squad.GUID -> new SquadService.SquadFeatures(squad).Start - memberToSquad += squad.Leader.CharId -> squad - squad + /** + * Overloaded message-sending operation. + * Always publishes on the `ActorRef` objects retained by the `UserEvents` object. + * @param to a unique character identifier used as the channel for the message + * @param msg a message that can be stored in a `SquadServiceResponse` object + * @param excluded a group of character identifier numbers who should not receive the message + */ + def Publish(to : Long, msg : SquadResponse.Response, excluded : Iterable[Long]) : Unit = { + if(!excluded.exists(_ == to)) { + Publish(to, msg) + } } - - def StartSquad(player : Player) : Squad = { - val squad = CreateSquad(player) - StartSquad(squad) - squad + /** + * Overloaded message-sending operation. + * No message can be sent using this distinction. + * Log a warning. + * @param to something that was expected to be used as the channel for the message + * but is not handled as such + * @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") + } + /** + * Overloaded message-sending operation. + * No message can be sent using this distinction. + * Log a warning. + * @param to something that was expected to be used as the channel for the message + * but is not handled as such + * @param msg a message that can be stored in a `SquadServiceResponse` object + * @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") } - - val SquadEvents = new GenericEventBus[SquadServiceResponse] 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 => val path = s"/$faction/Squad" val who = sender() - log.info(s"$who has joined $path") + debug(s"$who has joined $path") SquadEvents.subscribe(who, path) - //send initial squad catalog - sender ! SquadServiceResponse(s"/$faction/Squad", SquadResponse.InitList(publishedLists(PlanetSideEmpire(faction)).toVector)) //subscribe to the player's personal channel - necessary for future and previous squad information case Service.Join(char_id) => - val path = s"/$char_id/Squad" - val who = sender() - log.info(s"$who has joined $path") - SquadEvents.subscribe(who, path) - //check for renewable squad information - val longCharId = char_id.toLong - memberToSquad.get(longCharId) match { - case None => ; - case Some(squad) => - val guid = squad.GUID - val indices = squad.Membership.zipWithIndex.collect({ case (member, index) if member.CharId != 0 => index }).toList - SquadEvents.publish(SquadServiceResponse(s"/$char_id/Squad", SquadResponse.AssociateWithSquad(guid))) - SquadEvents.publish(SquadServiceResponse(s"/$char_id/Squad", SquadResponse.Join(squad, indices))) - InitSquadDetail(guid, Seq(longCharId), squad) - InitWaypoints(longCharId, guid) + try { + val longCharId = char_id.toLong + val path = s"/$char_id/Squad" + val who = sender() + debug(s"$who has joined $path") + context.watch(who) + UserEvents += longCharId -> who + refused(longCharId) = Nil + } + catch { + case _ : ClassCastException => + log.warn(s"Service.Join: tried $char_id as a unique character identifier, but it could not be casted") + case e : Exception => + log.error(s"Service.Join: unexpected exception using $char_id as data - ${e.getLocalizedMessage}") + e.printStackTrace() } - case Service.Leave(Some(char_id)) => - SquadEvents.unsubscribe(sender()) - val longCharId = char_id.toLong - val pSquadOpt = GetParticipatingSquad(longCharId) - (pSquadOpt, GetLeadingSquad(longCharId, pSquadOpt)) match { - case (Some(_), Some(squad)) => - //leader of a squad; the squad will be disbanded - DisbandSquad(squad) - case (Some(squad), None) if squad.Size == 2 => - //one of the last two members of a squad; the squad will be disbanded - DisbandSquad(squad) - case (Some(squad), None) => - //member of the squad; leave the squad - LeaveSquad(longCharId, squad) - case _ => - //not a member of any squad; nothing to do here - } - CleanupInvitesFromPlayer(longCharId) + case Service.Leave(Some(char_id)) => LeaveService(char_id, sender) - case Service.Leave(None) | Service.LeaveAll() => ; + case Service.Leave(None) | Service.LeaveAll() => UserEvents find { case(_, subscription) => subscription == sender} match { + case Some((to, _)) => + LeaveService(to, sender) + case _ => ; + } + + case Terminated(actorRef) => + context.unwatch(actorRef) + UserEvents find { case(_, subscription) => subscription eq actorRef} match { + case Some((to, _)) => + LeaveService(to, sender) + case _ => ; + } case SquadServiceMessage(tplayer, zone, squad_action) => squad_action match { - case SquadAction.Membership(SquadRequestType.Invite, invitingPlayer, Some(invitedPlayer), _, _) => + case SquadAction.InitSquadList() => + Publish( sender, SquadResponse.InitList(PublishedLists(tplayer.Faction)) ) //send initial squad catalog + + case SquadAction.InitCharId() => + val charId = tplayer.CharId + memberToSquad.get(charId) match { + case None => ; + case Some(squad) => + val guid = squad.GUID + val toChannel = s"/${squadFeatures(guid).ToChannel}/Squad" + val indices = squad.Membership.zipWithIndex.collect({ case (member, index) if member.CharId != 0 => index }).toList + Publish(charId, SquadResponse.AssociateWithSquad(guid)) + Publish(charId, SquadResponse.Join(squad, indices, toChannel)) + InitSquadDetail(guid, Seq(charId), squad) + InitWaypoints(charId, guid) + } + + case SquadAction.Membership(SquadRequestType.Invite, invitingPlayer, Some(_invitedPlayer), invitedName, _) => //this is just busy work; for actual joining operations, see SquadRequestType.Accept - (memberToSquad.get(invitingPlayer), memberToSquad.get(invitedPlayer)) match { - case (Some(squad1), Some(squad2)) - if squad1.GUID == squad2.GUID => - //both players are in the same squad; no need to do anything + (if(invitedName.nonEmpty) { + //validate player with name exists + LivePlayerList.WorldPopulation({ case (_, a : Avatar) => a.name == invitedName }).headOption match { + case Some(player) => UserEvents.keys.find(_ == player.CharId) + case None => None + } + } + else { + //validate player with id exists + UserEvents.keys.find(_ == _invitedPlayer) + }) match { + case Some(invitedPlayer) => + (memberToSquad.get(invitingPlayer), memberToSquad.get(invitedPlayer)) match { + case (Some(squad1), Some(squad2)) if squad1.GUID == squad2.GUID => + //both players are in the same squad; no need to do anything - case (Some(squad1), Some(squad2)) - if 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(squad1), Some(squad2)) if 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(squad1), Some(squad2)) - if squad2.Size == 1 && !squadFeatures(squad1.GUID).Refuse.contains(invitedPlayer) => - //both players belong to squads, but the invitedplayer's squad (squad2) is underutilized by comparison - //treat the same as "the classic situation" using squad1 - log.info(s"$invitedPlayer has been invited to squad ${squad1.Task} by $invitingPlayer") - val charId = tplayer.CharId - AddInviteAndRespond( - invitedPlayer, - VacancyInvite(charId, tplayer.Name, squad1.GUID), - charId, - tplayer.Name - ) + case (Some(squad1), Some(squad2)) if squad2.Size == 1 => + //both players belong to squads, but the invitedPlayer's squad (squad2) is underutilized by comparison + //treat the same as "the classic situation" using squad1 + if(!Refused(invitedPlayer).contains(invitingPlayer)) { + val charId = tplayer.CharId + AddInviteAndRespond( + invitedPlayer, + VacancyInvite(charId, tplayer.Name, squad1.GUID), + charId, + tplayer.Name + ) + } - case (Some(squad1), Some(squad2)) - if squad1.Size == 1 && !squadFeatures(squad2.GUID).Refuse.contains(invitingPlayer) => - //both players belong to squads, but the invitingPlayer's squad is underutilized by comparison - //treat the same as "indirection ..." using squad2 - log.warn(s"$invitedPlayer has asked $invitingPlayer for an invitation to squad ${squad2.Task}, but the squad leader may need to approve") - AddInviteAndRespond( - squad2.Leader.CharId, - IndirectInvite(tplayer, squad2.GUID), - invitingPlayer, - tplayer.Name - ) + case (Some(squad1), Some(squad2)) if squad1.Size == 1 => + //both players belong to squads, but the invitingPlayer's squad is underutilized by comparison + //treat the same as "indirection ..." using squad2 + val leader = squad2.Leader.CharId + if(Refused(invitingPlayer).contains(invitedPlayer)) { + debug(s"$invitedPlayer repeated a previous refusal to $invitingPlayer's invitation offer") + } + else if(Refused(invitingPlayer).contains(leader)) { + debug(s"$invitedPlayer repeated a previous refusal to $leader's invitation offer") + } + else { + AddInviteAndRespond( + leader, + IndirectInvite(tplayer, squad2.GUID), + invitingPlayer, + tplayer.Name + ) + } - case (Some(squad), None) - if !squadFeatures(squad.GUID).Refuse.contains(invitedPlayer) => - //the classic situation - log.info(s"$invitedPlayer has been invited to squad ${squad.Task} by $invitingPlayer") - AddInviteAndRespond( - invitedPlayer, - VacancyInvite(tplayer.CharId, tplayer.Name, squad.GUID), - invitingPlayer, - tplayer.Name - ) + case (Some(squad), None) => + //the classic situation + if(!Refused(invitedPlayer).contains(invitingPlayer)) { + AddInviteAndRespond( + invitedPlayer, + VacancyInvite(tplayer.CharId, tplayer.Name, squad.GUID), + invitingPlayer, + tplayer.Name + ) + } + else { + debug(s"$invitedPlayer repeated a previous refusal to $invitingPlayer's invitation offer") + } - case (None, Some(squad)) - if !squadFeatures(squad.GUID).Refuse.contains(invitingPlayer) => - //indirection; we're trying to invite ourselves to someone else's squad - log.warn(s"$invitedPlayer has asked $invitingPlayer for an invitation to squad ${squad.Task}, but the squad leader may need to approve") - AddInviteAndRespond( - squad.Leader.CharId, - IndirectInvite(tplayer, squad.GUID), - invitingPlayer, - tplayer.Name - ) + case (None, Some(squad)) => + //indirection; we're trying to invite ourselves to someone else's squad + val leader = squad.Leader.CharId + if(Refused(invitingPlayer).contains(invitedPlayer)) { + debug(s"$invitedPlayer repeated a previous refusal to $invitingPlayer's invitation offer") + } + else if(Refused(invitingPlayer).contains(leader)) { + debug(s"$invitedPlayer repeated a previous refusal to $leader's invitation offer") + } + else { + AddInviteAndRespond( + squad.Leader.CharId, + IndirectInvite(tplayer, squad.GUID), + invitingPlayer, + tplayer.Name + ) + } - case (None, None) => - //neither the invited player nor the inviting player belong to any squad - log.info(s"$invitedPlayer has been invited to join $invitingPlayer's spontaneous squad") - AddInviteAndRespond( - invitedPlayer, - SpontaneousInvite(tplayer), - invitingPlayer, - tplayer.Name - ) + case (None, None) => + //neither the invited player nor the inviting player belong to any squad + if(Refused(invitingPlayer).contains(invitedPlayer)) { + debug(s"$invitedPlayer repeated a previous refusal to $invitingPlayer's invitation offer") + } + else if(Refused(invitedPlayer).contains(invitingPlayer)) { + debug(s"$invitingPlayer repeated a previous refusal to $invitingPlayer's invitation offer") + } + else { + AddInviteAndRespond( + invitedPlayer, + SpontaneousInvite(tplayer), + invitingPlayer, + tplayer.Name + ) + } - case _ => ; + case _ => ; + } + case None => ; } case SquadAction.Membership(SquadRequestType.ProximityInvite, invitingPlayer, _, _, _) => - memberToSquad.get(invitingPlayer) match { + GetLeadingSquad(invitingPlayer, None) match { case Some(squad) => val sguid = squad.GUID val features = squadFeatures(sguid) features.SearchForRole match { case Some(-1) => //we've already issued a proximity invitation; no need to do another - log.info("ProximityInvite: waiting for existing proximity invitations to clear") + debug("ProximityInvite: wait for existing proximity invitations to clear") case _ => - log.info("ProximityInvite: looking for invitation targets ...") val outstandingActiveInvites = features.SearchForRole match { case Some(pos) => RemoveQueuedInvitesForSquadAndPosition(sguid, pos) - invites.collect { case(charId, InviteForRole(_,_, squad_guid, role)) if squad_guid == sguid && role == pos => charId } + invites.collect { case(charId, LookingForSquadRoleInvite(_,_, squad_guid, role)) if squad_guid == sguid && role == pos => charId } case None => List.empty[Long] } - features.SearchForRole = Some(-1) val faction = squad.Faction val center = tplayer.Position val excusedInvites = features.Refuse @@ -312,13 +547,14 @@ class SquadService extends Actor { - have Looking For Squad enabled - do not currently belong to a squad - are denied the opportunity to be invited - - are a certain distance from the squad leader + - are a certain distance from the squad leader (n < 25m) */ (zone.LivePlayers .collect { case player - if player.Faction == faction && player.LFS && memberToSquad.get(player.CharId).isEmpty && - !excusedInvites.contains(player.CharId) && - Vector3.DistanceSquared(player.Position, center) < 100f && + if player.Faction == faction && player.LFS && + (memberToSquad.get(player.CharId).isEmpty || memberToSquad(player.CharId).Size == 1) && + !excusedInvites.contains(player.CharId) && Refused(player.CharId).contains(squad.Leader.CharId) && + Vector3.DistanceSquared(player.Position, center) < 625f && { positions .map { role => @@ -331,15 +567,14 @@ class SquadService extends Actor { .partition { charId => outstandingActiveInvites.exists(_ == charId) } match { case (Nil, Nil) => //no one found - log.info("ProximityInvite: no invitation targets found") outstandingActiveInvites foreach RemoveInvite features.ProxyInvites = Nil None case (outstandingPlayerList, invitedPlayerList) => //players who were actively invited for the previous position and are eligible for the new position - log.info(s"ProximityInvite: found ${outstandingPlayerList.size} players already having been invited, and ${invitedPlayerList.size} players to invite") + features.SearchForRole = Some(-1) outstandingPlayerList.foreach { charId => - val bid = invites(charId).asInstanceOf[InviteForRole] + val bid = invites(charId).asInstanceOf[LookingForSquadRoleInvite] invites(charId) = ProximityInvite(bid.char_id, bid.name, sguid) } //players who were actively invited for the previous position but are ineligible for the new position @@ -368,40 +603,40 @@ class SquadService extends Actor { case SquadAction.Membership(SquadRequestType.Accept, invitedPlayer, _, _, _) => val acceptedInvite = RemoveInvite(invitedPlayer) - val msg = "Accept: the invited player is already a member of a squad and can not join a second one" acceptedInvite match { - case Some(BidForRole(petitioner, guid, position)) if EnsureEmptySquad(petitioner.CharId, msg) && squadFeatures.get(guid).nonEmpty => + case Some(RequestRole(petitioner, guid, position)) if EnsureEmptySquad(petitioner.CharId) && squadFeatures.get(guid).nonEmpty => //player requested to join a squad's specific position //invitedPlayer is actually the squad leader; petitioner is the actual "invitedPlayer" val features = squadFeatures(guid) + features.Prompt.cancel JoinSquad(petitioner, features.Squad, position) RemoveInvitesForSquadAndPosition(guid, position) - case Some(IndirectInvite(recruit, guid)) if EnsureEmptySquad(recruit.CharId, msg) => + case Some(IndirectInvite(recruit, guid)) if EnsureEmptySquad(recruit.CharId) => //tplayer / invitedPlayer is actually the squad leader val recruitCharId = recruit.CharId HandleVacancyInvite(guid, recruitCharId, invitedPlayer, recruit) match { case Some((squad, line)) => - SquadEvents.publish(SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitedPlayer, Some(recruitCharId), recruit.Name, true, Some(None)))) + Publish(invitedPlayer, SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitedPlayer, Some(recruitCharId), recruit.Name, true, Some(None))) JoinSquad(recruit, squad, line) RemoveInvitesForSquadAndPosition(squad.GUID, line) //since we are the squad leader, we do not want to brush off our queued squad invite tasks case _ => ; } - case Some(VacancyInvite(invitingPlayer, _, guid)) if EnsureEmptySquad(invitedPlayer, msg) => + case Some(VacancyInvite(invitingPlayer, _, guid)) if EnsureEmptySquad(invitedPlayer) => //accepted an invitation to join an existing squad HandleVacancyInvite(guid, invitedPlayer, invitingPlayer, tplayer) match { case Some((squad, line)) => - SquadEvents.publish(SquadServiceResponse(s"/$invitingPlayer/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitingPlayer, Some(invitedPlayer), tplayer.Name, false, Some(None)))) - SquadEvents.publish(SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitedPlayer, Some(invitingPlayer), "", true, Some(None)))) + Publish(invitingPlayer, SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitingPlayer, Some(invitedPlayer), tplayer.Name, false, Some(None))) + Publish(invitedPlayer, SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitedPlayer, Some(invitingPlayer), "", true, Some(None))) JoinSquad(tplayer, squad, line) RemoveQueuedInvites(invitedPlayer) //TODO deal with these somehow RemoveInvitesForSquadAndPosition(squad.GUID, line) case _ => ; } - case Some(SpontaneousInvite(invitingPlayer)) if EnsureEmptySquad(invitedPlayer, msg) => + case Some(SpontaneousInvite(invitingPlayer)) if EnsureEmptySquad(invitedPlayer) => //originally, we were invited by someone into a new squad they would form val invitingPlayerCharId = invitingPlayer.CharId (GetParticipatingSquad(invitingPlayer) match { @@ -412,14 +647,14 @@ class SquadService extends Actor { //generate a new squad, with invitingPlayer as the leader val squad = StartSquad(invitingPlayer) squad.Task = s"${invitingPlayer.Name}'s Squad" - SquadEvents.publish( SquadServiceResponse(s"/$invitingPlayerCharId/Squad", SquadResponse.AssociateWithSquad(squad.GUID)) ) + Publish(invitingPlayerCharId, SquadResponse.AssociateWithSquad(squad.GUID)) Some(squad) }) match { case Some(squad) => - HandleVacancyInvite(squad.GUID, tplayer.CharId, invitingPlayerCharId, tplayer) match { + HandleVacancyInvite(squad, tplayer.CharId, invitingPlayerCharId, tplayer) match { case Some((_, line)) => - SquadEvents.publish( SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitedPlayer, Some(invitingPlayerCharId), "", true, Some(None))) ) - SquadEvents.publish( SquadServiceResponse(s"/$invitingPlayerCharId/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitingPlayerCharId, Some(invitedPlayer), tplayer.Name, false, Some(None))) ) + Publish(invitedPlayer, SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitedPlayer, Some(invitingPlayerCharId), "", true, Some(None))) + Publish(invitingPlayerCharId, SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitingPlayerCharId, Some(invitedPlayer), tplayer.Name, false, Some(None))) JoinSquad(tplayer, squad, line) RemoveQueuedInvites(tplayer.CharId) //TODO deal with these somehow case _ => ; @@ -427,12 +662,12 @@ class SquadService extends Actor { case _ => ; } - case Some(InviteForRole(invitingPlayer, name, guid, position)) if EnsureEmptySquad(invitedPlayer, msg) => + case Some(LookingForSquadRoleInvite(invitingPlayer, name, guid, position)) if EnsureEmptySquad(invitedPlayer) => squadFeatures.get(guid) match { case Some(features) if JoinSquad(tplayer, features.Squad, position) => //join this squad - SquadEvents.publish( SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitedPlayer, Some(invitingPlayer), "", true, Some(None))) ) - SquadEvents.publish( SquadServiceResponse(s"/$invitingPlayer/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitingPlayer, Some(invitedPlayer), tplayer.Name, false, Some(None))) ) + Publish(invitedPlayer, SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitedPlayer, Some(invitingPlayer), "", true, Some(None))) + Publish(invitingPlayer, SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitingPlayer, Some(invitedPlayer), tplayer.Name, false, Some(None))) RemoveQueuedInvites(tplayer.CharId) features.ProxyInvites = Nil features.SearchForRole = None @@ -441,35 +676,31 @@ class SquadService extends Actor { case Some(features) => //can not join squad; position is unavailable or other reasons block action features.ProxyInvites = features.ProxyInvites.filterNot(_ == invitedPlayer) - NextInviteAndRespond(invitedPlayer) case _ => //squad no longer exists? - NextInviteAndRespond(invitedPlayer) } - case Some(ProximityInvite(invitingPlayer, _, guid)) if EnsureEmptySquad(invitedPlayer, msg) => + case Some(ProximityInvite(invitingPlayer, _, guid)) if EnsureEmptySquad(invitedPlayer) => squadFeatures.get(guid) match { case Some(features) => val squad = features.Squad if(squad.Size < squad.Capacity) { - val positions = squad.Membership.zipWithIndex - .collect { case (member, index) if member.CharId == 0 && squad.Availability(index) && { - val requirementsToMeet = member.Requirements - requirementsToMeet.intersect(tplayer.Certifications) == requirementsToMeet - } => - (index, member.Requirements.size) - } - .sortBy({ case (_, requirements) => requirements }) + val positions = (for { + (member, index) <- squad.Membership.zipWithIndex + if ValidOpenSquadPosition(squad, index, member, tplayer.Certifications) + } yield (index, member.Requirements.size)) + .toList + .sortBy({ case (_, reqs) => reqs }) ((positions.headOption, positions.lastOption) match { case (Some((first, size1)), Some((_, size2))) if size1 == size2 => Some(first) //join the first available position case (Some(_), Some((last, _))) => Some(last) //join the most demanding position - case _ => None //(None, None) + case _ => None }) match { case Some(position) if JoinSquad(tplayer, squad, position) => //join this squad - SquadEvents.publish( SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitedPlayer, Some(invitingPlayer), "", true, Some(None))) ) - SquadEvents.publish( SquadServiceResponse(s"/$invitingPlayer/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitingPlayer, Some(invitedPlayer), tplayer.Name, false, Some(None))) ) + Publish(invitedPlayer, SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitedPlayer, Some(invitingPlayer), "", true, Some(None))) + Publish(invitingPlayer, SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitingPlayer, Some(invitedPlayer), tplayer.Name, false, Some(None))) RemoveQueuedInvites(invitedPlayer) features.ProxyInvites = features.ProxyInvites.filterNot(_ == invitedPlayer) case _ => @@ -481,104 +712,185 @@ class SquadService extends Actor { } else if(squad.Size == squad.Capacity) { //all available squad positions filled; terminate all remaining invitations - features.SearchForRole = None - features.ProxyInvites = Nil - CleanupInvitesForSquad(guid) - //CleanupInvitesFromPlayer(invitingPlayer) + RemoveProximityInvites(guid) + RemoveAllInvitesToSquad(guid) + //RemoveAllInvitesWithPlayer(invitingPlayer) } case _ => //squad no longer exists? - NextInviteAndRespond(invitedPlayer) } case _ => - //the invite either timed-out or was withdrawn or is now invalid; select a new one? - NextInviteAndRespond(invitedPlayer) + //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(charId, name, _)) => (charId, name) + case Some(LookingForSquadRoleInvite(charId, name, _, _)) => (charId, name) + case _ => (0L, "") + }) match { + case (0L, "") => ; + case (charId, name) => + Publish(charId, SquadResponse.Membership(SquadResponseType.Cancel, 0, 0, charId, Some(0L), name, false, Some(None))) + } } + NextInviteAndRespond(invitedPlayer) - case SquadAction.Membership(SquadRequestType.Leave, leavingPlayer, optionalPlayer, _, _) => - val squad = memberToSquad(leavingPlayer) - val leader = squad.Leader.CharId - if(leavingPlayer == leader || squad.Size == 2) { - //squad leader is leaving his own squad, so it will be disbanded - //alternately, squad is only composed of two people, so it will be closed-out when one of them leaves - DisbandSquad(squad) - } - else { - if(optionalPlayer.contains(leavingPlayer)) { - //leaving the squad of own accord - LeaveSquad(tplayer.CharId, squad) - } - else if(optionalPlayer.contains(leader)) { - //kicked by the squad leader - SquadEvents.publish( SquadServiceResponse(s"/$leavingPlayer/Squad", SquadResponse.Membership(SquadResponseType.Leave, 0, 0, leavingPlayer, Some(leader), tplayer.Name, false, Some(None))) ) - SquadEvents.publish( SquadServiceResponse(s"/$leader/Squad", SquadResponse.Membership(SquadResponseType.Leave, 0, 0, leader, Some(leavingPlayer), "", true, Some(None))) ) - squadFeatures(squad.GUID).Refuse = leavingPlayer - LeaveSquad(leavingPlayer, squad) - } + case SquadAction.Membership(SquadRequestType.Leave, leavingPlayer, optionalPlayer, name, _) => + GetParticipatingSquad(leavingPlayer) match { + case Some(squad) => + val kickedPlayer = if(name.nonEmpty) { + //validate player with name + LivePlayerList.WorldPopulation({ case (_, a : Avatar) => a.name == name }).headOption match { + case Some(player) => UserEvents.keys.find(_ == player.CharId) + case None => None + } + } + else { + //validate player with id + optionalPlayer match { + case Some(id) => UserEvents.keys.find(_ == id) + case None => None + } + } + val leader = squad.Leader.CharId + if(leavingPlayer == leader || squad.Size == 2) { + //squad leader is leaving his own squad, so it will be disbanded + //alternately, squad is only composed of two people, so it will be closed-out when one of them leaves + DisbandSquad(squad) + } + else { + if(kickedPlayer.isEmpty || kickedPlayer.contains(leavingPlayer)) { + //leaving the squad of own accord + LeaveSquad(tplayer.CharId, squad) + } + else if(kickedPlayer.contains(leader)) { + //kicked by the squad leader + Publish(leavingPlayer, SquadResponse.Membership(SquadResponseType.Leave, 0, 0, leavingPlayer, Some(leader), tplayer.Name, false, Some(None))) + Publish(leader, SquadResponse.Membership(SquadResponseType.Leave, 0, 0, leader, Some(leavingPlayer), "", true, Some(None))) + squadFeatures(squad.GUID).Refuse = leavingPlayer + LeaveSquad(leavingPlayer, squad) + } + } + + case None => } case SquadAction.Membership(SquadRequestType.Reject, rejectingPlayer, _, _, _) => val rejectedBid = RemoveInvite(rejectingPlayer) //(A, B) -> person who made the rejection, person who was rejected (rejectedBid match { - case Some(SpontaneousInvite(invitingPlayer)) => - //rejectingPlayer is the would-be squad member - (Some(rejectingPlayer), Some(invitingPlayer.CharId)) - case Some(VacancyInvite(invitingPlayer, _, guid)) + case Some(SpontaneousInvite(leader)) => + //rejectingPlayer is the would-be squad member; the squad leader's request was rejected + val invitingPlayerCharId = leader.CharId + Refused(rejectingPlayer, invitingPlayerCharId) + (Some(rejectingPlayer), Some(invitingPlayerCharId)) + + case Some(VacancyInvite(leader, _, guid)) if squadFeatures.get(guid).nonEmpty && squadFeatures(guid).Squad.Leader.CharId != rejectingPlayer => - //rejectingPlayer is the would-be squad member - (Some(rejectingPlayer), Some(invitingPlayer)) - case Some(BidForRole(_, guid, _)) - if squadFeatures.get(guid).nonEmpty && squadFeatures(guid).Squad.Leader.CharId == rejectingPlayer => - //rejectingPlayer is the squad leader - (Some(rejectingPlayer), None) - case Some(InviteForRole(invitingPlayer, _, guid, position)) - if squadFeatures.get(guid).nonEmpty && squadFeatures(guid).Squad.Leader.CharId != rejectingPlayer => - //rejectingPlayer is the would-be squad member - val features = squadFeatures(guid) - features.Refuse = rejectingPlayer //do not bother this player anymore - features.ProxyInvites = features.ProxyInvites.filterNot(_ == rejectingPlayer) - if(features.ProxyInvites.isEmpty) { - features.SearchForRole = None - } - (None, None) + //rejectingPlayer is the would-be squad member; the squad leader's request was rejected + Refused(rejectingPlayer, leader) + (Some(rejectingPlayer), Some(leader)) + case Some(ProximityInvite(_, _, guid)) if squadFeatures.get(guid).nonEmpty && squadFeatures(guid).Squad.Leader.CharId != rejectingPlayer => - //rejectingPlayer is the would-be squad member + //rejectingPlayer is the would-be squad member; the squad leader's request was rejected val features = squadFeatures(guid) features.Refuse = rejectingPlayer //do not bother this player anymore - features.ProxyInvites = features.ProxyInvites.filterNot(_ == rejectingPlayer) - if(features.ProxyInvites.isEmpty) { - //all invitations exhausted; this invitation is concluded + if((features.ProxyInvites = features.ProxyInvites.filterNot(_ == rejectingPlayer)).isEmpty) { features.SearchForRole = None } (None, None) + + case Some(LookingForSquadRoleInvite(leader, _, guid, _)) + if squadFeatures.get(guid).nonEmpty && squadFeatures(guid).Squad.Leader.CharId != rejectingPlayer => + //rejectingPlayer is the would-be squad member; the squad leader's request was rejected + Refused(rejectingPlayer, leader) + val features = squadFeatures(guid) + features.Refuse = rejectingPlayer + if((features.ProxyInvites = features.ProxyInvites.filterNot(_ == rejectingPlayer)).isEmpty) { + features.SearchForRole = None + } + (None, None) + + case Some(RequestRole(candidate, guid, _)) + if squadFeatures.get(guid).nonEmpty && squadFeatures(guid).Squad.Leader.CharId == rejectingPlayer => + //rejectingPlayer is the squad leader; candidate is the would-be squad member who was rejected + val features = squadFeatures(guid) + features.Refuse = rejectingPlayer + features.Prompt.cancel + (Some(rejectingPlayer), None) + case _ => ; (None, None) }) match { case (Some(rejected), Some(invited)) => - SquadEvents.publish( SquadServiceResponse(s"/$rejected/Squad", SquadResponse.Membership(SquadResponseType.Reject, 0, 0, rejected, Some(invited), "", true, Some(None))) ) - SquadEvents.publish( SquadServiceResponse(s"/$invited/Squad", SquadResponse.Membership(SquadResponseType.Reject, 0, 0, invited, Some(rejected), tplayer.Name, false, Some(None))) ) + Publish(rejected, SquadResponse.Membership(SquadResponseType.Reject, 0, 0, rejected, Some(invited), "", true, Some(None))) + Publish(invited, SquadResponse.Membership(SquadResponseType.Reject, 0, 0, invited, Some(rejected), tplayer.Name, false, Some(None))) case (Some(rejected), None) => - SquadEvents.publish( SquadServiceResponse(s"/$rejected/Squad", SquadResponse.Membership(SquadResponseType.Reject, 0, 0, rejected, Some(rejected), "", true, Some(None))) ) + Publish(rejected, SquadResponse.Membership(SquadResponseType.Reject, 0, 0, rejected, Some(rejected), "", true, Some(None))) case _ => ; } NextInviteAndRespond(rejectingPlayer) - case SquadAction.Membership(SquadRequestType.Cancel, cancellingPlayer, _, _, _) => - //huh? - log.warn(s"Huh? what does player $cancellingPlayer want to cancel?") + case SquadAction.Membership(SquadRequestType.Disband, char_id, _, _, _) => + GetLeadingSquad(char_id, None) match { + case Some(squad) => + DisbandSquad(squad) + case None => ; + } - case SquadAction.Membership(SquadRequestType.Promote, promotingPlayer, Some(promotedPlayer), _, _) => - (memberToSquad.get(promotingPlayer), memberToSquad.get(promotedPlayer)) match { - case (Some(squad), Some(squad2)) if squad.GUID == squad2.GUID && squad.Leader.CharId == promotingPlayer => + case SquadAction.Membership(SquadRequestType.Cancel, cancellingPlayer, _, _, _) => + //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 + } + if(inList.isEmpty) { + queuedInvites.remove(id) + } + else { + queuedInvites(id) = inList + } + } + //get rid of ProximityInvite objects + RemoveProximityInvites(cancellingPlayer) + + case SquadAction.Membership(SquadRequestType.Promote, promotingPlayer, Some(_promotedPlayer), promotedName, _) => + val promotedPlayer = (if(promotedName.nonEmpty) { + //validate player with name exists + LivePlayerList.WorldPopulation({ case (_, a : Avatar) => a.name == promotedName }).headOption match { + case Some(player) => UserEvents.keys.find(_ == player.CharId) + case None => Some(_promotedPlayer) + } + } + else { + Some(_promotedPlayer) + }) match { + case Some(player) => player + case None => -1L + } + (GetLeadingSquad(promotingPlayer, None), GetParticipatingSquad(promotedPlayer)) match { + case (Some(squad), Some(squad2)) if squad.GUID == squad2.GUID => val membership = squad.Membership.filter { _member => _member.CharId > 0 } - val (leader, position) = (squad.Leader, 0) + val leader = squad.Leader val (member, index) = membership.zipWithIndex.find { case (_member, _) => _member.CharId == promotedPlayer }.get - log.info(s"Player ${leader.Name} steps down from leading ${squad.Task}") - SwapMemberPosition(squad, leader, member) + val features = squadFeatures(squad.GUID) + SwapMemberPosition(leader, member) + //cancel previous leader invite prompt, if any + features.Prompt.cancel //move around invites so that the proper squad leader deals with them val leaderInvite = invites.remove(promotingPlayer) val leaderQueuedInvites = queuedInvites.remove(promotingPlayer).toList.flatten @@ -600,31 +912,33 @@ class SquadService extends Actor { queuedInvites += promotedPlayer -> (xs ++ queuedInvites.remove(promotedPlayer).toList.flatten) } } - log.info(s"Promoting player ${leader.Name} to be the leader of ${squad.Task}") - membership.foreach { _member => - SquadEvents.publish(SquadServiceResponse(s"/${_member.CharId}/Squad", SquadResponse.PromoteMember(squad, promotedPlayer, index, position))) + debug(s"Promoting player ${leader.Name} to be the leader of ${squad.Task}") + Publish(features.ToChannel, SquadResponse.PromoteMember(squad, promotedPlayer, index, 0)) + if(features.Listed) { + Publish(promotingPlayer, SquadResponse.SetListSquad(PlanetSideGUID(0))) + Publish(promotedPlayer, SquadResponse.SetListSquad(squad.GUID)) } - SquadEvents.publish(SquadServiceResponse(s"/$promotingPlayer/Squad", SquadResponse.AssociateWithSquad(PlanetSideGUID(0)))) - SquadEvents.publish(SquadServiceResponse(s"/$promotedPlayer/Squad", SquadResponse.AssociateWithSquad(squad.GUID))) UpdateSquadListWhenListed( - squad, + features, SquadInfo().Leader(leader.Name) ) - UpdateSquadDetail(squad.GUID, squad, + UpdateSquadDetail( + squad.GUID, SquadDetail() .LeaderCharId(leader.CharId) .Field3(value = 0L) .LeaderName(leader.Name) .Members(List( - SquadPositionEntry(position, SquadPositionDetail().CharId(member.CharId).Name(member.Name)), - SquadPositionEntry(index, SquadPositionDetail().CharId(leader.CharId).Name(leader.Name)) + SquadPositionEntry(0, SquadPositionDetail().CharId(leader.CharId).Name(leader.Name)), + SquadPositionEntry(index, SquadPositionDetail().CharId(member.CharId).Name(member.Name)) )) ) - - case msg => - log.warn(s"Unsupported squad behavior: $msg") + case _ => ; } + case SquadAction.Membership(event, _, _, _, _) => + debug(s"SquadAction.Membership: $event is not yet supported") + case SquadAction.Waypoint(_, wtype, _, info) => val playerCharId = tplayer.CharId (GetLeadingSquad(tplayer, None) match { @@ -640,25 +954,11 @@ class SquadService extends Actor { }) match { case (Some(squad), Some(_)) => //waypoint added or updated - squad.Membership - .filterNot { member => member.CharId == tplayer.CharId } - .foreach { member => - val charId = member.CharId - SquadEvents.publish( - SquadServiceResponse(s"/$charId/Squad", SquadResponse.WaypointEvent(WaypointEventAction.Add, playerCharId, wtype, None, info, 1)) - ) - } + SquadServiceResponse(s"/${squadFeatures(squad.GUID).ToChannel}/Squad", tplayer.CharId, SquadResponse.WaypointEvent(WaypointEventAction.Add, playerCharId, wtype, None, info, 1)) case (Some(squad), None) => //waypoint removed? - squad.Membership - .filterNot { member => member.CharId == tplayer.CharId } - .foreach { member => - val charId = member.CharId - SquadEvents.publish( - SquadServiceResponse(s"/$charId/Squad", SquadResponse.WaypointEvent(WaypointEventAction.Remove, playerCharId, wtype, None, None, 0)) - ) - } + SquadServiceResponse(s"/${squadFeatures(squad.GUID).ToChannel}/Squad", tplayer.CharId, SquadResponse.WaypointEvent(WaypointEventAction.Remove, playerCharId, wtype, None, None, 0)) case msg => log.warn(s"Unsupported squad waypoint behavior: $msg") @@ -674,57 +974,60 @@ class SquadService extends Actor { val squad = lSquadOpt.getOrElse(StartSquad(tplayer)) if(squad.Task.nonEmpty && squad.ZoneId > 0) { tplayer.SquadLoadouts.SaveLoadout(squad, squad.Task, line) - sender ! SquadServiceResponse("", SquadResponse.ListSquadFavorite(line, squad.Task)) + Publish(sender, SquadResponse.ListSquadFavorite(line, squad.Task)) } case LoadSquadFavorite() => - val squad = lSquadOpt.getOrElse(StartSquad(tplayer)) - tplayer.SquadLoadouts.LoadLoadout(line) match { - case Some(loadout : SquadLoadout) if squad.Size == 1 => - log.info(s"${tplayer.Name} is loading a squad composition: $loadout") - SquadService.LoadSquadDefinition(squad, loadout) - sender ! SquadServiceResponse("", SquadResponse.AssociateWithSquad(squad.GUID)) - UpdateSquadList(squad, SquadService.SquadList.Publish(squad)) - UpdateSquadDetail(PlanetSideGUID(0), squad) - case _ => + if(pSquadOpt.isEmpty || pSquadOpt == lSquadOpt) { + val squad = lSquadOpt.getOrElse(StartSquad(tplayer)) + tplayer.SquadLoadouts.LoadLoadout(line) match { + case Some(loadout : SquadLoadout) if squad.Size == 1 => + SquadService.LoadSquadDefinition(squad, loadout) + UpdateSquadListWhenListed(squadFeatures(squad.GUID), SquadService.SquadList.Publish(squad)) + Publish(sender, SquadResponse.AssociateWithSquad(PlanetSideGUID(0))) + InitSquadDetail(PlanetSideGUID(0), Seq(tplayer.CharId), squad) + UpdateSquadDetail(squad) + Publish(sender, SquadResponse.AssociateWithSquad(squad.GUID)) + case _ => + } } case DeleteSquadFavorite() => tplayer.SquadLoadouts.DeleteLoadout(line) - sender ! SquadServiceResponse("", SquadResponse.ListSquadFavorite(line, "")) + Publish(sender, SquadResponse.ListSquadFavorite(line, "")) case ChangeSquadPurpose(purpose) => - log.info(s"${tplayer.Name}-${tplayer.Faction} has changed his squad's task to $purpose") val squad = lSquadOpt.getOrElse(StartSquad(tplayer)) squad.Task = purpose - UpdateSquadListWhenListed(squad, SquadInfo().Task(purpose)) - UpdateSquadDetail(squad.GUID, squad, SquadDetail().Task(purpose)) + UpdateSquadListWhenListed(squadFeatures(squad.GUID), SquadInfo().Task(purpose)) + UpdateSquadDetail(squad.GUID, SquadDetail().Task(purpose)) case ChangeSquadZone(zone_id) => - log.info(s"${tplayer.Name}-${tplayer.Faction} has changed squad's ops zone to $zone_id") val squad = lSquadOpt.getOrElse(StartSquad(tplayer)) squad.ZoneId = zone_id.zoneId.toInt - UpdateSquadListWhenListed(squad, SquadInfo().ZoneId(zone_id)) + UpdateSquadListWhenListed(squadFeatures(squad.GUID), SquadInfo().ZoneId(zone_id)) InitialAssociation(squad) - sender ! SquadServiceResponse("", SquadResponse.Detail( + Publish(sender, SquadResponse.Detail(squad.GUID, SquadService.Detail.Publish(squad))) + UpdateSquadDetail( squad.GUID, - SquadService.Detail.Publish(squad)) + squad.GUID, + Seq(squad.Leader.CharId), + SquadDetail().ZoneId(zone_id) ) - UpdateSquadDetail(squad.GUID, squad.Membership.map { _m => _m.CharId }.filterNot { _ == squad.Leader.CharId }, SquadDetail().ZoneId(zone_id)) case CloseSquadMemberPosition(position) => val squad = lSquadOpt.getOrElse(StartSquad(tplayer)) squad.Availability.lift(position) match { - case Some(true) => + case Some(true) if position > 0 => //do not close squad leader position; undefined behavior squad.Availability.update(position, false) - log.info(s"${tplayer.Name}-${tplayer.Faction} has closed the #$position position in squad") val memberPosition = squad.Membership(position) if(memberPosition.CharId > 0) { LeaveSquad(memberPosition.CharId, squad) } memberPosition.Close() - UpdateSquadListWhenListed(squad, SquadInfo().Capacity(squad.Capacity)) - UpdateSquadDetail(squad.GUID, squad, + UpdateSquadListWhenListed(squadFeatures(squad.GUID), SquadInfo().Capacity(squad.Capacity)) + UpdateSquadDetail( + squad.GUID, SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail.Closed))) ) case Some(false) | None => ; @@ -734,10 +1037,10 @@ class SquadService extends Actor { val squad = lSquadOpt.getOrElse(StartSquad(tplayer)) squad.Availability.lift(position) match { case Some(false) => - log.info(s"${tplayer.Name}-${tplayer.Faction} has opened the #$position position in squad") squad.Availability.update(position, true) - UpdateSquadListWhenListed(squad, SquadInfo().Capacity(squad.Capacity)) - UpdateSquadDetail(squad.GUID, squad, + UpdateSquadListWhenListed(squadFeatures(squad.GUID), SquadInfo().Capacity(squad.Capacity)) + UpdateSquadDetail( + squad.GUID, SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail.Open))) ) case Some(true) | None => ; @@ -747,9 +1050,9 @@ class SquadService extends Actor { val squad = lSquadOpt.getOrElse(StartSquad(tplayer)) squad.Availability.lift(position) match { case Some(true) => - log.info(s"${tplayer.Name}-${tplayer.Faction} has changed the role of squad position #$position") squad.Membership(position).Role = role - UpdateSquadDetail(squad.GUID, squad, + UpdateSquadDetail( + squad.GUID, SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail().Role(role)))) ) case Some(false) | None => ; @@ -759,9 +1062,9 @@ class SquadService extends Actor { val squad = lSquadOpt.getOrElse(StartSquad(tplayer)) squad.Availability.lift(position) match { case Some(true) => - log.info(s"${tplayer.Name}-${tplayer.Faction} has changed the orders for squad position #$position") squad.Membership(position).Orders = orders - UpdateSquadDetail(squad.GUID, squad, + UpdateSquadDetail( + squad.GUID, SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail().DetailedOrders(orders)))) ) case Some(false) | None => ; @@ -771,33 +1074,21 @@ class SquadService extends Actor { val squad = lSquadOpt.getOrElse(StartSquad(tplayer)) squad.Availability.lift(position) match { case Some(true) => - log.info(s"${tplayer.Name}-${tplayer.Faction} has changed the requirements for squad position #$position") squad.Membership(position).Requirements = certs - UpdateSquadDetail(squad.GUID, squad, + UpdateSquadDetail( + squad.GUID, SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail().Requirements(certs)))) ) case Some(false) | None => ; } case LocationFollowsSquadLead(state) => - if(state) { - log.info(s"${tplayer.Name}-${tplayer.Faction} has moves the rally to the leader's position") - } - else { - log.info(s"${tplayer.Name}-${tplayer.Faction} has let the rally move freely") - } - val squad = lSquadOpt.getOrElse(StartSquad(tplayer)) - squad.LocationFollowsSquadLead = state + val features = squadFeatures(lSquadOpt.getOrElse(StartSquad(tplayer)).GUID) + features.LocationFollowsSquadLead = state case AutoApproveInvitationRequests(state) => - if(state) { - log.info(s"${tplayer.Name}-${tplayer.Faction} is allowing all requests to join the squad") - } - else { - log.info(s"${tplayer.Name}-${tplayer.Faction} has started screening invitation requests") - } - val squad = lSquadOpt.getOrElse(StartSquad(tplayer)) - squad.AutoApproveInvitationRequests = state + val features = squadFeatures(lSquadOpt.getOrElse(StartSquad(tplayer)).GUID) + features.AutoApproveInvitationRequests = state case FindLfsSoldiersForRole(position) => lSquadOpt match { @@ -807,7 +1098,7 @@ class SquadService extends Actor { features.SearchForRole match { case Some(-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") + debug("FindLfsSoldiersForRole: waiting for proximity invitations to clear") case _ => //either no role has ever been recruited, or some other role has been recruited //normal LFS recruitment for the given position @@ -817,17 +1108,18 @@ class SquadService extends Actor { val outstandingActiveInvites = features.SearchForRole match { case Some(pos) => RemoveQueuedInvitesForSquadAndPosition(sguid, pos) - invites.collect { case(charId, InviteForRole(_,_, squad_guid, role)) if squad_guid == sguid && role == pos => charId } + invites.collect { case(charId, LookingForSquadRoleInvite(_,_, squad_guid, role)) if squad_guid == sguid && role == pos => charId } case None => List.empty[Long] } features.SearchForRole = position //this will update the role entry in the GUI to visually indicate being searched for; only one will be displayed at a time - SquadEvents.publish( - SquadServiceResponse(s"/${tplayer.CharId}/Squad", SquadResponse.Detail( + Publish( + tplayer.CharId, + SquadResponse.Detail( sguid, SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail().CharId(char_id = 0L).Name(name = "")))) - )) + ) ) //collect all players that are eligible for invitation to the new position //divide into players with an active invite (A) and players with a queued invite (B) @@ -849,8 +1141,8 @@ class SquadService extends Actor { case (outstandingPlayerList, invitedPlayerList) => //players who were actively invited for the previous position and are eligible for the new position outstandingPlayerList.foreach { charId => - val bid = invites(charId).asInstanceOf[InviteForRole] - invites(charId) = InviteForRole(bid.char_id, bid.name, sguid, position) + val bid = invites(charId).asInstanceOf[LookingForSquadRoleInvite] + invites(charId) = LookingForSquadRoleInvite(bid.char_id, bid.name, sguid, position) } //players who were actively invited for the previous position but are ineligible for the new position (features.ProxyInvites filterNot (outstandingPlayerList contains)) foreach RemoveInvite @@ -864,7 +1156,7 @@ class SquadService extends Actor { invitedPlayers.foreach { invitedPlayer => AddInviteAndRespond( invitedPlayer, - InviteForRole(invitingPlayer, name, sguid, position), + LookingForSquadRoleInvite(invitingPlayer, name, sguid, position), invitingPlayer, name ) @@ -884,7 +1176,7 @@ class SquadService extends Actor { squadFeatures(sguid).SearchForRole = None //remove active invites invites.filter { - case (_, InviteForRole(_, _, _guid, pos)) => _guid == sguid && position.contains(pos) + case (_, LookingForSquadRoleInvite(_, _, _guid, pos)) => _guid == sguid && position.contains(pos) case _ => false } .keys.foreach { charId => @@ -893,7 +1185,7 @@ class SquadService extends Actor { //remove queued invites queuedInvites.foreach { case (charId, queue) => val filtered = queue.filterNot { - case InviteForRole(_, _, _guid, _) => _guid == sguid + case LookingForSquadRoleInvite(_, _, _guid, _) => _guid == sguid case _ => false } queuedInvites += charId -> filtered @@ -908,20 +1200,20 @@ class SquadService extends Actor { case RequestListSquad() => val squad = lSquadOpt.getOrElse(StartSquad(tplayer)) - if(!squad.Listed && squad.Task.nonEmpty && squad.ZoneId > 0) { - log.info(s"${tplayer.Name}-${tplayer.Faction} has opened public recruitment for squad ${squad.Task}") - squad.Listed = true + val features = squadFeatures(squad.GUID) + if(!features.Listed && squad.Task.nonEmpty && squad.ZoneId > 0) { + features.Listed = true InitialAssociation(squad) - sender ! SquadServiceResponse("", SquadResponse.SetListSquad(squad.GUID)) + Publish(sender, SquadResponse.SetListSquad(squad.GUID)) UpdateSquadList(squad, None) } case StopListSquad() => val squad = lSquadOpt.getOrElse(StartSquad(tplayer)) - if(squad.Listed) { - log.info(s"${tplayer.Name}-${tplayer.Faction} has closed public recruitment for squad ${squad.Task}") - squad.Listed = false - sender ! SquadServiceResponse("", SquadResponse.AssociateWithSquad(PlanetSideGUID(0))) + val features = squadFeatures(squad.GUID) + if(features.Listed) { + features.Listed = false + Publish(sender, SquadResponse.SetListSquad(PlanetSideGUID(0))) UpdateSquadList(squad, None) } @@ -939,11 +1231,12 @@ class SquadService extends Actor { position.Orders = "" position.Requirements = Set() }) - squad.LocationFollowsSquadLead = false - squad.AutoApproveInvitationRequests = false - UpdateSquadListWhenListed(squad, SquadInfo().Task("").ZoneId(None).Capacity(squad.Capacity)) - UpdateSquadDetail(guid, squad) - sender ! SquadServiceResponse("", SquadResponse.AssociateWithSquad(PlanetSideGUID(0))) + val features = squadFeatures(squad.GUID) + features.LocationFollowsSquadLead = false + features.AutoApproveInvitationRequests = false + UpdateSquadListWhenListed(features, SquadInfo().Task("").ZoneId(None).Capacity(squad.Capacity)) + UpdateSquadDetail(squad) + Publish(sender, SquadResponse.AssociateWithSquad(PlanetSideGUID(0))) squadFeatures(guid).InitialAssociation = true //do not unlist an already listed squad case Some(squad) => @@ -956,49 +1249,35 @@ class SquadService extends Actor { (pSquadOpt, action) match { //the following action can be performed by the squad leader and maybe an unaffiliated player case (Some(squad), SelectRoleForYourself(position)) => - log.info(s"${tplayer.Name} would like the #${position+1} spot in the same squad") - val membership = squad.Membership.zipWithIndex - val toMember = squad.Membership(position) - if(squad.Leader.CharId == tplayer.CharId) { - //TODO squad leader currently disallowed - } else - //the squad leader may swap to any open position; a normal member has to validate against requirements - if((squad.Leader.CharId == tplayer.CharId && toMember.CharId == 0) || ValidOpenSquadPosition(squad, position, toMember, tplayer.Certifications)) { - membership.find { case (member, _) => member.CharId == tplayer.CharId } match { - case Some((fromMember, fromIndex)) => - SwapMemberPosition(squad, toMember, fromMember) - if(fromIndex == squad.LeaderPositionIndex) { - squad.LeaderPositionIndex = position - } - //RemoveInvite(tplayer.CharId).foreach { _ => - //close the old bids out - //} - membership - .filter { case (_member, _) => _member.CharId > 0 } - .foreach { case (_member, _) => - SquadEvents.publish(SquadServiceResponse(s"/${_member.CharId}/Squad", SquadResponse.AssignMember(squad, fromIndex, position))) - } - UpdateSquadDetail(squad.GUID, squad) - case _ => ; - //somehow, this is not our squad; do nothing, for now - } - } - else { - //not qualified for requested position - } + //TODO should be possible, but doesn't work correctly due to UI squad cards not updating as expected +// if(squad.Leader.CharId == tplayer.CharId) { +// //squad leader currently disallowed +// } else +// //the squad leader may swap to any open position; a normal member has to validate against requirements +// if(squad.Leader.CharId == tplayer.CharId || ValidOpenSquadPosition(squad, position, tplayer.Certifications)) { +// squad.Membership.zipWithIndex.find { case (member, _) => member.CharId == tplayer.CharId } match { +// case Some((fromMember, fromIndex)) => +// SwapMemberPosition(squad.Membership(position), fromMember) +// Publish(squadFeatures(squad.GUID).ToChannel, SquadResponse.AssignMember(squad, fromIndex, position)) +// UpdateSquadDetail(squad) +// case _ => ; +// //somehow, this is not our squad; do nothing, for now +// } +// } +// else { +// //not qualified for requested position +// } //the following action can be performed by an unaffiliated player case (None, SelectRoleForYourself(position)) => //not a member of any squad, but we might become a member of this one GetSquad(guid) match { case Some(squad) => - val toMember = squad.Membership(position) - if(ValidOpenSquadPosition(squad, position, toMember, tplayer.Certifications)) { + if(ValidOpenSquadPosition(squad, position, tplayer.Certifications)) { //we could join but we may need permission from the squad leader first - log.info(s"${tplayer.Name} would like the #${position+1} spot in the squad ${squad.Task}.") AddInviteAndRespond( squad.Leader.CharId, - BidForRole(tplayer, guid, position), + RequestRole(tplayer, guid, position), invitingPlayer = 0L, //we ourselves technically are ... tplayer.Name ) @@ -1014,42 +1293,42 @@ class SquadService extends Actor { case Some(squad) => //assumption: a player who is cancelling will rarely end up with their invite queued val leaderCharId = squad.Leader.CharId - //clean up any active BidForRole invite entry where we are the player who wants to join the leader's squad + //clean up any active RequestRole 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[BidForRole] && - entry.asInstanceOf[BidForRole].player.CharId == cancellingPlayer => + case out @ Some(entry) if entry.isInstanceOf[RequestRole] && + entry.asInstanceOf[RequestRole].player.CharId == cancellingPlayer => out case _ => None }) match { - case Some(entry : BidForRole) => + case Some(entry : RequestRole) => RemoveInvite(leaderCharId) - SquadEvents.publish( SquadServiceResponse(s"/$leaderCharId/Squad", SquadResponse.Membership(SquadResponseType.Cancel, 0, 0, cancellingPlayer, None, entry.player.Name, false, Some(None)))) + Publish(leaderCharId, SquadResponse.Membership(SquadResponseType.Cancel, 0, 0, cancellingPlayer, None, entry.player.Name, false, Some(None))) NextInviteAndRespond(leaderCharId) Some(true) case _ => None }).orElse( - //look for a queued BidForRole entry where we are the player who wants to join the leader's squad + //look for a queued RequestRole 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[BidForRole] && - entry.asInstanceOf[BidForRole].player.CharId == cancellingPlayer + entry.isInstanceOf[RequestRole] && + entry.asInstanceOf[RequestRole].player.CharId == cancellingPlayer }) case None => (Nil, -1) }) match { case (_, -1) => None //no change - case (list, index) if list.size == 1 => - val entry = list.head.asInstanceOf[BidForRole] - SquadEvents.publish( SquadServiceResponse(s"/$leaderCharId/Squad", SquadResponse.Membership(SquadResponseType.Cancel, 0, 0, cancellingPlayer, None, entry.player.Name, false, Some(None)))) + case (list, _) if list.size == 1 => + val entry = list.head.asInstanceOf[RequestRole] + Publish(leaderCharId, SquadResponse.Membership(SquadResponseType.Cancel, 0, 0, cancellingPlayer, None, entry.player.Name, false, Some(None))) queuedInvites.remove(leaderCharId) Some(true) case (list, index) => - val entry = list(index).asInstanceOf[BidForRole] - SquadEvents.publish( SquadServiceResponse(s"/$leaderCharId/Squad", SquadResponse.Membership(SquadResponseType.Cancel, 0, 0, cancellingPlayer, None, entry.player.Name, false, Some(None)))) + val entry = list(index).asInstanceOf[RequestRole] + Publish(leaderCharId, SquadResponse.Membership(SquadResponseType.Cancel, 0, 0, cancellingPlayer, None, entry.player.Name, false, Some(None))) queuedInvites(leaderCharId) = list.take(index) ++ list.drop(index+1) Some(true) } @@ -1065,16 +1344,10 @@ class SquadService extends Actor { //TODO squad leader currently disallowed case (Some((fromMember, fromPosition)), (toMember, _)) if fromPosition != 0 => val name = fromMember.Name - SwapMemberPosition(squad, toMember, fromMember) - if(fromPosition == squad.LeaderPositionIndex) { - squad.LeaderPositionIndex = position - } - membership - .filter({ case (_member, _) => _member.CharId > 0 }) - .foreach { case (_member, _) => - SquadEvents.publish(SquadServiceResponse(s"/${_member.CharId}/Squad", SquadResponse.AssignMember(squad, fromPosition, position))) - } - UpdateSquadDetail(squad.GUID, squad, + SwapMemberPosition(toMember, fromMember) + Publish(squadFeatures(guid).ToChannel, SquadResponse.AssignMember(squad, fromPosition, position)) + UpdateSquadDetail( + squad.GUID, SquadDetail().Members(List( SquadPositionEntry(position, SquadPositionDetail().CharId(fromMember.CharId).Name(fromMember.Name)), SquadPositionEntry(fromPosition, SquadPositionDetail().CharId(char_id).Name(name)) @@ -1087,183 +1360,32 @@ class SquadService extends Actor { case (_, SearchForSquadsWithParticularRole(_/*role*/, _/*requirements*/, _/*zone_id*/, _/*search_mode*/)) => //though we should be able correctly search squads as is intended //I don't know how search results should be prioritized or even how to return search results to the user - sender ! SquadServiceResponse("", SquadResponse.SquadSearchResults()) + Publish(sender, SquadResponse.SquadSearchResults()) //the following action can be performed by anyone case (_, DisplaySquad()) => + val charId = tplayer.CharId GetSquad(guid) match { + case Some(squad) if memberToSquad.get(charId).isEmpty => + continueToMonitorDetails += charId -> squad.GUID + Publish(sender, SquadResponse.Detail(squad.GUID, SquadService.Detail.Publish(squad))) case Some(squad) => - sender ! SquadServiceResponse("", SquadResponse.Detail(squad.GUID, SquadService.Detail.Publish(squad))) - case None => ; + Publish(sender, SquadResponse.Detail(squad.GUID, SquadService.Detail.Publish(squad))) + case _ => ; } //the following message is feedback from a specific client, awaiting proper initialization case (_, SquadMemberInitializationIssue()) => - // GetSquad(guid) match { - // case Some(squad) => - // sender ! SquadServiceResponse("", SquadResponse.Detail(squad.GUID, SquadService.Detail.Publish(squad))) - // case None => ; - // } +// GetSquad(guid) match { +// case Some(squad) => +// Publish(sender, SquadResponse.Detail(squad.GUID, SquadService.Detail.Publish(squad))) +// case None => ; +// } case msg => ; log.warn(s"Unsupported squad definition behavior: $msg") } } - // etc.. - (pSquadOpt, action) match { - //the following action can be performed by the squad leader and maybe an unaffiliated player - case (Some(squad), SelectRoleForYourself(position)) => - log.info(s"${tplayer.Name} would like the #${position+1} spot in this squad") - val membership = squad.Membership.zipWithIndex - val toMember = squad.Membership(position) - if(squad.Leader.CharId == tplayer.CharId) { - //TODO squad leader currently disallowed - } else - //the squad leader may swap to any open position; a normal member has to validate against requirements - if((squad.Leader.CharId == tplayer.CharId && toMember.CharId == 0) || ValidOpenSquadPosition(squad, position, toMember, tplayer.Certifications)) { - membership.find { case (member, _) => member.CharId == tplayer.CharId } match { - case Some((fromMember, fromIndex)) => - SwapMemberPosition(squad, toMember, fromMember) - if(fromIndex == squad.LeaderPositionIndex) { - squad.LeaderPositionIndex = position - } - //RemoveInvite(tplayer.CharId).foreach { _ => - //close the old bids out - //} - membership - .filter { case (_member, _) => _member.CharId > 0 } - .foreach { case (_member, _) => - SquadEvents.publish(SquadServiceResponse(s"/${_member.CharId}/Squad", SquadResponse.AssignMember(squad, fromIndex, position))) - } - UpdateSquadDetail(squad.GUID, squad) - case _ => ; - //somehow, this is not our squad; do nothing, for now - } - } - else { - //not qualified for requested position - } - - //the following action can be performed by an unaffiliated player - case (None, SelectRoleForYourself(position)) => - //not a member of any squad, but we might become a member of this one - GetSquad(guid) match { - case Some(squad) => - val toMember = squad.Membership(position) - if(ValidOpenSquadPosition(squad, position, toMember, tplayer.Certifications)) { - //we could join but we may need permission from the squad leader first - log.info(s"Player ${tplayer.Name} would like to join the squad ${squad.Task}.") - AddInviteAndRespond( - squad.Leader.CharId, - BidForRole(tplayer, guid, position), - invitingPlayer = 0L, //we ourselves technically are ... - tplayer.Name - ) - } - case None => ; - //squad does not exist? assume old local data; force update to correct discrepancy - } - - //the following action can be performed by anyone who has tried to join a squad - case (_, CancelSelectRoleForYourself(_)) => - val cancellingPlayer = tplayer.CharId - GetSquad(guid) match { - case Some(squad) => - //assumption: a player who is cancelling will rarely end up with their invite queued - val leaderCharId = squad.Leader.CharId - //clean up any active BidForRole 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[BidForRole] && - entry.asInstanceOf[BidForRole].player.CharId == cancellingPlayer => - out - case _ => - None - }) match { - case Some(entry : BidForRole) => - RemoveInvite(leaderCharId) - SquadEvents.publish( SquadServiceResponse(s"/$leaderCharId/Squad", SquadResponse.Membership(SquadResponseType.Cancel, 0, 0, cancellingPlayer, None, entry.player.Name, false, Some(None)))) - NextInviteAndRespond(leaderCharId) - Some(true) - case _ => - None - }).orElse( - //look for a queued BidForRole 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[BidForRole] && - entry.asInstanceOf[BidForRole].player.CharId == cancellingPlayer - }) - case None => - (Nil, -1) - }) match { - case (_, -1) => - None //no change - case (list, index) if list.size == 1 => - val entry = list.head.asInstanceOf[BidForRole] - SquadEvents.publish( SquadServiceResponse(s"/$leaderCharId/Squad", SquadResponse.Membership(SquadResponseType.Cancel, 0, 0, cancellingPlayer, None, entry.player.Name, false, Some(None)))) - queuedInvites.remove(leaderCharId) - Some(true) - case (list, index) => - val entry = list(index).asInstanceOf[BidForRole] - SquadEvents.publish( SquadServiceResponse(s"/$leaderCharId/Squad", SquadResponse.Membership(SquadResponseType.Cancel, 0, 0, cancellingPlayer, None, entry.player.Name, false, Some(None)))) - queuedInvites(leaderCharId) = list.take(index) ++ list.drop(index+1) - Some(true) - } - ) - - case _ => ; - } - - //the following action can be performed by ??? - case (Some(squad), AssignSquadMemberToRole(position, char_id)) => - val membership = squad.Membership.zipWithIndex - (membership.find({ case (member, _) => member.CharId == char_id}), membership(position)) match { - //TODO squad leader currently disallowed - case (Some((fromMember, fromPosition)), (toMember, _)) if fromPosition != 0 => - val name = fromMember.Name - SwapMemberPosition(squad, toMember, fromMember) - if(fromPosition == squad.LeaderPositionIndex) { - squad.LeaderPositionIndex = position - } - membership - .filter({ case (_member, _) => _member.CharId > 0 }) - .foreach { case (_member, _) => - SquadEvents.publish(SquadServiceResponse(s"/${_member.CharId}/Squad", SquadResponse.AssignMember(squad, fromPosition, position))) - } - UpdateSquadDetail(squad.GUID, squad, - SquadDetail().Members(List( - SquadPositionEntry(position, SquadPositionDetail().CharId(fromMember.CharId).Name(fromMember.Name)), - SquadPositionEntry(fromPosition, SquadPositionDetail().CharId(char_id).Name(name)) - )) - ) - case _ => ; - } - - //the following action can be peprformed by anyone - case (_, SearchForSquadsWithParticularRole(_/*role*/, _/*requirements*/, _/*zone_id*/, _/*search_mode*/)) => - //though we should be able correctly search squads as is intended - //I don't know how search results should be prioritized or even how to return search results to the user - sender ! SquadServiceResponse("", SquadResponse.SquadSearchResults()) - - //the following action can be performed by anyone - case (_, DisplaySquad()) => - GetSquad(guid) match { - case Some(squad) => - sender ! SquadServiceResponse("", SquadResponse.Detail(squad.GUID, SquadService.Detail.Publish(squad))) - case None => ; - } - - //the following message is feedback from a specific client, awaiting proper initialization - case (_, SquadMemberInitializationIssue()) => -// GetSquad(guid) match { -// case Some(squad) => -// sender ! SquadServiceResponse("", SquadResponse.Detail(squad.GUID, SquadService.Detail.Publish(squad))) -// case None => ; -// } - - case _ => ; - } case SquadAction.Update(char_id, health, max_health, armor, max_armor, pos, zone_number) => memberToSquad.get(char_id) match { @@ -1274,13 +1396,16 @@ class SquadService extends Actor { member.Armor = StatConverter.Health(armor, max_armor, min=1, max=64) member.Position = pos member.ZoneId = zone_number - sender ! SquadServiceResponse("", SquadResponse.UpdateMembers( - squad, - squad.Membership - .filterNot { _.CharId == 0 } - .map { member => SquadAction.Update(member.CharId, member.Health, 0, member.Armor, 0, member.Position, member.ZoneId) } - .toList - )) + Publish( + sender, + SquadResponse.UpdateMembers( + squad, + squad.Membership + .filterNot { _.CharId == 0 } + .map { member => SquadAction.Update(member.CharId, member.Health, 0, member.Armor, 0, member.Position, member.ZoneId) } + .toList + ) + ) case _ => ; } @@ -1288,18 +1413,72 @@ class SquadService extends Actor { } case msg => - log.info(s"Unhandled message $msg from $sender") + debug(s"Unhandled message $msg from $sender") + } + + case data @ SquadResponse.WantsSquadPosition(leader_char_id, _) => + Publish(leader_char_id, data) + + case msg => + debug(s"Unhandled message $msg from $sender") + } + + /** + * This player has refused to join squad leader's squads or some other players's offers to form a squad. + * @param charId the player who refused other players + * @return the list of other players who have been refused + */ + def Refused(charId : Long) : List[Long] = refused.getOrElse(charId, Nil) + + /** + * This player has refused to join squad leader's squads or some other players's offers to form a squad. + * @param charId the player who is doing the refusal + * @param refusedCharId the player who is refused + * @return the list of other players who have been refused + */ + def Refused(charId : Long, refusedCharId : Long) : List[Long] = { + if(charId != refusedCharId) { + Refused(charId, List(refusedCharId)) + } + else { + Nil } } /** - * na - * @param invitedPlayer the person who will handle the invitation, eventually if not immediately + * This player has refused to join squad leader's squads or some other players's offers to form a squad. + * @param charId the player who is doing the refusal + * @param list the players who are refused + * @return the list of other players who have been refused + */ + def Refused(charId : Long, list : List[Long]) : List[Long] = { + refused.get(charId) match { + case Some(refusedList) => + refused(charId) = list ++ refusedList + Refused(charId) + case None => + Nil + } + } + + /** + * 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; - * if added to the active invite position, return the parameter bid; - * if added to the queued invite, return the invite in the active position; - * if not added, return `None` + * 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 { @@ -1309,10 +1488,9 @@ class SquadService extends Actor { case Some(bidList) => //ensure that new invite does not interact with the queue's invites by invitingPlayer info if(_bid.InviterCharId != invite.InviterCharId && !bidList.exists { eachBid => eachBid.InviterCharId == invite.InviterCharId }) { - log.debug(s"Invite from ${invite.InviterCharId} to $invitedPlayer stored in queue while active invite request pending") queuedInvites(invitedPlayer) = invite match { - case _: BidForRole => - val (normals, others) = bidList.partition(_.isInstanceOf[BidForRole]) + case _: RequestRole => + val (normals, others) = bidList.partition(_.isInstanceOf[RequestRole]) (normals :+ invite) ++ others case _ => bidList :+ invite @@ -1324,7 +1502,6 @@ class SquadService extends Actor { } case None => if(_bid.InviterCharId != invite.InviterCharId) { - log.debug(s"Invite from ${invite.InviterCharId} to $invitedPlayer stored while active invite request pending") queuedInvites(invitedPlayer) = List[Invitation](invite) Some(_bid) } @@ -1339,44 +1516,150 @@ class SquadService extends Actor { } } - def RemoveInvite(invitedPlayer : Long) : Option[Invitation] = { - invites.remove(invitedPlayer) match { - case out @ Some(invite) => - previousInvites += invitedPlayer -> invite - out - case None => - None + /** + * 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 + */ + def indirectInviteResp(invite : IndirectInvite, player : Player, invitedPlayer : Long, invitingPlayer : Long, name : String) : Boolean = { + HandleRequestRole(invite, player) + } + + /** + * 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 + */ + def altIndirectInviteResp(invite : IndirectInvite, player : Player, invitedPlayer : Long, invitingPlayer : Long, name : String) : Boolean = { + Publish(invitingPlayer, SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitingPlayer, Some(invitedPlayer), player.Name, false, Some(None))) + HandleRequestRole(invite, player) + } + + /** + * 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 indirectVacancyFunc the method that cans the respondign behavior should an `IndirectVacancy` 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(indirectVacancyFunc : (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, _) => + Publish(invitedPlayer, SquadResponse.Membership(SquadResponseType.Invite, 0, 0, charId, Some(invitedPlayer), _name, false, Some(None))) + Publish(charId, SquadResponse.Membership(SquadResponseType.Invite, 0, 0, invitedPlayer, Some(charId), _name, true, Some(None))) + + case _bid @ IndirectInvite(player, _) => + indirectVacancyFunc(_bid, player, invitedPlayer, invitingPlayer, name) + + case _bid @ SpontaneousInvite(player) => + val bidInvitingPlayer = _bid.InviterCharId + Publish(invitedPlayer, SquadResponse.Membership(SquadResponseType.Invite, 0, 0, bidInvitingPlayer, Some(invitedPlayer), player.Name, false, Some(None))) + Publish(bidInvitingPlayer, SquadResponse.Membership(SquadResponseType.Invite, 0, 0, invitedPlayer, Some(bidInvitingPlayer), player.Name, true, Some(None))) + + case _bid @ RequestRole(player, _, _) => + HandleRequestRole(_bid, player) + + case LookingForSquadRoleInvite(charId, _name, _, _) => + Publish(invitedPlayer, SquadResponse.Membership(SquadResponseType.Invite, 0, 0, invitedPlayer, Some(charId), _name, false, Some(None))) + + case ProximityInvite(charId, _name, _) => + Publish(invitedPlayer, SquadResponse.Membership(SquadResponseType.Invite, 0, 0, invitedPlayer, Some(charId), _name, false, Some(None))) + + case _ => + log.warn(s"AddInviteAndRespond: can not parse discovered unhandled invitation type - $targetInvite") + } } } - def RemoveQueuedInvites(invitedPlayer : Long) : List[Invitation] = { - queuedInvites.remove(invitedPlayer) match { - case Some(_bidList) => _bidList - case None => Nil - } + /** + * 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 targetInvite a comparison invitation object + * @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 AddInviteAndRespond(invitedPlayer : Long, targetInvite : Invitation, invitingPlayer : Long, name : String) : Unit = { + InviteResponseTemplate(indirectInviteResp)( + targetInvite, + AddInvite(invitedPlayer, targetInvite), + invitedPlayer, + invitingPlayer, + name + ) } - def RemoveInvites(invitedPlayer : Long, invitingPlayer : Long) : Unit = { - queuedInvites.get(invitedPlayer) match { - case Some(bidList) => - val list = bidList.filterNot { _.InviterCharId == invitingPlayer } - if(list.nonEmpty) { - queuedInvites(invitedPlayer) = list - } - else { - queuedInvites.remove(invitedPlayer) - } - case None => ; - } - invites.get(invitedPlayer) match { - case Some(_bid) => - if(_bid.InviterCharId == invitingPlayer) { - //drop bid, try reload new bid - } - case None => ; - } + /** + * 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) : Unit = { + InviteResponseTemplate(altIndirectInviteResp)( + targetInvite, + AddInvite(invitedPlayer, targetInvite), + invitedPlayer, + invitingPlayer, + name + ) } + /** + * 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 { @@ -1404,166 +1687,106 @@ class SquadService extends Actor { } } - def HandleVacancyInvite(squad_guid : PlanetSideGUID, invitedPlayer : Long, invitingPlayer : Long, recruit : Player) : Option[(Squad, Int)] = { - //accepted an invitation to join an existing squad - if(squadFeatures.get(squad_guid).isEmpty) { - log.warn(s"Accept->Invite: the squad #${squad_guid.guid} no longer exists") - None - } - else if(memberToSquad.get(invitedPlayer).nonEmpty) { - log.warn(s"Accept->Invite: ${recruit.Name} is already a member of a squad and can not join squad #${squad_guid.guid}") - None - } - else { - val squad = squadFeatures(squad_guid).Squad - if(!squad.AutoApproveInvitationRequests && squad.Leader.CharId != invitingPlayer) { - //the inviting player was not the squad leader and this decision should be bounced off the squad leader - AltAddInviteAndRespond( - squad.Leader.CharId, - IndirectInvite(recruit, squad_guid), - invitingPlayer, - name = "" + /** + * Select the next invitation object to be shifted into the active position + * and dispatch a response for any invitation object that is discovered. + * @see `InviteResponseTemplate` + * @see `NextInvite` + * @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 NextInviteAndRespond(invitedPlayer : Long) : Unit = { + NextInvite(invitedPlayer) match { + case Some(invite) => + InviteResponseTemplate(indirectInviteResp)( + invite, + Some(invite), + invitedPlayer, + invite.InviterCharId, + invite.InviterName ) - log.info(s"Accept->Invite: ${recruit.Name} must await an invitation from the leader of squad #${squad_guid.guid}") + case None => ; + } + } + + /** + * 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. + * @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 - } - else { - //if a suitable position in the squad can be found, player may occupy it - squad.Membership.zipWithIndex.find({ case (member, index) => - ValidOpenSquadPosition(squad, index, member, recruit.Certifications) - }) match { - case Some((_, line)) => - Some((squad, line)) - case _ => - if(squad.Size == squad.Capacity) { - log.warn(s"Accept->Invite: squad #${squad_guid.guid} is already full and ${recruit.Name} can not join it") - } - else { - log.warn(s"Accept->Invite: squad #${squad_guid.guid} has no positions available that satisfy ${recruit.Name}") - } - None - } - } } } - def InitialAssociation(squad : Squad) : Boolean = { - val guid = squad.GUID - if(squadFeatures(guid).InitialAssociation) { - squadFeatures(guid).InitialAssociation = false - val charId = squad.Leader.CharId - SquadEvents.publish( - SquadServiceResponse(s"/$charId/Squad", SquadResponse.AssociateWithSquad(guid)) - ) - SquadEvents.publish( - SquadServiceResponse(s"/$charId/Squad", SquadResponse.Detail( - guid, - SquadService.Detail.Publish(squad)) - ) - ) - } - false - } - - def HandleBidForRole(bid : BidForRole, player : Player) : Boolean = { - HandleBidForRole(bid, bid.squad_guid, bid.player.Name, player) - } - def HandleBidForRole(bid : IndirectInvite, player : Player) : Boolean = { - HandleBidForRole(bid, bid.squad_guid, bid.player.Name, player) - } - - def HandleBidForRole(bid : Invitation, squad_guid : PlanetSideGUID, name : String, player : Player) : Boolean = { - GetSquad(squad_guid) match { - case Some(squad) => - val leaderCharId = squad.Leader.CharId - if(squad.AutoApproveInvitationRequests) { - self ! SquadServiceMessage(player, Zone.Nowhere, SquadAction.Membership(SquadRequestType.Accept, leaderCharId, None, "", None)) - } - else { - SquadEvents.publish(SquadServiceResponse(s"/$leaderCharId/Squad", SquadResponse.WantsSquadPosition(name))) - } - true - case _ => - //squad is missing; will this properly short-circuit? - log.error(s"Attempted to process ${bid.InviterName}'s bid for a position in a squad (id:${squad_guid.guid}) that does not exist") - false - } - } - - def JoinSquad(player : Player, squad : Squad, line : Int) : Boolean = { - val charId = player.CharId - val position = squad.Membership(line) - if(ValidOpenSquadPosition(squad, line, position, player.Certifications)) { - log.info(s"Player ${player.Name} will join the squad ${squad.Task} at position ${line+1}!") - position.Name = player.Name - position.CharId = charId - position.Health = StatConverter.Health(player.Health, player.MaxHealth, min=1, max=64) - position.Armor = StatConverter.Health(player.Armor, player.MaxArmor, min=1, max=64) - position.Position = player.Position - position.ZoneId = 13 - memberToSquad(charId) = squad - - InitialAssociation(squad) - SquadEvents.publish( SquadServiceResponse(s"/$charId/Squad", SquadResponse.AssociateWithSquad(squad.GUID)) ) - val size = squad.Size - if(size == 1) { - //leader joins the squad? do nothing? - squad.LeaderPositionIndex = line - } - else if(size == 2) { - //first squad member after leader; both members fully initialize - val indices = squad.Membership.zipWithIndex - .collect({ case (member, index) if member.CharId != 0 => index }).toList - squad.Membership - .filterNot { _.CharId == 0 } - .foreach { member => - SquadEvents.publish(SquadServiceResponse(s"/${member.CharId}/Squad", SquadResponse.Join(squad, indices))) - InitWaypoints(member.CharId, squad.GUID) - } - //fully update for all users - UpdateSquadDetail(squad.GUID, squad) - } - else { - //joining an active squad; everybody updates differently - //new member gets full squad UI updates - val indices = squad.Membership.zipWithIndex.collect({ case (member, index) if member.CharId != 0 => index }).toList - SquadEvents.publish(SquadServiceResponse(s"/$charId/Squad", SquadResponse.Join(squad, indices))) - InitSquadDetail(squad.GUID, Seq(charId), squad) - InitWaypoints(charId, squad.GUID) - //other squad members see new member joining the squad - val updatedIndex = List(line) - val otherMembers = squad.Membership.filterNot { member => member.CharId == 0 || member.CharId == charId }.map{ _.CharId } - otherMembers.foreach { member => - SquadEvents.publish(SquadServiceResponse(s"/$member/Squad", SquadResponse.Join(squad, updatedIndex))) - } - val details = SquadDetail().Members(List(SquadPositionEntry(line, SquadPositionDetail().CharId(charId).Name(player.Name)))) - UpdateSquadDetail(squad.GUID, otherMembers, details) - } - UpdateSquadListWhenListed(squad, SquadInfo().Size(size)) - true - } - else { - false + /** + * Remove all inactive invites. + * @param invitedPlayer 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 RemoveQueuedInvites(invitedPlayer : Long) : List[Invitation] = { + queuedInvites.remove(invitedPlayer) match { + case Some(_bidList) => _bidList + case None => Nil } } + /** + * 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. + * @see `RequestRole` + * @see `LookingForSquadRoleInvite` + * @see `RemoveInvite` + * @see `RemoveQueuedInvitesForSquadAndPosition` + * @param guid the squad identifier + * @param position the role position index + */ def RemoveInvitesForSquadAndPosition(guid : PlanetSideGUID, position : Int) : Unit = { //eliminate active invites for this role invites.collect { - case(charId, InviteForRole(_,_, sguid, pos)) if sguid == guid && pos == position => + case(charId, LookingForSquadRoleInvite(_,_, sguid, pos)) if sguid == guid && pos == position => RemoveInvite(charId) - case (charId, BidForRole(_, sguid, pos)) if sguid == guid && pos == position => + case (charId, RequestRole(_, sguid, pos)) if sguid == guid && pos == position => RemoveInvite(charId) } RemoveQueuedInvitesForSquadAndPosition(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. + * @see `RequestRole` + * @see `LookingForSquadRoleInvite` + * @see `RemoveInvitesForSquadAndPosition` + * @param guid the squad identifier + * @param position the role position index + */ def RemoveQueuedInvitesForSquadAndPosition(guid : PlanetSideGUID, position : Int) : Unit = { //eliminate other invites for this role queuedInvites.foreach { case(charId, queue) => val filtered = queue.filterNot { - case InviteForRole(_,_, sguid, pos) => sguid == guid && pos == position - case BidForRole(_, sguid, pos) => sguid == guid && pos == position + case LookingForSquadRoleInvite(_,_, sguid, pos) => sguid == guid && pos == position + case RequestRole(_, sguid, pos) => sguid == guid && pos == position case _ => false } if(filtered.isEmpty) { @@ -1575,107 +1798,40 @@ class SquadService extends Actor { } } - def EnsureEmptySquad(char_id : Long, msg : String = "default warning message") : Boolean = { - memberToSquad.get(char_id) match { - case None => - true - case Some(squad) if squad.Size == 1 => - CloseSquad(squad) - true - case _ => - log.warn(msg) - false - } - } - - def LeaveSquad(charId : Long, squad : Squad) : Boolean = { - val membership = squad.Membership.zipWithIndex - membership.find { case (_member, _) => _member.CharId == charId } match { - case Some((member, index)) => - val entry = (charId, index) - val updateList = entry +: membership - .collect { case (_member, _index) if _member.CharId > 0 && _member.CharId != charId => (_member.CharId, _index) } - .toList - //member leaves the squad completely - memberToSquad.remove(charId) - member.Name = "" - member.CharId = 0 - SquadEvents.publish( SquadServiceResponse(s"/$charId/Squad", SquadResponse.Leave(squad, updateList))) - //other squad members see the member leaving - val leavingMember = List(entry) - membership - .filter { case (_member, _) => _member.CharId > 0 } - .foreach { case (_member, _) => - SquadEvents.publish( SquadServiceResponse(s"/${_member.CharId}/Squad", SquadResponse.Leave(squad, leavingMember)) ) - } - UpdateSquadListWhenListed(squad, SquadInfo().Size(squad.Size)) - UpdateSquadDetail(squad.GUID, squad, - SquadDetail().Members(List(SquadPositionEntry(index, SquadPositionDetail().Player(char_id = 0, name = "")))) - ) - true - case None => - false - } - } - - def CloseSquad(squad : Squad) : Unit = { - val guid = squad.GUID - val membership = squad.Membership.zipWithIndex - val (updateMembers, updateIndices) = membership - .collect { case (member, index) if member.CharId > 0 => ((member, member.CharId, index), (member.CharId, index)) } - .unzip - val updateIndicesList = updateIndices.toList - val completelyBlankSquadDetail = SquadDetail().Complete - updateMembers - .foreach { case (member, charId, index) => - memberToSquad.remove(charId) - member.Name = "" - member.CharId = 0L - SquadEvents.publish( SquadServiceResponse(s"/$charId/Squad", SquadResponse.Leave(squad, - updateIndicesList.filterNot { case (_, outIndex) => outIndex == index } :+ (charId, index) //we need to be last to leave to see the events - )) ) - SquadEvents.publish( SquadServiceResponse(s"/$charId/Squad", SquadResponse.AssociateWithSquad(PlanetSideGUID(0))) ) - SquadEvents.publish( SquadServiceResponse(s"/$charId/Squad", SquadResponse.Detail(PlanetSideGUID(0), completelyBlankSquadDetail)) ) - } - UpdateSquadListWhenListed(squad, None) - CleanupInvitesForSquad(guid) - squadFeatures.remove(guid).get.Stop - TryResetSquadId() - } - - def DisbandSquad(squad : Squad) : Unit = { - CloseSquad(squad) - val leader = squad.Leader.CharId - SquadEvents.publish(SquadServiceResponse(s"/$leader/Squad", SquadResponse.Membership(SquadResponseType.Disband, 0, 0, leader, None, "", true, Some(None)))) - squad.Membership - .collect { case member if member.CharId > 0 && member.CharId != leader => member.CharId } - .foreach { charId => - SquadEvents.publish(SquadServiceResponse(s"/$charId/Squad", SquadResponse.Membership(SquadResponseType.Disband, 0, 0, charId, None, "", false, Some(None)))) - } - } - - def CleanupInvitesForSquad(squadGUID : PlanetSideGUID) : Unit = { + /** + * Remove all active and inactive 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 sguid the squad identifier + */ + def RemoveAllInvitesToSquad(sguid : PlanetSideGUID) : Unit = { //clean up invites invites.collect { - case (id, VacancyInvite(_, _, guid)) if squadGUID == guid => + case (id, VacancyInvite(_, _, guid)) if sguid == guid => RemoveInvite(id) - case (id, IndirectInvite(_, guid)) if squadGUID == guid => + case (id, IndirectInvite(_, guid)) if sguid == guid => RemoveInvite(id) - case (id, InviteForRole(_, _, guid, _)) if squadGUID == guid => + case (id, LookingForSquadRoleInvite(_, _, guid, _)) if sguid == guid => RemoveInvite(id) - case (id, BidForRole(_, guid, _)) if squadGUID == guid => + case (id, RequestRole(_, guid, _)) if sguid == guid => RemoveInvite(id) - case (id, ProximityInvite(_, _, guid)) if squadGUID == guid => + case (id, ProximityInvite(_, _, guid)) if sguid == guid => RemoveInvite(id) } //tidy the queued invitations queuedInvites.foreach { case(id, queue) => val filteredQueue = queue.filterNot { - case VacancyInvite(_, _, guid) => squadGUID == guid - case IndirectInvite(_, guid) => squadGUID == guid - case InviteForRole(_, _, guid, _) => squadGUID == guid - case BidForRole(_, guid, _) => squadGUID == guid - case ProximityInvite(_, _, guid) => squadGUID == guid + case VacancyInvite(_, _, guid) => sguid == guid + case IndirectInvite(_, guid) => sguid == guid + case LookingForSquadRoleInvite(_, _, guid, _) => sguid == guid + case RequestRole(_, guid, _) => sguid == guid + case ProximityInvite(_, _, guid) => sguid == guid case _ => false } if(filteredQueue.isEmpty) { @@ -1685,15 +1841,32 @@ class SquadService extends Actor { queuedInvites.update(id, filteredQueue) } } - squadFeatures(squadGUID).SearchForRole match { + squadFeatures(sguid).ProxyInvites = Nil + squadFeatures(sguid).SearchForRole match { case None => ; case Some(_) => - squadFeatures(squadGUID).SearchForRole = None + squadFeatures(sguid).SearchForRole = None + } + continueToMonitorDetails.collect { + case (charId, guid) if sguid == guid => + continueToMonitorDetails.remove(charId) } } - def CleanupInvitesFromPlayer(charId : Long) : Unit = { - invites.remove(charId) + /** + * 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 `RemoveProximityInvites` + * @see `VacancyInvite` + * @param charId the player's unique identifier number + */ + def RemoveAllInvitesWithPlayer(charId : Long) : Unit = { + RemoveInvite(charId) invites.collect { case (id, SpontaneousInvite(player)) if player.CharId == charId => RemoveInvite(id) @@ -1701,11 +1874,9 @@ class SquadService extends Actor { RemoveInvite(id) case (id, IndirectInvite(player, _)) if player.CharId == charId => RemoveInvite(id) - case (id, InviteForRole(_charId, _, _, _)) if _charId == charId => + case (id, LookingForSquadRoleInvite(_charId, _, _, _)) if _charId == charId => RemoveInvite(id) - case (id, BidForRole(player, _, _)) if player.CharId == charId => - RemoveInvite(id) - case (id, ProximityInvite(_charId, _, _)) if charId == _charId => + case (id, RequestRole(player, _, _)) if player.CharId == charId => RemoveInvite(id) } //tidy the queued invitations @@ -1715,9 +1886,8 @@ class SquadService extends Actor { case SpontaneousInvite(player) => player.CharId == charId case VacancyInvite(player, _, _) => player == charId case IndirectInvite(player, _) => player.CharId == charId - case InviteForRole(player, _, _, _) => player == charId - case BidForRole(player, _, _) => player.CharId == charId - case ProximityInvite(_charId, _, _) => _charId == charId + case LookingForSquadRoleInvite(player, _, _, _) => player == charId + case RequestRole(player, _, _) => player.CharId == charId case _ => false } if(filteredQueue.isEmpty) { @@ -1727,33 +1897,589 @@ class SquadService extends Actor { queuedInvites.update(id, filteredQueue) } } - previousInvites.remove(charId) + continueToMonitorDetails.remove(charId) + RemoveProximityInvites(charId) } - def CleanupInvitesToPosition(position : Int) : Unit = { - invites.collect { - case (id, InviteForRole(_, _, _, _position)) if _position == position => + /** + * Remove all active and inactive proximity squad invites related to the recruiter. + * @see `RemoveProximityInvites(Iterable[(Long, PlanetSideGUID)])` + * @param invitingPlayer the player who did the recruiting + * @return a list of all players (unique character identifier number and name) who had active proximity invitations + */ + def RemoveProximityInvites(invitingPlayer : Long) : Iterable[(Long, String)] = { + //invites + val (removedInvites, out) = invites.collect { + case (id, ProximityInvite(inviterCharId, inviterName, squadGUID)) if inviterCharId == invitingPlayer => RemoveInvite(id) - case (id, BidForRole(_, _, _position)) if _position == position => - RemoveInvite(id) - } - //tidy the queued invitations - queuedInvites.foreach { case(id, queue) => - val filteredQueue = queue.filterNot { - case InviteForRole(_, _, _, _position) => _position == position - case BidForRole(_, _, _position) => _position == position + ((id, squadGUID), (id, inviterName)) + }.unzip + RemoveProximityInvites(removedInvites) + //queued + RemoveProximityInvites(queuedInvites.flatMap { case (id : Long, inviteList) => + val (outList, inList) = inviteList.partition { + case ProximityInvite(inviterCharId, _, _) if inviterCharId == invitingPlayer => true case _ => false } - if(filteredQueue.isEmpty) { + if(inList.isEmpty) { queuedInvites.remove(id) } - else if(filteredQueue.size != queue.size) { - queuedInvites.update(id, filteredQueue) + else { + queuedInvites(id) = inList + } + outList.collect { case ProximityInvite(_, _, sguid : PlanetSideGUID) => id -> sguid } + }) + out.toSeq.distinct + } + + /** + * Remove all queued proximity squad invite information retained by the squad object. + * @see `RemoveProximityInvites(Long)` + * @see `SquadFeatures.ProxyInvites` + * @see `SquadFeatures.SearchForRole` + * @param list a list of players to squads with expected entry redundancy + */ + def RemoveProximityInvites(list : Iterable[(Long, PlanetSideGUID)]) : Unit = { + val (ids, squads) = list.unzip + squads.toSeq.distinct.foreach { squad => + squadFeatures.get(squad) match { + case Some(features) => + val out = list.collect { case (id, sguid) if sguid == squad => id } .toSeq + if((features.ProxyInvites = features.ProxyInvites filterNot out.contains) isEmpty) { + features.SearchForRole = None + } + case _ => ; } } } - def SwapMemberPosition(squad : Squad, toMember : Member, fromMember : Member) : Unit = { + /** + * Remove all active and inactive proximity squad invites for a specific squad. + * @param guid the squad + * @return a list of all players (unique character identifier number and name) who had active proximity invitations + */ + def RemoveProximityInvites(guid : PlanetSideGUID) : Iterable[(Long, String)] = { + //invites + val (removedInvites, out) = invites.collect { + case (id, ProximityInvite(_, inviterName, squadGUID)) if squadGUID == guid => + RemoveInvite(id) + (squadGUID, (id, inviterName)) + }.unzip + removedInvites.foreach { sguid => + squadFeatures(sguid).ProxyInvites = Nil + squadFeatures(sguid).SearchForRole = None + } + //queued + queuedInvites.flatMap { case (id : Long, inviteList) => + val (outList, inList) = inviteList.partition { + case ProximityInvite(_, _, squadGUID) if squadGUID == guid => true + case _ => false + } + if(inList.isEmpty) { + queuedInvites.remove(id) + } + else { + queuedInvites(id) = inList + } + outList.collect { case ProximityInvite(_, _, sguid : PlanetSideGUID) => + squadFeatures(sguid).ProxyInvites = Nil + squadFeatures(sguid).SearchForRole = None + } + } + out.toSeq.distinct + } + + /** + * Resolve an invitation to a general, not guaranteed, position in someone else's squad. + * For the moment, just validate the provided parameters and confirm the eligibility of the user. + * @see `VacancyInvite` + * @param squad_guid the unique squad identifier number + * @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(squad_guid : PlanetSideGUID, invitedPlayer : Long, invitingPlayer : Long, recruit : Player) : Option[(Squad, Int)] = { + squadFeatures.get(squad_guid) match { + case Some(features) => + val squad = features.Squad + memberToSquad.get(invitedPlayer) match { + case Some(enrolledSquad) => + if(enrolledSquad eq squad) { + log.warn(s"HandleVacancyInvite: ${recruit.Name} is already a member of squad ${squad.Task}") + } + else { + log.warn(s"HandleVacancyInvite: ${recruit.Name} is a member of squad ${enrolledSquad.Task} and can not join squad ${squad.Task}") + } + None + case _ => + HandleVacancyInvite(squad, invitedPlayer, invitingPlayer, recruit) + } + + case _ => + log.warn(s"HandleVacancyInvite: the squad #${squad_guid.guid} no longer exists") + 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` + * @see `ValidOpenSquadPosition` + * @param squad 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(squad : Squad, invitedPlayer : Long, invitingPlayer : Long, recruit : Player) : Option[(Squad, Int)] = { + //accepted an invitation to join an existing squad + squad.Membership.zipWithIndex.find({ case (member, index) => + ValidOpenSquadPosition(squad, index, member, recruit.Certifications) + }) match { + case Some((_, line)) => + //position in squad found + val sguid = squad.GUID + val features = squadFeatures(sguid) + 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( + squad.Leader.CharId, + IndirectInvite(recruit, sguid), + invitingPlayer, + name = "" + ) + debug(s"HandleVacancyInvite: ${recruit.Name} must await an invitation from the leader of squad ${squad.Task}") + None + } + else { + Some((squad, line)) + } + case _ => + None + } + } + + /** + * An overloaded entry point to the functionality for handling one player requesting a specific squad role. + * @param bid a specific kind of `Invitation` object + * @param player the player who wants to join the squad + * @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(bid : RequestRole, player : Player) : Boolean = { + HandleRequestRole(bid, bid.squad_guid, player) + } + + /** + * An overloaded entry point to the functionality for handling indirection when messaging the squad leader about an invite. + * @param bid a specific kind of `Invitation` object + * @param player the player who wants to join the squad + * @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(bid : IndirectInvite, player : Player) : Boolean = { + HandleRequestRole(bid, bid.squad_guid, player) + } + + /** + * 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 of 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 bid the `Invitation` object that was the target of this request + * @param guid the unique squad identifier number + * @param player the player who wants to join the squad + * @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(bid : Invitation, guid : PlanetSideGUID, player : Player) : Boolean = { + squadFeatures.get(guid) match { + case Some(features) => + val leaderCharId = features.Squad.Leader.CharId + if(features.AutoApproveInvitationRequests) { + self ! SquadServiceMessage(player, Zone.Nowhere, SquadAction.Membership(SquadRequestType.Accept, leaderCharId, None, "", None)) + } + else { + import scala.concurrent.duration._ + import scala.concurrent.ExecutionContext.Implicits.global + features.Prompt = context.system.scheduler.schedule(1 milliseconds, 45000 milliseconds, self, SquadResponse.WantsSquadPosition(leaderCharId, player.Name)) + } + true + case _ => + //squad is missing + log.error(s"Attempted to process ${bid.InviterName}'s bid for a position in a squad that does not exist") + false + } + } + + /** + * Pertains to the original message of squad synchronicity sent to the squad leader by the server under specific conditions. + * The initial formation of a squad of two players is the most common expected situation. + * While the underlying flag is normally only set once, its state can be reset and triggered anew if necessary. + * @see `Publish` + * @see `ResetAll` + * @see `SquadResponse.AssociateWithSquad` + * @see `SquadResponse.Detail` + * @see `SquadService.Detail.Publish` + * @param squad the squad + */ + def InitialAssociation(squad : Squad) : Unit = { + val guid = squad.GUID + if(squadFeatures(guid).InitialAssociation) { + squadFeatures(guid).InitialAssociation = false + val charId = squad.Leader.CharId + Publish(charId, SquadResponse.AssociateWithSquad(guid)) + Publish(charId, SquadResponse.Detail(guid, SquadService.Detail.Publish(squad))) + } + } + + /** + * Establish a new squad. + * Create all of the support structures for the squad and link into them. + * At a minimum, by default, the squad needs a squad leader + * and a stronger, more exposed connection between the squad and leader needs to be recognized.
+ *
+ * Usually, a squad is created by modifying some aspect of its necessary fields. + * The primary necessary fields required for a squad include the squad's task and the squad's zone of operation. + * @see `GetNextSquadId` + * @see `Squad` + * @see `SquadFeatures` + * @see `SquadFeatures::Start` + * @param player the player who would become the squad leader + * @return the squad that has been created + */ + def StartSquad(player : Player) : Squad = { + val faction = player.Faction + val name = player.Name + val squad = new Squad(GetNextSquadId(), faction) + val leadPosition = squad.Membership(0) + leadPosition.Name = name + leadPosition.CharId = player.CharId + leadPosition.Health = player.Health + leadPosition.Armor = player.Armor + leadPosition.Position = player.Position + leadPosition.ZoneId = 1 + squadFeatures += squad.GUID -> new SquadFeatures(squad).Start + memberToSquad += squad.Leader.CharId -> squad + debug(s"$name-$faction has created a new squad") + squad + } + + /** + * 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 `UserEvents` + * and conditions imposed by both the role and the player must be satisfied. + * @see `InitialAssociation` + * @see `InitSquadDetail` + * @see `InitWaypoints` + * @see `Publish` + * @see `RemoveAllInvitesWithPlayer` + * @see `SquadDetail` + * @see `SquadInfo` + * @see `SquadPositionDetail` + * @see `SquadPositionEntry` + * @see `SquadResponse.Join` + * @see `StatConverter.Health` + * @see `UpdateSquadListWhenListed` + * @see `ValidOpenSquadPosition` + * @param player the new squad member; + * this player is NOT the squad leader + * @param squad 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, squad : Squad, position : Int) : Boolean = { + val charId = player.CharId + val role = squad.Membership(position) + if(UserEvents.get(charId).nonEmpty && squad.Leader.CharId != charId && ValidOpenSquadPosition(squad, position, role, player.Certifications)) { + role.Name = player.Name + role.CharId = charId + role.Health = StatConverter.Health(player.Health, player.MaxHealth, min=1, max=64) + role.Armor = StatConverter.Health(player.Armor, player.MaxArmor, min=1, max=64) + role.Position = player.Position + role.ZoneId = 1 + memberToSquad(charId) = squad + + continueToMonitorDetails.remove(charId) + RemoveAllInvitesWithPlayer(charId) + InitialAssociation(squad) + Publish(charId, SquadResponse.AssociateWithSquad(squad.GUID)) + val features = squadFeatures(squad.GUID) + val size = squad.Size + if(size == 2) { + //first squad member after leader; both members fully initialize + val (memberCharIds, indices) = squad.Membership + .zipWithIndex + .filterNot { case (member, _) => member.CharId == 0 } + .toList + .unzip { case (member, index) => (member.CharId, index) } + val toChannel = s"/${features.ToChannel}/Squad" + memberCharIds.foreach { charId => + SquadEvents.subscribe(UserEvents(charId), toChannel) + Publish(charId, SquadResponse.Join(squad, indices, toChannel)) + InitWaypoints(charId, squad.GUID) + } + //fully update for all users + InitSquadDetail(squad) + } + else { + //joining an active squad; everybody updates differently + val updatedIndex = List(position) + val toChannel = s"/${features.ToChannel}/Squad" + //new member gets full squad UI updates + Publish( + charId, + SquadResponse.Join( + squad, + squad.Membership.zipWithIndex.collect({ case (member, index) if member.CharId > 0 => index }).toList, + toChannel + ) + ) + //other squad members see new member joining the squad + Publish(features.ToChannel, SquadResponse.Join(squad, updatedIndex, "")) + InitWaypoints(charId, squad.GUID) + InitSquadDetail(squad.GUID, Seq(charId), squad) + UpdateSquadDetail( + squad.GUID, + SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail().CharId(charId).Name(player.Name)))) + ) + SquadEvents.subscribe(UserEvents(charId), toChannel) + } + UpdateSquadListWhenListed(features, SquadInfo().Size(size)) + true + } + else { + false + } + } + + /** + * Determine whether a player is sufficiently unemployed + * and has no grand delusions of being a squad leader. + * @see `CloseSquad` + * @param charId the player + * @return `true`, if the target player possesses no squad or a squad that is suitably nonexistent; + * `false`, otherwise + */ + def EnsureEmptySquad(charId : Long) : Boolean = { + memberToSquad.get(charId) match { + case None => + true + case Some(squad) if squad.Size == 1 => + CloseSquad(squad) + true + case _ => + log.warn("EnsureEmptySquad: the invited player is already a member of a squad and can not join a second one") + false + } + } + + /** + * Behaviors and exchanges necessary to undo the recruitment process for the squad role. + * @see `PanicLeaveSquad` + * @see `Publish` + * @param charId the player + * @param squad the squad + * @return `true`, if the player, formerly a normal member of the squad, has been ejected from the squad; + * `false`, otherwise + */ + def LeaveSquad(charId : Long, squad : Squad) : Boolean = { + val membership = squad.Membership.zipWithIndex + membership.find { case (_member, _) => _member.CharId == charId } match { + case data @ Some((_, index)) if squad.Leader.CharId != charId => + PanicLeaveSquad(charId, squad, data) + //member leaves the squad completely (see PanicSquadLeave) + Publish( + charId, + SquadResponse.Leave( + squad, + (charId, index) +: membership + .collect { case (_member, _index) if _member.CharId > 0 && _member.CharId != charId => (_member.CharId, _index) } + .toList + ) + ) + SquadEvents.unsubscribe(UserEvents(charId), s"/${squadFeatures(squad.GUID).ToChannel}/Squad") + true + case _ => + false + } + } + + /** + * Behaviors and exchanges necessary to undo the recruitment process for the squad role.
+ *
+ * The complement of the prior `LeaveSquad` method. + * This method deals entirely with other squad members observing the given squad member leaving the squad + * while the other method handles messaging only for the squad member who is leaving. + * The distinction is useful when unsubscribing from this service, + * as the `ActorRef` object used to message the player's client is no longer reliable + * and has probably ceased to exist. + * @see `LeaveSquad` + * @see `Publish` + * @see `SquadDetail` + * @see `SquadInfo` + * @see `SquadPositionDetail` + * @see `SquadPositionEntry` + * @see `SquadResponse.Leave` + * @see `UpdateSquadDetail` + * @see `UpdateSquadListWhenListed` + * @param charId the player + * @param squad the squad + * @param entry a paired membership role with its index in the squad + * @return if a role/index pair is provided + */ + def PanicLeaveSquad(charId : Long, squad : Squad, entry : Option[(Member, Int)]) : Boolean = { + entry match { + case Some((member, index)) => + val entry = (charId, index) + //member leaves the squad completely + memberToSquad.remove(charId) + member.Name = "" + member.CharId = 0 + //other squad members see the member leaving + Publish(squadFeatures(squad.GUID).ToChannel, SquadResponse.Leave(squad, List(entry))) + UpdateSquadListWhenListed(squadFeatures(squad.GUID), SquadInfo().Size(squad.Size)) + UpdateSquadDetail( + squad.GUID, + SquadDetail().Members(List(SquadPositionEntry(index, SquadPositionDetail().Player(char_id = 0, name = "")))) + ) + true + case None => + false + } + } + + /** + * All players are made to leave the squad and the squad will stop existing. + * Any member of the squad missing an `ActorRef` object used to message the player's client + * 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 `Publish` + * @see `RemoveAllInvitesToSquad` + * @see `SquadDetail` + * @see `TryResetSquadId` + * @see `UpdateSquadList` + * @param squad the squad + */ + def CloseSquad(squad : Squad) : Unit = { + val guid = squad.GUID + RemoveAllInvitesToSquad(guid) + val membership = squad.Membership.zipWithIndex + val (updateMembers, updateIndices) = membership + .collect { case (member, index) if member.CharId > 0 => ((member, member.CharId, index, UserEvents.get(member.CharId)), (member.CharId, index)) } + .unzip + val updateIndicesList = updateIndices.toList + val completelyBlankSquadDetail = SquadDetail().Complete + val channel = s"/${squadFeatures(squad.GUID).ToChannel}/Squad" + updateMembers + .foreach { + case (member, charId, _, None) => + memberToSquad.remove(charId) + member.Name = "" + member.CharId = 0L + case (member, charId, index, Some(actor)) => + memberToSquad.remove(charId) + member.Name = "" + member.CharId = 0L + SquadEvents.unsubscribe(actor, channel) + Publish( + charId, + SquadResponse.Leave( + squad, + updateIndicesList.filterNot { case (_, outIndex) => outIndex == index } :+ (charId, index) //we need to be last + ) + ) + Publish(charId, SquadResponse.AssociateWithSquad(PlanetSideGUID(0))) + Publish(charId, SquadResponse.Detail(PlanetSideGUID(0), completelyBlankSquadDetail)) + } + UpdateSquadListWhenListed( + squadFeatures.remove(guid).get.Stop, + None + ) + TryResetSquadId() + } + + /** + * All players are made to leave the squad and the squad will stop existing. + * Essentially, perform the same operations as `CloseSquad` + * but treat the process as if the squad is being disbanded in terms of messaging. + * @see `PanicDisbandSquad` + * @see `Publish` + * @see `SquadResponse.Membership` + * @param squad the squad + */ + def DisbandSquad(squad : Squad) : Unit = { + val leader = squad.Leader.CharId + PanicDisbandSquad( + squad, + squad.Membership.collect { case member if member.CharId > 0 && member.CharId != leader => member.CharId } + ) + //the squad is being disbanded, the squad events channel is also going away; use cached character ids + Publish(leader, SquadResponse.Membership(SquadResponseType.Disband, 0, 0, leader, None, "", true, Some(None))) + } + + /** + * All players are made to leave the squad and the squad will stop existing.
+ *
+ * The complement of the prior `DisbandSquad` method. + * This method deals entirely with other squad members observing the squad become abandoned. + * The distinction is useful when unsubscribing from this service, + * as the `ActorRef` object used to message the player's client is no longer reliable + * and has probably ceased to exist. + * @see `CloseSquad` + * @see `DisbandSquad` + * @see `Publish` + * @see `SquadResponse.Membership` + * @param squad the squad + * @param membership the unique character identifier numbers of the other squad members + * @return if a role/index pair is provided + */ + def PanicDisbandSquad(squad : Squad, membership : Iterable[Long]) : Unit = { + CloseSquad(squad) + membership.foreach { charId => + Publish(charId, SquadResponse.Membership(SquadResponseType.Disband, 0, 0, charId, None, "", false, Some(None))) + } + } + + /** + * Move one player into one squad role and, + * if encountering a player already recruited to the destination role, + * swap that other player into the first player's position. + * If no encounter, just blank the original role. + * @see `AssignSquadMemberToRole` + * @see `SelectRoleForYourself` + * @param toMember the squad role where the player is being placed + * @param fromMember the squad role where the player is being encountered; + * if a conflicting player is discovered, swap that player into `fromMember` + */ + def SwapMemberPosition(toMember: Member, fromMember: Member) : Unit = { val (name, charId, zoneId, pos, health, armor) = (fromMember.Name, fromMember.CharId, fromMember.ZoneId, fromMember.Position, fromMember.Health, fromMember.Armor) if(toMember.CharId > 0) { fromMember.Name = toMember.Name @@ -1775,119 +2501,56 @@ class SquadService extends Actor { toMember.Armor = armor } - def UpdateSquadList(faction : PlanetSideEmpire.Value): Unit = { - val factionListings = publishedLists(faction) - SquadEvents.publish( - SquadServiceResponse(s"/$faction/Squad", SquadResponse.InitList(factionListings.toVector)) - ) - } - - def UpdateSquadList(squad : Squad, changes : SquadInfo) : Unit = { - UpdateSquadList(squad, Some(changes)) - } - - def UpdateSquadListWhenListed(squad : Squad, changes : SquadInfo) : Unit = { - UpdateSquadListWhenListed(squad, Some(changes)) - } - - def UpdateSquadListWhenListed(squad : Squad, changes : Option[SquadInfo]) : Unit = { - if(squad.Listed || squad.Size > 1) { - UpdateSquadList(squad, changes) - } - } - - def UpdateSquadList(squad : Squad, changes : Option[SquadInfo]) : Unit = { - val faction = squad.Faction - val factionListings = publishedLists(faction) - factionListings.find(info => { - info.squad_guid match { - case Some(sguid) => sguid == squad.GUID - case _ => false - } - }) match { - case Some(listedSquad) => - val index = factionListings.indexOf(listedSquad) - changes match { - case Some(changedFields) => - //squad information update - log.info(s"Squad will be updated") - factionListings(index) = SquadService.SquadList.Publish(squad) - SquadEvents.publish( - SquadServiceResponse(s"/$faction/Squad", SquadResponse.UpdateList(Seq((index, changedFields)))) - ) + /** + * Display the indicated waypoint.
+ *
+ * Despite the name, no waypoints are actually "added." + * All of the waypoints constantly exist as long as the squad to which they are attached exists. + * They are merely "activated" and "deactivated." + * @see `SquadWaypointRequest` + * @see `WaypointInfo` + * @param guid the squad's unique identifier + * @param waypointType the type of the waypoint as an integer; + * 0-4 are squad waypoints; + * 5 is the squad leader's experience waypoint + * @param info information about the waypoint, as was reported by the client's packet + * @return the waypoint data, if the waypoint type is changed + */ + def AddWaypoint(guid : PlanetSideGUID, waypointType : Int, info : WaypointInfo) : Option[WaypointData] = { + squadFeatures.get(guid) match { + case Some(features) => + features.Waypoints.lift(waypointType) match { + case Some(point) => + point.zone_number = info.zone_number + point.pos = info.pos + Some(point) case None => - //remove squad from listing - log.info(s"Squad will be removed") - factionListings.remove(index) - SquadEvents.publish( - //SquadServiceResponse(s"$faction/Squad", SquadResponse.RemoveFromList(Seq(index))) - SquadServiceResponse(s"/$faction/Squad", SquadResponse.InitList(factionListings.toVector)) - ) + log.error(s"no squad waypoint $waypointType found") + None } case None => - //first time being published - log.info(s"Squad will be introduced") - factionListings += SquadService.SquadList.Publish(squad) - SquadEvents.publish( - SquadServiceResponse(s"/$faction/Squad", SquadResponse.InitList(factionListings.toVector)) - ) - } - } - - def InitSquadDetail(squad : Squad) : Unit = { - InitSquadDetail(squad.GUID, squad.Membership.map { member => member.CharId }, squad) - } - - def InitSquadDetail(guid : PlanetSideGUID, squad : Squad) : Unit = { - InitSquadDetail(guid, squad.Membership.map { member => member.CharId }, squad) - } - - def InitSquadDetail(guid : PlanetSideGUID, toMembers : Iterable[Long], squad : Squad) : Unit = { - val output = SquadResponse.Detail(guid, SquadService.Detail.Publish(squad)) - toMembers.foreach { charId => SquadEvents.publish(SquadServiceResponse(s"/$charId/Squad", output)) } - } - - def UpdateSquadDetail(guid : PlanetSideGUID, squad : Squad) : Unit = { - UpdateSquadDetail(guid, squad, SquadService.Detail.Publish(squad)) - } - - def UpdateSquadDetail(squad : Squad, details : SquadDetail) : Unit = { - UpdateSquadDetail(squad.GUID, squad.Membership.map { member => member.CharId }, details) - } - - def UpdateSquadDetail(guid : PlanetSideGUID, squad : Squad, details : SquadDetail) : Unit = { - UpdateSquadDetail(guid, squad.Membership.map { member => member.CharId }, details) - } - - def UpdateSquadDetail(guid : PlanetSideGUID, toMembers : Iterable[Long], details : SquadDetail) : Unit = { - if(toMembers.nonEmpty) { - val output = SquadResponse.Detail(guid, details) - toMembers.foreach { charId => SquadEvents.publish(SquadServiceResponse(s"/$charId/Squad", output)) } - } - } - - def AddWaypoint(guid : PlanetSideGUID, waypointType : Int, info : WaypointInfo) : Option[WaypointData] = { - squadFeatures(guid).Waypoints.lift(waypointType) match { - case Some(point) => - //update the waypoint - log.debug(s"rendering squad waypoint $waypointType for squad #${guid.guid}") - point.zone_number = info.zone_number - point.pos = info.pos - Some(point) - case _ => - log.warn(s"no squad waypoint $waypointType found") + log.error(s"no squad waypoint $waypointType found") None } } + /** + * Hide the indicated waypoint. + * Unused waypoints are marked by having a non-zero z-coordinate.
+ *
+ * Despite the name, no waypoints are actually "removed." + * All of the waypoints constantly exist as long as the squad to which they are attached exists. + * They are merely "activated" and "deactivated." + * @param guid the squad's unique identifier + * @param waypointType the type of the waypoint as an integer; + * 0-4 are squad waypoints; + * 5 is the squad leader's experience waypoint + */ def RemoveWaypoint(guid : PlanetSideGUID, waypointType : Int) : Unit = { squadFeatures.get(guid) match { case Some(features) => features.Waypoints.lift(waypointType) match { case Some(point) => - //update the waypoint - log.debug(s"removing squad waypoint $waypointType for squad #${guid.guid}") - point.zone_number = 1 point.pos = Vector3.z(1) case _ => log.warn(s"no squad waypoint $waypointType found") @@ -1897,104 +2560,301 @@ class SquadService extends Actor { } } + /** + * Dispatch all of the information about a given squad's waypoints to a user. + * @param toCharId the player to whom the waypoint data will be dispatched + * @param guid the squad's unique identifier + */ def InitWaypoints(toCharId : Long, guid : PlanetSideGUID) : Unit = { squadFeatures.get(guid) match { case Some(features) => val squad = features.Squad val vz1 = Vector3.z(value = 1) val list = features.Waypoints - SquadEvents.publish( - SquadServiceResponse(s"/$toCharId/Squad", SquadResponse.InitWaypoints(squad.Leader.CharId, + Publish( + toCharId, SquadResponse.InitWaypoints(squad.Leader.CharId, list.zipWithIndex.collect { case (point, index) if point.pos != vz1 => (index, WaypointInfo(point.zone_number, point.pos), 1) } - )) + ) ) case None => ; } } - def indirectInviteResp(bid : IndirectInvite, player : Player, invitedPlayer : Long, invitingPlayer : Long, name : String) : Boolean = { - HandleBidForRole(bid, player) + def LeaveService(charId : String, sender : ActorRef) : Unit = { + LeaveService(charId.toLong, sender) } - def altIndirectInviteResp(bid : IndirectInvite, player : Player, invitedPlayer : Long, invitingPlayer : Long, name : String) : Boolean = { - SquadEvents.publish(SquadServiceResponse(s"/$invitingPlayer/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitingPlayer, Some(invitedPlayer), player.Name, false, Some(None)))) - HandleBidForRole(bid, player) + def LeaveService(charId : Long, sender : ActorRef) : Unit = { + refused.remove(charId) + continueToMonitorDetails.remove(charId) + RemoveAllInvitesWithPlayer(charId) + val pSquadOpt = GetParticipatingSquad(charId) + val lSquadOpt = GetLeadingSquad(charId, pSquadOpt) + pSquadOpt match { + //member of the squad; leave the squad + case Some(squad) => + val size = squad.Size + SquadEvents.unsubscribe(UserEvents(charId), s"/${squadFeatures(squad.GUID).ToChannel}/Squad") + UserEvents.remove(charId) + lSquadOpt match { + case Some(_) => + //leader of a squad; the squad will be disbanded + PanicDisbandSquad(squad, squad.Membership.collect { case member if member.CharId > 0 && member.CharId != charId => member.CharId }) + case None if size == 2 => + //one of the last two members of a squad; the squad will be disbanded + PanicDisbandSquad(squad, squad.Membership.collect { case member if member.CharId > 0 && member.CharId != charId => member.CharId }) + case None => + //not the leader of the squad; tell other members that we are leaving + PanicLeaveSquad(charId, squad, squad.Membership.zipWithIndex.find { case (_member, _) => _member.CharId == charId }) + } + case None => + //not a member of any squad; nothing to do here + UserEvents.remove(charId) + } + SquadEvents.unsubscribe(sender) //just to make certain } - def InviteResponseTemplate(indirectVacancyFunc : (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, _) => - SquadEvents.publish(SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, charId, Some(invitedPlayer), _name, false, Some(None)))) - SquadEvents.publish(SquadServiceResponse(s"/$charId/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, invitedPlayer, Some(charId), _name, true, Some(None)))) + /** + * Dispatch a message entailing the composition of this squad + * and focus on any specific aspects of it, purported as being changed recently. + * @see `SquadInfo` + * @see `UpdateSquadList(Squad, Option[SquadInfo])` + * @param squad the squad + * @param changes the highlighted aspects of the squad; + * these "changes" do not have to reflect the actual squad but are related to the contents of the message + */ + private def UpdateSquadList(squad : Squad, changes : SquadInfo) : Unit = { + UpdateSquadList(squad, Some(changes)) + } - case _bid @ IndirectInvite(player, _) => - indirectVacancyFunc(_bid, player, invitedPlayer, invitingPlayer, name) + /** + * Dispatch a message entailing the composition of this squad when that squad is publicly available + * and focus on any specific aspects of it, purported as being changed recently. + * @see `SquadInfo` + * @see `UpdateSquadList(Squad, Option[SquadInfo])` + * @param features the related information about the squad + * @param changes the highlighted aspects of the squad; + * these "changes" do not have to reflect the actual squad but are related to the contents of the message + */ + private def UpdateSquadListWhenListed(features : SquadFeatures, changes : SquadInfo) : Unit = { + UpdateSquadListWhenListed(features, Some(changes)) + } - case _bid @ SpontaneousInvite(player) => - val bidInvitingPlayer = _bid.InviterCharId - SquadEvents.publish(SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, bidInvitingPlayer, Some(invitedPlayer), player.Name, false, Some(None)))) - SquadEvents.publish(SquadServiceResponse(s"/$bidInvitingPlayer/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, invitedPlayer, Some(bidInvitingPlayer), player.Name, true, Some(None)))) + /** + * Dispatch a message entailing the composition of this squad when that squad is publicly available + * and focus on any specific aspects of it, purported as being changed recently. + * The only requirement is that the squad is publicly available for recruitment ("listed"). + * @see `SquadInfo` + * @see `UpdateSquadList(Squad, Option[SquadInfo])` + * @param features the related information about the squad + * @param changes the optional highlighted aspects of the squad; + * these "changes" do not have to reflect the actual squad but are related to the contents of the message + */ + private def UpdateSquadListWhenListed(features : SquadFeatures, changes : Option[SquadInfo]) : Unit = { + val squad = features.Squad + if(features.Listed) { + UpdateSquadList(squad, changes) + } + } - case _bid @ BidForRole(player, _, _) => - HandleBidForRole(_bid, player) + /** + * Dispatch a message entailing the composition of this squad + * and focus on any specific aspects of it, purported as being changed recently.
+ *
+ * What sort of message is dispatched is not only based on the input parameters + * but also on the state of previously listed squad information. + * Listed squad information is queued when it is first published, organized first by faction affinity, then by chronology. + * The output is first determinate on whether the squad had previously been listed as available. + * If so, it will either update its data to all valid faction associated entities with the provided changed data; + * or, it will be removed from the list of available squads, if there is no provided change data. + * If the squad can not be found, + * the change data, whatever it is, is unimportant, and the squad will be listed in full for the first time.
+ *
+ * When a squad is first introduced to the aforementioned list, + * thus first being published to all faction-associated parties, + * the entirety of the squad list for that faction will be updated in one go. + * It is not necessary to do this, but doing so saves index and unique squad identifier management + * at the cost of the size of the packet to be dispatched. + * When a squad is removed to the aforementioned list, + * the same process occurs where the full list for that faction affiliation is sent as an update. + * The procedure for updating individual squad fields is precise and targeted, + * and has been or should be prepared in advance by the caller to this method. + * As a consequence, when updating the entry for that squad, + * the information used as the update does not necessarily reflect the actual information currently in the squad. + * @see `SquadResponse.InitList` + * @see `SquadResponse.UpdateList` + * @see `SquadService.SquadList.Publish` + * @param squad the squad + * @param changes the optional highlighted aspects of the squad; + * these "changes" do not have to reflect the actual squad but are related to the contents of the message + */ + def UpdateSquadList(squad : Squad, changes : Option[SquadInfo]) : Unit = { + val guid = squad.GUID + val faction = squad.Faction + val factionListings = publishedLists(faction) + factionListings.find(_ == guid) match { + case Some(listedSquad) => + val index = factionListings.indexOf(listedSquad) + changes match { + case Some(changedFields) => + //squad information update + Publish(faction, SquadResponse.UpdateList(Seq((index, changedFields)))) + case None => + //remove squad from listing + factionListings.remove(index) + //Publish(faction, SquadResponse.RemoveFromList(Seq(index))) + Publish(faction, SquadResponse.InitList(PublishedLists(factionListings))) + } + case None => + //first time being published + factionListings += guid + Publish(faction, SquadResponse.InitList(PublishedLists(factionListings))) + } + } - case InviteForRole(charId, _name, _, _) => - SquadEvents.publish(SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, invitedPlayer, Some(charId), _name, false, Some(None)))) + /** + * Dispatch a message entailing the composition of this squad. + * This is considered the first time this information will be dispatched to any relevant observers + * so the details of the squad will be updated in full and be sent to all relevant observers, + * namely, all the occupants of the squad. + * External observers are ignored. + * @see `InitSquadDetail(PlanetSideGUID, Iterable[Long], Squad)` + * @param squad the squad + */ + def InitSquadDetail(squad : Squad) : Unit = { + InitSquadDetail( + squad.GUID, + squad.Membership.collect { case member if member.CharId > 0 => member.CharId }, + squad + ) + } - case ProximityInvite(charId, _name, _) => - SquadEvents.publish(SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, invitedPlayer, Some(charId), _name, false, Some(None)))) + /** + * Dispatch an intial 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.Detail.Publish` + * @param guid the unique squad identifier to be used when composing the details for this message + * @param to the unique character identifier numbers of the players who will receive this message + * @param squad the squad from which the squad details shall be composed + */ + def InitSquadDetail(guid : PlanetSideGUID, to : Iterable[Long], squad : Squad) : Unit = { + val output = SquadResponse.Detail(guid, SquadService.Detail.Publish(squad)) + to.foreach { Publish(_, output) } + } - case _ => - log.warn(s"AddInviteAndRespond: can not parse discovered unhandled invitation type - $targetInvite") + /** + * Send a message entailing the strategic information and the composition of the squad to the existing members of the squad. + * @see `SquadService.Detail.Publish` + * @see `UpdateSquadDetail(PlanetSideGUID, PlanetSideGUID, List[Long], SquadDetail)` + * @param squad the squad + */ + def UpdateSquadDetail(squad : Squad) : Unit = { + UpdateSquadDetail( + squad.GUID, + squad.GUID, + Nil, + SquadService.Detail.Publish(squad) + ) + } + + /** + * Send a message entailing the strategic information and the composition of the squad to the existing members of the squad. + * Rather than using the squad's existing unique identifier number, + * a meaningful substitute identifier will be employed in the message. + * The "meaningful substitute" is usually `PlanetSideGUID(0)` + * which indicates the local non-squad squad data on the client of a squad leader. + * @see `SquadService.Detail.Publish` + * @see `UpdateSquadDetail(PlanetSideGUID, PlanetSideGUID, List[Long], SquadDetail)` + * @param squad the squad + */ + def UpdateSquadDetail(guid : PlanetSideGUID, squad : Squad) : Unit = { + UpdateSquadDetail( + guid, + squad.GUID, + Nil, + SquadService.Detail.Publish(squad) + ) + } + + /** + * Send Send a message entailing some of the strategic information and the composition to the existing members of the squad. + * @see `SquadResponse.Detail` + * @see `UpdateSquadDetail(PlanetSideGUID, PlanetSideGUID, List[Long], SquadDetail)` + * @param guid the unique identifier number of the squad + * @param details the squad details to be included in the message + */ + def UpdateSquadDetail(guid : PlanetSideGUID, details : SquadDetail) : Unit = { + UpdateSquadDetail( + guid, + guid, + Nil, + details + ) + } + + /** + * Send a message entailing some of the strategic information and the composition to the existing members of the squad. + * Also send the same information to any users who are watching the squad, potentially for want to join it. + * The squad-specific message is contingent on finding the squad's features using the unique identifier number + * and, from that, reporting to the specific squad's messaging channel. + * Anyone watching the squad will always be updated the given details. + * @see `DisplaySquad` + * @see `Publish` + * @see `SquadDetail` + * @see `SquadResponse.Detail` + * @param guid the unique squad identifier number to be used for the squad detail message + * @param toGuid the unique squad identifier number indicating the squad broadcast channel name + * @param excluding the explicit unique character identifier numbers of individuals who should not receive the message + * @param details the squad details to be included in the message + */ + def UpdateSquadDetail(guid : PlanetSideGUID, toGuid : PlanetSideGUID, excluding : Iterable[Long], details : SquadDetail) : Unit = { + val output = SquadResponse.Detail(guid, details) + squadFeatures.get(toGuid) match { + case Some(features) => + Publish(features.ToChannel, output, excluding) + case _ => ; + } + continueToMonitorDetails + .collect { case (charId, sguid) if sguid == guid && !excluding.exists(_ == charId) => + Publish(charId, output, Nil) } - } } - def AddInviteAndRespond(invitedPlayer : Long, targetInvite : Invitation, invitingPlayer : Long, name : String) : Unit = { - InviteResponseTemplate(indirectInviteResp)( - targetInvite, - AddInvite(invitedPlayer, targetInvite), - invitedPlayer, - invitingPlayer, - name - ) + /** + * Transform a list of squad unique identifiers into a list of `SquadInfo` objects for updating the squad list window. + * @param faction the faction to which the squads belong + * @return a `Vector` of transformed squad data + */ + def PublishedLists(faction : PlanetSideEmpire.Type) : Vector[SquadInfo] = { + PublishedLists(publishedLists(faction)) } - - def AltAddInviteAndRespond(invitedPlayer : Long, targetInvite : Invitation, invitingPlayer : Long, name : String) : Unit = { - InviteResponseTemplate(altIndirectInviteResp)( - targetInvite, - AddInvite(invitedPlayer, targetInvite), - invitedPlayer, - invitingPlayer, - name - ) - } - - def NextInviteAndRespond(invitedPlayer : Long) : Unit = { - NextInvite(invitedPlayer) match { - case Some(invite) => - InviteResponseTemplate(indirectInviteResp)( - invite, - Some(invite), - invitedPlayer, - invite.InviterCharId, - invite.InviterName - ) - case None => ; - } + /** + * Transform a list of squad unique identifiers into a list of `SquadInfo` objects for updating the squad list window. + * @param guids the list of squad unique identifier numbers + * @return a `Vector` of transformed squad data + */ + def PublishedLists(guids : Iterable[PlanetSideGUID]) : Vector[SquadInfo] = { + guids.map {guid => SquadService.SquadList.Publish(squadFeatures(guid).Squad) }.toVector } } object SquadService { + + /** + * Information necessary to display a specific map marker. + */ class WaypointData() { var zone_number : Int = 1 var pos : Vector3 = Vector3.z(1) //a waypoint with a non-zero z-coordinate will flag as not getting drawn } + /** + * 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 + */ abstract class Invitation(char_id : Long, name : String) { def InviterCharId : Long = char_id def InviterName : String = name @@ -2007,7 +2867,7 @@ object SquadService { * @param squad_guid the squad with the role * @param position the index of the role */ - final case class BidForRole(player : Player, squad_guid : PlanetSideGUID, position : Int) + final case class RequestRole(player : Player, squad_guid : PlanetSideGUID, position : Int) extends Invitation(player.CharId, player.Name) /** @@ -2050,7 +2910,7 @@ object SquadService { * @param squad_guid the squad with the role * @param position the index of the role */ - final case class InviteForRole(char_id : Long, name : String, squad_guid : PlanetSideGUID, position : Int) + final case class LookingForSquadRoleInvite(char_id : Long, name : String, squad_guid : PlanetSideGUID, position : Int) extends Invitation(char_id, name) /** @@ -2060,105 +2920,13 @@ object SquadService { final case class SpontaneousInvite(player : Player) extends Invitation(player.CharId, player.Name) - class SquadFeatures(val Squad : Squad) { - /** - * `initialAssociation` per squad is similar to "Does this squad want to recruit members?" - * The squad does not have to be flagged. - * Dispatches an `AssociateWithSquad` `SDAM` to the squad leader and ??? - * and then a `SDDUM` that includes at least the squad owner name and char id. - * Dispatched only once when a squad is first listed - * or when the squad leader searches for recruits by proximity or for certain roles or by invite - * or when a spontaneous squad forms, - * whichever happens first. - * Additionally, the packets are also sent when the check is made when the continent is changed (or set). - */ - private var initialAssociation : Boolean = true - /** - * na - */ - private var switchboard : ActorRef = ActorRef.noSender - /** - * Waypoint data. - * The first four slots are used for squad waypoints. - * The fifth slot is used for the squad leader experience waypoint. - * @see `Start` - */ - private var waypoints : Array[WaypointData] = Array[WaypointData]() - /** - * The particular position being recruited right at the moment. - * When `None`. no highlighted searches have been indicated. - * When a positive integer or 0, indicates distributed `InviteForRole` messages as recorded by `proxyInvites`. - * Only one position may bne actively recruited at a time in this case. - * When -1, indicates distributed `ProximityIvite` messages as recorded by `proxyInvites`. - * Previous efforts may or may not be forgotten if there is a switch between the two modes. - */ - private var searchForRole : Option[Int] = None - /** - * Handle persistent data related to `ProximityInvite` and `InviteForRole` messages - */ - private var proxyInvites : List[Long] = Nil - /** - * These useres rejected invitation to this squad. - * For the purposes of wide-searches for membership - * such as Looking For Squad checks and proximity invitation, - * the unique character identifier numbers in this list are skipped. - * Direct invitation requests from the non sqad member should remain functional. - */ - private var refusedPlayers : List[Long] = Nil - - def Start(implicit context : ActorContext) : SquadFeatures = { - switchboard = context.actorOf(Props[SquadSwitchboard], s"squad${Squad.GUID.guid}") - waypoints = Array.fill[WaypointData](5)(new WaypointData()) - this - } - - def Stop : SquadFeatures = { - switchboard ! akka.actor.PoisonPill - switchboard = Actor.noSender - waypoints = Array.empty - this - } - - def InitialAssociation : Boolean = initialAssociation - - def InitialAssociation_=(assoc : Boolean) : Boolean = { - initialAssociation = assoc - InitialAssociation - } - - def Switchboard : ActorRef = switchboard - - def Waypoints : Array[WaypointData] = waypoints - - def SearchForRole : Option[Int] = searchForRole - - def SearchForRole_=(role : Int) : Option[Int] = SearchForRole_=(Some(role)) - - def SearchForRole_=(role : Option[Int]) : Option[Int] = { - searchForRole = role - SearchForRole - } - - def ProxyInvites : List[Long] = proxyInvites - - def ProxyInvites_=(list : List[Long]) : List[Long] = { - proxyInvites = list - ProxyInvites - } - - def Refuse : List[Long] = refusedPlayers - - def Refuse_=(charId : Long) : List[Long] = { - Refuse_=(List(charId)) - } - - def Refuse_=(list : List[Long]) : List[Long] = { - refusedPlayers = list ++ refusedPlayers - Refuse - } - } - object SquadList { + /** + * Produce complete squad information. + * @see `SquadInfo` + * @param squad the squad + * @return the squad's information to be used in the squad list + */ def Publish(squad : Squad) : SquadInfo = { SquadInfo( squad.Leader.Name, @@ -2172,6 +2940,12 @@ object SquadService { } object Detail { + /** + * Produce complete squad membership details. + * @see `SquadDetail` + * @param squad the squad + * @return the squad's information to be used in the squad's detail window + */ def Publish(squad : Squad) : SquadDetail = { SquadDetail() .Field1(squad.GUID.guid) @@ -2193,6 +2967,11 @@ object SquadService { } } + /** + * Clear the current detail about a squad's membership and replace it with a previously stored details. + * @param squad the squad + * @param favorite the loadout object + */ def LoadSquadDefinition(squad : Squad, favorite : SquadLoadout) : Unit = { squad.Task = favorite.task squad.ZoneId = favorite.zone_id.getOrElse(squad.ZoneId) @@ -2211,11 +2990,62 @@ object SquadService { } } - def ValidOpenSquadPosition(squad : Squad, position : Int, member : Member, reqs : Set[CertificationType.Value]) : Boolean = { - ValidSquadPosition(squad, position, member, reqs) && member.CharId == 0 + /** + * Determine if the indicated squad role can be used for a player trying to join the squad. + * @see `ValidOpenSquadPosition` + * @param squad the squad + * @param position the position of the squad role being tested + * @param certs the certifications being offered for comparison testing + * @return `true`, if the squad role may accept the player with the given certification permissions; + * `false`, otherwise + */ + def ValidOpenSquadPosition(squad : Squad, position : Int, certs : Set[CertificationType.Value]) : Boolean = { + squad.Membership.lift(position) match { + case Some(member) => + ValidOpenSquadPosition(squad, position, member, certs) + case None => + false + } } - def ValidSquadPosition(squad : Squad, position : Int, member : Member, reqs : Set[CertificationType.Value]) : Boolean = { - squad.Availability(position) && reqs.intersect(member.Requirements) == member.Requirements + /** + * Determine if the indicated squad role can be used for a player trying to join the squad. + * @see `AvailableSquadPosition` + * @param squad the squad + * @param position the position of the squad role being tested + * @param member the squad role being tested + * @param certs the certifications being offered for comparison testing + * @return `true`, if the squad role may accept the player with the given certification permissions; + * `false`, otherwise + */ + def ValidOpenSquadPosition(squad : Squad, position : Int, member : Member, certs : Set[CertificationType.Value]) : Boolean = { + AvailableSquadPosition(squad, position, member) && certs.intersect(member.Requirements) == member.Requirements + } + + /** + * Determine if the indicated squad role is unoccupied but is also capable of being occupied. + * @see `AvailableSquadPosition(Squad, Int, Member)` + * @param squad the squad + * @param position the position of the squad role being tested + * @return `true`, if the squad role is unoccupied; + * `false`, otherwise + */ + def AvailableSquadPosition(squad : Squad, position : Int) : Boolean = squad.Membership.lift(position) match { + case Some(member) => + AvailableSquadPosition(squad, position, member) + case None => + false + } + + /** + * Determine if the indicated squad role is unoccupied but is also capable of being occupied. + * @param squad the squad + * @param position the position of the squad role being tested + * @param member the squad role being tested + * @return `true`, if the squad role is unoccupied; + * `false`, otherwise + */ + def AvailableSquadPosition(squad : Squad, position : Int, member : Member) : Boolean = { + squad.Availability(position) && member.CharId == 0 } } diff --git a/common/src/main/scala/services/teamwork/SquadServiceResponse.scala b/common/src/main/scala/services/teamwork/SquadServiceResponse.scala index 5e6bc998..47cf0387 100644 --- a/common/src/main/scala/services/teamwork/SquadServiceResponse.scala +++ b/common/src/main/scala/services/teamwork/SquadServiceResponse.scala @@ -1,7 +1,14 @@ // Copyright (c) 2019 PSForever package services.teamwork -import net.psforever.packet.game.SquadInfo import services.GenericEventBusMsg -final case class SquadServiceResponse(toChannel : String, response : SquadResponse.Response) extends GenericEventBusMsg +final case class SquadServiceResponse(toChannel : String, exclude : Iterable[Long], response : SquadResponse.Response) extends GenericEventBusMsg + +object SquadServiceResponse { + def apply(toChannel : String, response : SquadResponse.Response) : SquadServiceResponse = + SquadServiceResponse(toChannel, Nil, response) + + def apply(toChannel : String, exclude : Long, response : SquadResponse.Response) : SquadServiceResponse = + SquadServiceResponse(toChannel, Seq(exclude), response) +} diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index cacf4a97..2f5a03a8 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -101,6 +101,7 @@ class WorldSessionActor extends Actor with MDCContextAware { var whenUsedLastKit : Long = 0 val projectiles : Array[Option[Projectile]] = Array.fill[Option[Projectile]](Projectile.RangeUID - Projectile.BaseUID)(None) var drawDeloyableIcon : PlanetSideGameObject with Deployable => Unit = RedrawDeployableIcons + var updateSquad : () => Unit = NoSquadUpdates var recentTeleportAttempt : Long = 0 var lastTerminalOrderFulfillment : Boolean = true /** * used during zone transfers to maintain reference to seated vehicle (which does not yet exist in the new zone) @@ -118,12 +119,18 @@ class WorldSessionActor extends Actor with MDCContextAware { */ var interstellarFerryTopLevelGUID : Option[PlanetSideGUID] = None val squadUI : LongMap[SquadUIElement] = new LongMap[SquadUIElement]() + val squad_supplement_id : Int = 11 /** * `AvatarConverter` can only rely on the `Avatar`-local Looking For Squad variable. - * When joining or creating a squad, the original state of the avatar's LFS variable is stored here. - * Upon leaving or disbanding a squad, this value is restored to the avatar's LFS variable. + * When joining or creating a squad, the original state of the avatar's local LFS variable is blanked. + * This `WSA`-local variable is then used to indicate the ongoing state of the LFS UI component, + * now called "Looking for Squad Member." + * Upon leaving or disbanding a squad, this value is made false. + * Control switching between the `Avatar`-local and the `WSA`-local variable is contingent on `squadUI` being populated. */ var lfs : Boolean = false + var squadChannel : Option[String] = None + var squadSetup : () => Unit = FirstTimeSquadSetup var amsSpawnPoints : List[SpawnPoint] = Nil var clientKeepAlive : Cancellable = DefaultCancellable.obj @@ -168,9 +175,6 @@ class WorldSessionActor extends Actor with MDCContextAware { ) ++ player.Inventory.Items) .filterNot({ case InventoryItem(obj, _) => obj.isInstanceOf[BoomerTrigger] || obj.isInstanceOf[Telepad] }) //put any temporary value back into the avatar - if(squadUI.nonEmpty) { - avatar.LFS = lfs - } //TODO final character save before doing any of this (use equipment) continent.Population ! Zone.Population.Release(avatar) if(player.isAlive) { @@ -356,216 +360,221 @@ class WorldSessionActor extends Actor with MDCContextAware { case VehicleServiceResponse(toChannel, guid, reply) => HandleVehicleServiceResponse(toChannel, guid, reply) - case SquadServiceResponse(toChannel, response) => - response match { - case SquadResponse.ListSquadFavorite(line, task) => - sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), line, SquadAction.ListSquadFavorite(task))) + case SquadServiceResponse(_, excluded, response) => + if(!excluded.exists(_ == avatar.CharId)) { + response match { + case SquadResponse.ListSquadFavorite(line, task) => + sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), line, SquadAction.ListSquadFavorite(task))) - case SquadResponse.InitList(infos) if infos.nonEmpty => - sendResponse(ReplicationStreamMessage(infos)) + case SquadResponse.InitList(infos) => + sendResponse(ReplicationStreamMessage(infos)) - case SquadResponse.UpdateList(infos) if infos.nonEmpty => - val o = ReplicationStreamMessage(6, None, - infos.map { case (index, squadInfo) => - SquadListing(index, squadInfo) - }.toVector - ) - sendResponse( - ReplicationStreamMessage(6, None, - infos.map { case (index, squadInfo) => - SquadListing(index, squadInfo) - }.toVector - ) - ) - - case SquadResponse.RemoveFromList(infos) if infos.nonEmpty => - sendResponse( - ReplicationStreamMessage(1, None, - infos.map { index => - SquadListing(index, None) - }.toVector - ) - ) - - case SquadResponse.Detail(guid, detail) => - sendResponse(SquadDetailDefinitionUpdateMessage(guid, detail)) - - case SquadResponse.AssociateWithSquad(squad_guid) => - sendResponse(SquadDefinitionActionMessage(squad_guid, 0, SquadAction.AssociateWithSquad())) - - case SquadResponse.SetListSquad(squad_guid) => - sendResponse(SquadDefinitionActionMessage(squad_guid, 0, SquadAction.SetListSquad())) - - case SquadResponse.Unknown17(squad, char_id) => - sendResponse( - SquadDefinitionActionMessage(squad.GUID, 0, SquadAction.Unknown(33)) - ) - - case SquadResponse.Membership(request_type, unk1, unk2, char_id, opt_char_id, player_name, unk5, unk6) => - val name = request_type match { - case SquadResponseType.Invite if unk5 => - //player_name is our name; the name of the player indicated by unk3 is needed - LivePlayerList.WorldPopulation({ case (_, a : Avatar) => char_id == a.CharId }).headOption match { - case Some(player) => - player.name - case None => - player_name - } - case _ => - player_name - } - sendResponse(SquadMembershipResponse(request_type, unk1, unk2, char_id, opt_char_id, name, unk5, unk6)) - - case SquadResponse.Invite(from_char_id, to_char_id, name) => - sendResponse(SquadMembershipResponse(SquadResponseType.Invite, 0, 0, from_char_id, Some(to_char_id), s"$name", false, Some(None))) - - case SquadResponse.WantsSquadPosition(name : String) => - sendResponse( - ChatMsg( - ChatMessageType.CMT_TELL, true, "", - s"\\#6[SQUAD] \\#3$name\\#6 would like to join your squad. (respond with \\#3/accept\\#6 or \\#3/reject\\#6)", - None - ) - ) - - case SquadResponse.Join(squad, positionsToUpdate) => - val leader = squad.Leader - val id = 11 - val membershipPositions = squad.Membership - .zipWithIndex - .filter { case (_, index ) => positionsToUpdate.contains(index) } - membershipPositions.find({ case(member, _) => member.CharId == avatar.CharId }) match { - case Some((ourMember, ourIndex)) => - //we are joining the squad - //load each member's entry (our own too) - membershipPositions.foreach { case(member, index) => - sendResponse(SquadMemberEvent.Add(id, member.CharId, index, member.Name, member.ZoneId, unk7 = 0)) - squadUI(member.CharId) = SquadUIElement(member.Name, index, member.ZoneId, member.Health, member.Armor, member.Position) - } - //initialization - sendResponse(SquadMemberEvent.Add(id, ourMember.CharId, ourIndex, ourMember.Name, ourMember.ZoneId, unk7 = 0)) //repeat of our entry - sendResponse(PlanetsideAttributeMessage(player.GUID, 31, id)) //associate with squad? - sendResponse(PlanetsideAttributeMessage(player.GUID, 32, ourIndex)) //associate with member position in squad? - //a finalization? what does this do? - sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(18))) - //lfs state management (always OFF) - if(avatar.LFS) { - lfs = avatar.LFS - avatar.LFS = false - sendResponse(PlanetsideAttributeMessage(player.GUID, 53, 0)) - } - case _ => - //other player is joining our squad - //load each member's entry - membershipPositions.foreach { case(member, index) => - sendResponse(SquadMemberEvent.Add(id, member.CharId, index, member.Name, member.ZoneId, unk7 = 0)) - squadUI(member.CharId) = SquadUIElement(member.Name, index, member.ZoneId, member.Health, member.Armor, member.Position) - } - } - //send an initial dummy update for map icon(s) - sendResponse(SquadState(PlanetSideGUID(id), - membershipPositions - .filterNot { case (member, _) => member.CharId == avatar.CharId } - .map{ case (member, _) => SquadStateInfo(member.CharId, member.Health, member.Armor, member.Position, 2,2, false, 429, None,None) } - .toList - )) - log.info(s"SquadCards: ${squadUI.map { case(id, card) => s"[${card.index}:${card.name}/$id]" }.mkString}") - - case SquadResponse.Leave(_, positionsToUpdate) => - val id = 11 - positionsToUpdate.find({ case(member, _) => member == avatar.CharId }) match { - case Some((ourMember, ourIndex)) => - //we are leaving the squad - //remove each member's entry (our own too) - positionsToUpdate.foreach { case(member, index) => - sendResponse(SquadMemberEvent.Remove(id, member, index)) - squadUI.remove(member) - } - //uninitialize - sendResponse(SquadMemberEvent.Remove(id, ourMember, ourIndex)) //repeat of our entry - sendResponse(PlanetsideAttributeMessage(player.GUID, 31, 0)) //disassociate with squad? - sendResponse(PlanetsideAttributeMessage(player.GUID, 32, 0)) //disassociate with member position in squad? - sendResponse(PlanetsideAttributeMessage(player.GUID, 34, 4294967295L)) //unknown, perhaps unrelated? - //a finalization? what does this do? - sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(18))) - //lfs state management (maybe ON) - if(lfs) { - avatar.LFS = lfs - sendResponse(PlanetsideAttributeMessage(player.GUID, 53, 1)) - lfs = false - } - case _ => - //remove each member's entry - positionsToUpdate.foreach { case(member, index) => - sendResponse(SquadMemberEvent.Remove(id, member, index)) - squadUI.remove(member) - } - log.info(s"SquadCards: ${squadUI.map { case(id, card) => s"[${card.index}:${card.name}/$id]" }.mkString}") - } - - case SquadResponse.UpdateMembers(squad, positions) => - import services.teamwork.SquadAction.{Update => SAUpdate} - val id = 11 - val pairedEntries = positions.collect { - case entry if squadUI.contains(entry.char_id) => - (entry, 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(11, entry.char_id, element.index, entry.zone_number)) - squadUI(entry.char_id) = SquadUIElement(element.name, 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 - squadUI(entry.char_id) = SquadUIElement(element.name, element.index, entry.zone_number, entry.health, entry.armor, entry.pos) - entry - }) - .filterNot(_.char_id == avatar.CharId) //we want to update our backend, but not our frontend - if(updatedEntries.nonEmpty) { + case SquadResponse.UpdateList(infos) if infos.nonEmpty => sendResponse( - SquadState( - PlanetSideGUID(id), - updatedEntries.map { entry => SquadStateInfo(entry.char_id, entry.health, entry.armor, entry.pos, 2,2, false, 429, None,None)} + ReplicationStreamMessage(6, None, + infos.map { case (index, squadInfo) => + SquadListing(index, squadInfo) + }.toVector ) ) - } - case SquadResponse.AssignMember(squad, from_index, to_index) => - //we've already swapped position internally; now we swap the cards - SwapSquadUIElements(squad, from_index, to_index) - log.info(s"SquadCards: ${squadUI.map { case(id, card) => s"[${card.index}:${card.name}/$id]" }.mkString}") + case SquadResponse.RemoveFromList(infos) if infos.nonEmpty => + sendResponse( + ReplicationStreamMessage(1, None, + infos.map { index => + SquadListing(index, None) + }.toVector + ) + ) - case SquadResponse.PromoteMember(squad, char_id, from_index, to_index) => - //promotion will swap cards visually, but we must fix the backend - val id = 11 - sendResponse(SquadMemberEvent.Promote(id, char_id)) - SwapSquadUIElements(squad, from_index, to_index) - log.info(s"SquadCards: ${squadUI.map { case(id, card) => s"[${card.index}:${card.name}/$id]" }.mkString}") + case SquadResponse.Detail(guid, detail) => + sendResponse(SquadDetailDefinitionUpdateMessage(guid, detail)) - case SquadResponse.SquadSearchResults() => - //I don't actually know how to return search results - sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.NoSquadSearchResults())) + case SquadResponse.AssociateWithSquad(squad_guid) => + sendResponse(SquadDefinitionActionMessage(squad_guid, 0, SquadAction.AssociateWithSquad())) - case SquadResponse.InitWaypoints(char_id, waypoints) => - val id = 11 - StartBundlingPackets() - waypoints.foreach { case (waypoint_type, info, unk) => - sendResponse(SquadWaypointEvent.Add(id, char_id, waypoint_type, WaypointEvent(info.zone_number, info.pos, unk))) - } - StopBundlingPackets() + case SquadResponse.SetListSquad(squad_guid) => + sendResponse(SquadDefinitionActionMessage(squad_guid, 0, SquadAction.SetListSquad())) - case SquadResponse.WaypointEvent(WaypointEventAction.Add, char_id, waypoint_type, _, Some(info), unk) => - val id = 11 - sendResponse(SquadWaypointEvent.Add(id, char_id, waypoint_type, WaypointEvent(info.zone_number, info.pos, unk))) + case SquadResponse.Unknown17(squad, char_id) => + sendResponse( + SquadDefinitionActionMessage(squad.GUID, 0, SquadAction.Unknown(33)) + ) - case SquadResponse.WaypointEvent(WaypointEventAction.Remove, char_id, waypoint_type, _, _, _) => - val id = 11 - sendResponse(SquadWaypointEvent.Remove(id, char_id, waypoint_type)) + case SquadResponse.Membership(request_type, unk1, unk2, char_id, opt_char_id, player_name, unk5, unk6) => + val name = request_type match { + case SquadResponseType.Invite if unk5 => + //player_name is our name; the name of the player indicated by unk3 is needed + LivePlayerList.WorldPopulation({ case (_, a : Avatar) => char_id == a.CharId }).headOption match { + case Some(player) => + player.name + case None => + player_name + } + case _ => + player_name + } + sendResponse(SquadMembershipResponse(request_type, unk1, unk2, char_id, opt_char_id, name, unk5, unk6)) - case _ => ; + case SquadResponse.Invite(from_char_id, to_char_id, name) => + sendResponse(SquadMembershipResponse(SquadResponseType.Invite, 0, 0, from_char_id, Some(to_char_id), s"$name", false, Some(None))) + + case SquadResponse.WantsSquadPosition(_, name) => + sendResponse( + ChatMsg( + ChatMessageType.CMT_SQUAD, true, name, + s"\\#6 would like to join your squad. (respond with \\#3/accept\\#6 or \\#3/reject\\#6)", + None + ) + ) + + case SquadResponse.Join(squad, positionsToUpdate, toChannel) => + val leader = squad.Leader + val membershipPositions = squad.Membership + .zipWithIndex + .filter { case (_, index ) => positionsToUpdate.contains(index) } + membershipPositions.find({ case(member, _) => member.CharId == avatar.CharId }) match { + case Some((ourMember, ourIndex)) => + //we are joining the squad + //load each member's entry (our own too) + membershipPositions.foreach { case(member, index) => + sendResponse(SquadMemberEvent.Add(squad_supplement_id, member.CharId, index, member.Name, member.ZoneId, unk7 = 0)) + squadUI(member.CharId) = SquadUIElement(member.Name, index, member.ZoneId, member.Health, member.Armor, member.Position) + } + //repeat our entry + sendResponse(SquadMemberEvent.Add(squad_supplement_id, ourMember.CharId, ourIndex, ourMember.Name, ourMember.ZoneId, unk7 = 0)) //repeat of our entry + //turn lfs off + val factionOnContinentChannel = s"${continent.Id}/${player.Faction}" + if(avatar.LFS) { + avatar.LFS = false + sendResponse(PlanetsideAttributeMessage(player.GUID, 53, 0)) + avatarService ! AvatarServiceMessage(factionOnContinentChannel, AvatarAction.PlanetsideAttribute(player.GUID, 53, 0)) + } + //squad colors + sendResponse(PlanetsideAttributeMessage(player.GUID, 31, squad_supplement_id)) + avatarService ! AvatarServiceMessage(factionOnContinentChannel, AvatarAction.PlanetsideAttribute(player.GUID, 31, squad_supplement_id)) + sendResponse(PlanetsideAttributeMessage(player.GUID, 32, ourIndex)) //associate with member position in squad + //a finalization? what does this do? + sendResponse(SquadDefinitionActionMessage(squad.GUID, 0, SquadAction.Unknown(18))) + updateSquad = UpdatesWhenEnrolledInSquad + squadChannel = Some(toChannel) + case _ => + //other player is joining our squad + //load each member's entry + membershipPositions.foreach { case(member, index) => + sendResponse(SquadMemberEvent.Add(squad_supplement_id, member.CharId, index, member.Name, member.ZoneId, unk7 = 0)) + squadUI(member.CharId) = SquadUIElement(member.Name, index, member.ZoneId, member.Health, member.Armor, member.Position) + } + } + //send an initial dummy update for map icon(s) + sendResponse(SquadState(PlanetSideGUID(squad_supplement_id), + membershipPositions + .filterNot { case (member, _) => member.CharId == avatar.CharId } + .map{ case (member, _) => SquadStateInfo(member.CharId, member.Health, member.Armor, member.Position, 2,2, false, 429, None,None) } + .toList + )) + + case SquadResponse.Leave(squad, positionsToUpdate) => + positionsToUpdate.find({ case(member, _) => member == avatar.CharId }) match { + case Some((ourMember, ourIndex)) => + //we are leaving the squad + //remove each member's entry (our own too) + positionsToUpdate.foreach { case(member, index) => + sendResponse(SquadMemberEvent.Remove(squad_supplement_id, member, index)) + squadUI.remove(member) + } + //uninitialize + sendResponse(SquadMemberEvent.Remove(squad_supplement_id, ourMember, ourIndex)) //repeat of our entry + sendResponse(PlanetsideAttributeMessage(player.GUID, 31, 0)) //disassociate with squad? + sendResponse(PlanetsideAttributeMessage(player.GUID, 32, 0)) //disassociate with member position in squad? + sendResponse(PlanetsideAttributeMessage(player.GUID, 34, 4294967295L)) //unknown, perhaps unrelated? + lfs = false + //a finalization? what does this do? + sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(18))) + updateSquad = NoSquadUpdates + squadChannel = None + case _ => + //remove each member's entry + positionsToUpdate.foreach { case(member, index) => + sendResponse(SquadMemberEvent.Remove(squad_supplement_id, member, index)) + squadUI.remove(member) + } + } + + case SquadResponse.AssignMember(squad, from_index, to_index) => + //we've already swapped position internally; now we swap the cards + SwapSquadUIElements(squad, from_index, to_index) + + case SquadResponse.PromoteMember(squad, char_id, from_index, to_index) => + val charId = player.CharId + val guid = player.GUID + lazy val factionOnContinentChannel = s"${continent.Id}/${player.Faction}" + //are we being demoted? + if(squadUI(charId).index == 0) { + //lfsm -> lfs + if(lfs) { + sendResponse(PlanetsideAttributeMessage(guid, 53, 0)) + avatarService ! AvatarServiceMessage(factionOnContinentChannel, AvatarAction.PlanetsideAttribute(guid, 53, 0)) + } + lfs = false + sendResponse(PlanetsideAttributeMessage(guid, 32, from_index)) //associate with member position in squad + } + //are we being promoted? + else if(charId == char_id) { + sendResponse(PlanetsideAttributeMessage(guid, 32, 0)) //associate with member position in squad + } + avatarService ! AvatarServiceMessage(factionOnContinentChannel, AvatarAction.PlanetsideAttribute(guid, 31, squad_supplement_id)) + //we must fix the squad cards backend + SwapSquadUIElements(squad, from_index, to_index) + + case SquadResponse.UpdateMembers(squad, positions) => + import services.teamwork.SquadAction.{Update => SAUpdate} + val pairedEntries = positions.collect { + case entry if squadUI.contains(entry.char_id) => + (entry, 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(squad_supplement_id, entry.char_id, element.index, entry.zone_number)) + squadUI(entry.char_id) = SquadUIElement(element.name, 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 + squadUI(entry.char_id) = SquadUIElement(element.name, element.index, entry.zone_number, entry.health, entry.armor, entry.pos) + entry + }) + .filterNot(_.char_id == avatar.CharId) //we want to update our backend, but not our frontend + if(updatedEntries.nonEmpty) { + sendResponse( + SquadState( + PlanetSideGUID(squad_supplement_id), + updatedEntries.map { entry => SquadStateInfo(entry.char_id, entry.health, entry.armor, entry.pos, 2,2, false, 429, None,None)} + ) + ) + } + + case SquadResponse.SquadSearchResults() => + //I don't actually know how to return search results + sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.NoSquadSearchResults())) + + case SquadResponse.InitWaypoints(char_id, waypoints) => + StartBundlingPackets() + waypoints.foreach { case (waypoint_type, info, unk) => + sendResponse(SquadWaypointEvent.Add(squad_supplement_id, char_id, waypoint_type, WaypointEvent(info.zone_number, info.pos, unk))) + } + StopBundlingPackets() + + case SquadResponse.WaypointEvent(WaypointEventAction.Add, char_id, waypoint_type, _, Some(info), unk) => + sendResponse(SquadWaypointEvent.Add(squad_supplement_id, char_id, waypoint_type, WaypointEvent(info.zone_number, info.pos, unk))) + + case SquadResponse.WaypointEvent(WaypointEventAction.Remove, char_id, waypoint_type, _, _, _) => + sendResponse(SquadWaypointEvent.Remove(squad_supplement_id, char_id, waypoint_type)) + + case _ => ; + } } case Deployment.CanDeploy(obj, state) => @@ -1040,6 +1049,7 @@ class WorldSessionActor extends Actor with MDCContextAware { galaxyService ! Service.Join(s"${avatar.faction}") //for hotspots squadService ! Service.Join(s"${avatar.faction}") //channel will be player.Faction squadService ! Service.Join(s"${avatar.CharId}") //channel will be player.CharId (in order to work with packets) + squadService ! Service.Join(s"${avatar.faction}") //channel will be player.Faction cluster ! InterstellarCluster.GetWorld("home3") case InterstellarCluster.GiveWorld(zoneId, zone) => @@ -3034,7 +3044,15 @@ class WorldSessionActor extends Actor with MDCContextAware { sendResponse(SetChatFilterMessage(ChatChannel.Local, false, ChatChannel.values.toList)) //TODO will not always be "on" like this deadState = DeadState.Alive sendResponse(AvatarDeadStateMessage(DeadState.Alive, 0, 0, tplayer.Position, player.Faction, true)) - sendResponse(PlanetsideAttributeMessage(guid, 53, tplayer.LFS)) + //looking for squad (members) + if(squadUI.nonEmpty && squadUI(avatar.CharId).index == 0) { + sendResponse(PlanetsideAttributeMessage(guid, 31, 1)) + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(guid, 31, 1)) + } + if(tplayer.LFS || lfs) { + sendResponse(PlanetsideAttributeMessage(guid, 53, 1)) + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(guid, 53, 1)) + } sendResponse(AvatarSearchCriteriaMessage(guid, List(0, 0, 0, 0, 0, 0))) (1 to 73).foreach(i => { // not all GUID's are set, and not all of the set ones will always be zero; what does this section do? @@ -3047,14 +3065,8 @@ class WorldSessionActor extends Actor with MDCContextAware { //AvatarAwardMessage //DisplayAwardMessage sendResponse(PlanetsideStringAttributeMessage(guid, 0, "Outfit Name")) - sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(6))) - (0 to 9).foreach(line => { - sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), line, SquadAction.ListSquadFavorite(""))) - }) - sendResponse(SquadDetailDefinitionUpdateMessage.Init) - sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0,SquadAction.AssociateWithSquad())) - sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0,SquadAction.SetListSquad())) - sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0,SquadAction.Unknown(18))) + //squad stuff (loadouts, assignment) + squadSetup() //MapObjectStateBlockMessage and ObjectCreateMessage? //TacticsMessage? //change the owner on our deployables (re-draw the icons for our deployables too) @@ -3089,7 +3101,30 @@ class WorldSessionActor extends Actor with MDCContextAware { interstellarFerryTopLevelGUID = None case _ => ; } - squadService ! Service.Join(s"${avatar.faction}") //channel will be player.Faction + } + + def FirstTimeSquadSetup() : Unit = { + sendResponse(SquadDetailDefinitionUpdateMessage.Init) + sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(6))) + //only need to load these once + avatar.SquadLoadouts.Loadouts.foreach { + case (index, loadout : SquadLoadout) => + sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), index, SquadAction.ListSquadFavorite(loadout.task))) + } + sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.AssociateWithSquad())) + sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.SetListSquad())) + sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(18))) + squadService ! SquadServiceMessage(player, continent, SquadServiceAction.InitSquadList()) + squadService ! SquadServiceMessage(player, continent, SquadServiceAction.InitCharId()) + squadSetup = SubsequentSpawnSquadSetup + } + + def SubsequentSpawnSquadSetup() : Unit = { + if(squadUI.nonEmpty) { + //sendResponse(PlanetsideAttributeMessage(player.GUID, 31, squad_supplement_id)) + val (_, ourCard) = squadUI.find { case (id, card) => id == player.CharId }.get + sendResponse(PlanetsideAttributeMessage(player.GUID, 32, ourCard.index)) + } } def handleControlPkt(pkt : PlanetSideControlPacket) = { @@ -3503,23 +3538,7 @@ class WorldSessionActor extends Actor with MDCContextAware { case msg @ PlayerStateMessageUpstream(avatar_guid, pos, vel, yaw, pitch, yaw_upper, seq_time, unk3, is_crouching, is_jumping, unk4, is_cloaking, unk5, unk6) => if(deadState == DeadState.Alive) { if(!player.Crouching && is_crouching) { //SQUAD TESTING CODE - sendResponse( - CharacterKnowledgeMessage( - 41577140L, - CharacterKnowledgeInfo( - "Degrado", - Set( - CertificationType.StandardAssault, - CertificationType.AgileExoSuit, - CertificationType.StandardExoSuit - ), - 37, - 5, - PlanetSideGUID(7) - ) - ) - ) - sendResponse(SquadInvitationRequestMessage(PlanetSideGUID(1), 4, 41577140L, "Degrado")) + //... } player.Position = pos player.Velocity = vel @@ -3562,7 +3581,7 @@ class WorldSessionActor extends Actor with MDCContextAware { case None => false } avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlayerState(avatar_guid, msg, spectator, wepInHand)) - //squadService ! SquadServiceMessage(tplayer, continent, SquadAction.Update(tplayer.CharId, tplayer.Health, tplayer.MaxHealth, tplayer.Armor, tplayer.MaxArmor, pos, zone.Number)) + updateSquad() } case msg @ ChildObjectStateMessage(object_guid, pitch, yaw) => @@ -3603,6 +3622,8 @@ class WorldSessionActor extends Actor with MDCContextAware { } vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.VehicleState(player.GUID, vehicle_guid, unk1, pos, ang, vel, flight, unk6, unk7, wheels, unk9, unkA)) } + //vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.VehicleState(player.GUID, vehicle_guid, unk1, pos, ang, vel, flight, unk6, unk7, wheels, unk9, unkA)) + updateSquad() case (None, _) => //log.error(s"VehicleState: no vehicle $vehicle_guid found in zone") //TODO placing a "not driving" warning here may trigger as we are disembarking the vehicle @@ -4704,24 +4725,28 @@ class WorldSessionActor extends Actor with MDCContextAware { log.info(s"GenericObject: $player is MAX with an unexpected weapon - ${definition.Name}") } } - else if(action == 37) { //Looking For Squad OFF - if(squadUI.nonEmpty) { - lfs = false - } - else if(avatar.LFS) { - avatar.LFS = false - avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(player.GUID, 53, 0)) - log.info(s"GenericObject: ${player.Name} is no longer looking for a squad to join") - } - } else if(action == 36) { //Looking For Squad ON if(squadUI.nonEmpty) { - lfs = true + if(!lfs && squadUI(player.CharId).index == 0) { + lfs = true + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(player.GUID, 53, 1)) + } } else if(!avatar.LFS) { avatar.LFS = true avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(player.GUID, 53, 1)) - log.info(s"GenericObject: ${player.Name} has made himself available to join a squad") + } + } + else if(action == 37) { //Looking For Squad OFF + if(squadUI.nonEmpty) { + if(lfs && squadUI(player.CharId).index == 0) { + lfs = false + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(player.GUID, 53, 0)) + } + } + else if(avatar.LFS) { + avatar.LFS = false + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(player.GUID, 53, 0)) } } @@ -9185,17 +9210,15 @@ class WorldSessionActor extends Actor with MDCContextAware { def SwapSquadUIElements(squad : Squad, fromIndex : Int, toIndex : Int) : Unit = { if(squadUI.nonEmpty) { - val fromMember = squad.Membership(fromIndex) + val fromMember = squad.Membership(toIndex) //the players have already been swapped in the backend object val fromCharId = fromMember.CharId - val toMember = squad.Membership(toIndex) + val toMember = squad.Membership(fromIndex) //the players have already been swapped in the backend object val toCharId = toMember.CharId val id = 11 - if(fromCharId > 0) { + if(toCharId > 0) { //toMember and fromMember have swapped places val fromElem = squadUI(fromCharId) val toElem = squadUI(toCharId) - sendResponse(SquadMemberEvent.Remove(id, toCharId, fromIndex)) - sendResponse(SquadMemberEvent.Remove(id, fromCharId, toIndex)) squadUI(toCharId) = SquadUIElement(fromElem.name, toIndex, fromElem.zone, fromElem.health, fromElem.armor, fromElem.position) squadUI(fromCharId) = SquadUIElement(toElem.name, fromIndex, toElem.zone, toElem.health, toElem.armor, toElem.position) sendResponse(SquadMemberEvent.Add(id, toCharId, toIndex, fromElem.name, fromElem.zone, unk7 = 0)) @@ -9212,14 +9235,14 @@ class WorldSessionActor extends Actor with MDCContextAware { } else { //previous fromMember has moved toMember - val elem = squadUI(toCharId) - sendResponse(SquadMemberEvent.Remove(id, toCharId, fromIndex)) - squadUI(toCharId) = SquadUIElement(elem.name, toIndex, elem.zone, elem.health, elem.armor, elem.position) - sendResponse(SquadMemberEvent.Add(id, toCharId, toIndex, elem.name, elem.zone, unk7 = 0)) + val elem = squadUI(fromCharId) + squadUI(fromCharId) = SquadUIElement(elem.name, toIndex, elem.zone, elem.health, elem.armor, elem.position) + sendResponse(SquadMemberEvent.Remove(id, fromCharId, fromIndex)) + sendResponse(SquadMemberEvent.Add(id, fromCharId, toIndex, elem.name, elem.zone, unk7 = 0)) sendResponse( SquadState( PlanetSideGUID(id), - List(SquadStateInfo(toCharId, elem.health, elem.armor, elem.position, 2, 2, false, 429, None, None)) + List(SquadStateInfo(fromCharId, elem.health, elem.armor, elem.position, 2, 2, false, 429, None, None)) ) ) } @@ -9233,6 +9256,23 @@ class WorldSessionActor extends Actor with MDCContextAware { } } + def NoSquadUpdates() : Unit = { } + + def UpdatesWhenEnrolledInSquad() : Unit = { + squadService ! SquadServiceMessage( + player, + continent, + continent.GUID(player.VehicleSeated) match { + case Some(vehicle : Vehicle) => + SquadServiceAction.Update(player.CharId, vehicle.Health, vehicle.MaxHealth, vehicle.Shields, vehicle.MaxShields, vehicle.Position, continent.Number) + case Some(obj : PlanetSideGameObject with WeaponTurret) => + SquadServiceAction.Update(player.CharId, obj.Health, obj.MaxHealth, 0, 0, obj.Position, continent.Number) + case _ => + SquadServiceAction.Update(player.CharId, player.Health, player.MaxHealth, player.Armor, player.MaxArmor, player.Position, continent.Number) + } + ) + } + def failWithError(error : String) = { log.error(error) sendResponse(ConnectionClose())