diff --git a/src/main/scala/net/psforever/actors/net/MiddlewareActor.scala b/src/main/scala/net/psforever/actors/net/MiddlewareActor.scala index 138f650f..36db2b5a 100644 --- a/src/main/scala/net/psforever/actors/net/MiddlewareActor.scala +++ b/src/main/scala/net/psforever/actors/net/MiddlewareActor.scala @@ -521,6 +521,10 @@ class MiddlewareActor( case packet: PlanetSideCryptoPacket => log.error(s"Unexpected crypto packet '$packet'") Behaviors.same + + case packet => + log.error(s"Unexpected type of packet '$packet'") + Behaviors.same } } diff --git a/src/main/scala/net/psforever/packet/PacketCoding.scala b/src/main/scala/net/psforever/packet/PacketCoding.scala index 84d1d35e..a67e26ee 100644 --- a/src/main/scala/net/psforever/packet/PacketCoding.scala +++ b/src/main/scala/net/psforever/packet/PacketCoding.scala @@ -106,7 +106,8 @@ object PacketCoding { packet.encode match { case Successful(payload) => packet match { - case _: PlanetSideCryptoPacket => Successful(payload) + case _: PlanetSideCryptoPacket => + Successful(payload) case packet: PlanetSideControlPacket => ControlPacketOpcode.codec.encode(packet.opcode) match { case Successful(opcode) => Successful(hex"00".bits ++ opcode ++ payload) @@ -117,6 +118,8 @@ object PacketCoding { case Successful(opcode) => Successful(opcode ++ payload) case f @ Failure(_) => f } + case _ => + Failure(Err("packet not supported")) } case f @ Failure(_) => f } diff --git a/src/main/scala/net/psforever/services/teamwork/SquadService.scala b/src/main/scala/net/psforever/services/teamwork/SquadService.scala index ab90f163..65fe1972 100644 --- a/src/main/scala/net/psforever/services/teamwork/SquadService.scala +++ b/src/main/scala/net/psforever/services/teamwork/SquadService.scala @@ -2,21 +2,13 @@ package net.psforever.services.teamwork import akka.actor.{Actor, ActorRef, Terminated} -import net.psforever.objects.avatar.Avatar +import net.psforever.objects.avatar.{Avatar, Certification} import net.psforever.objects.definition.converter.StatConverter import net.psforever.objects.loadouts.SquadLoadout import net.psforever.objects.teamwork.{Member, Squad, SquadFeatures} import net.psforever.objects.zones.Zone import net.psforever.objects.{LivePlayerList, Player} -import net.psforever.packet.game.{ - PlanetSideZoneID, - SquadDetail, - SquadInfo, - SquadPositionDetail, - SquadPositionEntry, - WaypointEventAction, - WaypointInfo -} +import net.psforever.packet.game.{PlanetSideZoneID, SquadDetail, SquadInfo, SquadPositionDetail, SquadPositionEntry, WaypointEventAction, WaypointInfo, SquadAction => SquadRequestAction} import net.psforever.types._ import net.psforever.services.{GenericEventBus, Service} @@ -113,9 +105,9 @@ class SquadService extends Actor { * This collection contains the message-sending contact reference for individuals. * When the user joins the `SquadService` with a `Service.Join` message * that includes their unique character identifier, - * and the origin `ActorRef` is added as a subscription. + * 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`.
+ * The subscription is anticipated to belong to an instance of `SessionActor`.
* key - unique character identifier number; value - `ActorRef` reference for that character * @see `Service.Join` */ @@ -124,7 +116,7 @@ class SquadService extends Actor { private[this] val log = org.log4s.getLogger private def debug(msg: String): Unit = { - log.debug(msg) + log.trace(msg) } override def postStop(): Unit = { @@ -314,7 +306,7 @@ class SquadService extends Actor { 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+") => + case str if str.matches("\\d+") => Publish(to.toLong, msg, excluded) case _ => log.warn(s"Publish(String): subscriber information is an unhandled format - $to") @@ -377,57 +369,23 @@ class SquadService extends Actor { 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() - SquadEvents.subscribe(who, path) + JoinByFaction(faction, sender()) //subscribe to the player's personal channel - necessary for future and previous squad information case Service.Join(char_id) => - try { - val longCharId = char_id.toLong - val path = s"/$char_id/Squad" - val who = sender() - 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() - } + JoinByCharacterId(char_id, sender()) case Service.Leave(Some(faction)) if "TRNCVS".indexOf(faction) > -1 => - val path = s"/$faction/Squad" - val who = sender() - SquadEvents.unsubscribe(who, path) + LeaveByFaction(faction, sender()) case Service.Leave(Some(char_id)) => - try { - LeaveService(char_id.toLong, sender()) - } catch { - case _: ClassCastException => - log.warn(s"Service.Leave: tried $char_id as a unique character identifier, but it could not be casted") - case e: Exception => - log.error(s"Service.Leave: unexpected exception using $char_id as data - ${e.getLocalizedMessage}") - e.printStackTrace() - } + LeaveByCharacterId(char_id, sender()) case Service.Leave(None) | Service.LeaveAll() => - UserEvents find { case (_, subscription) => subscription.path.equals(sender().path) } match { - case Some((to, _)) => - LeaveService(to, sender()) - case _ => ; - } + LeaveInGeneral(sender()) case Terminated(actorRef) => - context.unwatch(actorRef) - UserEvents find { case (_, subscription) => subscription eq actorRef } match { - case Some((to, _)) => - LeaveService(to, sender()) - case _ => ; - } + TerminatedBy(actorRef) case SquadServiceMessage(tplayer, zone, squad_action) => squad_action match { @@ -435,345 +393,516 @@ class SquadService extends Actor { 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) - } + SquadActionInitCharId(tplayer) case SquadAction.Membership(SquadRequestType.Invite, invitingPlayer, Some(_invitedPlayer), invitedName, _) => - //this is just busy work; for actual joining operations, see SquadRequestType.Accept - (if (invitedName.nonEmpty) { - //validate player with name exists - LivePlayerList - .WorldPopulation({ - case (_, a: Avatar) => a.name.equalsIgnoreCase(invitedName) && a.faction == tplayer.Faction - }) - .headOption match { - case Some(player) => UserEvents.keys.find(_ == player.id) - case None => None - } - } else { - //validate player with id exists - LivePlayerList - .WorldPopulation({ case (_, a: Avatar) => a.id == _invitedPlayer && a.faction == tplayer.Faction }) - .headOption match { - case Some(player) => Some(_invitedPlayer) - case None => None - } - }) match { - case Some(invitedPlayer) if invitingPlayer != 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 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 => - //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) => - //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)) => - //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 - 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 _ => ; - } + SquadActionMembershipInvite(tplayer, invitingPlayer, _invitedPlayer, invitedName) case SquadAction.Membership(SquadRequestType.ProximityInvite, invitingPlayer, _, _, _) => - 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 - debug("ProximityInvite: wait for existing proximity invitations to clear") - case _ => - val outstandingActiveInvites = features.SearchForRole match { - case Some(pos) => - RemoveQueuedInvitesForSquadAndPosition(sguid, pos) - invites.collect { - case (charId, LookingForSquadRoleInvite(_, _, squad_guid, role)) - if squad_guid == sguid && role == pos => - charId - } - case None => - List.empty[Long] - } - val faction = squad.Faction - val center = tplayer.Position - val excusedInvites = features.Refuse - //positions that can be recruited to - val positions = squad.Membership.zipWithIndex - .collect { case (member, index) if member.CharId == 0 && squad.Availability(index) => member } - /* - players who are: - - the same faction as the squad - - 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 (n < 25m) - */ - (zone.LivePlayers - .collect { - case player - if player.Faction == faction && player.avatar.lookingForSquad && - (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 => - val requirementsToMeet = role.Requirements - requirementsToMeet.intersect(player.avatar.certifications) == requirementsToMeet - } - .foldLeft(false)(_ || _) - } => - player.CharId - } - .partition { charId => outstandingActiveInvites.exists(_ == charId) } match { - case (Nil, Nil) => - //no one 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 - features.SearchForRole = Some(-1) - outstandingPlayerList.foreach { charId => - 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 - (features.ProxyInvites filterNot (outstandingPlayerList contains)) foreach RemoveInvite - features.ProxyInvites = outstandingPlayerList ++ invitedPlayerList - Some(invitedPlayerList) - }) match { - //add invitations for position in squad - case Some(invitedPlayers) => - val invitingPlayer = tplayer.CharId - val name = tplayer.Name - invitedPlayers.foreach { invitedPlayer => - AddInviteAndRespond( - invitedPlayer, - ProximityInvite(invitingPlayer, name, sguid), - invitingPlayer, - name - ) - } - case None => ; - } - } - - case None => - } + SquadActionMembershipProximityInvite(tplayer, zone, invitingPlayer) case SquadAction.Membership(SquadRequestType.Accept, invitedPlayer, _, _, _) => - val acceptedInvite = RemoveInvite(invitedPlayer) - acceptedInvite match { - 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) - JoinSquad(petitioner, features.Squad, position) - RemoveInvitesForSquadAndPosition(guid, position) + SquadActionMembershipAccept(tplayer, invitedPlayer) - 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)) => - 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 SquadAction.Membership(SquadRequestType.Leave, actingPlayer, _leavingPlayer, name, _) => + SquadActionMembershipLeave(tplayer, actingPlayer, _leavingPlayer, name) + + case SquadAction.Membership(SquadRequestType.Reject, rejectingPlayer, _, _, _) => + SquadActionMembershipReject(tplayer, rejectingPlayer) + + case SquadAction.Membership(SquadRequestType.Disband, char_id, _, _, _) => + SquadActionMembershipDisband(char_id) + + case SquadAction.Membership(SquadRequestType.Cancel, cancellingPlayer, _, _, _) => + SquadActionMembershipCancel(cancellingPlayer) + + case SquadAction.Membership(SquadRequestType.Promote, promotingPlayer, Some(_promotedPlayer), promotedName, _) => + SquadActionMembershipPromote(promotingPlayer, _promotedPlayer, promotedName) + + case SquadAction.Membership(event, _, _, _, _) => + debug(s"SquadAction.Membership: $event is not yet supported") + + case SquadAction.Waypoint(_, wtype, _, info) => + SquadActionWaypoint(tplayer, wtype, info) + + case SquadAction.Definition(guid, line, action) => + SquadActionDefinition(tplayer, zone, guid, line, action, sender()) + + case SquadAction.Update(char_id, health, max_health, armor, max_armor, pos, zone_number) => + SquadActionUpdate(char_id, health, max_health, armor, max_armor, pos, zone_number, sender()) + + case msg => + debug(s"Unhandled message $msg from ${sender()}") + } + + case msg => + debug(s"Unhandled message $msg from ${sender()}") + } + + def JoinByFaction(faction: String, sender: ActorRef): Unit = { + val path = s"/$faction/Squad" + debug(s"$sender has joined $path") + SquadEvents.subscribe(sender, path) + } + + def JoinByCharacterId(charId: String, sender: ActorRef): Unit = { + try { + val longCharId = charId.toLong + val path = s"/$charId/Squad" + debug(s"$sender has joined $path") + context.watch(sender) + UserEvents += longCharId -> sender + refused(longCharId) = Nil + } catch { + case _: ClassCastException => + log.warn(s"Service.Join: tried $charId as a unique character identifier, but it could not be casted") + case e: Exception => + log.error(s"Service.Join: unexpected exception using $charId as data - ${e.getLocalizedMessage}") + e.printStackTrace() + } + } + + def LeaveByFaction(faction: String, sender: ActorRef): Unit = { + val path = s"/$faction/Squad" + debug(s"$sender has left $path") + SquadEvents.unsubscribe(sender, path) + } + + def LeaveByCharacterId(charId: String, sender: ActorRef): Unit = { + try { + LeaveService(charId.toLong, sender) + } catch { + case _: ClassCastException => + log.warn(s"Service.Leave: tried $charId as a unique character identifier, but it could not be casted") + case e: Exception => + log.error(s"Service.Leave: unexpected exception using $charId as data - ${e.getLocalizedMessage}") + e.printStackTrace() + } + } + + def LeaveInGeneral(sender: ActorRef): Unit = { + UserEvents find { case (_, subscription) => subscription.path.equals(sender.path) } match { + case Some((to, _)) => + LeaveService(to, sender) + case _ => ; + } + } + + def TerminatedBy(requestee: ActorRef): Unit = { + context.unwatch(requestee) + UserEvents find { case (_, subscription) => subscription eq requestee } match { + case Some((to, _)) => + LeaveService(to, requestee) + case _ => ; + } + } + + def SquadActionInitCharId(tplayer: Player): Unit = { + 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) + } + } + + def SquadActionMembershipInvite( + tplayer: Player, + invitingPlayer: Long, + _invitedPlayer: Long, + invitedName: String + ): Unit = { + //this is just busy work; for actual joining operations, see SquadRequestType.Accept + (if (invitedName.nonEmpty) { + //validate player with name exists + LivePlayerList + .WorldPopulation({ + case (_, a: Avatar) => a.name.equalsIgnoreCase(invitedName) && a.faction == tplayer.Faction + }) + .headOption match { + case Some(player) => UserEvents.keys.find(_ == player.id) + case None => None + } + } else { + //validate player with id exists + LivePlayerList + .WorldPopulation({ case (_, a: Avatar) => a.id == _invitedPlayer && a.faction == tplayer.Faction }) + .headOption match { + case Some(player) => Some(_invitedPlayer) + case None => None + } + }) match { + case Some(invitedPlayer) if invitingPlayer != 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 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 => + //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) => + //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)) => + //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 + 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 _ => ; + } + } + + def SquadActionMembershipProximityInvite(tplayer: Player, zone: Zone, invitingPlayer: Long): Unit = { + 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 + debug("ProximityInvite: wait for existing proximity invitations to clear") + case _ => + val outstandingActiveInvites: List[Long] = features.SearchForRole match { + case Some(pos) => + RemoveQueuedInvitesForSquadAndPosition(sguid, pos) + invites.filter { + case (_, LookingForSquadRoleInvite(_, _, squad_guid, role)) => + squad_guid == sguid && role == pos + case _ => + false + }.keys.toList + case None => + List.empty[Long] + } + val faction = squad.Faction + val center = tplayer.Position + val excusedInvites = features.Refuse + //positions that can be recruited to + val positions = squad.Membership.zipWithIndex + .collect { case (member, index) if member.CharId == 0 && squad.Availability(index) => member } + /* + players who are: + - the same faction as the squad + - 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 (n < 25m) + */ + (zone.LivePlayers + .collect { + case player + if player.Faction == faction && player.avatar.lookingForSquad && + (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 => + val requirementsToMeet = role.Requirements + requirementsToMeet.intersect(player.avatar.certifications) == requirementsToMeet + } + .foldLeft(false)(_ || _) + } => + player.CharId } - - 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)) => - Publish( + .partition { charId => outstandingActiveInvites.contains(charId) } match { + case (Nil, Nil) => + //no one 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 + features.SearchForRole = Some(-1) + outstandingPlayerList.foreach { charId => + 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 + (features.ProxyInvites filterNot (outstandingPlayerList contains)) foreach RemoveInvite + features.ProxyInvites = outstandingPlayerList ++ invitedPlayerList + Some(invitedPlayerList) + }) match { + //add invitations for position in squad + case Some(invitedPlayers) => + val invitingPlayer = tplayer.CharId + val name = tplayer.Name + invitedPlayers.foreach { invitedPlayer => + AddInviteAndRespond( + invitedPlayer, + ProximityInvite(invitingPlayer, name, sguid), invitingPlayer, - SquadResponse.Membership( - SquadResponseType.Accept, - 0, - 0, - invitingPlayer, - Some(invitedPlayer), - tplayer.Name, - false, - Some(None) - ) + name ) - Publish( + } + case None => ; + } + } + + case None => + } + } + + def SquadActionMembershipAccept(tplayer: Player, invitedPlayer: Long): Unit = { + val acceptedInvite = RemoveInvite(invitedPlayer) + acceptedInvite match { + 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) + JoinSquad(petitioner, features.Squad, position) + RemoveInvitesForSquadAndPosition(guid, position) + + 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)) => + 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) => + //accepted an invitation to join an existing squad + HandleVacancyInvite(guid, invitedPlayer, invitingPlayer, tplayer) match { + case Some((squad, line)) => + 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) => + //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" + Publish(invitingPlayerCharId, SquadResponse.AssociateWithSquad(squad.GUID)) + Some(squad) + }) match { + case Some(squad) => + HandleVacancyInvite(squad, tplayer.CharId, invitingPlayerCharId, tplayer) match { + case Some((_, line)) => + Publish( + invitedPlayer, + SquadResponse.Membership( + SquadResponseType.Accept, + 0, + 0, invitedPlayer, - SquadResponse.Membership( - SquadResponseType.Accept, - 0, - 0, - invitedPlayer, - Some(invitingPlayer), - "", - true, - Some(None) - ) + Some(invitingPlayerCharId), + "", + true, + Some(None) ) - JoinSquad(tplayer, squad, line) - RemoveQueuedInvites(invitedPlayer) //TODO deal with these somehow - RemoveInvitesForSquadAndPosition(squad.GUID, line) - case _ => ; - } + ) + 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 _ => ; + } + case _ => ; + } - 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 { - 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" - Publish(invitingPlayerCharId, SquadResponse.AssociateWithSquad(squad.GUID)) - Some(squad) + 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 + 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 + RemoveInvitesForSquadAndPosition(guid, position) + + case Some(features) => + //can not join squad; position is unavailable or other reasons block action + features.ProxyInvites = features.ProxyInvites.filterNot(_ == invitedPlayer) + + case _ => + //squad no longer exists? + } + + 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 = (for { + (member, index) <- squad.Membership.zipWithIndex + if squad.isAvailable(index, tplayer.avatar.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 }) match { - case Some(squad) => - HandleVacancyInvite(squad, tplayer.CharId, invitingPlayerCharId, tplayer) match { - case Some((_, line)) => - 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 _ => ; - } - case _ => ; - } - - case Some(LookingForSquadRoleInvite(invitingPlayer, name, guid, position)) - if EnsureEmptySquad(invitedPlayer) => - squadFeatures.get(guid) match { - case Some(features) if JoinSquad(tplayer, features.Squad, position) => + case Some(position) if JoinSquad(tplayer, squad, position) => //join this squad Publish( invitedPlayer, @@ -801,952 +930,1045 @@ class SquadService extends Actor { Some(None) ) ) - RemoveQueuedInvites(tplayer.CharId) - features.ProxyInvites = Nil - features.SearchForRole = None - RemoveInvitesForSquadAndPosition(guid, position) - - case Some(features) => - //can not join squad; position is unavailable or other reasons block action + RemoveQueuedInvites(invitedPlayer) features.ProxyInvites = features.ProxyInvites.filterNot(_ == invitedPlayer) - case _ => - //squad no longer exists? } + } + if (features.ProxyInvites.isEmpty) { + //all invitations exhausted; this invitation period is concluded + features.SearchForRole = None + } else if (squad.Size == squad.Capacity) { + //all available squad positions filled; terminate all remaining invitations + RemoveProximityInvites(guid) + RemoveAllInvitesToSquad(guid) + //RemoveAllInvitesWithPlayer(invitingPlayer) + } - 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 = (for { - (member, index) <- squad.Membership.zipWithIndex - if squad.isAvailable(index, tplayer.avatar.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 - }) match { - case Some(position) if JoinSquad(tplayer, squad, position) => - //join this squad - 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 _ => - } - } - if (features.ProxyInvites.isEmpty) { - //all invitations exhausted; this invitation period is concluded - features.SearchForRole = None - } else if (squad.Size == squad.Capacity) { - //all available squad positions filled; terminate all remaining invitations - RemoveProximityInvites(guid) - RemoveAllInvitesToSquad(guid) - //RemoveAllInvitesWithPlayer(invitingPlayer) - } + case _ => + //squad no longer exists? + } - case _ => - //squad no longer exists? - } + case _ => + //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 _ => - //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)) + def SquadActionMembershipLeave(tplayer: Player, actingPlayer: Long, _leavingPlayer: Option[Long], name: String): Unit = { + GetParticipatingSquad(actingPlayer) match { + case Some(squad) => + val leader = squad.Leader.CharId + (if (name.nonEmpty) { + //validate player with name + LivePlayerList + .WorldPopulation({ case (_, a: Avatar) => a.name.equalsIgnoreCase(name) }) + .headOption match { + case Some(a) => UserEvents.keys.find(_ == a.id) + case None => None + } + } else { + //validate player with id + _leavingPlayer match { + case Some(id) => UserEvents.keys.find(_ == id) + case None => None + } + }) match { + case out @ Some(leavingPlayer) + if GetParticipatingSquad(leavingPlayer).contains(squad) => //kicked player must be in the same squad + if (actingPlayer == leader) { + if (leavingPlayer == leader || squad.Size == 2) { + //squad leader is leaving his own squad, so it will be disbanded + //OR squad is only composed of two people, so it will be closed-out when one of them leaves + DisbandSquad(squad) + } else { + //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) } - } - NextInviteAndRespond(invitedPlayer) - - case SquadAction.Membership(SquadRequestType.Leave, actingPlayer, _leavingPlayer, name, _) => - GetParticipatingSquad(actingPlayer) match { - case Some(squad) => - val leader = squad.Leader.CharId - (if (name.nonEmpty) { - //validate player with name - LivePlayerList - .WorldPopulation({ case (_, a: Avatar) => a.name.equalsIgnoreCase(name) }) - .headOption match { - case Some(a) => UserEvents.keys.find(_ == a.id) - case None => None - } - } else { - //validate player with id - _leavingPlayer match { - case Some(id) => UserEvents.keys.find(_ == id) - case None => None - } - }) match { - case out @ Some(leavingPlayer) - if GetParticipatingSquad(leavingPlayer).contains(squad) => //kicked player must be in the same squad - if (actingPlayer == leader) { - if (leavingPlayer == leader || squad.Size == 2) { - //squad leader is leaving his own squad, so it will be disbanded - //OR squad is only composed of two people, so it will be closed-out when one of them leaves - DisbandSquad(squad) - } else { - //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) - } - } else if (leavingPlayer == actingPlayer) { - if (squad.Size == 2) { - //squad is only composed of two people, so it will be closed-out when one of them leaves - DisbandSquad(squad) - } else { - //leaving the squad of own accord - LeaveSquad(actingPlayer, squad) - } - } - - case _ => ; + } else if (leavingPlayer == actingPlayer) { + if (squad.Size == 2) { + //squad is only composed of two people, so it will be closed-out when one of them leaves + DisbandSquad(squad) + } else { + //leaving the squad of own accord + LeaveSquad(actingPlayer, squad) } - case _ => ; - } + } - 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(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 _ => ; + } + case _ => ; + } + } - case Some(VacancyInvite(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) - (Some(rejectingPlayer), Some(leader)) + def SquadActionMembershipReject(tplayer: Player, rejectingPlayer: Long): Unit = { + val rejectedBid = RemoveInvite(rejectingPlayer) + //(A, B) -> person who made the rejection, person who was rejected + (rejectedBid match { + 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(ProximityInvite(_, _, 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 - val features = squadFeatures(guid) - features.Refuse = rejectingPlayer //do not bother this player anymore - if ((features.ProxyInvites = features.ProxyInvites.filterNot(_ == rejectingPlayer)).isEmpty) { - features.SearchForRole = None - } - (None, None) + case Some(VacancyInvite(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) + (Some(rejectingPlayer), Some(leader)) - 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(ProximityInvite(_, _, 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 + val features = squadFeatures(guid) + features.Refuse = rejectingPlayer //do not bother this player anymore + if ((features.ProxyInvites = features.ProxyInvites.filterNot(_ == rejectingPlayer)).isEmpty) { + features.SearchForRole = None + } + (None, None) - case Some(RequestRole(_, 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 - (Some(rejectingPlayer), 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 _ => ; - (None, None) - }) match { - case (Some(rejected), Some(invited)) => - Publish( - rejected, - SquadResponse.Membership(SquadResponseType.Reject, 0, 0, rejected, Some(invited), "", true, Some(None)) + case Some(RequestRole(_, 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 + (Some(rejectingPlayer), None) + + case _ => ; + (None, None) + }) match { + case (Some(rejected), Some(invited)) => + 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) => + Publish( + rejected, + SquadResponse.Membership(SquadResponseType.Reject, 0, 0, rejected, Some(rejected), "", true, Some(None)) + ) + case _ => ; + } + NextInviteAndRespond(rejectingPlayer) + } + + def SquadActionMembershipDisband(charId: Long): Unit = { + GetLeadingSquad(charId, None) match { + case Some(squad) => + DisbandSquad(squad) + case None => ; + } + } + + def SquadActionMembershipCancel(cancellingPlayer: Long): Unit = { + //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) + } + + def SquadActionMembershipPromote(promotingPlayer: Long, _promotedPlayer: Long, promotedName: String): Unit = { + 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.id) + 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 = squad.Leader + val (member, index) = membership.zipWithIndex.find { + case (_member, _) => _member.CharId == promotedPlayer + }.get + val features = squadFeatures(squad.GUID) + SwapMemberPosition(leader, member) + //move around invites so that the proper squad leader deals with them + val leaderInvite = invites.remove(promotingPlayer) + val leaderQueuedInvites = queuedInvites.remove(promotingPlayer).toList.flatten + invites.get(promotedPlayer).orElse(previousInvites.get(promotedPlayer)) match { + case Some(_) => + //the promoted player has an active invite; queue these + queuedInvites += promotedPlayer -> (leaderInvite.toList ++ leaderQueuedInvites ++ queuedInvites + .remove(promotedPlayer) + .toList + .flatten) + case None if leaderInvite.nonEmpty => + //no active invite for the promoted player, but the leader had an active invite; trade the queued invites + val invitation = leaderInvite.get + AddInviteAndRespond(promotedPlayer, invitation, invitation.InviterCharId, invitation.InviterName) + queuedInvites += promotedPlayer -> (leaderQueuedInvites ++ queuedInvites + .remove(promotedPlayer) + .toList + .flatten) + case None => + //no active invites for anyone; assign the first queued invite from the promoting player, if available, and queue the rest + leaderQueuedInvites match { + case Nil => ; + case x :: xs => + AddInviteAndRespond(promotedPlayer, x, x.InviterCharId, x.InviterName) + queuedInvites += promotedPlayer -> (xs ++ queuedInvites.remove(promotedPlayer).toList.flatten) + } + } + 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)) + } + UpdateSquadListWhenListed( + features, + SquadInfo().Leader(leader.Name) + ) + UpdateSquadDetail( + squad.GUID, + SquadDetail() + .LeaderCharId(leader.CharId) + .Field3(value = 0L) + .LeaderName(leader.Name) + .Members( + List( + SquadPositionEntry(0, SquadPositionDetail().CharId(leader.CharId).Name(leader.Name)), + SquadPositionEntry(index, SquadPositionDetail().CharId(member.CharId).Name(member.Name)) ) + ) + ) + case _ => ; + } + } + + def SquadActionWaypoint(tplayer: Player, waypointType: SquadWaypoints.Value, info: Option[WaypointInfo]): Unit = { + val playerCharId = tplayer.CharId + (GetLeadingSquad(tplayer, None) match { + case Some(squad) => + info match { + case Some(winfo) => + (Some(squad), AddWaypoint(squad.GUID, waypointType, winfo)) + case _ => + RemoveWaypoint(squad.GUID, waypointType) + (Some(squad), None) + } + case _ => (None, None) + }) match { + case (Some(squad), Some(_)) => + //waypoint added or updated + Publish( + s"${squadFeatures(squad.GUID).ToChannel}", + SquadResponse.WaypointEvent(WaypointEventAction.Add, playerCharId, waypointType, None, info, 1), + Seq(tplayer.CharId) + ) + case (Some(squad), None) => + //waypoint removed + Publish( + s"${squadFeatures(squad.GUID).ToChannel}", + SquadResponse.WaypointEvent(WaypointEventAction.Remove, playerCharId, waypointType, None, None, 0), + Seq(tplayer.CharId) + ) + + case msg => + log.warn(s"Unsupported squad waypoint behavior: $msg") + } + } + + def SquadActionDefinition( + tplayer: Player, + zone: Zone, + guid: PlanetSideGUID, + line: Int, + action: SquadRequestAction, + sendTo: ActorRef): Unit = { + import net.psforever.packet.game.SquadAction._ + val pSquadOpt = GetParticipatingSquad(tplayer) + val lSquadOpt = GetLeadingSquad(tplayer, pSquadOpt) + //the following actions can only be performed by a squad's leader + action match { + case SaveSquadFavorite() => + SquadActionDefinitionSaveSquadFavorite(tplayer, line, lSquadOpt, sendTo) + + case LoadSquadFavorite() => + SquadActionDefinitionLoadSquadFavorite(tplayer, line, pSquadOpt, lSquadOpt, sendTo) + + case DeleteSquadFavorite() => + SquadActionDefinitionDeleteSquadFavorite(tplayer, line, sendTo) + + case ChangeSquadPurpose(purpose) => + SquadActionDefinitionChangeSquadPurpose(tplayer, lSquadOpt, purpose) + + case ChangeSquadZone(zone_id) => + SquadActionDefinitionChangeSquadZone(tplayer, lSquadOpt, zone_id, sendTo) + + case CloseSquadMemberPosition(position) => + SquadActionDefinitionCloseSquadMemberPosition(tplayer, lSquadOpt, position) + + case AddSquadMemberPosition(position) => + SquadActionDefinitionAddSquadMemberPosition(tplayer, lSquadOpt, position) + + case ChangeSquadMemberRequirementsRole(position, role) => + SquadActionDefinitionChangeSquadMemberRequirementsRole(tplayer, lSquadOpt, position, role) + + case ChangeSquadMemberRequirementsDetailedOrders(position, orders) => + SquadActionDefinitionChangeSquadMemberRequirementsDetailedOrders(tplayer, lSquadOpt, position, orders) + + case ChangeSquadMemberRequirementsCertifications(position, certs) => + SquadActionDefinitionChangeSquadMemberRequirementsCertifications(tplayer, lSquadOpt, position, certs) + + case LocationFollowsSquadLead(state) => + SquadActionDefinitionLocationFollowsSquadLead(tplayer, lSquadOpt, state) + + case AutoApproveInvitationRequests(state) => + SquadActionDefinitionAutoApproveInvitationRequests(tplayer, lSquadOpt, state) + + case FindLfsSoldiersForRole(position) => + SquadActionDefinitionFindLfsSoldiersForRole(tplayer, zone, lSquadOpt, position) + + case CancelFind() => + SquadActionDefinitionCancelFind(lSquadOpt) + + case RequestListSquad() => + SquadActionDefinitionRequestListSquad(tplayer, lSquadOpt, sendTo) + + case StopListSquad() => + SquadActionDefinitionStopListSquad(tplayer, lSquadOpt, sendTo) + + case ResetAll() => + SquadActionDefinitionResetAll(lSquadOpt) + + case _ => + (pSquadOpt, action) match { + //the following action can be performed by the squad leader and maybe an unaffiliated player + case (Some(_), SelectRoleForYourself(_)) => + //TODO this 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 || squad.isAvailable(position, tplayer.Certifications)) { + //the squad leader may swap to any open position; a normal member has to validate against requirements + 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 + } + */ + + case (None, SelectRoleForYourself(position)) => + SquadActionDefinitionSelectRoleForYourself(tplayer, guid, position) + + case (_, CancelSelectRoleForYourself(_)) => + SquadActionDefinitionCancelSelectRoleForYourself(tplayer, guid) + + case (Some(squad), AssignSquadMemberToRole(position, char_id)) => + SquadActionDefinitionAssignSquadMemberToRole(squad, guid, char_id, position) + + //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 + Publish(sendTo, SquadResponse.SquadSearchResults()) + + case (_, DisplaySquad()) => + SquadActionDefinitionDisplaySquad(tplayer, guid, sendTo) + + //the following message is feedback from a specific client, awaiting proper initialization + // how to respond? + case (_, SquadMemberInitializationIssue()) => ; + + case msg => + log.warn(s"Unsupported squad definition behavior: $msg") + } + } + } + + def SquadActionDefinitionSaveSquadFavorite( + tplayer: Player, + line: Int, + lSquadOpt: Option[Squad], + sendTo: ActorRef + ): Unit = { + val squad = lSquadOpt.getOrElse(StartSquad(tplayer)) + if (squad.Task.nonEmpty && squad.ZoneId > 0) { + tplayer.squadLoadouts.SaveLoadout(squad, squad.Task, line) + Publish(sendTo, SquadResponse.ListSquadFavorite(line, squad.Task)) + } + } + + def SquadActionDefinitionLoadSquadFavorite( + tplayer: Player, + line: Int, + pSquadOpt: Option[Squad], + lSquadOpt: Option[Squad], + sendTo: ActorRef + ): Unit = { + 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.PublishFullListing(squad)) + Publish(sendTo, SquadResponse.AssociateWithSquad(PlanetSideGUID(0))) + InitSquadDetail(PlanetSideGUID(0), Seq(tplayer.CharId), squad) + UpdateSquadDetail(squad) + Publish(sendTo, SquadResponse.AssociateWithSquad(squad.GUID)) + case _ => + } + } + } + + def SquadActionDefinitionDeleteSquadFavorite(tplayer: Player, line: Int, sendTo: ActorRef): Unit = { + tplayer.squadLoadouts.DeleteLoadout(line) + Publish(sendTo, SquadResponse.ListSquadFavorite(line, "")) + } + + def SquadActionDefinitionChangeSquadPurpose(tplayer: Player, lSquadOpt: Option[Squad], purpose: String): Unit = { + val squad = lSquadOpt.getOrElse(StartSquad(tplayer)) + squad.Task = purpose + UpdateSquadListWhenListed(squadFeatures(squad.GUID), SquadInfo().Task(purpose)) + UpdateSquadDetail(squad.GUID, SquadDetail().Task(purpose)) + } + + def SquadActionDefinitionChangeSquadZone( + tplayer: Player, + lSquadOpt: Option[Squad], + zone_id: PlanetSideZoneID, + sendTo: ActorRef + ): Unit = { + val squad = lSquadOpt.getOrElse(StartSquad(tplayer)) + squad.ZoneId = zone_id.zoneId.toInt + UpdateSquadListWhenListed(squadFeatures(squad.GUID), SquadInfo().ZoneId(zone_id)) + InitialAssociation(squad) + Publish(sendTo, SquadResponse.Detail(squad.GUID, SquadService.PublishFullDetails(squad))) + UpdateSquadDetail( + squad.GUID, + squad.GUID, + Seq(squad.Leader.CharId), + SquadDetail().ZoneId(zone_id) + ) + } + + def SquadActionDefinitionCloseSquadMemberPosition( + tplayer: Player, + lSquadOpt: Option[Squad], + position: Int + ): Unit = { + val squad = lSquadOpt.getOrElse(StartSquad(tplayer)) + squad.Availability.lift(position) match { + case Some(true) if position > 0 => //do not close squad leader position; undefined behavior + squad.Availability.update(position, false) + val memberPosition = squad.Membership(position) + if (memberPosition.CharId > 0) { + LeaveSquad(memberPosition.CharId, squad) + } + UpdateSquadListWhenListed(squadFeatures(squad.GUID), SquadInfo().Capacity(squad.Capacity)) + UpdateSquadDetail( + squad.GUID, + SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail.Closed))) + ) + case Some(_) | None => ; + } + } + + def SquadActionDefinitionAddSquadMemberPosition( + tplayer: Player, + lSquadOpt: Option[Squad], + position: Int + ): Unit = { + val squad = lSquadOpt.getOrElse(StartSquad(tplayer)) + squad.Availability.lift(position) match { + case Some(false) => + squad.Availability.update(position, true) + UpdateSquadListWhenListed(squadFeatures(squad.GUID), SquadInfo().Capacity(squad.Capacity)) + UpdateSquadDetail( + squad.GUID, + SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail.Open))) + ) + case Some(true) | None => ; + } + } + + def SquadActionDefinitionChangeSquadMemberRequirementsRole( + tplayer: Player, + lSquadOpt: Option[Squad], + position: Int, + role: String + ): Unit ={ + val squad = lSquadOpt.getOrElse(StartSquad(tplayer)) + squad.Availability.lift(position) match { + case Some(true) => + squad.Membership(position).Role = role + UpdateSquadDetail( + squad.GUID, + SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail().Role(role)))) + ) + case Some(false) | None => ; + } + } + + def SquadActionDefinitionChangeSquadMemberRequirementsDetailedOrders( + tplayer: Player, + lSquadOpt: Option[Squad], + position: Int, + orders: String + ): Unit = { + val squad = lSquadOpt.getOrElse(StartSquad(tplayer)) + squad.Availability.lift(position) match { + case Some(true) => + squad.Membership(position).Orders = orders + UpdateSquadDetail( + squad.GUID, + SquadDetail().Members( + List(SquadPositionEntry(position, SquadPositionDetail().DetailedOrders(orders))) + ) + ) + case Some(false) | None => ; + } + } + + def SquadActionDefinitionChangeSquadMemberRequirementsCertifications( + tplayer: Player, + lSquadOpt: Option[Squad], + position: Int, + certs: Set[Certification] + ): Unit = { + val squad = lSquadOpt.getOrElse(StartSquad(tplayer)) + squad.Availability.lift(position) match { + case Some(true) => + squad.Membership(position).Requirements = certs + UpdateSquadDetail( + squad.GUID, + SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail().Requirements(certs)))) + ) + case Some(false) | None => ; + } + } + + def SquadActionDefinitionLocationFollowsSquadLead( + tplayer: Player, + lSquadOpt: Option[Squad], + state: Boolean + ): Unit = { + val features = squadFeatures(lSquadOpt.getOrElse(StartSquad(tplayer)).GUID) + features.LocationFollowsSquadLead = state + } + + def SquadActionDefinitionAutoApproveInvitationRequests( + tplayer: Player, + lSquadOpt: Option[Squad], + state: Boolean + ): Unit = { + val features = squadFeatures(lSquadOpt.getOrElse(StartSquad(tplayer)).GUID) + features.AutoApproveInvitationRequests = state + if (state) { + //allowed auto-approval - resolve the requests (only) + val charId = tplayer.CharId + val (requests, others) = (invites.get(charId).toList ++ queuedInvites.get(charId).toList) + .partition({ case _: RequestRole => true }) + invites.remove(charId) + queuedInvites.remove(charId) + previousInvites.remove(charId) + requests.foreach { + case request: RequestRole => + JoinSquad(request.player, features.Squad, request.position) + case _ => ; + } + others.collect { case invite: Invitation => invite } match { + case Nil => ; + case x :: Nil => + AddInviteAndRespond(charId, x, x.InviterCharId, x.InviterName) + case x :: xs => + AddInviteAndRespond(charId, x, x.InviterCharId, x.InviterName) + queuedInvites += charId -> xs + } + } + } + + def SquadActionDefinitionFindLfsSoldiersForRole( + tplayer: Player, + zone: Zone, + lSquadOpt: Option[Squad], + position: Int + ): Unit = { + lSquadOpt match { + case Some(squad) => + val sguid = squad.GUID + val features = squadFeatures(sguid) + 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 + 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 + val excusedInvites = features.Refuse + val faction = squad.Faction + val requirementsToMeet = squad.Membership(position).Requirements + val outstandingActiveInvites: List[Long] = features.SearchForRole match { + case Some(pos) => + RemoveQueuedInvitesForSquadAndPosition(sguid, pos) + invites.filter { + case (charId, LookingForSquadRoleInvite(_, _, squad_guid, role)) => + squad_guid == sguid && role == pos + case _ => + false + }.keys.toList + 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 + 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) + //further filter (A) into players whose invitation is renewed (A1) and new invitations (A2) + //TODO only checks the leader's current zone; should check all zones + (zone.LivePlayers + .collect { + case player + if !excusedInvites.contains(player.CharId) && + faction == player.Faction && player.avatar.lookingForSquad && !memberToSquad.contains( + player.CharId + ) && + requirementsToMeet.intersect(player.avatar.certifications) == requirementsToMeet => + player.CharId + } + .partition { charId => outstandingActiveInvites.contains(charId) } match { + case (Nil, Nil) => + outstandingActiveInvites foreach RemoveInvite + features.ProxyInvites = Nil + //TODO cancel the LFS search from the server so that the client updates properly; how? + None + 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[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 + features.ProxyInvites = outstandingPlayerList ++ invitedPlayerList + Some(invitedPlayerList) + }) match { + //add invitations for position in squad + case Some(invitedPlayers) => + val invitingPlayer = tplayer.CharId + val name = tplayer.Name + invitedPlayers.foreach { invitedPlayer => + AddInviteAndRespond( + invitedPlayer, + LookingForSquadRoleInvite(invitingPlayer, name, sguid, position), + invitingPlayer, + name + ) + } + case None => ; + } + } + + case _ => ; + } + } + + def SquadActionDefinitionCancelFind(lSquadOpt: Option[Squad]): Unit = { + lSquadOpt match { + case Some(squad) => + val sguid = squad.GUID + val position = squadFeatures(sguid).SearchForRole + squadFeatures(sguid).SearchForRole = None + //remove active invites + invites + .filter { + case (_, LookingForSquadRoleInvite(_, _, _guid, pos)) => _guid == sguid && position.contains(pos) + case _ => false + } + .keys + .foreach { charId => + RemoveInvite(charId) + } + //remove queued invites + queuedInvites.foreach { + case (charId, queue) => + val filtered = queue.filterNot { + case LookingForSquadRoleInvite(_, _, _guid, _) => _guid == sguid + case _ => false + } + queuedInvites += charId -> filtered + if (filtered.isEmpty) { + queuedInvites.remove(charId) + } + } + //remove yet-to-be invitedPlayers + squadFeatures(sguid).ProxyInvites = Nil + case _ => ; + } + } + + def SquadActionDefinitionRequestListSquad(tplayer: Player, lSquadOpt: Option[Squad], sendTo: ActorRef): Unit = { + val squad = lSquadOpt.getOrElse(StartSquad(tplayer)) + val features = squadFeatures(squad.GUID) + if (!features.Listed && squad.Task.nonEmpty && squad.ZoneId > 0) { + features.Listed = true + InitialAssociation(squad) + Publish(sendTo, SquadResponse.SetListSquad(squad.GUID)) + UpdateSquadList(squad, None) + } + } + + def SquadActionDefinitionStopListSquad(tplayer: Player, lSquadOpt: Option[Squad], sendTo: ActorRef): Unit = { + val squad = lSquadOpt.getOrElse(StartSquad(tplayer)) + val features = squadFeatures(squad.GUID) + if (features.Listed) { + features.Listed = false + Publish(sendTo, SquadResponse.SetListSquad(PlanetSideGUID(0))) + UpdateSquadList(squad, None) + } + } + + def SquadActionDefinitionResetAll(lSquadOpt: Option[Squad]): Unit = { + lSquadOpt match { + case Some(squad) if squad.Size > 1 => + val guid = squad.GUID + 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() + }) + val features = squadFeatures(squad.GUID) + features.LocationFollowsSquadLead = true + features.AutoApproveInvitationRequests = true + if (features.Listed) { + //unlist the squad + features.Listed = false + Publish(features.ToChannel, SquadResponse.SetListSquad(PlanetSideGUID(0))) + UpdateSquadList(squad, None) + } + UpdateSquadDetail(squad) + InitialAssociation(squad) + squadFeatures(guid).InitialAssociation = true + case Some(squad) => + //underutilized squad; just close it out + CloseSquad(squad) + case _ => ; + } + } + + /** the following action can be performed by an unaffiliated player */ + def SquadActionDefinitionSelectRoleForYourself( + tplayer: Player, + guid: PlanetSideGUID, + position: Int + ): Unit = { + //not a member of any squad, but we might become a member of this one + GetSquad(guid) match { + case Some(squad) => + if (squad.isAvailable(position, tplayer.avatar.certifications)) { + //we could join but we may need permission from the squad leader first + AddInviteAndRespond( + squad.Leader.CharId, + RequestRole(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 */ + def SquadActionDefinitionCancelSelectRoleForYourself(tplayer: Player, guid: PlanetSideGUID): Unit = { + 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 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[RequestRole] && + entry.asInstanceOf[RequestRole].player.CharId == cancellingPlayer => + out + case _ => + None + }) match { + case Some(entry: RequestRole) => + RemoveInvite(leaderCharId) + 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 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[RequestRole] && + entry.asInstanceOf[RequestRole].player.CharId == cancellingPlayer + } + ) + case None => + (Nil, -1) + }) match { + case (_, -1) => + None //no change + case (list, _) if list.size == 1 => + val entry = list.head.asInstanceOf[RequestRole] Publish( - invited, + leaderCharId, SquadResponse.Membership( - SquadResponseType.Reject, + SquadResponseType.Cancel, 0, 0, - invited, - Some(rejected), - tplayer.Name, + cancellingPlayer, + None, + entry.player.Name, false, Some(None) ) ) - case (Some(rejected), None) => + queuedInvites.remove(leaderCharId) + Some(true) + case (list, index) => + val entry = list(index).asInstanceOf[RequestRole] Publish( - rejected, - SquadResponse.Membership(SquadResponseType.Reject, 0, 0, rejected, Some(rejected), "", true, Some(None)) + leaderCharId, + SquadResponse.Membership( + SquadResponseType.Cancel, + 0, + 0, + cancellingPlayer, + None, + entry.player.Name, + false, + Some(None) + ) ) - case _ => ; + queuedInvites(leaderCharId) = list.take(index) ++ list.drop(index + 1) + Some(true) } - NextInviteAndRespond(rejectingPlayer) + ) - case SquadAction.Membership(SquadRequestType.Disband, char_id, _, _, _) => - GetLeadingSquad(char_id, None) match { - case Some(squad) => - DisbandSquad(squad) - case None => ; - } + case _ => ; + } + } - 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) + /** the following action can be performed by ??? */ + def SquadActionDefinitionAssignSquadMemberToRole( + squad: Squad, + guid: PlanetSideGUID, + char_id: Long, + position: Int + ): Unit = { + 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(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)) + ) + ) + ) + case _ => ; + } + } - 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.id) - 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 = squad.Leader - val (member, index) = membership.zipWithIndex.find { - case (_member, _) => _member.CharId == promotedPlayer - }.get - val features = squadFeatures(squad.GUID) - SwapMemberPosition(leader, member) - //move around invites so that the proper squad leader deals with them - val leaderInvite = invites.remove(promotingPlayer) - val leaderQueuedInvites = queuedInvites.remove(promotingPlayer).toList.flatten - invites.get(promotedPlayer).orElse(previousInvites.get(promotedPlayer)) match { - case Some(_) => - //the promoted player has an active invite; queue these - queuedInvites += promotedPlayer -> (leaderInvite.toList ++ leaderQueuedInvites ++ queuedInvites - .remove(promotedPlayer) - .toList - .flatten) - case None if leaderInvite.nonEmpty => - //no active invite for the promoted player, but the leader had an active invite; trade the queued invites - val invitation = leaderInvite.get - AddInviteAndRespond(promotedPlayer, invitation, invitation.InviterCharId, invitation.InviterName) - queuedInvites += promotedPlayer -> (leaderQueuedInvites ++ queuedInvites - .remove(promotedPlayer) - .toList - .flatten) - case None => - //no active invites for anyone; assign the first queued invite from the promoting player, if available, and queue the rest - leaderQueuedInvites match { - case Nil => ; - case x :: xs => - AddInviteAndRespond(promotedPlayer, x, x.InviterCharId, x.InviterName) - queuedInvites += promotedPlayer -> (xs ++ queuedInvites.remove(promotedPlayer).toList.flatten) + /** the following action can be performed by anyone */ + def SquadActionDefinitionDisplaySquad(tplayer: Player, guid: PlanetSideGUID, sendTo: ActorRef): Unit = { + val charId = tplayer.CharId + GetSquad(guid) match { + case Some(squad) if memberToSquad.get(charId).isEmpty => + continueToMonitorDetails += charId -> squad.GUID + Publish(sendTo, SquadResponse.Detail(squad.GUID, SquadService.PublishFullDetails(squad))) + case Some(squad) => + Publish(sendTo, SquadResponse.Detail(squad.GUID, SquadService.PublishFullDetails(squad))) + case _ => ; + } + } + + def SquadActionUpdate( + charId: Long, + health: Int, + maxHealth: Int, + armor: Int, + maxArmor: Int, + pos: Vector3, + zoneNumber: Int, + sendTo: ActorRef + ): Unit = { + memberToSquad.get(charId) match { + case Some(squad) => + squad.Membership.find(_.CharId == charId) match { + case Some(member) => + member.Health = StatConverter.Health(health, maxHealth, min = 1, max = 64) + member.Armor = StatConverter.Health(armor, maxArmor, min = 1, max = 64) + member.Position = pos + member.ZoneId = zoneNumber + Publish( + sendTo, + SquadResponse.UpdateMembers( + squad, + squad.Membership + .filterNot { + _.CharId == 0 } - } - 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)) - } - UpdateSquadListWhenListed( - features, - SquadInfo().Leader(leader.Name) + .map { member => + SquadAction + .Update(member.CharId, member.Health, 0, member.Armor, 0, member.Position, member.ZoneId) + } + .toList ) - UpdateSquadDetail( - squad.GUID, - SquadDetail() - .LeaderCharId(leader.CharId) - .Field3(value = 0L) - .LeaderName(leader.Name) - .Members( - List( - SquadPositionEntry(0, SquadPositionDetail().CharId(leader.CharId).Name(leader.Name)), - SquadPositionEntry(index, SquadPositionDetail().CharId(member.CharId).Name(member.Name)) - ) - ) - ) - case _ => ; - } + ) + 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 { - case Some(squad) => - info match { - case Some(winfo) => - (Some(squad), AddWaypoint(squad.GUID, wtype, winfo)) - case _ => - RemoveWaypoint(squad.GUID, wtype) - (Some(squad), None) - } - case _ => (None, None) - }) match { - case (Some(squad), Some(_)) => - //waypoint added or updated - Publish( - s"${squadFeatures(squad.GUID).ToChannel}", - SquadResponse.WaypointEvent(WaypointEventAction.Add, playerCharId, wtype, None, info, 1), - Seq(tplayer.CharId) - ) - case (Some(squad), None) => - //waypoint removed - Publish( - s"${squadFeatures(squad.GUID).ToChannel}", - SquadResponse.WaypointEvent(WaypointEventAction.Remove, playerCharId, wtype, None, None, 0), - Seq(tplayer.CharId) - ) - - case msg => - log.warn(s"Unsupported squad waypoint behavior: $msg") - } - - case SquadAction.Definition(guid, line, action) => - import net.psforever.packet.game.SquadAction._ - val pSquadOpt = GetParticipatingSquad(tplayer) - val lSquadOpt = GetLeadingSquad(tplayer, pSquadOpt) - //the following actions can only be performed by a squad's leader - action match { - case SaveSquadFavorite() => - val squad = lSquadOpt.getOrElse(StartSquad(tplayer)) - if (squad.Task.nonEmpty && squad.ZoneId > 0) { - tplayer.squadLoadouts.SaveLoadout(squad, squad.Task, line) - Publish(sender(), SquadResponse.ListSquadFavorite(line, squad.Task)) - } - - case LoadSquadFavorite() => - 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) - Publish(sender(), SquadResponse.ListSquadFavorite(line, "")) - - case ChangeSquadPurpose(purpose) => - val squad = lSquadOpt.getOrElse(StartSquad(tplayer)) - squad.Task = purpose - UpdateSquadListWhenListed(squadFeatures(squad.GUID), SquadInfo().Task(purpose)) - UpdateSquadDetail(squad.GUID, SquadDetail().Task(purpose)) - - case ChangeSquadZone(zone_id) => - val squad = lSquadOpt.getOrElse(StartSquad(tplayer)) - squad.ZoneId = zone_id.zoneId.toInt - UpdateSquadListWhenListed(squadFeatures(squad.GUID), SquadInfo().ZoneId(zone_id)) - InitialAssociation(squad) - Publish(sender(), SquadResponse.Detail(squad.GUID, SquadService.Detail.Publish(squad))) - UpdateSquadDetail( - squad.GUID, - squad.GUID, - Seq(squad.Leader.CharId), - SquadDetail().ZoneId(zone_id) - ) - - case CloseSquadMemberPosition(position) => - val squad = lSquadOpt.getOrElse(StartSquad(tplayer)) - squad.Availability.lift(position) match { - case Some(true) if position > 0 => //do not close squad leader position; undefined behavior - squad.Availability.update(position, false) - val memberPosition = squad.Membership(position) - if (memberPosition.CharId > 0) { - LeaveSquad(memberPosition.CharId, squad) - } - UpdateSquadListWhenListed(squadFeatures(squad.GUID), SquadInfo().Capacity(squad.Capacity)) - UpdateSquadDetail( - squad.GUID, - SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail.Closed))) - ) - case Some(_) | None => ; - } - - case AddSquadMemberPosition(position) => - val squad = lSquadOpt.getOrElse(StartSquad(tplayer)) - squad.Availability.lift(position) match { - case Some(false) => - squad.Availability.update(position, true) - UpdateSquadListWhenListed(squadFeatures(squad.GUID), SquadInfo().Capacity(squad.Capacity)) - UpdateSquadDetail( - squad.GUID, - SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail.Open))) - ) - case Some(true) | None => ; - } - - case ChangeSquadMemberRequirementsRole(position, role) => - val squad = lSquadOpt.getOrElse(StartSquad(tplayer)) - squad.Availability.lift(position) match { - case Some(true) => - squad.Membership(position).Role = role - UpdateSquadDetail( - squad.GUID, - SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail().Role(role)))) - ) - case Some(false) | None => ; - } - - case ChangeSquadMemberRequirementsDetailedOrders(position, orders) => - val squad = lSquadOpt.getOrElse(StartSquad(tplayer)) - squad.Availability.lift(position) match { - case Some(true) => - squad.Membership(position).Orders = orders - UpdateSquadDetail( - squad.GUID, - SquadDetail().Members( - List(SquadPositionEntry(position, SquadPositionDetail().DetailedOrders(orders))) - ) - ) - case Some(false) | None => ; - } - - case ChangeSquadMemberRequirementsCertifications(position, certs) => - val squad = lSquadOpt.getOrElse(StartSquad(tplayer)) - squad.Availability.lift(position) match { - case Some(true) => - squad.Membership(position).Requirements = certs - UpdateSquadDetail( - squad.GUID, - SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail().Requirements(certs)))) - ) - case Some(false) | None => ; - } - - case LocationFollowsSquadLead(state) => - val features = squadFeatures(lSquadOpt.getOrElse(StartSquad(tplayer)).GUID) - features.LocationFollowsSquadLead = state - - case AutoApproveInvitationRequests(state) => - val features = squadFeatures(lSquadOpt.getOrElse(StartSquad(tplayer)).GUID) - features.AutoApproveInvitationRequests = state - if (state) { - //allowed auto-approval - resolve the requests (only) - val charId = tplayer.CharId - val (requests, others) = (invites.get(charId).toList ++ queuedInvites.get(charId).toList) - .partition({ case _: RequestRole => true }) - invites.remove(charId) - queuedInvites.remove(charId) - previousInvites.remove(charId) - requests.foreach { - case request: RequestRole => - JoinSquad(request.player, features.Squad, request.position) - case _ => ; - } - others.collect { case invite: Invitation => invite } match { - case Nil => ; - case x :: Nil => - AddInviteAndRespond(charId, x, x.InviterCharId, x.InviterName) - case x :: xs => - AddInviteAndRespond(charId, x, x.InviterCharId, x.InviterName) - queuedInvites += charId -> xs - } - } - - case FindLfsSoldiersForRole(position) => - lSquadOpt match { - case Some(squad) => - val sguid = squad.GUID - val features = squadFeatures(sguid) - 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 - 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 - val excusedInvites = features.Refuse - val faction = squad.Faction - val requirementsToMeet = squad.Membership(position).Requirements - val outstandingActiveInvites = features.SearchForRole match { - case Some(pos) => - RemoveQueuedInvitesForSquadAndPosition(sguid, pos) - 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 - 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) - //further filter (A) into players whose invitation is renewed (A1) and new invitations (A2) - //TODO only checks the leader's current zone; should check all zones - (zone.LivePlayers - .collect { - case player - if !excusedInvites.contains(player.CharId) && - faction == player.Faction && player.avatar.lookingForSquad && !memberToSquad.contains( - player.CharId - ) && - requirementsToMeet.intersect(player.avatar.certifications) == requirementsToMeet => - player.CharId - } - .partition { charId => outstandingActiveInvites.exists(charId == _) } match { - case (Nil, Nil) => - outstandingActiveInvites foreach RemoveInvite - features.ProxyInvites = Nil - //TODO cancel the LFS search from the server so that the client updates properly; how? - None - 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[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 - features.ProxyInvites = outstandingPlayerList ++ invitedPlayerList - Some(invitedPlayerList) - }) match { - //add invitations for position in squad - case Some(invitedPlayers) => - val invitingPlayer = tplayer.CharId - val name = tplayer.Name - invitedPlayers.foreach { invitedPlayer => - AddInviteAndRespond( - invitedPlayer, - LookingForSquadRoleInvite(invitingPlayer, name, sguid, position), - invitingPlayer, - name - ) - } - case None => ; - } - } - - case _ => ; - } - - case CancelFind() => - lSquadOpt match { - case Some(squad) => - val sguid = squad.GUID - val position = squadFeatures(sguid).SearchForRole - squadFeatures(sguid).SearchForRole = None - //remove active invites - invites - .filter { - case (_, LookingForSquadRoleInvite(_, _, _guid, pos)) => _guid == sguid && position.contains(pos) - case _ => false - } - .keys - .foreach { charId => - RemoveInvite(charId) - } - //remove queued invites - queuedInvites.foreach { - case (charId, queue) => - val filtered = queue.filterNot { - case LookingForSquadRoleInvite(_, _, _guid, _) => _guid == sguid - case _ => false - } - queuedInvites += charId -> filtered - if (filtered.isEmpty) { - queuedInvites.remove(charId) - } - } - //remove yet-to-be invitedPlayers - squadFeatures(sguid).ProxyInvites = Nil - case _ => ; - } - - case RequestListSquad() => - val squad = lSquadOpt.getOrElse(StartSquad(tplayer)) - val features = squadFeatures(squad.GUID) - if (!features.Listed && squad.Task.nonEmpty && squad.ZoneId > 0) { - features.Listed = true - InitialAssociation(squad) - Publish(sender(), SquadResponse.SetListSquad(squad.GUID)) - UpdateSquadList(squad, None) - } - - case StopListSquad() => - val squad = lSquadOpt.getOrElse(StartSquad(tplayer)) - val features = squadFeatures(squad.GUID) - if (features.Listed) { - features.Listed = false - Publish(sender(), SquadResponse.SetListSquad(PlanetSideGUID(0))) - UpdateSquadList(squad, None) - } - - case ResetAll() => - lSquadOpt match { - case Some(squad) if squad.Size > 1 => - val guid = squad.GUID - 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() - }) - val features = squadFeatures(squad.GUID) - features.LocationFollowsSquadLead = true - features.AutoApproveInvitationRequests = true - if (features.Listed) { - //unlist the squad - features.Listed = false - Publish(features.ToChannel, SquadResponse.SetListSquad(PlanetSideGUID(0))) - UpdateSquadList(squad, None) - } - UpdateSquadDetail(squad) - InitialAssociation(squad) - squadFeatures(guid).InitialAssociation = true - case Some(squad) => - //underutilized squad; just close it out - CloseSquad(squad) - case _ => ; - } - - case _ => - (pSquadOpt, action) match { - //the following action can be performed by the squad leader and maybe an unaffiliated player - case (Some(_), SelectRoleForYourself(_)) => - //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 || squad.isAvailable(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) => - if (squad.isAvailable(position, tplayer.avatar.certifications)) { - //we could join but we may need permission from the squad leader first - AddInviteAndRespond( - squad.Leader.CharId, - RequestRole(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 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[RequestRole] && - entry.asInstanceOf[RequestRole].player.CharId == cancellingPlayer => - out - case _ => - None - }) match { - case Some(entry: RequestRole) => - RemoveInvite(leaderCharId) - 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 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[RequestRole] && - entry.asInstanceOf[RequestRole].player.CharId == cancellingPlayer - } - ) - case None => - (Nil, -1) - }) match { - case (_, -1) => - None //no change - 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[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) - } - ) - - 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(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)) - ) - ) - ) - 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 - 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) => - 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) => -// Publish(sender, SquadResponse.Detail(squad.GUID, SquadService.Detail.Publish(squad))) -// case None => ; -// } - - case msg => ; - log.warn(s"Unsupported squad definition behavior: $msg") - } - } - - case SquadAction.Update(char_id, health, max_health, armor, max_armor, pos, zone_number) => - memberToSquad.get(char_id) match { - case Some(squad) => - squad.Membership.find(_.CharId == char_id) match { - case Some(member) => - member.Health = StatConverter.Health(health, max_health, min = 1, max = 64) - member.Armor = StatConverter.Health(armor, max_armor, min = 1, max = 64) - member.Position = pos - member.ZoneId = zone_number - 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 _ => ; - } - - case None => ; - } - - case msg => - debug(s"Unhandled message $msg from ${sender()}") - } - - case msg => - debug(s"Unhandled message $msg from ${sender()}") + case None => ; + } } /** @@ -2343,7 +2565,7 @@ class SquadService extends Actor { */ def RemoveProximityInvites(invitingPlayer: Long): Iterable[(Long, String)] = { //invites - val (removedInvites, out) = invites.collect { + val (removedInvites, out): (Iterable[(Long, PlanetSideGUID)], Iterable[(Long, String)]) = invites.collect { case (id, ProximityInvite(inviterCharId, inviterName, squadGUID)) if inviterCharId == invitingPlayer => RemoveInvite(id) ((id, squadGUID), (id, inviterName)) @@ -2351,7 +2573,7 @@ class SquadService extends Actor { RemoveProximityInvites(removedInvites) //queued RemoveProximityInvites(queuedInvites.flatMap { - case (id: Long, inviteList) => + case (id: Long, inviteList: List[Invitation]) => val (outList, inList) = inviteList.partition { case ProximityInvite(inviterCharId, _, _) if inviterCharId == invitingPlayer => true case _ => false @@ -2394,7 +2616,7 @@ class SquadService extends Actor { */ def RemoveProximityInvites(guid: PlanetSideGUID): Iterable[(Long, String)] = { //invites - val (removedInvites, out) = invites.collect { + val (removedInvites, out): (Iterable[PlanetSideGUID], Iterable[(Long, String)]) = invites.collect { case (id, ProximityInvite(_, inviterName, squadGUID)) if squadGUID == guid => RemoveInvite(id) (squadGUID, (id, inviterName)) @@ -2405,7 +2627,7 @@ class SquadService extends Actor { } //queued queuedInvites.flatMap { - case (id: Long, inviteList) => + case (id: Long, inviteList: List[Invitation]) => val (outList, inList) = inviteList.partition { case ProximityInvite(_, _, squadGUID) if squadGUID == guid => true case _ => false @@ -2589,7 +2811,7 @@ class SquadService extends Actor { * @see ``ResetAll * @see `SquadResponse.AssociateWithSquad` * @see `SquadResponse.Detail` - * @see `SquadService.Detail.Publish` + * @see `SquadService.PublishFullDetails` * @param squad the squad */ def InitialAssociation(squad: Squad): Unit = { @@ -2598,7 +2820,7 @@ class SquadService extends Actor { squadFeatures(guid).InitialAssociation = false val charId = squad.Leader.CharId Publish(charId, SquadResponse.AssociateWithSquad(guid)) - Publish(charId, SquadResponse.Detail(guid, SquadService.Detail.Publish(squad))) + Publish(charId, SquadResponse.Detail(guid, SquadService.PublishFullDetails(squad))) } } @@ -3206,19 +3428,19 @@ class SquadService extends Actor { /** * 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` + * @see `SquadService.PublishFullDetails` * @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)) + val output = SquadResponse.Detail(guid, SquadService.PublishFullDetails(squad)) to.foreach { Publish(_, output) } } /** * 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 `SquadService.PublishFullDetails` * @see `UpdateSquadDetail(PlanetSideGUID, PlanetSideGUID, List[Long], SquadDetail)` * @param squad the squad */ @@ -3227,7 +3449,7 @@ class SquadService extends Actor { squad.GUID, squad.GUID, Nil, - SquadService.Detail.Publish(squad) + SquadService.PublishFullDetails(squad) ) } @@ -3237,7 +3459,7 @@ class SquadService extends Actor { * 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 `SquadService.PublishFullDetails` * @see `UpdateSquadDetail(PlanetSideGUID, PlanetSideGUID, List[Long], SquadDetail)` * @param squad the squad */ @@ -3246,12 +3468,12 @@ class SquadService extends Actor { guid, squad.GUID, Nil, - SquadService.Detail.Publish(squad) + SquadService.PublishFullDetails(squad) ) } /** - * Send Send a message entailing some of the strategic information and the composition to the existing members of the squad. + * 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 @@ -3295,7 +3517,7 @@ class SquadService extends Actor { } continueToMonitorDetails .collect { - case (charId, sguid) if sguid == guid && !excluding.exists(_ == charId) => + case (charId: Long, sguid: PlanetSideGUID) if sguid == guid && !excluding.exists(_ == charId) => Publish(charId, output, Nil) } } @@ -3315,7 +3537,7 @@ class SquadService extends Actor { * @return a `Vector` of transformed squad data */ def PublishedLists(guids: Iterable[PlanetSideGUID]): Vector[SquadInfo] = { - guids.map { guid => SquadService.SquadList.Publish(squadFeatures(guid).Squad) }.toVector + guids.map { guid => SquadService.PublishFullListing(squadFeatures(guid).Squad) }.toVector } } @@ -3398,58 +3620,52 @@ object SquadService { */ final case class SpontaneousInvite(player: Player) extends Invitation(player.CharId, player.Name) - 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, - squad.Task, - PlanetSideZoneID(squad.ZoneId), - squad.Size, - squad.Capacity, - squad.GUID - ) - } + /** + * Produce complete squad information. + * @see `SquadInfo` + * @param squad the squad + * @return the squad's information to be used in the squad list + */ + def PublishFullListing(squad: Squad): SquadInfo = { + SquadInfo( + squad.Leader.Name, + squad.Task, + PlanetSideZoneID(squad.ZoneId), + squad.Size, + squad.Capacity, + squad.GUID + ) } - 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) - .LeaderCharId(squad.Leader.CharId) - .LeaderName(squad.Leader.Name) - .Task(squad.Task) - .ZoneId(PlanetSideZoneID(squad.ZoneId)) - .Members( - squad.Membership.zipWithIndex - .map({ - case (p, index) => - SquadPositionEntry( - index, - if (squad.Availability(index)) { - SquadPositionDetail(p.Role, p.Orders, p.Requirements, p.CharId, p.Name) - } else { - SquadPositionDetail.Closed - } - ) - }) - .toList - ) - .Complete - } + /** + * 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 PublishFullDetails(squad: Squad): SquadDetail = { + SquadDetail() + .Field1(squad.GUID.guid) + .LeaderCharId(squad.Leader.CharId) + .LeaderName(squad.Leader.Name) + .Task(squad.Task) + .ZoneId(PlanetSideZoneID(squad.ZoneId)) + .Members( + squad.Membership.zipWithIndex + .map({ + case (p, index) => + SquadPositionEntry( + index, + if (squad.Availability(index)) { + SquadPositionDetail(p.Role, p.Orders, p.Requirements, p.CharId, p.Name) + } else { + SquadPositionDetail.Closed + } + ) + }) + .toList + ) + .Complete } /**