From 56d8748e994c0c768a4c3808ffab109997255ecb Mon Sep 17 00:00:00 2001 From: FateJH Date: Tue, 13 Aug 2019 12:52:43 -0400 Subject: [PATCH] modified squad user initialization messages to better focus on specific steps of the process; dealt with different methods of communicating squad information to users, though not yet ready to implement the switchboard protocol --- .../game/SquadDefinitionActionMessage.scala | 24 +- .../scala/services/teamwork/SquadAction.scala | 3 +- .../services/teamwork/SquadResponse.scala | 4 +- .../services/teamwork/SquadService.scala | 399 ++++++++++++++---- .../src/main/scala/WorldSessionActor.scala | 11 +- 5 files changed, 356 insertions(+), 85 deletions(-) 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 11a56ee2..6fd99e69 100644 --- a/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala @@ -35,7 +35,7 @@ object SquadAction{ * Dispatched from client to server to indicate a squad detail update that has no foundation entry to update? * Not dissimilar from `DisplaySquad`. */ - final case class DisplayFullSquad() extends SquadAction(1) + final case class SquadMemberInitializationIssue() extends SquadAction(1) final case class SaveSquadFavorite() extends SquadAction(3) @@ -53,6 +53,8 @@ object SquadAction{ final case class CancelSelectRoleForYourself(value: Long = 0) extends SquadAction(15) + final case class AssociateWithSquad() extends SquadAction(16) + final case class SetListSquad() extends SquadAction(17) final case class ChangeSquadPurpose(purpose : String) extends SquadAction(19) @@ -110,10 +112,10 @@ object SquadAction{ } ) - val displayFullSquadCodec = everFailCondition.xmap[DisplayFullSquad] ( - _ => DisplayFullSquad(), + val squadMemberInitializationIssueCodec = everFailCondition.xmap[SquadMemberInitializationIssue] ( + _ => SquadMemberInitializationIssue(), { - case DisplayFullSquad() => None + case SquadMemberInitializationIssue() => None } ) @@ -173,6 +175,13 @@ object SquadAction{ } ) + val associateWithSquadCodec = everFailCondition.xmap[AssociateWithSquad] ( + _ => AssociateWithSquad(), + { + case AssociateWithSquad() => None + } + ) + val setListSquadCodec = everFailCondition.xmap[SetListSquad] ( _ => SetListSquad(), { @@ -347,7 +356,7 @@ object SquadAction{ *     `6 ` - UNKNOWN
*     `8 ` - Request List Squad
*     `9 ` - Stop List Squad
- *     `16` - UNKNOWN
+ *     `16` - Associate with Squad
*     `17` - Set List Squad (ui)
*     `18` - UNKNOWN
*     `26` - Reset All
@@ -415,7 +424,7 @@ object SquadDefinitionActionMessage extends Marshallable[SquadDefinitionActionMe import scala.annotation.switch ((code : @switch) match { case 0 => displaySquadCodec - case 1 => displayFullSquadCodec + case 1 => squadMemberInitializationIssueCodec case 3 => saveSquadFavoriteCodec case 4 => loadSquadFavoriteCodec case 5 => deleteSquadFavoriteCodec @@ -424,6 +433,7 @@ object SquadDefinitionActionMessage extends Marshallable[SquadDefinitionActionMe case 9 => stopListSquadCodec case 10 => selectRoleForYourselfCodec case 15 => cancelSelectRoleForYourselfCodec + case 16 => associateWithSquadCodec case 17 => setListSquadCodec case 19 => changeSquadPurposeCodec case 20 => changeSquadZoneCodec @@ -442,7 +452,7 @@ object SquadDefinitionActionMessage extends Marshallable[SquadDefinitionActionMe case 40 => findLfsSoldiersForRoleCodec case 41 => cancelFindCodec case 2 | 6 | 11 | - 12 | 13 | 14 | 16 | + 12 | 13 | 14 | 18 | 29 | 30 | 32 | 33 | 36 | 37 | 42 | 43 => unknownCodec(code) case _ => failureCodec(code) diff --git a/common/src/main/scala/services/teamwork/SquadAction.scala b/common/src/main/scala/services/teamwork/SquadAction.scala index 1677cf75..62e3f2fe 100644 --- a/common/src/main/scala/services/teamwork/SquadAction.scala +++ b/common/src/main/scala/services/teamwork/SquadAction.scala @@ -2,13 +2,14 @@ package services.teamwork import net.psforever.objects.Player +import net.psforever.objects.zones.Zone import net.psforever.packet.game._ import net.psforever.types.{SquadRequestType, Vector3} object SquadAction { trait Action - final case class Definition(player : Player, zone_ordinal_number : Int, guid : PlanetSideGUID, line : Int, action : SquadAction) extends Action + final case class Definition(player : Player, zone : Zone, 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 Update(char_id : Long, health : Int, max_health : Int, armor : Int, max_armor : Int, pos : Vector3, zone_number : Int) extends Action } diff --git a/common/src/main/scala/services/teamwork/SquadResponse.scala b/common/src/main/scala/services/teamwork/SquadResponse.scala index 562082ae..fa06d866 100644 --- a/common/src/main/scala/services/teamwork/SquadResponse.scala +++ b/common/src/main/scala/services/teamwork/SquadResponse.scala @@ -14,8 +14,10 @@ object SquadResponse { final case class UpdateList(infos : Iterable[(Int, SquadInfo)]) extends Response final case class RemoveFromList(infos : Iterable[Int]) extends Response - final case class InitSquad(squad_guid : PlanetSideGUID) extends Response + final case class AssociateWithSquad(squad_guid : PlanetSideGUID) extends Response + final case class SetListSquad(squad_guid : PlanetSideGUID) extends Response final case class Unknown17(squad : Squad, char_id : Long) extends Response + 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 diff --git a/common/src/main/scala/services/teamwork/SquadService.scala b/common/src/main/scala/services/teamwork/SquadService.scala index 8f3b0de2..17569777 100644 --- a/common/src/main/scala/services/teamwork/SquadService.scala +++ b/common/src/main/scala/services/teamwork/SquadService.scala @@ -6,6 +6,7 @@ import net.psforever.objects.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.zones.Zone import net.psforever.packet.game._ import net.psforever.types._ import services.{GenericEventBus, Service} @@ -29,6 +30,19 @@ class SquadService extends Actor { PlanetSideEmpire.VS -> ListBuffer.empty ) private val invites : mutable.LongMap[Invitation] = mutable.LongMap[Invitation]() + /** + * `initialAssociation` per squad is similar to "Does this squad want to recruit members?" + * The squad does not have to be listed. + * Dispatches an `AssociateWithSquad` `SDAM` to the squad leader and ??? + * and then a `SDDUM` that includes at least the squad owner name and char id + * when a squad entry is removed from the list. + * 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 val initialAssociation : ListBuffer[PlanetSideGUID] = new ListBuffer[PlanetSideGUID]() private val queuedInvites : mutable.LongMap[List[Invitation]] = mutable.LongMap[List[Invitation]]() private val viewDetails : mutable.LongMap[PlanetSideGUID] = mutable.LongMap[PlanetSideGUID]() @@ -166,6 +180,7 @@ class SquadService extends Actor { memberToSquad += charId -> squad idToSquad += id -> squad idToSwitchboard += id -> switchboard + initialAssociation += squad.GUID squad } @@ -241,6 +256,7 @@ class SquadService extends Actor { case SquadServiceMessage(tplayer, squad_action) => squad_action match { case SquadAction.Membership(SquadRequestType.Invite, invitingPlayer, Some(invitedPlayer), _, _) => //this is just busy work; for actual joining operations, see SquadRequestType.Accept + //for the purposes of this code, tplayer.CharId == invitingPlayer // FindBid(invitingPlayer, invitedPlayer) match { // case Some(bid) => // //invitingPlayer and invitedPlayer have both tried to join each others's squads @@ -262,29 +278,25 @@ class SquadService extends Actor { case (Some(squad), None) => //the classic situation log.info(s"$invitedPlayer has been invited to squad ${squad.Task} by $invitingPlayer") - val leader = squad.Leader - val leaderCharId = leader.CharId - val bid = VacancyInvite(leaderCharId, leader.Name, squad.GUID) + val charId = tplayer.CharId + val bid = VacancyInvite(charId, tplayer.Name, squad.GUID) AddInvite(invitedPlayer, bid) match { case out @ Some(_) if out.contains(bid) => - SquadEvents.publish(SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, leaderCharId, Some(invitedPlayer), tplayer.Name, false, Some(None)))) - SquadEvents.publish(SquadServiceResponse(s"/$leaderCharId/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, invitedPlayer, Some(leaderCharId), tplayer.Name, true, Some(None)))) + 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"/$leaderCharId/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, invitedPlayer, Some(leaderCharId), tplayer.Name, true, Some(None)))) + SquadEvents.publish(SquadServiceResponse(s"/$charId/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, invitedPlayer, Some(charId), tplayer.Name, true, Some(None)))) case _ => ; } case (None, Some(squad)) => - //flip around the roles - the inviting becomes the invited - //TODO needs work - log.info(s"$invitedPlayer has asked $invitingPlayer for an invition to squad ${squad.Task}") - val bid = VacancyInvite(invitedPlayer, "", squad.GUID) - AddInvite(invitingPlayer, bid) match { + //indirection; we're trying to invite ourselves to someone else's squad + val leaderCharId = squad.Leader.CharId + val bid = IndirectVacancy(tplayer, squad.GUID) + log.warn(s"$invitedPlayer has asked $invitingPlayer for an invitation to squad ${squad.Task}, but the squad leader needs to approve") + AddInvite(leaderCharId, bid) match { case out @ Some(_) if out.contains(bid) => - SquadEvents.publish(SquadServiceResponse(s"/$invitingPlayer/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, invitedPlayer, Some(invitingPlayer), tplayer.Name, false, Some(None)))) - SquadEvents.publish(SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, invitingPlayer, Some(invitedPlayer), tplayer.Name, true, Some(None)))) - case Some(_) => - SquadEvents.publish(SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, invitingPlayer, Some(invitedPlayer), tplayer.Name, true, Some(None)))) + HandleBidForPosition(bid, tplayer) case _ => ; } @@ -314,28 +326,61 @@ class SquadService extends Actor { 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") + log.warn("Accept->Bid: the invited player is already a member of a squad and can not join a second one") } - case Some(VacancyInvite(invitingPlayer, _, guid)) - if idToSquad.get(guid).nonEmpty => - //we were invited by the squad leader into an existing squad - if(memberToSquad.get(invitedPlayer).isEmpty) { - val squad = idToSquad(guid) - squad.Membership.zipWithIndex.find({ case (member, index) => - ValidOpenSquadPosition(squad, index, member, tplayer.Certifications) - }) match { - case Some((_, 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(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) + //since we are the squad leader, we do not want to brush off our queued squad invite tasks + case _ => ; } - else { - log.warn("Accept -> Invite: the invited player is already a member of a squad and can not join a second one") + + 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 _ => ; } +// if(idToSquad.get(guid).isEmpty) { +// log.warn("Accept->Invite: the squad no longer exists") +// } +// else if(memberToSquad.get(invitedPlayer).nonEmpty) { +// log.warn("Accept->Invite: player is already a member of a squad and can not join a second one") +// } +// else { +// val squad = idToSquad(guid) +// 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 +// val bid = IndirectVacancy(tplayer, guid) +// AddInvite(squad.Leader.CharId, bid) match { +// case out @ Some(_) if out.contains(bid) => +// HandleBidForPosition(bid, tplayer) +// case _ => ; +// } +// } +// 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, tplayer.Certifications) +// }) match { +// case Some((_, 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)) => //we were invited by someone into a new squad they would form @@ -364,7 +409,7 @@ class SquadService extends Actor { case None => val squad = StartSquad(invitingPlayer) squad.Task = s"${tplayer.Name}'s Squad" - SquadEvents.publish(SquadServiceResponse(s"/$invitingPlayer/Squad", SquadResponse.InitSquad(squad.GUID))) + SquadEvents.publish(SquadServiceResponse(s"/$invitingPlayer/Squad", SquadResponse.AssociateWithSquad(squad.GUID))) Some(squad) }) match { case Some(squad) => @@ -409,7 +454,7 @@ class SquadService extends Actor { .foreach { charId => SquadEvents.publish(SquadServiceResponse(s"/$charId/Squad", SquadResponse.Membership(SquadResponseType.Disband, 0, 0, charId, None, "", false, Some(None)))) } - SquadEvents.publish( SquadServiceResponse(s"/$leader/Squad", SquadResponse.InitSquad(PlanetSideGUID(0))) ) + SquadEvents.publish( SquadServiceResponse(s"/$leader/Squad", SquadResponse.AssociateWithSquad(PlanetSideGUID(0))) ) SquadEvents.publish( SquadServiceResponse(s"/$leader/Squad", SquadResponse.Detail(PlanetSideGUID(0), SquadDetail().Complete)) ) } else { @@ -456,19 +501,53 @@ class SquadService extends Actor { case None => ; } +// case SquadAction.Membership(SquadRequestType.Cancel, cancellingPlayer, _, _, _) => +// //look for queued BidForPosition entries where we are the player who wants to join +// queuedInvites.foreach { case (leader, queueOfInvites) => +// val list = queueOfInvites.filterNot { entry => +// entry.isInstanceOf[BidForPosition] && +// entry.asInstanceOf[BidForPosition].player.CharId == cancellingPlayer +// } +// if(list.nonEmpty && list.size != queueOfInvites.size) { +// queuedInvites(leader) = list +// } +// else if(list.isEmpty) { +// queuedInvites.remove(leader) +// } +// } +// //clean up active BidForPosition invite entries where we are the player who wants to join +// val list = invites.filter { case(_, entry) => +// entry.isInstanceOf[BidForPosition] && +// entry.asInstanceOf[BidForPosition].player.CharId == cancellingPlayer +// } +// list.foreach { +// case(charId, entry : BidForPosition) => +// RemoveInvite(charId) +// SquadEvents.publish( SquadServiceResponse(s"/$charId/Squad", SquadResponse.Membership(SquadResponseType.Cancel, 0, 0, cancellingPlayer, None, entry.player.Name, false, None))) +// NextInvite(charId) match { +// case Some(bid : BidForPosition) => +// HandleBidForPosition(bid, tplayer) +// case Some(bid) => +// SquadEvents.publish(SquadServiceResponse(s"/$charId/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, bid.InviterCharId, Some(charId), bid.InviterName, false, Some(None)))) +// case _ => ; +// } +// case _ => ; +// } + 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 => val membership = squad.Membership.filter { _member => _member.CharId > 0 } val (leader, position) = (squad.Leader, 0) 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) 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))) } - SquadEvents.publish(SquadServiceResponse(s"/$promotingPlayer/Squad", SquadResponse.InitSquad(PlanetSideGUID(0)))) - SquadEvents.publish(SquadServiceResponse(s"/$promotedPlayer/Squad", SquadResponse.InitSquad(squad.GUID))) + SquadEvents.publish(SquadServiceResponse(s"/$promotingPlayer/Squad", SquadResponse.AssociateWithSquad(PlanetSideGUID(0)))) + SquadEvents.publish(SquadServiceResponse(s"/$promotedPlayer/Squad", SquadResponse.AssociateWithSquad(squad.GUID))) UpdateSquadListWhenListed( squad, SquadInfo().Leader(leader.Name) @@ -509,10 +588,10 @@ class SquadService extends Actor { case None => ; } - case SquadAction.Definition(tplayer : Player, zone_ordinal_number : Int, guid : PlanetSideGUID, line : Int, action : SquadAction) => + case SquadAction.Definition(tplayer : Player, zone : Zone, guid : PlanetSideGUID, line : Int, action : SquadAction) => import net.psforever.packet.game.SquadAction._ val pSquadOpt = GetParticipatingSquad(tplayer) - val lSquadOpt = GetLeadingSquad(tplayer, zone_ordinal_number, pSquadOpt) + val lSquadOpt = GetLeadingSquad(tplayer, zone.Number, pSquadOpt) //the following actions can only be performed by a squad's leader action match { case SaveSquadFavorite() => @@ -528,8 +607,8 @@ class SquadService extends Actor { 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.InitSquad(squad.GUID)) - UpdateSquadList(squad, SquadInfo().Task(squad.Task).ZoneId(PlanetSideZoneID(squad.ZoneId)).Capacity(squad.Capacity)) + sender ! SquadServiceResponse("", SquadResponse.AssociateWithSquad(squad.GUID)) + UpdateSquadList(squad, SquadService.SquadList.Publish(squad)) UpdateSquadDetail(PlanetSideGUID(0), squad) case _ => } @@ -550,7 +629,12 @@ class SquadService extends Actor { val squad = lSquadOpt.getOrElse(StartSquad(tplayer)) squad.ZoneId = zone.zoneId.toInt UpdateSquadListWhenListed(squad, SquadInfo().ZoneId(zone)) - UpdateSquadDetail(squad.GUID, squad, SquadDetail().ZoneId(zone)) + InitialAssociation(squad) + sender ! SquadServiceResponse("", SquadResponse.Detail( + squad.GUID, + SquadService.Detail.Publish(squad)) + ) + UpdateSquadDetail(squad.GUID, squad.Membership.map { _m => _m.CharId }.filterNot { _ == squad.Leader.CharId }, SquadDetail().ZoneId(zone)) case CloseSquadMemberPosition(position) => val squad = lSquadOpt.getOrElse(StartSquad(tplayer)) @@ -647,7 +731,8 @@ class SquadService extends Actor { 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 - sender ! SquadServiceResponse("", SquadResponse.InitSquad(squad.GUID)) + InitialAssociation(squad) + sender ! SquadServiceResponse("", SquadResponse.SetListSquad(squad.GUID)) UpdateSquadList(squad, None) } @@ -656,7 +741,7 @@ class SquadService extends Actor { if(squad.Listed) { log.info(s"${tplayer.Name}-${tplayer.Faction} has closed public recruitment for squad ${squad.Task}") squad.Listed = false - sender ! SquadServiceResponse("", SquadResponse.InitSquad(PlanetSideGUID(0))) + sender ! SquadServiceResponse("", SquadResponse.AssociateWithSquad(PlanetSideGUID(0))) UpdateSquadList(squad, None) } @@ -676,6 +761,9 @@ class SquadService extends Actor { 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 _ => } @@ -736,6 +824,63 @@ class SquadService extends Actor { //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 + idToSquad.get(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 BidForPosition 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[BidForPosition] && + entry.asInstanceOf[BidForPosition].player.CharId == cancellingPlayer => + out + case _ => + None + }) match { + case Some(entry : BidForPosition) => + RemoveInvite(leaderCharId) + SquadEvents.publish( SquadServiceResponse(s"/$leaderCharId/Squad", SquadResponse.Membership(SquadResponseType.Cancel, 0, 0, cancellingPlayer, None, entry.player.Name, false, Some(None)))) + NextInvite(leaderCharId) match { + case Some(bid : BidForPosition) => + HandleBidForPosition(bid, tplayer) + case Some(bid) => + SquadEvents.publish(SquadServiceResponse(s"/$leaderCharId/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, bid.InviterCharId, Some(leaderCharId), bid.InviterName, false, Some(None)))) + case _ => ; + } + Some(true) + case _ => + None + }).orElse( + //look for a queued BidForPosition 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[BidForPosition] && + entry.asInstanceOf[BidForPosition].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[BidForPosition] + 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[BidForPosition] + 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 @@ -771,12 +916,12 @@ class SquadService extends Actor { } //the following message is feedback from a specific client, awaiting proper initialization - case (_, DisplayFullSquad()) => - idToSquad.get(guid) match { - case Some(squad) => - sender ! SquadServiceResponse("", SquadResponse.InitSquad(squad.GUID)) - case None => ; - } + case (_, SquadMemberInitializationIssue()) => +// idToSquad.get(guid) match { +// case Some(squad) => +// sender ! SquadServiceResponse("", SquadResponse.Detail(squad.GUID, SquadService.Detail.Publish(squad))) +// case None => ; +// } case _ => ; } @@ -886,19 +1031,90 @@ 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(idToSquad.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 = idToSquad(squad_guid) + 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 + val bid = IndirectVacancy(recruit, squad_guid) + AddInvite(squad.Leader.CharId, bid) match { + case out @ Some(_) if out.contains(bid) => + SquadEvents.publish(SquadServiceResponse(s"/$invitingPlayer/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitingPlayer, Some(invitedPlayer), recruit.Name, false, Some(None)))) + HandleBidForPosition(bid, recruit) + case _ => ; + } + log.info(s"Accept->Invite: ${recruit.Name} must await an invitation from the leader of squad #${squad_guid.guid}") + 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 + initialAssociation.indexOf(guid) match { + case -1 => ; + case index => + initialAssociation.remove(index) + 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 HandleBidForPosition(bid : BidForPosition, player : Player) : Unit = { - idToSquad.get(bid.squad_guid) match { + HandleBidForPosition(bid, bid.squad_guid, bid.player.Name, player) + } + def HandleBidForPosition(bid : IndirectVacancy, player : Player) : Unit = { + HandleBidForPosition(bid, bid.squad_guid, bid.player.Name, player) + } + + def HandleBidForPosition(bid : Invitation, squad_guid : PlanetSideGUID, name : String, player : Player) : Unit = { + idToSquad.get(squad_guid) match { case Some(squad) => val leaderCharId = squad.Leader.CharId if(squad.AutoApproveInvitationRequests) { self ! SquadServiceMessage(player, SquadAction.Membership(SquadRequestType.Accept, leaderCharId, None, "", None)) } else { - SquadEvents.publish(SquadServiceResponse(s"/$leaderCharId/Squad", SquadResponse.WantsSquadPosition(bid.player.Name))) + SquadEvents.publish(SquadServiceResponse(s"/$leaderCharId/Squad", SquadResponse.WantsSquadPosition(name))) } 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:${bid.squad_guid.guid}) that does not exist") + log.error(s"Attempted to process ${bid.InviterName}'s bid for a position in a squad (id:${squad_guid.guid}) that does not exist") } } @@ -915,6 +1131,8 @@ class SquadService extends Actor { 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? @@ -934,20 +1152,18 @@ class SquadService extends Actor { } else { //joining an active squad; everybody updates differently - //new member gets full UI updates - val indices = squad.Membership.zipWithIndex - .collect({ case (member, index) if member.CharId != 0 => index }).toList + //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))) - //other squad members see us joining the squad + InitSquadDetail(squad.GUID, Seq(charId), squad) + //other squad members see new member joining the squad val updatedIndex = List(line) - squad.Membership - .filterNot { member => member.CharId == 0 || member.CharId == charId } - .foreach { member => - SquadEvents.publish(SquadServiceResponse(s"/${member.CharId}/Squad", SquadResponse.Join(squad, updatedIndex))) - } - UpdateSquadDetail(squad.GUID, squad, - SquadDetail().Members(List(SquadPositionEntry(line, SquadPositionDetail().CharId(charId).Name(player.Name)))) - ) + 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, Seq(charId), details) } UpdateSquadListWhenListed(squad, SquadInfo().Size(size)) true @@ -1085,17 +1301,36 @@ class SquadService extends Actor { } } + 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(guid : PlanetSideGUID, squad : Squad, detail : SquadDetail) : Unit = { - val output = SquadResponse.Detail(guid, detail) - squad.Membership - .filter { _.CharId > 0L } - .foreach { member => - SquadEvents.publish(SquadServiceResponse(s"/${member.CharId}/Squad", output)) - } + 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)) } + } } } @@ -1105,12 +1340,31 @@ object SquadService { def InviterName : String = name } + /** + * Utilized when one player attempts to join an existing squad in a specific role. + * @param player na + * @param squad_guid na + * @param position na + */ final case class BidForPosition(player : Player, squad_guid : PlanetSideGUID, position : Int) extends Invitation(player.CharId, player.Name) + /** + * Utilized when one squad member issues an invite for some other player. + * @param char_id na + * @param name na + * @param squad_guid na + */ final case class VacancyInvite(char_id : Long, name : String, squad_guid : PlanetSideGUID) extends Invitation(char_id, name) + final case class IndirectVacancy(player : Player, squad_guid : PlanetSideGUID) + extends Invitation(player.CharId, player.Name) + + /** + * Utilized when one player issues an invite for some other player for a squad that does not yet exist. + * @param player na + */ final case class SpontaneousInvite(player : Player) extends Invitation(player.CharId, player.Name) @@ -1130,6 +1384,7 @@ object SquadService { object Detail { def Publish(squad : Squad) : SquadDetail = { SquadDetail() + .Field1(squad.GUID.guid) .LeaderCharId(squad.Leader.CharId) .LeaderName(squad.Leader.Name) .Task(squad.Task) diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index a7bbe9ef..8eadcead 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -379,8 +379,10 @@ class WorldSessionActor extends Actor with MDCContextAware { case SquadResponse.Detail(guid, detail) => sendResponse(SquadDetailDefinitionUpdateMessage(guid, detail)) - case SquadResponse.InitSquad(squad_guid) => - sendResponse(SquadDefinitionActionMessage(squad_guid, 0, SquadAction.Unknown(16))) + 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) => @@ -3010,7 +3012,7 @@ class WorldSessionActor extends Actor with MDCContextAware { sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), line, SquadAction.ListSquadFavorite(""))) }) sendResponse(SquadDetailDefinitionUpdateMessage.Init) - sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0,SquadAction.Unknown(16))) + sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0,SquadAction.AssociateWithSquad())) sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0,SquadAction.SetListSquad())) sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0,SquadAction.Unknown(18))) //MapObjectStateBlockMessage and ObjectCreateMessage? @@ -3108,6 +3110,7 @@ class WorldSessionActor extends Actor with MDCContextAware { avatar.Certifications += AssaultEngineering avatar.Certifications += Hacking avatar.Certifications += AdvancedHacking + avatar.CEP = 6000001 this.avatar = avatar InitializeDeployableQuantities(avatar) //set deployables ui elements @@ -4957,7 +4960,7 @@ class WorldSessionActor extends Actor with MDCContextAware { case msg @ SquadDefinitionActionMessage(u1, u2, action) => log.info(s"SquadDefinitionAction: $msg") - squadService ! SquadServiceMessage(player, SquadServiceAction.Definition(player, continent.Number, u1, u2, action)) + squadService ! SquadServiceMessage(player, SquadServiceAction.Definition(player, continent, u1, u2, action)) case msg @ SquadMembershipRequest(request_type, unk2, unk3, player_name, unk5) => log.info(s"$msg")