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