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)