diff --git a/common/src/main/scala/net/psforever/objects/Avatar.scala b/common/src/main/scala/net/psforever/objects/Avatar.scala
index ceae122b..0416b6a9 100644
--- a/common/src/main/scala/net/psforever/objects/Avatar.scala
+++ b/common/src/main/scala/net/psforever/objects/Avatar.scala
@@ -46,6 +46,17 @@ 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.
+ */
+ private var lfs : Boolean = false
def CharId : Long = char_id
@@ -184,6 +195,13 @@ class Avatar(private val char_id : Long, val name : String, val faction : Planet
def Deployables : DeployableToolbox = deployables
+ def LFS : Boolean = lfs
+
+ def LFS_=(looking : Boolean) : Boolean = {
+ lfs = looking
+ LFS
+ }
+
def Definition : AvatarDefinition = GlobalDefinitions.avatar
/*
diff --git a/common/src/main/scala/net/psforever/objects/Player.scala b/common/src/main/scala/net/psforever/objects/Player.scala
index 7577b545..5ab70f6a 100644
--- a/common/src/main/scala/net/psforever/objects/Player.scala
+++ b/common/src/main/scala/net/psforever/objects/Player.scala
@@ -70,6 +70,8 @@ class Player(private val core : Avatar) extends PlanetSideGameObject
def Voice : CharacterVoice.Value = core.voice
+ def LFS : Boolean = core.LFS
+
def isAlive : Boolean = alive
def isBackpack : Boolean = backpack
diff --git a/common/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala
index bbd9f1f5..828a434c 100644
--- a/common/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala
+++ b/common/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala
@@ -98,7 +98,7 @@ object AvatarConverter {
false,
facingPitch = obj.Orientation.y,
facingYawUpper = obj.FacingYawUpper,
- lfs = true,
+ obj.LFS,
GrenadeState.None,
obj.Cloaked,
false,
diff --git a/common/src/main/scala/net/psforever/types/SquadRequestType.scala b/common/src/main/scala/net/psforever/types/SquadRequestType.scala
index 1b22bc38..a9c35f7a 100644
--- a/common/src/main/scala/net/psforever/types/SquadRequestType.scala
+++ b/common/src/main/scala/net/psforever/types/SquadRequestType.scala
@@ -8,7 +8,7 @@ object SquadRequestType extends Enumeration {
type Type = Value
val
Invite,
- Unk01,
+ ProximityInvite,
Accept,
Reject,
Cancel,
diff --git a/common/src/main/scala/services/teamwork/SquadService.scala b/common/src/main/scala/services/teamwork/SquadService.scala
index 5a439432..5bbd895c 100644
--- a/common/src/main/scala/services/teamwork/SquadService.scala
+++ b/common/src/main/scala/services/teamwork/SquadService.scala
@@ -266,14 +266,41 @@ class SquadService extends Actor {
// case _ => ;
// }
(memberToSquad.get(invitingPlayer), memberToSquad.get(invitedPlayer)) match {
- case (Some(squad1), Some(squad2)) =>
- //both players are in squads
- if(squad1.GUID == squad2.GUID) {
- //both players are in the same squad; no need to do anything
+ 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 squad2.Size == 1 =>
+ //both players belong to squads, but the invitedplayer's squad 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
+ val bid = VacancyInvite(charId, tplayer.Name, squad1.GUID)
+ AddInvite(invitedPlayer, bid) match {
+ case out @ Some(_) if out.contains(bid) =>
+ SquadEvents.publish(SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, charId, Some(invitedPlayer), tplayer.Name, false, Some(None))))
+ SquadEvents.publish(SquadServiceResponse(s"/$charId/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, invitedPlayer, Some(charId), tplayer.Name, true, Some(None))))
+ case Some(_) =>
+ SquadEvents.publish(SquadServiceResponse(s"/$charId/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, invitedPlayer, Some(charId), tplayer.Name, true, Some(None))))
+ case _ => ;
}
- else {
- //we might do some platoon chicanery with this case later
- //TODO platoons
+
+ 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 leaderCharId = squad2.Leader.CharId
+ val bid = IndirectVacancy(tplayer, squad2.GUID)
+ log.warn(s"$invitedPlayer has asked $invitingPlayer for an invitation to squad ${squad2.Task}, but the squad leader needs to approve")
+ AddInvite(leaderCharId, bid) match {
+ case out @ Some(_) if out.contains(bid) =>
+ HandleBidForPosition(bid, tplayer)
+ case _ => ;
}
case (Some(squad), None) =>
@@ -314,79 +341,76 @@ class SquadService extends Actor {
case _ => ;
}
- case _ => ;
+ case _ => //
}
case SquadAction.Membership(SquadRequestType.Accept, invitedPlayer, _, _, _) =>
val acceptedInvite = RemoveInvite(invitedPlayer)
- acceptedInvite match {
- case Some(BidForPosition(petitioner, guid, position)) if idToSquad.get(guid).nonEmpty =>
- //player requested to join a squad's specific position
- //invitedPlayer is actually the squad leader; petitioner is the actual "invitedPlayer"
- if(memberToSquad.get(petitioner.CharId).isEmpty) {
+ if(EnsureEmptySquad(invitedPlayer, "Accept: the invited player is already a member of a squad and can not join a second one")) {
+ acceptedInvite match {
+ case Some(BidForPosition(petitioner, guid, position)) if idToSquad.get(guid).nonEmpty =>
+ //player requested to join a squad's specific position
+ //invitedPlayer is actually the squad leader; petitioner is the actual "invitedPlayer"
JoinSquad(petitioner, idToSquad(guid), position)
- }
- else {
- log.warn("Accept->Bid: the invited player is already a member of a squad and can not join a second one")
- }
- case Some(IndirectVacancy(recruit, guid)) =>
- //tplayer / invitedPlayer is actually the squad leader
- val recuitCharId = recruit.CharId
- HandleVacancyInvite(guid, recuitCharId, invitedPlayer, recruit) match {
- case Some((squad, line)) =>
- SquadEvents.publish(SquadServiceResponse(s"/$recuitCharId/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitedPlayer, Some(recuitCharId), "", true, Some(None))))
- JoinSquad(recruit, squad, line)
+ case Some(IndirectVacancy(recruit, guid)) =>
+ //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"/$recruitCharId/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitedPlayer, Some(recruitCharId), "", true, Some(None))))
+ JoinSquad(recruit, squad, line)
//since we are the squad leader, we do not want to brush off our queued squad invite tasks
- case _ => ;
- }
+ case _ => ;
+ }
- case Some(VacancyInvite(invitingPlayer, _, guid)) =>
- //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))))
- JoinSquad(tplayer, squad, line)
- RemoveQueuedInvites(invitedPlayer) //TODO deal with these somehow
- case _ => ;
- }
+ case Some(VacancyInvite(invitingPlayer, _, guid)) =>
+ //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))))
+ JoinSquad(tplayer, squad, line)
+ RemoveQueuedInvites(invitedPlayer) //TODO deal with these somehow
+ case _ => ;
+ }
- case Some(SpontaneousInvite(invitingPlayer)) =>
- //originally, we were invited by someone into a new squad they would form
- val invitingPlayerCharId = invitingPlayer.CharId
- (GetParticipatingSquad(invitingPlayer) match {
- case Some(participating) =>
- //invitingPlayer became part of a squad while invited player was answering the original summons
- Some(participating)
- case _ =>
- //generate a new squad, with invitingPlayer as the leader
- val squad = StartSquad(invitingPlayer)
- squad.Task = s"${invitingPlayer.Name}'s Squad"
- SquadEvents.publish( SquadServiceResponse(s"/$invitingPlayerCharId/Squad", SquadResponse.AssociateWithSquad(squad.GUID)) )
- Some(squad)
- }) match {
- case Some(squad) =>
- HandleVacancyInvite(squad.GUID, 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))) )
- JoinSquad(tplayer, squad, line)
- SquadEvents.publish( SquadServiceResponse(s"/$invitingPlayerCharId/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitingPlayerCharId, Some(invitedPlayer), tplayer.Name, false, Some(None))) )
- RemoveQueuedInvites(tplayer.CharId) //TODO deal with these somehow
- case _ => ;
- }
- case _ => ;
- }
+ case Some(SpontaneousInvite(invitingPlayer)) =>
+ //originally, we were invited by someone into a new squad they would form
+ val invitingPlayerCharId = invitingPlayer.CharId
+ (GetParticipatingSquad(invitingPlayer) match {
+ case Some(participating) =>
+ //invitingPlayer became part of a squad while invited player was answering the original summons
+ Some(participating)
+ case _ =>
+ //generate a new squad, with invitingPlayer as the leader
+ val squad = StartSquad(invitingPlayer)
+ squad.Task = s"${invitingPlayer.Name}'s Squad"
+ SquadEvents.publish( SquadServiceResponse(s"/$invitingPlayerCharId/Squad", SquadResponse.AssociateWithSquad(squad.GUID)) )
+ Some(squad)
+ }) match {
+ case Some(squad) =>
+ HandleVacancyInvite(squad.GUID, 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))) )
+ JoinSquad(tplayer, squad, line)
+ SquadEvents.publish( SquadServiceResponse(s"/$invitingPlayerCharId/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitingPlayerCharId, Some(invitedPlayer), tplayer.Name, false, Some(None))) )
+ RemoveQueuedInvites(tplayer.CharId) //TODO deal with these somehow
+ case _ => ;
+ }
+ case _ => ;
+ }
- case None =>
- //the invite either timed-out or was withdrawn; select a new one?
- NextInvite(invitedPlayer) match {
- case Some(bid : BidForPosition) if !acceptedInvite.contains(bid) =>
- HandleBidForPosition(bid, tplayer)
- case Some(bid) if !acceptedInvite.contains(bid) =>
- SquadEvents.publish(SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, bid.InviterCharId, Some(invitedPlayer), bid.InviterName, false, Some(None))))
- case None => ;
- }
+ case None =>
+ //the invite either timed-out or was withdrawn; select a new one?
+ NextInvite(invitedPlayer) match {
+ case Some(bid : BidForPosition) if !acceptedInvite.contains(bid) =>
+ HandleBidForPosition(bid, tplayer)
+ case Some(bid) if !acceptedInvite.contains(bid) =>
+ SquadEvents.publish(SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, bid.InviterCharId, Some(invitedPlayer), bid.InviterName, false, Some(None))))
+ case None => ;
+ }
+ }
}
case SquadAction.Membership(SquadRequestType.Leave, leavingPlayer, optionalPlayer, _, _) =>
@@ -712,24 +736,32 @@ class SquadService extends Actor {
}
case ResetAll() =>
- val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
- squad.Task = ""
- squad.ZoneId = None
- squad.Availability.indices.foreach { i =>
- squad.Availability.update(i, true)
+ lSquadOpt match {
+ case Some(squad) if squad.Size > 1 =>
+ squad.Task = ""
+ squad.ZoneId = None
+ squad.Availability.indices.foreach { i =>
+ squad.Availability.update(i, true)
+ }
+ squad.Membership.foreach(position => {
+ position.Role = ""
+ position.Orders = ""
+ position.Requirements = Set()
+ })
+ squad.LocationFollowsSquadLead = false
+ squad.AutoApproveInvitationRequests = false
+ UpdateSquadListWhenListed(squad, SquadInfo().Task("").ZoneId(None).Capacity(squad.Capacity))
+ UpdateSquadDetail(squad.GUID, squad)
+ sender ! SquadServiceResponse("", SquadResponse.AssociateWithSquad(PlanetSideGUID(0)))
+ if(!initialAssociation.contains(squad.GUID)) {
+ initialAssociation += squad.GUID
+ }
+ //do not unlist an already listed squad
+ case Some(squad) =>
+ //underutilized squad; just close it out
+ CloseOutSquad(squad)
+ case _ => ;
}
- squad.Membership.foreach(position => {
- position.Role = ""
- position.Orders = ""
- position.Requirements = Set()
- })
- squad.LocationFollowsSquadLead = false
- squad.AutoApproveInvitationRequests = false
- UpdateSquadListWhenListed(squad, SquadInfo().Task("").ZoneId(None).Capacity(squad.Capacity))
- UpdateSquadDetail(squad.GUID, squad)
- sender ! SquadServiceResponse("", SquadResponse.AssociateWithSquad(PlanetSideGUID(0)))
- initialAssociation += squad.GUID
- //do not unlist an already listed squad
case _ =>
}
@@ -1193,6 +1225,19 @@ 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 =>
+ CloseOutSquad(squad)
+ true
+ case _ =>
+ log.warn(msg)
+ false
+ }
+ }
+
def CloseOutSquad(squad : Squad) : Unit = {
val membership = squad.Membership.zipWithIndex
CloseOutSquad(
diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala
index bde61079..3fe21933 100644
--- a/pslogin/src/main/scala/WorldSessionActor.scala
+++ b/pslogin/src/main/scala/WorldSessionActor.scala
@@ -118,6 +118,12 @@ class WorldSessionActor extends Actor with MDCContextAware {
*/
var interstellarFerryTopLevelGUID : Option[PlanetSideGUID] = None
val squadUI : LongMap[SquadUIElement] = new LongMap[SquadUIElement]()
+ /**
+ * `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.
+ */
+ var lfs : Boolean = false
var amsSpawnPoints : List[SpawnPoint] = Nil
var clientKeepAlive : Cancellable = DefaultCancellable.obj
@@ -160,6 +166,10 @@ class WorldSessionActor extends Actor with MDCContextAware {
.collect { case ((index, Some(obj))) => InventoryItem(obj, index) }
) ++ 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) {
@@ -437,6 +447,12 @@ class WorldSessionActor extends Actor with MDCContextAware {
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
@@ -471,6 +487,12 @@ class WorldSessionActor extends Actor with MDCContextAware {
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) =>
@@ -3011,7 +3033,7 @@ 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, 1))
+ sendResponse(PlanetsideAttributeMessage(guid, 53, tplayer.LFS))
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?
@@ -4663,7 +4685,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
log.info(s"GenericObject: $player is MAX with an unexpected weapon - ${definition.Name}")
}
}
- else if(action == 16) {
+ else if(action == 16) { //max deployment
log.info(s"GenericObject: $player has released the anchors")
player.UsingSpecial = SpecialExoSuitDefinition.Mode.Normal
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(player.GUID, 19, 0))
@@ -4681,6 +4703,26 @@ 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
+ }
+ 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")
+ }
+ }
case msg @ ItemTransactionMessage(terminal_guid, transaction_type, _, _, _, _) =>
log.info("ItemTransaction: " + msg)