From ebfc028f5ce38ef299de422822354479d97b0fd5 Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Mon, 16 Jan 2023 10:42:05 -0500 Subject: [PATCH] Team Building Exercise [Incomplete] (#1013) * Squad -> SquadFeatures from a common lookup * refactor to SquadService, etc.; clarification of fields related to outfit ids * thorough pass over squad functionality in regards to joining a squad or rejecting a squad invitation * decorating squads in the squad list to indicate availability based on size/capacity or role requirements * squad list decoration; rudimentary squad list searching * renovations to squad joining, promotion, and certain invitation/cleanup procedures * further renovations to squad joining, promotion, and certain invitation/cleanup procedures * overhaul of squad joining and squad promotion * separated the invitation system from the squad publishing and manipulating system --- .../actors/session/AvatarActor.scala | 15 + .../psforever/actors/session/ChatActor.scala | 32 +- .../actors/session/SessionActor.scala | 439 +- .../psforever/objects/teamwork/Member.scala | 22 +- .../objects/teamwork/SquadFeatures.scala | 94 +- .../objects/teamwork/WaypointData.scala | 21 + .../game/CharacterKnowledgeMessage.scala | 21 +- .../game/ReplicationStreamMessage.scala | 29 +- .../game/SquadDefinitionActionMessage.scala | 90 +- .../SquadDetailDefinitionUpdateMessage.scala | 104 +- .../packet/game/SquadMemberEvent.scala | 30 +- .../packet/game/SquadMembershipResponse.scala | 2 +- .../psforever/packet/game/SquadState.scala | 24 +- .../CharacterAppearanceData.scala | 14 +- .../teamwork/SquadInvitationManager.scala | 2153 ++++++++++ .../services/teamwork/SquadService.scala | 3793 +++-------------- .../teamwork/SquadServiceMessage.scala | 8 +- .../teamwork/SquadServiceResponse.scala | 21 +- .../teamwork/SquadSubscriptionEntity.scala | 316 ++ .../services/teamwork/SquadSwitchboard.scala | 1091 ++++- .../psforever/types/SquadListDecoration.scala | 27 + .../psforever/types/SquadResponseType.scala | 3 +- .../net/psforever/types/SquadWaypoints.scala | 2 +- .../game/CharacterKnowledgeMessageTest.scala | 4 +- .../SquadDefinitionActionMessageTest.scala | 21 +- ...uadDetailDefinitionUpdateMessageTest.scala | 4 +- .../game/SquadMembershipResponseTest.scala | 8 +- .../game/objectcreate/CharacterDataTest.scala | 6 +- .../DetailedCharacterDataTest.scala | 12 +- .../MountedVehiclesTest.scala | 2 +- 30 files changed, 4782 insertions(+), 3626 deletions(-) create mode 100644 src/main/scala/net/psforever/objects/teamwork/WaypointData.scala create mode 100644 src/main/scala/net/psforever/services/teamwork/SquadInvitationManager.scala create mode 100644 src/main/scala/net/psforever/services/teamwork/SquadSubscriptionEntity.scala create mode 100644 src/main/scala/net/psforever/types/SquadListDecoration.scala diff --git a/src/main/scala/net/psforever/actors/session/AvatarActor.scala b/src/main/scala/net/psforever/actors/session/AvatarActor.scala index 0151e609..5def0d08 100644 --- a/src/main/scala/net/psforever/actors/session/AvatarActor.scala +++ b/src/main/scala/net/psforever/actors/session/AvatarActor.scala @@ -53,6 +53,7 @@ import net.psforever.util.Database._ import net.psforever.persistence import net.psforever.util.{Config, Database, DefinitionUtil} import net.psforever.services.Service +//import org.log4s.Logger import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} object AvatarActor { @@ -435,6 +436,15 @@ object AvatarActor { } } + + def displayLookingForSquad(session: Session, state: Int): Unit = { + val player = session.player + session.zone.AvatarEvents ! AvatarServiceMessage( + player.Faction.toString, + AvatarAction.PlanetsideAttribute(player.GUID, 53, state) + ) + } + /** * Check for an avatar being online at the moment by matching against their name. * If discovered, run a function based on the avatar's characteristics. @@ -831,6 +841,11 @@ class AvatarActor( session = Some(newSession) Behaviors.same + case SetLookingForSquad(lfs) => + avatarCopy(avatar.copy(lookingForSquad = lfs)) + AvatarActor.displayLookingForSquad(session.get, if (lfs) 1 else 0) + Behaviors.same + case CreateAvatar(name, head, voice, gender, empire) => import ctx._ ctx.run(query[persistence.Avatar].filter(_.name ilike lift(name)).filter(!_.deleted)).onComplete { diff --git a/src/main/scala/net/psforever/actors/session/ChatActor.scala b/src/main/scala/net/psforever/actors/session/ChatActor.scala index e031b452..14763ef5 100644 --- a/src/main/scala/net/psforever/actors/session/ChatActor.scala +++ b/src/main/scala/net/psforever/actors/session/ChatActor.scala @@ -823,20 +823,26 @@ class ChatActor( val popTR = players.count(_.faction == PlanetSideEmpire.TR) val popNC = players.count(_.faction == PlanetSideEmpire.NC) val popVS = players.count(_.faction == PlanetSideEmpire.VS) - val contName = session.zone.map.name - sessionActor ! SessionActor.SendResponse( - ChatMsg(ChatMessageType.CMT_WHO, true, "", "That command doesn't work for now, but : ", None) - ) - sessionActor ! SessionActor.SendResponse( - ChatMsg(ChatMessageType.CMT_WHO, true, "", "NC online : " + popNC + " on " + contName, None) - ) - sessionActor ! SessionActor.SendResponse( - ChatMsg(ChatMessageType.CMT_WHO, true, "", "TR online : " + popTR + " on " + contName, None) - ) - sessionActor ! SessionActor.SendResponse( - ChatMsg(ChatMessageType.CMT_WHO, true, "", "VS online : " + popVS + " on " + contName, None) - ) + if (popNC + popTR + popVS == 0) { + sessionActor ! SessionActor.SendResponse( + ChatMsg(ChatMessageType.CMT_WHO, false, "", "@Nomatches", None) + ) + } else { + val contName = session.zone.map.name + sessionActor ! SessionActor.SendResponse( + ChatMsg(ChatMessageType.CMT_WHO, true, "", "That command doesn't work for now, but : ", None) + ) + sessionActor ! SessionActor.SendResponse( + ChatMsg(ChatMessageType.CMT_WHO, true, "", "NC online : " + popNC + " on " + contName, None) + ) + sessionActor ! SessionActor.SendResponse( + ChatMsg(ChatMessageType.CMT_WHO, true, "", "TR online : " + popTR + " on " + contName, None) + ) + sessionActor ! SessionActor.SendResponse( + ChatMsg(ChatMessageType.CMT_WHO, true, "", "VS online : " + popVS + " on " + contName, None) + ) + } case (CMT_ZONE, _, contents) if gmCommandAllowed => val buffer = contents.toLowerCase.split("\\s+") diff --git a/src/main/scala/net/psforever/actors/session/SessionActor.scala b/src/main/scala/net/psforever/actors/session/SessionActor.scala index 25976389..f24d4136 100644 --- a/src/main/scala/net/psforever/actors/session/SessionActor.scala +++ b/src/main/scala/net/psforever/actors/session/SessionActor.scala @@ -41,7 +41,7 @@ import net.psforever.objects.serverobject.tube.SpawnTube import net.psforever.objects.serverobject.turret.{FacilityTurret, WeaponTurret} import net.psforever.objects.serverobject.zipline.ZipLinePath import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject, ServerObject} -import net.psforever.objects.teamwork.Squad +import net.psforever.objects.teamwork.{Member, Squad} import net.psforever.objects.vehicles.Utility.InternalTelepad import net.psforever.objects.vehicles._ import net.psforever.objects.vehicles.control.BfrFlight @@ -151,12 +151,13 @@ object SessionActor { private final val zoningCountdownMessages: Seq[Int] = Seq(5, 10, 20) protected final case class SquadUIElement( - name: String, - index: Int, - zone: Int, - health: Int, - armor: Int, - position: Vector3 + name: String = "", + outfit: Long = 0, + index: Int = -1, + zone: Int = 0, + health: Int = 0, + armor: Int = 0, + position: Vector3 = Vector3.Zero ) private final case class NtuCharging(tplayer: Player, vehicle: Vehicle) @@ -207,6 +208,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con val projectiles: Array[Option[Projectile]] = Array.fill[Option[Projectile]](Projectile.rangeUID - Projectile.baseUID)(None) var drawDeloyableIcon: PlanetSideGameObject with Deployable => Unit = RedrawDeployableIcons + var updateSquadRef: ActorRef = Default.Actor var updateSquad: () => Unit = NoSquadUpdates var recentTeleportAttempt: Long = 0 var lastTerminalOrderFulfillment: Boolean = true @@ -740,26 +742,6 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con zoningType = Zoning.Method.InstantAction zoningChatMessageType = ChatMessageType.CMT_INSTANTACTION zoningStatus = Zoning.Status.Request - /* TODO no ask or adapters from classic to typed so this logic is happening in SpawnPointResponse - implicit val timeout = Timeout(1 seconds) - val future = - ask(cluster.toClassic, ICS.GetInstantActionSpawnPoint(player.Faction, context.self)) - .mapTo[ICS.SpawnPointResponse] - Await.result(future, 2 second) match { - case ICS.SpawnPointResponse(None) => - sendResponse( - ChatMsg(ChatMessageType.CMT_INSTANTACTION, false, "", "@InstantActionNoHotspotsAvailable", None) - ) - case ICS.SpawnPointResponse(Some(_)) => - beginZoningCountdown(() => { - cluster ! ICS.GetInstantActionSpawnPoint(player.Faction, context.self) - }) - } - - beginZoningCountdown(() => { - cluster ! ICS.GetInstantActionSpawnPoint(player.Faction, context.self) - }) - */ cluster ! ICS.GetInstantActionSpawnPoint(player.Faction, context.self) case Quit() => @@ -991,11 +973,26 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con ) ) + case SquadResponse.SquadDecoration(guid, squad) => + val decoration = if ( + squadUI.nonEmpty || + squad.Size == squad.Capacity || + { + val offer = avatar.certifications + !squad.Membership.exists { _.isAvailable(offer) } + } + ) { + SquadListDecoration.NotAvailable + } else { + SquadListDecoration.Available + } + sendResponse(SquadDefinitionActionMessage(guid, 0, SquadAction.SquadListDecorator(decoration))) + case SquadResponse.Detail(guid, detail) => sendResponse(SquadDetailDefinitionUpdateMessage(guid, detail)) - case SquadResponse.AssociateWithSquad(squad_guid) => - sendResponse(SquadDefinitionActionMessage(squad_guid, 0, SquadAction.AssociateWithSquad())) + case SquadResponse.IdentifyAsSquadLeader(squad_guid) => + sendResponse(SquadDefinitionActionMessage(squad_guid, 0, SquadAction.IdentifyAsSquadLeader())) case SquadResponse.SetListSquad(squad_guid) => sendResponse(SquadDefinitionActionMessage(squad_guid, 0, SquadAction.SetListSquad())) @@ -1003,7 +1000,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case SquadResponse.Membership(request_type, unk1, unk2, charId, opt_char_id, player_name, unk5, unk6) => val name = request_type match { case SquadResponseType.Invite if unk5 => - //player_name is our name; the name of the player indicated by unk3 is needed + //the name of the player indicated by unk3 is needed LivePlayerList.WorldPopulation({ case (_, a: Avatar) => charId == a.id }).headOption match { case Some(player) => player.name @@ -1026,10 +1023,13 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con ) ) - case SquadResponse.Join(squad, positionsToUpdate, toChannel) => - val leader = squad.Leader - val membershipPositions = positionsToUpdate map squad.Membership.zipWithIndex - membershipPositions.find({ case (member, _) => member.CharId == avatar.id }) match { + case SquadResponse.Join(squad, positionsToUpdate, _, ref) => + val avatarId = avatar.id + val membershipPositions = (positionsToUpdate map squad.Membership.zipWithIndex) + .filter { case (mem, index) => + mem.CharId > 0 && positionsToUpdate.contains(index) + } + membershipPositions.find { case (mem, _) => mem.CharId == avatarId } match { case Some((ourMember, ourIndex)) => //we are joining the squad //load each member's entry (our own too) @@ -1043,11 +1043,11 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con index, member.Name, member.ZoneId, - unk7 = 0 + outfit_id = 0 ) ) squadUI(member.CharId) = - SquadUIElement(member.Name, index, member.ZoneId, member.Health, member.Armor, member.Position) + SquadUIElement(member.Name, outfit=0L, index, member.ZoneId, member.Health, member.Armor, member.Position) } //repeat our entry sendResponse( @@ -1057,39 +1057,38 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con ourIndex, ourMember.Name, ourMember.ZoneId, - unk7 = 0 + outfit_id = 0 ) ) - val playerGuid = player.GUID //turn lfs off - val factionChannel = s"${player.Faction}" if (avatar.lookingForSquad) { avatarActor ! AvatarActor.SetLookingForSquad(false) } + val playerGuid = player.GUID + val factionChannel = s"${player.Faction}" //squad colors - GiveSquadColorsInZone() - continent.AvatarEvents ! AvatarServiceMessage( - factionChannel, - AvatarAction.PlanetsideAttribute(playerGuid, 31, squad_supplement_id) - ) + GiveSquadColorsToMembers() + GiveSquadColorsForOthers(playerGuid, factionChannel, squad_supplement_id) //associate with member position in squad sendResponse(PlanetsideAttributeMessage(playerGuid, 32, ourIndex)) //a finalization? what does this do? sendResponse(SquadDefinitionActionMessage(squad.GUID, 0, SquadAction.Unknown(18))) + squadService ! SquadServiceMessage(player, continent, SquadServiceAction.ReloadDecoration()) + updateSquadRef = ref updateSquad = PeriodicUpdatesWhenEnrolledInSquad chatActor ! ChatActor.JoinChannel(ChatService.ChatChannel.Squad(squad.GUID)) case _ => //other player is joining our squad //load each member's entry - GiveSquadColorsInZone( + GiveSquadColorsToMembers( membershipPositions.map { case (member, index) => val charId = member.CharId sendResponse( - SquadMemberEvent.Add(squad_supplement_id, charId, index, member.Name, member.ZoneId, unk7 = 0) + SquadMemberEvent.Add(squad_supplement_id, charId, index, member.Name, member.ZoneId, outfit_id = 0) ) squadUI(charId) = - SquadUIElement(member.Name, index, member.ZoneId, member.Health, member.Armor, member.Position) + SquadUIElement(member.Name, outfit=0L, index, member.ZoneId, member.Health, member.Armor, member.Position) charId } ) @@ -1098,23 +1097,9 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con sendResponse( SquadState( PlanetSideGUID(squad_supplement_id), - membershipPositions - .filterNot { case (member, _) => member.CharId == avatar.id } - .map { - case (member, _) => - SquadStateInfo( - member.CharId, - member.Health, - member.Armor, - member.Position, - 2, - 2, - false, - 429, - None, - None - ) - } + membershipPositions.map { case (member, _) => + SquadStateInfo(member.CharId, member.Health, member.Armor, member.Position) + } ) ) @@ -1123,6 +1108,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case Some((ourMember, ourIndex)) => //we are leaving the squad //remove each member's entry (our own too) + updateSquadRef = Default.Actor positionsToUpdate.foreach { case (member, index) => sendResponse(SquadMemberEvent.Remove(squad_supplement_id, member, index)) @@ -1131,16 +1117,11 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con //uninitialize val playerGuid = player.GUID sendResponse(SquadMemberEvent.Remove(squad_supplement_id, ourMember, ourIndex)) //repeat of our entry - sendResponse(PlanetsideAttributeMessage(playerGuid, 31, 0)) //disassociate with squad? - continent.AvatarEvents ! AvatarServiceMessage( - s"${player.Faction}", - AvatarAction.PlanetsideAttribute(playerGuid, 31, 0) - ) - sendResponse( - PlanetsideAttributeMessage(playerGuid, 32, 0) - ) //disassociate with member position in squad? + GiveSquadColorsToSelf(value = 0) + sendResponse(PlanetsideAttributeMessage(playerGuid, 32, 0)) //disassociate with member position in squad? sendResponse(PlanetsideAttributeMessage(playerGuid, 34, 4294967295L)) //unknown, perhaps unrelated? lfsm = false + avatarActor ! AvatarActor.SetLookingForSquad(false) //a finalization? what does this do? sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(18))) squad_supplement_id = 0 @@ -1149,7 +1130,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con chatActor ! ChatActor.LeaveChannel(ChatService.ChatChannel.Squad(squad.GUID)) case _ => //remove each member's entry - GiveSquadColorsInZone( + GiveSquadColorsToMembers( positionsToUpdate.map { case (member, index) => sendResponse(SquadMemberEvent.Remove(squad_supplement_id, member, index)) @@ -1164,35 +1145,19 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con //we've already swapped position internally; now we swap the cards SwapSquadUIElements(squad, from_index, to_index) - case SquadResponse.PromoteMember(squad, char_id, from_index, to_index) => - val charId = player.CharId - val guid = player.GUID - lazy val factionChannel = s"${player.Faction}" - //are we being demoted? - if (squadUI(charId).index == 0) { - //lfsm -> lfs + case SquadResponse.PromoteMember(squad, promotedPlayer, from_index) => + if (promotedPlayer != player.CharId) { + //demoted from leader; no longer lfsm if (lfsm) { - sendResponse(PlanetsideAttributeMessage(guid, 53, 0)) - continent.AvatarEvents ! AvatarServiceMessage( - factionChannel, - AvatarAction.PlanetsideAttribute(guid, 53, 0) - ) + lfsm = false + AvatarActor.displayLookingForSquad(session, state = 0) } - lfsm = false - sendResponse(PlanetsideAttributeMessage(guid, 32, from_index)) //associate with member position in squad } - //are we being promoted? - else if (charId == char_id) { - sendResponse(PlanetsideAttributeMessage(guid, 32, 0)) //associate with member position in squad - } - continent.AvatarEvents ! AvatarServiceMessage( - factionChannel, - AvatarAction.PlanetsideAttribute(guid, 31, squad_supplement_id) - ) - //we must fix the squad cards backend - SwapSquadUIElements(squad, from_index, to_index) + sendResponse(SquadMemberEvent(MemberEvent.Promote, squad.GUID.guid, promotedPlayer, position = 0)) + //the players have already been swapped in the backend object + PromoteSquadUIElements(squad, from_index) - case SquadResponse.UpdateMembers(squad, positions) => + case SquadResponse.UpdateMembers(_, positions) => val pairedEntries = positions.collect { case entry if squadUI.contains(entry.char_id) => (entry, squadUI(entry.char_id)) @@ -1206,13 +1171,13 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con SquadMemberEvent.UpdateZone(squad_supplement_id, entry.char_id, element.index, entry.zone_number) ) squadUI(entry.char_id) = - SquadUIElement(element.name, element.index, entry.zone_number, entry.health, entry.armor, entry.pos) + SquadUIElement(element.name, element.outfit, element.index, entry.zone_number, entry.health, entry.armor, entry.pos) entry case (entry, element) if entry.health != element.health || entry.armor != element.armor || entry.pos != element.position => //other elements that need to be updated squadUI(entry.char_id) = - SquadUIElement(element.name, element.index, entry.zone_number, entry.health, entry.armor, entry.pos) + SquadUIElement(element.name, element.outfit, element.index, entry.zone_number, entry.health, entry.armor, entry.pos) entry }) .filterNot(_.char_id == avatar.id) //we want to update our backend, but not our frontend @@ -1221,15 +1186,29 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con SquadState( PlanetSideGUID(squad_supplement_id), updatedEntries.map { entry => - SquadStateInfo(entry.char_id, entry.health, entry.armor, entry.pos, 2, 2, false, 429, None, None) + SquadStateInfo(entry.char_id, entry.health, entry.armor, entry.pos) } ) ) } - case SquadResponse.SquadSearchResults() => - //I don't actually know how to return search results - sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.NoSquadSearchResults())) + case SquadResponse.CharacterKnowledge(charId, name, certs, u1, u2, zone) => + sendResponse(CharacterKnowledgeMessage(charId, Some(CharacterKnowledgeInfo(name, certs, u1, u2, zone)))) + + case SquadResponse.SquadSearchResults(results) => + //TODO positive squad search results message? + if(results.nonEmpty) { + results.foreach { guid => + sendResponse(SquadDefinitionActionMessage( + guid, + 0, + SquadAction.SquadListDecorator(SquadListDecoration.SearchResult)) + ) + } + } else { + sendResponse(SquadDefinitionActionMessage(player.GUID, 0, SquadAction.NoSquadSearchResults())) + } + sendResponse(SquadDefinitionActionMessage(player.GUID, 0, SquadAction.CancelSquadSearch())) case SquadResponse.InitWaypoints(char_id, waypoints) => waypoints.foreach { @@ -3086,7 +3065,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case None => avatarActor ! AvatarActor.UpdatePurchaseTime(item.Definition) TaskWorkflow.execute(BuyNewEquipmentPutInInventory( - continent.GUID(tplayer.VehicleSeated) match { case Some(v: Vehicle) => v; case _ => player }, + continent.GUID(tplayer.VehicleSeated) match { case Some(v: Vehicle) => v; case _ => tplayer }, tplayer, msg.terminal_guid )(item)) @@ -3642,8 +3621,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con sendResponse(AvatarDeadStateMessage(DeadState.Alive, 0, 0, tplayer.Position, player.Faction, unk5=true)) //looking for squad (members) if (tplayer.avatar.lookingForSquad || lfsm) { - sendResponse(PlanetsideAttributeMessage(guid, 53, 1)) - continent.AvatarEvents ! AvatarServiceMessage(continent.id, AvatarAction.PlanetsideAttribute(guid, 53, 1)) + AvatarActor.displayLookingForSquad(session, state = 1) } sendResponse(AvatarSearchCriteriaMessage(guid, List(0, 0, 0, 0, 0, 0))) //these are facilities and towers and bunkers in the zone, but not necessarily all of them for some reason @@ -3862,7 +3840,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case (None, _) => ; } //non-squad GUID-0 counts as the settings when not joined with a squad - sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.AssociateWithSquad())) + sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.IdentifyAsSquadLeader())) sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.SetListSquad())) sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(18))) squadService ! SquadServiceMessage(player, continent, SquadServiceAction.InitSquadList()) @@ -3880,11 +3858,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con if (squad_supplement_id > 0) { squadUI.get(player.CharId) match { case Some(elem) => - sendResponse(PlanetsideAttributeMessage(player.GUID, 31, squad_supplement_id)) - continent.AvatarEvents ! AvatarServiceMessage( - s"${player.Faction}", - AvatarAction.PlanetsideAttribute(player.GUID, 31, squad_supplement_id) - ) + GiveSquadColorsToSelf(squad_supplement_id) sendResponse(PlanetsideAttributeMessage(player.GUID, 32, elem.index)) case _ => log.warn(s"RespawnSquadSetup: asked to redraw squad information, but ${player.Name} has no squad element for squad $squad_supplement_id") @@ -3892,6 +3866,45 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } } + /** + * Give the squad colors associated with the current squad to the client's player character. + * @param value value to associate the player + */ + def GiveSquadColorsToSelf(value: Long): Unit = { + GiveSquadColorsToSelf(player.GUID, player.Faction, value) + } + + /** + * Give the squad colors associated with the current squad to the client's player character. + * @param guid player guid + * @param faction faction for targeted updates to other players + * @param value value to associate the player + */ + def GiveSquadColorsToSelf(guid: PlanetSideGUID, faction: PlanetSideEmpire.Value, value: Long): Unit = { + sendResponse(PlanetsideAttributeMessage(guid, 31, value)) + GiveSquadColorsForOthers(guid, faction, value) + } + + /** + * Give the squad colors associated with the current squad to the client's player character. + * @param guid player guid + * @param faction faction for targeted updates to other players + * @param value value to associate the player + */ + def GiveSquadColorsForOthers(guid: PlanetSideGUID, faction: PlanetSideEmpire.Value, value: Long): Unit = { + GiveSquadColorsForOthers(guid, faction.toString, value) + } + + /** + * Give the squad colors associated with the current squad to the client's player character to other players. + * @param guid player guid + * @param faction faction for targeted updates to other players + * @param value value to associate the player + */ + def GiveSquadColorsForOthers(guid: PlanetSideGUID, factionChannel: String, value: Long): Unit = { + continent.AvatarEvents ! AvatarServiceMessage(factionChannel, AvatarAction.PlanetsideAttribute(guid, 31, value)) + } + /** * These messages are used during each subsequent respawn to reset the squad colors on player nameplates and marquees. * During a zone change, @@ -3902,23 +3915,23 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con def ZoneChangeSquadSetup(): Unit = { RespawnSquadSetup() squadService ! SquadServiceMessage(player, continent, SquadServiceAction.InitSquadList()) - GiveSquadColorsInZone() + GiveSquadColorsToMembers() squadSetup = RespawnSquadSetup } /** * Allocate all squad members in zone and give their nameplates and their marquees the appropriate squad color. */ - def GiveSquadColorsInZone(): Unit = { - GiveSquadColorsInZone(squadUI.keys, squad_supplement_id) + def GiveSquadColorsToMembers(): Unit = { + GiveSquadColorsToMembers(squadUI.keys, squad_supplement_id) } /** * Allocate the listed squad members in zone and give their nameplates and their marquees the appropriate squad color. * @param members members of the squad to target */ - def GiveSquadColorsInZone(members: Iterable[Long]): Unit = { - GiveSquadColorsInZone(members, squad_supplement_id) + def GiveSquadColorsToMembers(members: Iterable[Long]): Unit = { + GiveSquadColorsToMembers(members, squad_supplement_id) } /** @@ -3927,7 +3940,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con * @param members members of the squad to target * @param value the assignment value */ - def GiveSquadColorsInZone(members: Iterable[Long], value: Long): Unit = { + def GiveSquadColorsToMembers(members: Iterable[Long], value: Long): Unit = { SquadMembersInZone(members).foreach { members => sendResponse(PlanetsideAttributeMessage(members.GUID, 31, value)) } @@ -5784,15 +5797,13 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } else if (action == 30) { log.info(s"${player.Name} is back") player.AwayFromKeyboard = false + } else if (action == GenericActionEnum.DropSpecialItem.id) { + DropSpecialSlotItem() renewCharSavedTimer( Config.app.game.savedMsg.renewal.fixed, Config.app.game.savedMsg.renewal.variable ) - } - if (action == GenericActionEnum.DropSpecialItem.id) { - DropSpecialSlotItem() - } - if (action == 15) { //max deployment + } else if (action == 15) { //max deployment log.info(s"${player.Name} has anchored ${player.Sex.pronounObject}self to the ground") player.UsingSpecial = SpecialExoSuitDefinition.Mode.Anchored continent.AvatarEvents ! AvatarServiceMessage( @@ -5851,32 +5862,17 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } else { log.warn(s"GenericActionMessage: ${player.Name} can't handle action code 21") } - } else if (action == 36) { //Looking For Squad ON - if (squadUI.nonEmpty) { - if (!lfsm && squadUI(player.CharId).index == 0) { - lfsm = true - continent.AvatarEvents ! AvatarServiceMessage( - s"${player.Faction}", - AvatarAction.PlanetsideAttribute(player.GUID, 53, 1) - ) - } - } else if (!avatar.lookingForSquad) { - avatarActor ! AvatarActor.SetLookingForSquad(true) - } - } else if (action == 37) { //Looking For Squad OFF - if (squadUI.nonEmpty) { - if (lfsm && squadUI(player.CharId).index == 0) { - lfsm = false - continent.AvatarEvents ! AvatarServiceMessage( - s"${player.Faction}", - AvatarAction.PlanetsideAttribute(player.GUID, 53, 0) - ) - } - } else if (avatar.lookingForSquad) { - avatarActor ! AvatarActor.SetLookingForSquad(false) + } else if (action == 36 || action == 37) { //Looking For Squad (Members) (on/off) + val state = if (action == 36) { true } else { false } + squadUI.get(player.CharId) match { + case Some(elem) if elem.index == 0 => + lfsm = state + AvatarActor.displayLookingForSquad(session, boolToInt(state)) + case _ => + avatarActor ! AvatarActor.SetLookingForSquad(state) } } else { - log.debug(s"$msg") + log.warn(s"GenericActionMessage: ${player.Name} can't handle $msg") } } @@ -9280,66 +9276,131 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con } } - def SwapSquadUIElements(squad: Squad, fromIndex: Int, toIndex: Int): Unit = { - if (squadUI.nonEmpty) { - val fromMember = squad.Membership(toIndex) //the players have already been swapped in the backend object - val fromCharId = fromMember.CharId - val toMember = squad.Membership(fromIndex) //the players have already been swapped in the backend object - val toCharId = toMember.CharId - val id = 11 - if (toCharId > 0) { - //toMember and fromMember have swapped places - val fromElem = squadUI(fromCharId) - val toElem = squadUI(toCharId) - squadUI(toCharId) = - SquadUIElement(fromElem.name, toIndex, fromElem.zone, fromElem.health, fromElem.armor, fromElem.position) - squadUI(fromCharId) = - SquadUIElement(toElem.name, fromIndex, toElem.zone, toElem.health, toElem.armor, toElem.position) - sendResponse(SquadMemberEvent.Add(id, toCharId, toIndex, fromElem.name, fromElem.zone, unk7 = 0)) - sendResponse(SquadMemberEvent.Add(id, fromCharId, fromIndex, toElem.name, toElem.zone, unk7 = 0)) + /** + * Swap the squad UI elements along the top of the screen + * (colloquially referred to as "cards") + * to match recently updated positions of the members in a squad.
+ *
+ * The squad membership structure should have already updated to the correct positions of the members. + * By referencing one of the indices in this structure, + * obtain the character identification number, + * use the character identification number to locate that member's card, + * and update the card index to match the squad position in which that member is discovered. + * @see `PlanetsideAttributeMessage` + * @see `Squad` + * @see `SquadMemberEvent.Add` + * @see `SquadMemberEvent.Remove` + * @see `SquadState` + * @see `SquadStateInfo` + * @param squad the squad that supports the membership structure + * @param firstIndex the index of a player in the squad's membership; + * the member position must be occupied + * @param secondIndex the optional index of a player in the squad's membership; + * if the member position is occupied, + * the member at the first index swaps with the member at this second index + */ + def SwapSquadUIElements(squad: Squad, firstIndex: Int, secondIndex: Int): Unit = { + //the players should have already been swapped in the backend object + val firstMember = squad.Membership(firstIndex) + val firstCharId = firstMember.CharId + if (squadUI.nonEmpty && firstCharId > 0) { + //the firstIndex should always point to a valid player in the squad + val sguid = squad.GUID + val id = squad_supplement_id + val isFirstMoving = firstCharId == player.CharId + val secondMember = squad.Membership(secondIndex) + val secondCharId = secondMember.CharId + if (secondCharId > 0 && firstCharId != secondCharId) { + //secondMember and firstMember swap places + val newFirstElem = squadUI(firstCharId).copy(index = firstIndex) + val newSecondElem = squadUI(secondCharId).copy(index = secondIndex) + squadUI.put(firstCharId, newFirstElem) + squadUI.put(secondCharId, newSecondElem) +// sendResponse(SquadMemberEvent.Remove(id, secondCharId, firstIndex)) +// sendResponse(SquadMemberEvent.Remove(id, firstCharId, secondIndex)) + sendResponse(SquadMemberEvent.Add(id, firstCharId, firstIndex, newFirstElem.name, newFirstElem.zone, outfit_id = 0)) + sendResponse(SquadMemberEvent.Add(id, secondCharId, secondIndex, newSecondElem.name, newSecondElem.zone, outfit_id = 0)) + if (isFirstMoving) { + sendResponse(PlanetsideAttributeMessage(firstMember.GUID, 32, firstIndex)) + sendResponse(SquadDefinitionActionMessage(sguid, 0, SquadAction.Unknown(18))) + } else if (secondCharId == player.CharId) { + sendResponse(PlanetsideAttributeMessage(secondMember.GUID, 32, secondIndex)) + sendResponse(SquadDefinitionActionMessage(sguid, 0, SquadAction.Unknown(18))) + } sendResponse( - SquadState( - PlanetSideGUID(id), - List( - SquadStateInfo(fromCharId, toElem.health, toElem.armor, toElem.position, 2, 2, false, 429, None, None), - SquadStateInfo(toCharId, fromElem.health, fromElem.armor, fromElem.position, 2, 2, false, 429, None, None) - ) - ) + SquadState(PlanetSideGUID(id), List( + SquadStateInfo(firstCharId, newFirstElem.health, newFirstElem.armor, newFirstElem.position), + SquadStateInfo(secondCharId, newSecondElem.health, newSecondElem.armor, newSecondElem.position) + )) ) } else { - //previous fromMember has moved toMember - val elem = squadUI(fromCharId) - squadUI(fromCharId) = SquadUIElement(elem.name, toIndex, elem.zone, elem.health, elem.armor, elem.position) - sendResponse(SquadMemberEvent.Remove(id, fromCharId, fromIndex)) - sendResponse(SquadMemberEvent.Add(id, fromCharId, toIndex, elem.name, elem.zone, unk7 = 0)) + //firstMember has moved to a new position in squad + val elem = squadUI(firstCharId) + squadUI.put(firstCharId, elem.copy(index = firstIndex)) +// sendResponse(SquadMemberEvent.Remove(id, firstCharId, elem.index)) + sendResponse(SquadMemberEvent.Add(id, firstCharId, firstIndex, elem.name, elem.zone, outfit_id = 0)) + if (firstCharId == player.CharId) { + sendResponse(PlanetsideAttributeMessage(firstMember.GUID, 32, firstIndex)) + sendResponse(SquadDefinitionActionMessage(sguid, 0, SquadAction.Unknown(18))) + } sendResponse( - SquadState( - PlanetSideGUID(id), - List(SquadStateInfo(fromCharId, elem.health, elem.armor, elem.position, 2, 2, false, 429, None, None)) - ) + SquadState(PlanetSideGUID(id), List(SquadStateInfo(firstCharId, elem.health, elem.armor, elem.position))) ) } - val charId = avatar.id - if (toCharId == charId) { - sendResponse(PlanetsideAttributeMessage(player.GUID, 32, toIndex)) - } else if (fromCharId == charId) { - sendResponse(PlanetsideAttributeMessage(player.GUID, 32, fromIndex)) - } } } - def NoSquadUpdates(): Unit = {} + def PromoteSquadUIElements(squad: Squad, fromIndex: Int): Unit = { + //the players should have already been swapped in the backend object + val firstMember = squad.Membership(0) + val firstCharId = firstMember.CharId + val secondMember = squad.Membership(fromIndex) + val secondCharId = secondMember.CharId + if (squadUI.nonEmpty && fromIndex != 0 && firstCharId > 0 && secondCharId > 0) { + val newFirstElem = squadUI(firstCharId).copy(index = 0) + val newSecondElem = squadUI(secondCharId).copy(index = fromIndex) + val charId = player.CharId + val pguid = player.GUID + val sguid = squad.GUID + val id = squad_supplement_id + //secondMember and firstMember swap places + squadUI.put(firstCharId, newFirstElem) + squadUI.put(secondCharId, newSecondElem) + sendResponse(SquadMemberEvent(MemberEvent.Promote, id, firstCharId, position = 0)) + //player is being either promoted or demoted? + if (firstCharId == charId) { + sendResponse(PlanetsideAttributeMessage(pguid, 32, 0)) + sendResponse(SquadDefinitionActionMessage(sguid, 0, SquadAction.IdentifyAsSquadLeader())) + sendResponse(SquadDefinitionActionMessage(sguid, 0, SquadAction.Unknown(18))) + } else if (secondCharId == charId) { + sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.IdentifyAsSquadLeader())) + sendResponse(PlanetsideAttributeMessage(pguid, 32, fromIndex)) + sendResponse(SquadDefinitionActionMessage(sguid, 0, SquadAction.Unknown(18))) + } + //seed updates (just for the swapped players) + sendResponse( + SquadState(PlanetSideGUID(id), List( + SquadStateInfo(firstCharId, newFirstElem.health, newFirstElem.armor, newFirstElem.position), + SquadStateInfo(secondCharId, newSecondElem.health, newSecondElem.armor, newSecondElem.position) + )) + ) + } + } + + def NoSquadUpdates(): Unit = { } def SquadUpdates(): Unit = { - squadService ! SquadServiceMessage( + updateSquadRef ! SquadServiceMessage( player, continent, SquadServiceAction.Update( player.CharId, + player.GUID, player.Health, player.MaxHealth, player.Armor, player.MaxArmor, + avatar.certifications, player.Position, continent.Number ) diff --git a/src/main/scala/net/psforever/objects/teamwork/Member.scala b/src/main/scala/net/psforever/objects/teamwork/Member.scala index 019d0292..ca66bbb3 100644 --- a/src/main/scala/net/psforever/objects/teamwork/Member.scala +++ b/src/main/scala/net/psforever/objects/teamwork/Member.scala @@ -2,7 +2,7 @@ package net.psforever.objects.teamwork import net.psforever.objects.avatar.Certification -import net.psforever.types.Vector3 +import net.psforever.types.{PlanetSideGUID, Vector3} class Member { //about the position to be filled @@ -12,10 +12,12 @@ class Member { //about the individual filling the position private var name: String = "" private var charId: Long = 0L + private var guid: Int = 0 private var health: Int = 0 private var armor: Int = 0 private var zoneId: Int = 0 private var position: Vector3 = Vector3.Zero + private var certs: Set[Certification] = Set() def Role: String = role @@ -52,6 +54,17 @@ class Member { CharId } + def GUID: PlanetSideGUID = PlanetSideGUID(guid) + + def GUID_=(guid: PlanetSideGUID): PlanetSideGUID = { + GUID_=(guid.guid) + } + + def GUID_=(thisGuid: Int): PlanetSideGUID = { + guid = thisGuid + GUID + } + def Health: Int = health def Health_=(red: Int): Int = { @@ -80,6 +93,13 @@ class Member { Position } + def Certifications: Set[Certification] = certs + + def Certifications_=(req: Set[Certification]): Set[Certification] = { + certs = req + Certifications + } + def isAvailable: Boolean = { charId == 0 } diff --git a/src/main/scala/net/psforever/objects/teamwork/SquadFeatures.scala b/src/main/scala/net/psforever/objects/teamwork/SquadFeatures.scala index 21dbc1f9..8a27077f 100644 --- a/src/main/scala/net/psforever/objects/teamwork/SquadFeatures.scala +++ b/src/main/scala/net/psforever/objects/teamwork/SquadFeatures.scala @@ -2,9 +2,10 @@ package net.psforever.objects.teamwork import akka.actor.{ActorContext, ActorRef, Props} -import net.psforever.types.SquadWaypoint -import net.psforever.services.teamwork.SquadService.WaypointData -import net.psforever.services.teamwork.SquadSwitchboard +import net.psforever.objects.Default +import net.psforever.packet.game.WaypointInfo +import net.psforever.types.{PlanetSideGUID, SquadWaypoint, Vector3} +import net.psforever.services.teamwork.{SquadSubscriptionEntity, SquadSwitchboard} class SquadFeatures(val Squad: Squad) { @@ -40,7 +41,11 @@ class SquadFeatures(val Squad: Squad) { * Waypoints manifest in the game world as a (usually far-off) beam of light that extends into the sky * and whose ground contact utilizes a downwards pulsating arrow. * On the continental map and deployment map, they appear as a diamond, with a different number where applicable. - * The squad leader experience rally, for example, does not have a number like the preceding four waypoints. + * The squad leader experience rally, for example, does not have a number like the preceding four waypoints.
+ *
+ * Laze waypoints are as numerous as the number of players in a squad and + * exist only for fifteen seconds at a time. + * They are not counted in this list. * @see `Start` */ private var waypoints: Array[WaypointData] = Array[WaypointData]() @@ -65,25 +70,25 @@ class SquadFeatures(val Squad: Squad) { * For the purposes of wide-searches for membership * such as Looking For Squad checks and proximity invitation, * the unique character identifier numbers in this list are skipped. - * Direct invitation requests from the non sqad member should remain functional. + * Direct invitation requests from the non squad member should remain functional. */ private var refusedPlayers: List[Long] = Nil private var autoApproveInvitationRequests: Boolean = false - private var locationFollowsSquadLead: Boolean = true + private var locationFollowsSquadLead: Boolean = true //TODO false private var listed: Boolean = false private lazy val channel: String = s"${Squad.Faction}-Squad${Squad.GUID.guid}" - def Start(implicit context: ActorContext): SquadFeatures = { - switchboard = context.actorOf(Props[SquadSwitchboard](), s"squad_${Squad.GUID.guid}_${System.currentTimeMillis}") + def Start(implicit context: ActorContext, subs: SquadSubscriptionEntity): SquadFeatures = { + switchboard = context.actorOf(Props(classOf[SquadSwitchboard], this, subs), s"squad_${Squad.GUID.guid}_${System.currentTimeMillis}") waypoints = Array.fill[WaypointData](SquadWaypoint.values.size)(new WaypointData()) this } def Stop: SquadFeatures = { switchboard ! akka.actor.PoisonPill - switchboard = ActorRef.noSender + switchboard = Default.Actor waypoints = Array.empty this } @@ -99,6 +104,58 @@ class SquadFeatures(val Squad: Squad) { def Waypoints: Array[WaypointData] = waypoints + + + /** + * Display the indicated waypoint.
+ *
+ * Despite the name, no waypoints are actually "added." + * All of the waypoints constantly exist as long as the squad to which they are attached exists. + * They are merely "activated" and "deactivated." + * No waypoint is ever remembered for the laze-indicated target. + * @see `SquadWaypointRequest` + * @see `WaypointInfo` + * @param guid the squad's unique identifier + * @param waypointType the type of the waypoint + * @param info information about the waypoint, as was reported by the client's packet + * @return the waypoint data, if the waypoint type is changed + */ + def AddWaypoint( + guid: PlanetSideGUID, + waypointType: SquadWaypoint, + info: WaypointInfo + ): Option[WaypointData] = { + waypoints.lift(waypointType.value) match { + case Some(point) => + point.zone_number = info.zone_number + point.pos = info.pos + Some(point) + case None => + None + } + } + + /** + * Hide the indicated waypoint. + * Unused waypoints are marked by having a non-zero z-coordinate.
+ *
+ * Despite the name, no waypoints are actually "removed." + * All of the waypoints constantly exist as long as the squad to which they are attached exists. + * They are merely "activated" and "deactivated." + * @param guid the squad's unique identifier + * @param waypointType the type of the waypoint + */ + def RemoveWaypoint(guid: PlanetSideGUID, waypointType: SquadWaypoint): Option[WaypointData] = { + waypoints.lift(waypointType.value) match { + case Some(point) => + val oldWaypoint = WaypointData(point.zone_number, point.pos) + point.pos = Vector3.z(1) + Some(oldWaypoint) + case None => + None + } + } + def SearchForRole: Option[Int] = searchForRole def SearchForRole_=(role: Int): Option[Int] = SearchForRole_=(Some(role)) @@ -115,15 +172,24 @@ class SquadFeatures(val Squad: Squad) { ProxyInvites } - def Refuse: List[Long] = refusedPlayers + def DeniedPlayers(): List[Long] = refusedPlayers - def Refuse_=(charId: Long): List[Long] = { - Refuse_=(List(charId)) + def DeniedPlayers(charId: Long): List[Long] = { + DeniedPlayers(List(charId)) } - def Refuse_=(list: List[Long]): List[Long] = { + def DeniedPlayers(list: List[Long]): List[Long] = { refusedPlayers = list ++ refusedPlayers - Refuse + DeniedPlayers() + } + + def AllowedPlayers(charId: Long): List[Long] = { + AllowedPlayers(List(charId)) + } + + def AllowedPlayers(list: List[Long]): List[Long] = { + refusedPlayers = refusedPlayers.filterNot(list.contains) + DeniedPlayers() } def LocationFollowsSquadLead: Boolean = locationFollowsSquadLead diff --git a/src/main/scala/net/psforever/objects/teamwork/WaypointData.scala b/src/main/scala/net/psforever/objects/teamwork/WaypointData.scala new file mode 100644 index 00000000..919a3cd2 --- /dev/null +++ b/src/main/scala/net/psforever/objects/teamwork/WaypointData.scala @@ -0,0 +1,21 @@ +// Copyright (c) 2022 PSForever +package net.psforever.objects.teamwork + +import net.psforever.types.Vector3 + +/** + * Information necessary to display a specific map marker. + */ +class WaypointData() { + var zone_number: Int = 1 + var pos: Vector3 = Vector3.z(1) //a waypoint with a non-zero z-coordinate will flag as not getting drawn +} + +object WaypointData{ + def apply(zone_number: Int, pos: Vector3): WaypointData = { + val data = new WaypointData() + data.zone_number = zone_number + data.pos = pos + data + } +} diff --git a/src/main/scala/net/psforever/packet/game/CharacterKnowledgeMessage.scala b/src/main/scala/net/psforever/packet/game/CharacterKnowledgeMessage.scala index 2247e5e0..a82231c2 100644 --- a/src/main/scala/net/psforever/packet/game/CharacterKnowledgeMessage.scala +++ b/src/main/scala/net/psforever/packet/game/CharacterKnowledgeMessage.scala @@ -10,10 +10,10 @@ import shapeless.{::, HNil} final case class CharacterKnowledgeInfo( name: String, - permissions: Set[Certification], + certifications: Set[Certification], unk1: Int, unk2: Int, - unk3: PlanetSideGUID + zoneNumber: Int ) final case class CharacterKnowledgeMessage(char_id: Long, info: Option[CharacterKnowledgeInfo]) @@ -30,25 +30,22 @@ object CharacterKnowledgeMessage extends Marshallable[CharacterKnowledgeMessage] def apply(char_id: Long, info: CharacterKnowledgeInfo): CharacterKnowledgeMessage = CharacterKnowledgeMessage(char_id, Some(info)) - private val inverter: Codec[Boolean] = bool.xmap[Boolean]( - state => !state, - state => !state - ) + private val inverter: Codec[Boolean] = bool.xmap[Boolean](state => !state, state => !state) private val info_codec: Codec[CharacterKnowledgeInfo] = ( ("name" | PacketHelpers.encodedWideStringAligned(adjustment = 7)) :: - ("permissions" | ulongL(bits = 46)) :: + ("certifications" | ulongL(bits = 46)) :: ("unk1" | uint(bits = 6)) :: ("unk2" | uint(bits = 3)) :: - ("unk3" | PlanetSideGUID.codec) + ("zone" | uint16L) ).xmap[CharacterKnowledgeInfo]( { - case name :: permissions :: u1 :: u2 :: u3 :: HNil => - CharacterKnowledgeInfo(name, Certification.fromEncodedLong(permissions), u1, u2, u3) + case name :: certs :: u1 :: u2 :: zone :: HNil => + CharacterKnowledgeInfo(name, Certification.fromEncodedLong(certs), u1, u2, zone) }, { - case CharacterKnowledgeInfo(name, permissions, u1, u2, u3) => - name :: Certification.toEncodedLong(permissions) :: u1 :: u2 :: u3 :: HNil + case CharacterKnowledgeInfo(name, certs, u1, u2, zone) => + name :: Certification.toEncodedLong(certs) :: u1 :: u2 :: zone :: HNil } ) diff --git a/src/main/scala/net/psforever/packet/game/ReplicationStreamMessage.scala b/src/main/scala/net/psforever/packet/game/ReplicationStreamMessage.scala index a020bb3c..4120c386 100644 --- a/src/main/scala/net/psforever/packet/game/ReplicationStreamMessage.scala +++ b/src/main/scala/net/psforever/packet/game/ReplicationStreamMessage.scala @@ -65,8 +65,8 @@ final case class SquadInfo( this And SquadInfo(None, None, Some(zone), None, None, None) def ZoneId(zone: Option[PlanetSideZoneID]): SquadInfo = zone match { - case Some(zoneId) => this And SquadInfo(None, None, zone, None, None, None) - case None => SquadInfo(leader, task, zone, size, capacity, squad_guid) + case Some(_) => this And SquadInfo(None, None, zone, None, None, None) + case None => SquadInfo(leader, task, zone, size, capacity, squad_guid) } def Size(sz: Int): SquadInfo = this And SquadInfo(None, None, None, Some(sz), None, None) @@ -612,8 +612,8 @@ object SquadHeader { (bool >>:~ { unk1 => uint8 >>:~ { unk2 => conditional(!unk1 && unk2 == 1, removeCodec) :: - conditional(unk1 && unk2 == 6, providedCodec) :: - conditional(unk1 && unk2 != 6, listing_codec(unk2)) + conditional(unk1 && unk2 == 6, providedCodec) :: + conditional(unk1 && unk2 != 6, listing_codec(unk2)) } }).exmap[Option[SquadInfo]]( { @@ -691,10 +691,8 @@ object SquadListing { private def meta_codec(entryFunc: Int => Codec[Option[SquadInfo]]): Codec[SquadListing] = (("index" | uint8L) >>:~ { index => conditional(index < 255, "listing" | entryFunc(index)) :: - conditional( - index == 255, - bits - ) //consume n < 8 bits after the tail entry, else vector will try to operate on invalid data + conditional(index == 255, bits) + //consume n < 8 bits after the tail entry, else vector will try to operate on invalid data }).xmap[SquadListing]( { case ndx :: Some(lstng) :: _ :: HNil => @@ -717,7 +715,7 @@ object SquadListing { * `Codec` for branching types of `SquadListing` initializations. */ val info_codec: Codec[SquadListing] = meta_codec({ index: Int => - newcodecs.binary_choice(index == 0, "listing" | SquadHeader.info_codec, "listing" | SquadHeader.alt_info_codec) + newcodecs.binary_choice(index == 0, SquadHeader.info_codec, SquadHeader.alt_info_codec) }) } @@ -738,14 +736,15 @@ object ReplicationStreamMessage extends Marshallable[ReplicationStreamMessage] { ) } - implicit val codec: Codec[ReplicationStreamMessage] = (("behavior" | uintL(3)) >>:~ { behavior => - conditional(behavior == 5, "behavior2" | uintL(3)) :: + implicit val codec: Codec[ReplicationStreamMessage] = ( + ("behavior" | uintL(bits = 3)) >>:~ { behavior => + ("behavior2" | conditional(behavior == 5, uintL(bits = 3))) :: conditional(behavior != 1, bool) :: - newcodecs.binary_choice( + ("entries" | newcodecs.binary_choice( behavior != 5, - "entries" | vector(SquadListing.codec), - "entries" | vector(SquadListing.info_codec) - ) + vector(SquadListing.codec), + vector(SquadListing.info_codec) + )) }).xmap[ReplicationStreamMessage]( { case bhvr :: bhvr2 :: _ :: lst :: HNil => diff --git a/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala b/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala index e233abb9..14358c0b 100644 --- a/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala +++ b/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala @@ -3,7 +3,7 @@ package net.psforever.packet.game import net.psforever.objects.avatar.Certification import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} -import net.psforever.types.PlanetSideGUID +import net.psforever.types.{PlanetSideGUID, SquadListDecoration} import scodec.bits.BitVector import scodec.{Attempt, Codec, Err} import scodec.codecs._ @@ -25,71 +25,73 @@ object SquadAction { implicit val codec: Codec[SearchMode.Value] = PacketHelpers.createEnumerationCodec(enum = this, uint(bits = 3)) } - final case class DisplaySquad() extends SquadAction(0) + final case class DisplaySquad() extends SquadAction(code = 0) /** * Dispatched from client to server to indicate a squad detail update that has no foundation entry to update? * Not dissimilar from `DisplaySquad`. */ - final case class SquadMemberInitializationIssue() extends SquadAction(1) + final case class SquadInitializationIssue() extends SquadAction(code = 1) - final case class SaveSquadFavorite() extends SquadAction(3) + final case class SaveSquadFavorite() extends SquadAction(code = 3) - final case class LoadSquadFavorite() extends SquadAction(4) + final case class LoadSquadFavorite() extends SquadAction(code = 4) - final case class DeleteSquadFavorite() extends SquadAction(5) + final case class DeleteSquadFavorite() extends SquadAction(code = 5) - final case class ListSquadFavorite(name: String) extends SquadAction(7) + final case class ListSquadFavorite(name: String) extends SquadAction(code = 7) - final case class RequestListSquad() extends SquadAction(8) + final case class RequestListSquad() extends SquadAction(code = 8) - final case class StopListSquad() extends SquadAction(9) + final case class StopListSquad() extends SquadAction(code = 9) - final case class SelectRoleForYourself(state: Int) extends SquadAction(10) + final case class SelectRoleForYourself(state: Int) extends SquadAction(code = 10) - final case class CancelSelectRoleForYourself(value: Long = 0) extends SquadAction(15) + final case class CancelSelectRoleForYourself(value: Long = 0) extends SquadAction(code = 15) - final case class AssociateWithSquad() extends SquadAction(16) + final case class IdentifyAsSquadLeader() extends SquadAction(code = 16) - final case class SetListSquad() extends SquadAction(17) + final case class SetListSquad() extends SquadAction(code = 17) - final case class ChangeSquadPurpose(purpose: String) extends SquadAction(19) + final case class ChangeSquadPurpose(purpose: String) extends SquadAction(code = 19) - final case class ChangeSquadZone(zone: PlanetSideZoneID) extends SquadAction(20) + final case class ChangeSquadZone(zone: PlanetSideZoneID) extends SquadAction(code = 20) - final case class CloseSquadMemberPosition(position: Int) extends SquadAction(21) + final case class CloseSquadMemberPosition(position: Int) extends SquadAction(code = 21) - final case class AddSquadMemberPosition(position: Int) extends SquadAction(22) + final case class AddSquadMemberPosition(position: Int) extends SquadAction(code = 22) - final case class ChangeSquadMemberRequirementsRole(u1: Int, role: String) extends SquadAction(23) + final case class ChangeSquadMemberRequirementsRole(u1: Int, role: String) extends SquadAction(code = 23) - final case class ChangeSquadMemberRequirementsDetailedOrders(u1: Int, orders: String) extends SquadAction(24) + final case class ChangeSquadMemberRequirementsDetailedOrders(u1: Int, orders: String) extends SquadAction(code = 24) final case class ChangeSquadMemberRequirementsCertifications(u1: Int, certs: Set[Certification]) - extends SquadAction(25) + extends SquadAction(code = 25) - final case class ResetAll() extends SquadAction(26) + final case class ResetAll() extends SquadAction(code = 26) - final case class AutoApproveInvitationRequests(state: Boolean) extends SquadAction(28) + final case class AutoApproveInvitationRequests(state: Boolean) extends SquadAction(code = 28) - final case class LocationFollowsSquadLead(state: Boolean) extends SquadAction(31) + final case class LocationFollowsSquadLead(state: Boolean) extends SquadAction(code = 31) + + final case class SquadListDecorator(state: SquadListDecoration.Value) extends SquadAction(code = 33) final case class SearchForSquadsWithParticularRole( role: String, requirements: Set[Certification], zone_id: Int, mode: SearchMode.Value - ) extends SquadAction(34) + ) extends SquadAction(code = 34) - final case class CancelSquadSearch() extends SquadAction(35) + final case class CancelSquadSearch() extends SquadAction(code = 35) - final case class AssignSquadMemberToRole(position: Int, char_id: Long) extends SquadAction(38) + final case class AssignSquadMemberToRole(position: Int, char_id: Long) extends SquadAction(code = 38) - final case class NoSquadSearchResults() extends SquadAction(39) + final case class NoSquadSearchResults() extends SquadAction(code = 39) - final case class FindLfsSoldiersForRole(state: Int) extends SquadAction(40) + final case class FindLfsSoldiersForRole(state: Int) extends SquadAction(code = 40) - final case class CancelFind() extends SquadAction(41) + final case class CancelFind() extends SquadAction(code = 41) final case class Unknown(badCode: Int, data: BitVector) extends SquadAction(badCode) @@ -114,10 +116,10 @@ object SquadAction { } ) - val squadMemberInitializationIssueCodec = everFailCondition.xmap[SquadMemberInitializationIssue]( - _ => SquadMemberInitializationIssue(), + val squadMemberInitializationIssueCodec = everFailCondition.xmap[SquadInitializationIssue]( + _ => SquadInitializationIssue(), { - case SquadMemberInitializationIssue() => None + case SquadInitializationIssue() => None } ) @@ -179,10 +181,10 @@ object SquadAction { } ) - val associateWithSquadCodec = everFailCondition.xmap[AssociateWithSquad]( - _ => AssociateWithSquad(), + val identifyAsSquadLeaderCodec = everFailCondition.xmap[IdentifyAsSquadLeader]( + _ => IdentifyAsSquadLeader(), { - case AssociateWithSquad() => None + case IdentifyAsSquadLeader() => None } ) @@ -276,6 +278,18 @@ object SquadAction { } ) + val squadListDecoratorCodec = ( + SquadListDecoration.codec :: + ignore(size = 3) + ).xmap[SquadListDecorator]( + { + case value :: _ :: HNil => SquadListDecorator(value) + }, + { + case SquadListDecorator(value) => value :: () :: HNil + } + ) + val searchForSquadsWithParticularRoleCodec = (PacketHelpers.encodedWideStringAligned(6) :: ulongL(46) :: uint16L :: @@ -389,7 +403,7 @@ object SquadAction { *     `20` - (Squad leader) Change Squad Zone
*     `21` - (Squad leader) Close Squad Member Position
*     `22` - (Squad leader) Add Squad Member Position
- *     `33` - UNKNOWN
+ *     `33` - Decorate a Squad in the List of Squads with Color
*     `40` - Find LFS Soldiers that Meet the Requirements for this Role
*   `Long`
*     `13` - UNKNOWN
@@ -451,7 +465,7 @@ object SquadDefinitionActionMessage extends Marshallable[SquadDefinitionActionMe case 13 => unknownCodec(action = 13) case 14 => unknownCodec(action = 14) case 15 => cancelSelectRoleForYourselfCodec - case 16 => associateWithSquadCodec + case 16 => identifyAsSquadLeaderCodec case 17 => setListSquadCodec case 18 => unknownCodec(action = 18) case 19 => changeSquadPurposeCodec @@ -468,7 +482,7 @@ object SquadDefinitionActionMessage extends Marshallable[SquadDefinitionActionMe case 30 => unknownCodec(action = 30) case 31 => locationFollowsSquadLeadCodec case 32 => unknownCodec(action = 32) - case 33 => unknownCodec(action = 33) + case 33 => squadListDecoratorCodec case 34 => searchForSquadsWithParticularRoleCodec case 35 => cancelSquadSearchCodec case 36 => unknownCodec(action = 36) diff --git a/src/main/scala/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala b/src/main/scala/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala index abeaf80f..e1adb114 100644 --- a/src/main/scala/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala +++ b/src/main/scala/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala @@ -143,12 +143,12 @@ final case class SquadPositionEntry(index: Int, info: Option[SquadPositionDetail * All fields are optional for that reason.
*
* The squad leader does not necessarily have to be a person from the `member_info` list. - * @param unk1 na; + * @param guid na; * must be non-zero when parsed in a FullSquad pattern * @param unk2 na; * not associated with any fields during itemized parsing * @param leader_char_id he unique character identification number for the squad leader - * @param unk3 na + * @param outfit_id na * @param leader_name the name of the player who is the squad leader * @param task the suggested responsibilities or mission statement of the squad * @param zone_id the suggested area of engagement for this squad's activities; @@ -157,10 +157,10 @@ final case class SquadPositionEntry(index: Int, info: Option[SquadPositionDetail * @param member_info a list of squad position data */ final case class SquadDetail( - unk1: Option[Int], + guid: Option[Int], unk2: Option[Int], leader_char_id: Option[Long], - unk3: Option[Long], + outfit_id: Option[Long], leader_name: Option[String], task: Option[String], zone_id: Option[PlanetSideZoneID], @@ -176,10 +176,10 @@ final case class SquadDetail( */ def And(info: SquadDetail): SquadDetail = { SquadDetail( - unk1.orElse(info.unk1), + guid.orElse(info.guid), unk2.orElse(info.unk2), leader_char_id.orElse(info.leader_char_id), - unk3.orElse(info.unk3), + outfit_id.orElse(info.outfit_id), leader_name.orElse(info.leader_name), task.orElse(info.task), zone_id.orElse(info.zone_id), @@ -202,12 +202,14 @@ final case class SquadDetail( } //methods intended to combine the fields of itself and another object - def Field1(value: Int): SquadDetail = - this And SquadDetail(Some(value), None, None, None, None, None, None, None, None) + def Guid(guid: Int): SquadDetail = + this And SquadDetail(Some(guid), None, None, None, None, None, None, None, None) + def Field2(value: Int): SquadDetail = + this And SquadDetail(None, Some(value), None, None, None, None, None, None, None) def LeaderCharId(char_id: Long): SquadDetail = this And SquadDetail(None, None, Some(char_id), None, None, None, None, None, None) - def Field3(value: Long): SquadDetail = - this And SquadDetail(None, None, None, Some(value), None, None, None, None, None) + def OutfitId(outfit: Long): SquadDetail = + this And SquadDetail(None, None, None, Some(outfit), None, None, None, None, None) def LeaderName(name: String): SquadDetail = this And SquadDetail(None, None, None, None, Some(name), None, None, None, None) def Leader(char_id: Long, name: String): SquadDetail = @@ -228,14 +230,14 @@ final case class SquadDetail( */ def Complete: SquadDetail = SquadDetail( - unk1.orElse(Some(1)), + guid.orElse(Some(1)), unk2.orElse(Some(0)), leader_char_id.orElse(Some(0L)), - unk3.orElse(Some(0L)), + outfit_id.orElse(Some(0L)), leader_name.orElse(Some("")), task.orElse(Some("")), zone_id.orElse(Some(PlanetSideZoneID(0))), - unk7.orElse(Some(4983296)), //FullSquad value + unk7.orElse(Some(4983296)), //FullSquad value? { val complete = SquadPositionDetail().Complete Some(member_info match { @@ -359,7 +361,7 @@ object SquadDetail { unk1: Int, unk2: Int, leader_char_id: Long, - unk3: Long, + outfit_id: Long, leader_name: String, task: String, zone_id: PlanetSideZoneID, @@ -370,7 +372,7 @@ object SquadDetail { Some(unk1), Some(unk2), Some(leader_char_id), - Some(unk3), + Some(outfit_id), Some(leader_name), Some(task), Some(zone_id), @@ -380,12 +382,12 @@ object SquadDetail { } //individual field overloaded constructors - def Field1(unk1: Int): SquadDetail = - SquadDetail(Some(unk1), None, None, None, None, None, None, None, None) + def Guid(guid: Int): SquadDetail = + SquadDetail(Some(guid), None, None, None, None, None, None, None, None) def LeaderCharId(char_id: Long): SquadDetail = SquadDetail(None, None, Some(char_id), None, None, None, None, None, None) - def Field3(char_id: Option[Long], unk3: Long): SquadDetail = - SquadDetail(None, None, None, Some(unk3), None, None, None, None, None) + def OutfitId(char_id: Option[Long], outfit_id: Long): SquadDetail = + SquadDetail(None, None, None, Some(outfit_id), None, None, None, None, None) def LeaderName(name: String): SquadDetail = SquadDetail(None, None, None, None, Some(name), None, None, None, None) def Leader(char_id: Long, name: String): SquadDetail = @@ -400,9 +402,9 @@ object SquadDetail { SquadDetail(None, None, None, None, None, None, None, None, Some(list)) object Fields { - final val Field1 = 1 + final val Guid = 1 final val CharId = 2 - final val Field3 = 3 + final val Outfit = 3 final val Leader = 4 final val Task = 5 final val ZoneId = 6 @@ -541,10 +543,10 @@ object SquadDetailDefinitionUpdateMessage extends Marshallable[SquadDetailDefini val codec: Codec[SquadDetail] = { import shapeless.:: ( - ("unk1" | uint8) :: - ("unk2" | uint24) :: //unknown, but can be 0'd + ("guid" | uint16L) :: + ("unk2" | uint16L) :: //unknown, but can be 0'd ("leader_char_id" | uint32L) :: - ("unk3" | uint32L) :: //variable fields, but can be 0'd + ("outfit_id" | uint32L) :: //can be 0'd ("leader" | PacketHelpers.encodedWideStringAligned(7)) :: ("task" | PacketHelpers.encodedWideString) :: ("zone_id" | PlanetSideZoneID.codec) :: @@ -552,13 +554,13 @@ object SquadDetailDefinitionUpdateMessage extends Marshallable[SquadDetailDefini optional(bool, "member_info" | initial_member_codec) ).exmap[SquadDetail]( { - case u1 :: u2 :: char_id :: u3 :: leader :: task :: zone :: unk7 :: Some(member_list) :: HNil => + case guid :: u2 :: char_id :: outfit_id :: leader :: task :: zone :: unk7 :: Some(member_list) :: HNil => Attempt.Successful( SquadDetail( - Some(u1), + Some(guid), Some(u2), Some(char_id), - Some(u3), + Some(outfit_id), Some(leader), Some(task), Some(zone), @@ -573,10 +575,10 @@ object SquadDetailDefinitionUpdateMessage extends Marshallable[SquadDetailDefini }, { case SquadDetail( - Some(u1), + Some(guid), Some(u2), Some(char_id), - Some(u3), + Some(outfit_id), Some(leader), Some(task), Some(zone), @@ -584,7 +586,7 @@ object SquadDetailDefinitionUpdateMessage extends Marshallable[SquadDetailDefini Some(member_list) ) => Attempt.Successful( - math.max(u1, 1) :: u2 :: char_id :: u3 :: leader :: task :: zone :: unk7 :: + math.max(guid, 1) :: u2 :: char_id :: outfit_id :: leader :: task :: zone :: unk7 :: Some(linkFields(member_list.collect { case SquadPositionEntry(_, Some(entry)) => entry }.reverse)) :: HNil ) @@ -605,15 +607,15 @@ object SquadDetailDefinitionUpdateMessage extends Marshallable[SquadDetailDefini object ItemizedSquad { /** - * A pattern for data related to "field1." + * A pattern for data related to the `guid` field. */ - private val field1Codec: Codec[SquadDetail] = uint16L.exmap[SquadDetail]( - unk1 => Attempt.successful(SquadDetail(Some(unk1), None, None, None, None, None, None, None, None)), + private val guidCodec: Codec[SquadDetail] = uint16L.exmap[SquadDetail]( + guid => Attempt.successful(SquadDetail(Some(guid), None, None, None, None, None, None, None, None)), { - case SquadDetail(Some(unk1), _, _, _, _, _, _, _, _) => - Attempt.successful(unk1) + case SquadDetail(Some(guid), _, _, _, _, _, _, _, _) => + Attempt.successful(guid) case _ => - Attempt.failure(Err("failed to encode squad data for unknown field #1")) + Attempt.failure(Err("failed to encode squad data for the guid")) } ) @@ -631,13 +633,13 @@ object SquadDetailDefinitionUpdateMessage extends Marshallable[SquadDetailDefini ) /** - * A pattern for data related to "field3." + * A pattern for data related to the "outfit id" field. */ - private val field3Codec: Codec[SquadDetail] = uint32L.exmap[SquadDetail]( - unk3 => Attempt.successful(SquadDetail(None, None, None, Some(unk3), None, None, None, None, None)), + private val outfitCodec: Codec[SquadDetail] = uint32L.exmap[SquadDetail]( + outfit_id => Attempt.successful(SquadDetail(None, None, None, Some(outfit_id), None, None, None, None, None)), { - case SquadDetail(_, _, _, Some(unk3), _, _, _, _, _) => - Attempt.successful(unk3) + case SquadDetail(_, _, _, Some(outfit_id), _, _, _, _, _) => + Attempt.successful(outfit_id) case _ => Attempt.failure(Err("failed to encode squad data for unknown field #3")) } @@ -807,9 +809,9 @@ object SquadDetailDefinitionUpdateMessage extends Marshallable[SquadDetailDefini */ private def selectCodedAction(code: Int, bitsOverByte: StreamLengthToken): Codec[SquadDetail] = { code match { - case 1 => field1Codec + case 1 => guidCodec case 2 => leaderCharIdCodec - case 3 => field3Codec + case 3 => outfitCodec case 4 => leaderNameCodec(bitsOverByte) case 5 => taskCodec(bitsOverByte) case 6 => zoneCodec @@ -820,7 +822,7 @@ object SquadDetailDefinitionUpdateMessage extends Marshallable[SquadDetailDefini } /** - * Advance information about the current stream length because on which pattern was previously utilized. + * Advance information about the current stream length based on which pattern was previously utilized. * @see `selectCodedAction(Int, StreamLengthToken)` * @param code the action code, connecting to a field pattern * @param bitsOverByte a token maintaining stream misalignment for purposes of calculating string padding @@ -834,7 +836,7 @@ object SquadDetailDefinitionUpdateMessage extends Marshallable[SquadDetailDefini case 4 => bitsOverByte.Length = 0 //byte-aligned string; padding zero'd case 5 => bitsOverByte.Length = 0 //byte-aligned string; padding zero'd case 6 => bitsOverByte //32u = no added padding - case 7 => bitsOverByte.Add(4) //additional 4u + case 7 => bitsOverByte.Add(more = 4) //additional 4u case 8 => bitsOverByte.Length = 0 //end of stream case _ => bitsOverByte.Length = Int.MinValue //wildly incorrect } @@ -886,9 +888,9 @@ object SquadDetailDefinitionUpdateMessage extends Marshallable[SquadDetailDefini (6, SquadDetail(None, None, None, None, None, None, info.zone_id, None, None)), (5, SquadDetail(None, None, None, None, None, info.task, None, None, None)), (4, SquadDetail(None, None, None, None, info.leader_name, None, None, None, None)), - (3, SquadDetail(None, None, None, info.unk3, None, None, None, None, None)), + (3, SquadDetail(None, None, None, info.outfit_id, None, None, None, None, None)), (2, SquadDetail(None, None, info.leader_char_id, None, None, None, None, None, None)), - (1, SquadDetail(info.unk1, None, None, None, None, None, None, None, None)) + (1, SquadDetail(info.guid, None, None, None, None, None, None, None, None)) ) //in reverse order so that the linked list is in the correct order .filterNot { case (_, sqInfo) => sqInfo == SquadDetail.Blank } match { case Nil => @@ -1102,12 +1104,12 @@ object SquadDetailDefinitionUpdateMessage extends Marshallable[SquadDetailDefini */ private def modifyCodedPadValue(code: Int, bitsOverByte: StreamLengthToken): StreamLengthToken = { code match { - case 0 => bitsOverByte.Add(1) //additional 1u + case 0 => bitsOverByte.Add(1) //additional 1u case 1 => bitsOverByte.Length = 0 //byte-aligned string; padding zero'd case 2 => bitsOverByte.Length = 0 //byte-aligned string; padding zero'd case 3 => bitsOverByte //32u = no added padding case 4 => bitsOverByte.Length = 0 //byte-aligned string; padding zero'd - case 5 => bitsOverByte.Add(6) //46u = 5*8u + 6u = additional 6u + case 5 => bitsOverByte.Add(6) //46u = 5*8u + 6u = additional 6u case _ => bitsOverByte.Length = Int.MinValue //wildly incorrect } } @@ -1587,10 +1589,10 @@ object SquadDetailDefinitionUpdateMessage extends Marshallable[SquadDetailDefini { case SquadDetailDefinitionUpdateMessage(guid, info) => val occupiedSquadFieldCount = List( - info.unk1, + info.guid, info.unk2, info.leader_char_id, - info.unk3, + info.outfit_id, info.leader_name, info.task, info.zone_id, diff --git a/src/main/scala/net/psforever/packet/game/SquadMemberEvent.scala b/src/main/scala/net/psforever/packet/game/SquadMemberEvent.scala index 8fdb83e7..3bd5c949 100644 --- a/src/main/scala/net/psforever/packet/game/SquadMemberEvent.scala +++ b/src/main/scala/net/psforever/packet/game/SquadMemberEvent.scala @@ -9,7 +9,7 @@ import shapeless.{::, HNil} object MemberEvent extends Enumeration { type Type = Value - val Add, Remove, Promote, UpdateZone, Unknown4 = Value + val Add, Remove, Promote, UpdateZone, Outfit = Value implicit val codec = PacketHelpers.createEnumerationCodec(enum = this, uint(bits = 3)) } @@ -21,7 +21,7 @@ final case class SquadMemberEvent( position: Int, player_name: Option[String], zone_number: Option[Int], - unk7: Option[Long] + outfit_id: Option[Long] ) extends PlanetSideGamePacket { type Packet = SquadMemberEvent def opcode = GamePacketOpcode.SquadMemberEvent @@ -38,9 +38,9 @@ object SquadMemberEvent extends Marshallable[SquadMemberEvent] { position: Int, player_name: String, zone_number: Int, - unk7: Long + outfit_id: Long ): SquadMemberEvent = - SquadMemberEvent(MemberEvent.Add, unk2, char_id, position, Some(player_name), Some(zone_number), Some(unk7)) + SquadMemberEvent(MemberEvent.Add, unk2, char_id, position, Some(player_name), Some(zone_number), Some(outfit_id)) def Remove(unk2: Int, char_id: Long, position: Int): SquadMemberEvent = SquadMemberEvent(MemberEvent.Remove, unk2, char_id, position, None, None, None) @@ -51,20 +51,20 @@ object SquadMemberEvent extends Marshallable[SquadMemberEvent] { def UpdateZone(unk2: Int, char_id: Long, position: Int, zone_number: Int): SquadMemberEvent = SquadMemberEvent(MemberEvent.UpdateZone, unk2, char_id, position, None, Some(zone_number), None) - def Unknown4(unk2: Int, char_id: Long, position: Int, unk7: Long): SquadMemberEvent = - SquadMemberEvent(MemberEvent.Unknown4, unk2, char_id, position, None, None, Some(unk7)) + def Outfit(unk2: Int, char_id: Long, position: Int, outfit_id: Long): SquadMemberEvent = + SquadMemberEvent(MemberEvent.Outfit, unk2, char_id, position, None, None, Some(outfit_id)) implicit val codec: Codec[SquadMemberEvent] = (("action" | MemberEvent.codec) >>:~ { action => ("unk2" | uint16L) :: ("char_id" | uint32L) :: ("position" | uint4) :: - conditional(action == MemberEvent.Add, "player_name" | PacketHelpers.encodedWideStringAligned(1)) :: - conditional(action == MemberEvent.Add || action == MemberEvent.UpdateZone, "zone_number" | uint16L) :: - conditional(action == MemberEvent.Add || action == MemberEvent.Unknown4, "unk7" | uint32L) + ("player_name" | conditional(action == MemberEvent.Add, PacketHelpers.encodedWideStringAligned(adjustment = 1))) :: + ("zone_number" | conditional(action == MemberEvent.Add || action == MemberEvent.UpdateZone, uint16L)) :: + ("outfit_id" | conditional(action == MemberEvent.Add || action == MemberEvent.Outfit, uint32L)) }).exmap[SquadMemberEvent]( { - case action :: unk2 :: char_id :: member_position :: player_name :: zone_number :: unk7 :: HNil => - Attempt.Successful(SquadMemberEvent(action, unk2, char_id, member_position, player_name, zone_number, unk7)) + case action :: unk2 :: char_id :: member_position :: player_name :: zone_number :: outfit_id :: HNil => + Attempt.Successful(SquadMemberEvent(action, unk2, char_id, member_position, player_name, zone_number, outfit_id)) }, { case SquadMemberEvent( @@ -74,20 +74,20 @@ object SquadMemberEvent extends Marshallable[SquadMemberEvent] { member_position, Some(player_name), Some(zone_number), - Some(unk7) + Some(outfit_id) ) => Attempt.Successful( MemberEvent.Add :: unk2 :: char_id :: member_position :: Some(player_name) :: Some(zone_number) :: Some( - unk7 + outfit_id ) :: HNil ) case SquadMemberEvent(MemberEvent.UpdateZone, unk2, char_id, member_position, None, Some(zone_number), None) => Attempt.Successful( MemberEvent.UpdateZone :: unk2 :: char_id :: member_position :: None :: Some(zone_number) :: None :: HNil ) - case SquadMemberEvent(MemberEvent.Unknown4, unk2, char_id, member_position, None, None, Some(unk7)) => + case SquadMemberEvent(MemberEvent.Outfit, unk2, char_id, member_position, None, None, Some(outfit_id)) => Attempt.Successful( - MemberEvent.Unknown4 :: unk2 :: char_id :: member_position :: None :: None :: Some(unk7) :: HNil + MemberEvent.Outfit :: unk2 :: char_id :: member_position :: None :: None :: Some(outfit_id) :: HNil ) case SquadMemberEvent(action, unk2, char_id, member_position, None, None, None) => Attempt.Successful(action :: unk2 :: char_id :: member_position :: None :: None :: None :: HNil) diff --git a/src/main/scala/net/psforever/packet/game/SquadMembershipResponse.scala b/src/main/scala/net/psforever/packet/game/SquadMembershipResponse.scala index 96224fd0..8a5c8f74 100644 --- a/src/main/scala/net/psforever/packet/game/SquadMembershipResponse.scala +++ b/src/main/scala/net/psforever/packet/game/SquadMembershipResponse.scala @@ -26,7 +26,7 @@ import scodec.codecs._ * - `Invite` (0)
* false => [PROMPT] "`player_name` has invited you into a squad." [YES/NO]
* true => "You have invited `player_name` to join your squad."
- * - `Unk01` (1)
+ * - `ProximityInvite` (1)
* false => n/a
* true => n/a
* - `Accept` (2)
diff --git a/src/main/scala/net/psforever/packet/game/SquadState.scala b/src/main/scala/net/psforever/packet/game/SquadState.scala index bd8a4cf4..12efe618 100644 --- a/src/main/scala/net/psforever/packet/game/SquadState.scala +++ b/src/main/scala/net/psforever/packet/game/SquadState.scala @@ -58,14 +58,16 @@ final case class SquadState(guid: PlanetSideGUID, info_list: List[SquadStateInfo } object SquadStateInfo { - def apply(unk1: Long, unk2: Int, unk3: Int, pos: Vector3, unk4: Int, unk5: Int, unk6: Boolean, unk7: Int) - : SquadStateInfo = - SquadStateInfo(unk1, unk2, unk3, pos, unk4, unk5, unk6, unk7, None, None) + def apply(charId: Long, health: Int, armor: Int, pos: Vector3): SquadStateInfo = + SquadStateInfo(charId, health, armor, pos, 2, 2, unk6=false, 429, None, None) + + def apply(charId: Long, health: Int, armor: Int, pos: Vector3, unk4: Int, unk5: Int, unk6: Boolean, unk7: Int): SquadStateInfo = + SquadStateInfo(charId, health, armor, pos, unk4, unk5, unk6, unk7, None, None) def apply( - unk1: Long, - unk2: Int, - unk3: Int, + charId: Long, + health: Int, + armor: Int, pos: Vector3, unk4: Int, unk5: Int, @@ -74,22 +76,22 @@ object SquadStateInfo { unk8: Int, unk9: Boolean ): SquadStateInfo = - SquadStateInfo(unk1, unk2, unk3, pos, unk4, unk5, unk6, unk7, Some(unk8), Some(unk9)) + SquadStateInfo(charId, health, armor, pos, unk4, unk5, unk6, unk7, Some(unk8), Some(unk9)) } object SquadState extends Marshallable[SquadState] { private val info_codec: Codec[SquadStateInfo] = ( ("char_id" | uint32L) :: - ("health" | uint(7)) :: - ("armor" | uint(7)) :: + ("health" | uint(bits = 7)) :: + ("armor" | uint(bits = 7)) :: ("pos" | Vector3.codec_pos) :: ("unk4" | uint2) :: ("unk5" | uint2) :: ("unk6" | bool) :: ("unk7" | uint16L) :: (bool >>:~ { out => - conditional(out, "unk8" | uint16L) :: - conditional(out, "unk9" | bool) + ("unk8" | conditional(out, uint16L)) :: + ("unk9" | conditional(out, bool)) }) ).exmap[SquadStateInfo]( { diff --git a/src/main/scala/net/psforever/packet/game/objectcreate/CharacterAppearanceData.scala b/src/main/scala/net/psforever/packet/game/objectcreate/CharacterAppearanceData.scala index 70a0ee16..08018cf7 100644 --- a/src/main/scala/net/psforever/packet/game/objectcreate/CharacterAppearanceData.scala +++ b/src/main/scala/net/psforever/packet/game/objectcreate/CharacterAppearanceData.scala @@ -68,7 +68,7 @@ final case class CharacterAppearanceA( * @param on_zipline player's model is changed into a faction-color ball of energy, as if on a zip line */ final case class CharacterAppearanceB( - unk0: Long, + outfit_id: Long, outfit_name: String, outfit_logo: Int, unk1: Boolean, @@ -394,7 +394,7 @@ object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] { */ def b_codec(alt_model: Boolean, name_padding: Int): Codec[CharacterAppearanceB] = ( - ("unk0" | uint32L) :: //for outfit_name (below) to be visible in-game, this value should be non-zero + ("outfit_id" | uint32L) :: //for outfit_name (below) to be visible in-game, this value should be non-zero ("outfit_name" | PacketHelpers.encodedWideStringAligned(outfitNamePadding)) :: ("outfit_logo" | uint8L) :: ("unk1" | bool) :: //unknown @@ -414,12 +414,12 @@ object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] { optional(bool, "on_zipline" | zipline_codec) ).exmap[CharacterAppearanceB]( { - case u0 :: outfit :: logo :: u1 :: bpack :: u2 :: u3 :: u4 :: facingPitch :: facingYawUpper :: lfs :: gstate :: cloaking :: u5 :: u6 :: charging :: u7 :: zipline :: HNil => + case outfit_id :: outfit :: logo :: u1 :: bpack :: u2 :: u3 :: u4 :: facingPitch :: facingYawUpper :: lfs :: gstate :: cloaking :: u5 :: u6 :: charging :: u7 :: zipline :: HNil => val lfsBool = if (lfs == 0) false else true val bpackBool = bpack match { case Some(_) => alt_model; case None => false } Attempt.successful( CharacterAppearanceB( - u0, + outfit_id, outfit, logo, u1, @@ -442,7 +442,7 @@ object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] { }, { case CharacterAppearanceB( - u0, + outfit_id, outfit, logo, u1, @@ -461,10 +461,10 @@ object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] { u7, zipline ) => - val u0Long = if (u0 == 0 && outfit.nonEmpty) { + val u0Long = if (outfit_id == 0 && outfit.nonEmpty) { outfit.length.toLong } else { - u0 + outfit_id } //TODO this is a kludge; unk0 must be (some) non-zero if outfit_name is defined val (bpackOpt, zipOpt) = if (alt_model) { val bpackOpt = if (bpack) { Some(true) } diff --git a/src/main/scala/net/psforever/services/teamwork/SquadInvitationManager.scala b/src/main/scala/net/psforever/services/teamwork/SquadInvitationManager.scala new file mode 100644 index 00000000..9b7f3eb8 --- /dev/null +++ b/src/main/scala/net/psforever/services/teamwork/SquadInvitationManager.scala @@ -0,0 +1,2153 @@ +// Copyright (c) 2022 PSForever +package net.psforever.services.teamwork + +import akka.actor.ActorRef +import akka.pattern.ask +import akka.util.Timeout +import scala.collection.mutable +import scala.concurrent.duration._ +import scala.concurrent.ExecutionContext.Implicits.global +import scala.util.Success +// +import net.psforever.objects.{LivePlayerList, Player} +import net.psforever.objects.avatar.Avatar +import net.psforever.objects.teamwork.{Member, SquadFeatures} +import net.psforever.objects.zones.Zone +import net.psforever.packet.game.{SquadDetail, SquadPositionDetail, SquadPositionEntry, SquadAction => SquadRequestAction} +import net.psforever.types.{PlanetSideGUID, SquadResponseType} + +class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { + import SquadInvitationManager._ + + private implicit val timeout: Timeout = Timeout(1.second) + + /** + * key - a unique character identifier number; value - the active invitation object + */ + private val invites: mutable.LongMap[Invitation] = mutable.LongMap[Invitation]() + + /** + * key - a unique character identifier number; value - a list of inactive invitation objects waiting to be resolved + */ + private val queuedInvites: mutable.LongMap[List[Invitation]] = mutable.LongMap[List[Invitation]]() + + /** + * A placeholder for an absent active invite that has not (yet) been accepted or rejected, + * equal to the then-current active invite. + * Created when removing an active invite. + * Checked when trying to add a new invite (if found, the new invite is queued). + * Cleared when the next queued invite becomes active.
+ * key - unique character identifier number; value, unique character identifier number + */ + private val previousInvites: mutable.LongMap[Invitation] = mutable.LongMap[Invitation]() + + /* + When a player refuses an invitation by a squad leader, + that squad leader will not be able to send further invitations (this field). + If the player submits an invitation request for that squad, + the current squad leader is cleared from the blocked list. + When a squad leader refuses an invitation by a player, + that player will not be able to send further invitations (field on sqaud's features). + If the squad leader sends an invitation request for that player, + the current player is cleared from the blocked list. + */ + /** + * The given player has refused participation into this other player's squad.
+ * key - a unique character identifier number; + * value - a list of unique character identifier numbers; squad leaders or once-squad leaders + */ + private val refused: mutable.LongMap[List[Long]] = mutable.LongMap[List[Long]]() + + private[this] val log = org.log4s.getLogger + + def postStop(): Unit = { + invites.clear() + queuedInvites.clear() + previousInvites.clear() + refused.clear() + } + + def handleJoin(charId: Long): Unit = { + refused.put(charId, List[Long]()) + } + + def createRequestRole(player: Player, features: SquadFeatures, position: Int): Unit = { + //we could join directly but we need permission from the squad leader first + val charId = features.Squad.Leader.CharId + val requestRole = RequestRole(player, features, position) + if (features.AutoApproveInvitationRequests) { + SquadActionMembershipAcceptInvite( + player, + charId, + Some(requestRole), + None + ) + } else { + //circumvent tests in AddInviteAndRespond + InviteResponseTemplate(indirectInviteResp)( + requestRole, + AddInvite(charId, requestRole), + charId, + invitingPlayer = 0L, //we ourselves technically are ... + player.Name + ) + } + } + + def createVacancyInvite(player: Player, invitedPlayer: Long, features: SquadFeatures): Unit = { + val invitingPlayer = player.CharId + val squad = features.Squad + Allowed(invitedPlayer, invitingPlayer) + if (squad.Size == squad.Capacity) { + log.debug(s"$invitingPlayer tried to invite $invitedPlayer to a squad without available positions") + } else if (Refused(invitingPlayer).contains(invitedPlayer)) { + log.debug(s"$invitedPlayer repeated a previous refusal to $invitingPlayer's invitation offer") + } else { + features.AllowedPlayers(invitingPlayer) + AddInviteAndRespond( + invitedPlayer, + VacancyInvite(invitingPlayer, player.Name, features), + invitingPlayer, + player.Name + ) + } + } + + def createIndirectInvite(player: Player, invitingPlayer: Long, features: SquadFeatures): Unit = { + val invitedPlayer = player.CharId + val squad2 = features.Squad + val leader = squad2.Leader.CharId + Allowed(invitedPlayer, invitingPlayer) + Allowed(leader, invitingPlayer) + if (squad2.Size == squad2.Capacity) { + log.debug(s"$invitingPlayer's invitation got reversed to $invitedPlayer's squad, but the squad has no available positions") + } else if (Refused(invitingPlayer).contains(invitedPlayer)) { + log.debug(s"$invitingPlayer's invitation got reversed to $invitedPlayer's squad, but $invitedPlayer repeated a previous refusal to $invitingPlayer's invitation offer") + } else if (Refused(invitingPlayer).contains(leader)) { + log.debug(s"$invitingPlayer's invitation got reversed to $invitedPlayer's squad, but $leader repeated a previous refusal to $invitingPlayer's invitation offer") + } else if (features.DeniedPlayers().contains(invitingPlayer)) { + log.debug(s"$invitingPlayer's invitation got reversed to $invitedPlayer's squad, but $invitingPlayer is denied the invitation") + } else { + features.AllowedPlayers(invitedPlayer) + AddInviteAndRespond( + leader, + IndirectInvite(player, features), + invitingPlayer, + player.Name + ) + } + } + + def createSpontaneousInvite(player: Player, invitedPlayer: Long): Unit = { + //neither the invited player nor the inviting player belong to any squad + val invitingPlayer = player.CharId + Allowed(invitedPlayer, invitingPlayer) + if (Refused(invitingPlayer).contains(invitedPlayer)) { + log.debug(s"$invitedPlayer repeated a previous refusal to $invitingPlayer's invitation offer") + } else if (Refused(invitedPlayer).contains(invitingPlayer)) { + log.debug(s"$invitingPlayer repeated a previous refusal to $invitedPlayer's invitation offer") + } else { + AddInviteAndRespond( + invitedPlayer, + SpontaneousInvite(player), + invitingPlayer, + player.Name + ) + } + } + + def SquadActionMembershipAcceptInvite( + tplayer: Player, + invitedPlayer: Long, + acceptedInvite: Option[Invitation], + invitedPlayerSquadOpt: Option[SquadFeatures] + ): Unit = { + val availableForJoiningSquad = notLimitedByEnrollmentInSquad(invitedPlayerSquadOpt, invitedPlayer) + acceptedInvite match { + case Some(RequestRole(petitioner, features, position)) + if availableForJoiningSquad && canEnrollInSquad(features, petitioner.CharId) => + //player requested to join a squad's specific position + //invitedPlayer is actually the squad leader; petitioner is the actual "invitedPlayer" + if (JoinSquad(petitioner, features, position)) { + DeliverAcceptanceMessages(invitedPlayer, petitioner.CharId, petitioner.Name) + CleanUpInvitesForSquadAndPosition(features, position) + } + + case Some(IndirectInvite(recruit, features)) + if availableForJoiningSquad && canEnrollInSquad(features, recruit.CharId) => + //tplayer / invitedPlayer is actually the squad leader + val recruitCharId = recruit.CharId + HandleVacancyInvite(features, recruitCharId, invitedPlayer, recruit) match { + case Some((_, line)) => + DeliverAcceptanceMessages(invitedPlayer, recruitCharId, recruit.Name) + JoinSquad(recruit, features, line) + CleanUpAllInvitesWithPlayer(recruitCharId) + CleanUpInvitesForSquadAndPosition(features, line) + //TODO since we are the squad leader, we do not want to brush off our queued squad invite tasks + case _ => ; + } + + case Some(VacancyInvite(invitingPlayer, _, features)) + if availableForJoiningSquad && canEnrollInSquad(features, invitedPlayer) => + //accepted an invitation to join an existing squad + HandleVacancyInvite(features, invitedPlayer, invitingPlayer, tplayer) match { + case Some((_, line)) => + DeliverAcceptanceMessages(invitingPlayer, invitedPlayer, tplayer.Name) + JoinSquad(tplayer, features, line) + CleanUpQueuedInvites(invitedPlayer) + CleanUpInvitesForSquadAndPosition(features, line) + case _ => ; + } + + case Some(SpontaneousInvite(invitingPlayer)) + if availableForJoiningSquad => + SquadMembershipAcceptInviteAction(invitingPlayer, tplayer, invitedPlayer) + + case Some(LookingForSquadRoleInvite(member, features, position)) + if availableForJoiningSquad && canEnrollInSquad(features, invitedPlayer) => + val invitingPlayer = member.CharId + features.ProxyInvites = features.ProxyInvites.filterNot { _ == invitedPlayer } + if (JoinSquad(tplayer, features, position)) { + //join this squad + DeliverAcceptanceMessages(invitingPlayer, invitedPlayer, tplayer.Name) + CleanUpQueuedInvites(tplayer.CharId) + CleanUpInvitesForSquadAndPosition(features, position) + } + + case Some(ProximityInvite(member, features, position)) + if availableForJoiningSquad && canEnrollInSquad(features, invitedPlayer) => + val invitingPlayer = member.CharId + features.ProxyInvites = features.ProxyInvites.filterNot { _ == invitedPlayer } + if (JoinSquad(tplayer, features, position)) { + //join this squad + DeliverAcceptanceMessages(invitingPlayer, invitedPlayer, tplayer.Name) + CleanUpAllInvitesWithPlayer(invitedPlayer) + val squad = features.Squad + if (squad.Size == squad.Capacity) { + //all available squad positions filled; terminate all remaining invitations + CleanUpAllInvitesToSquad(features) + } + } else { + ReloadProximityInvite(tplayer.Zone.Players, invitedPlayer, features, position) //TODO ? + } + + 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(member, _, _)) => (member.CharId, member.Name) + case Some(LookingForSquadRoleInvite(member, _, _)) => (member.CharId, member.Name) + case _ => (0L, "") + }) match { + case (0L, "") => ; + case (charId, name) => + subs.Publish( + charId, + SquadResponse.Membership(SquadResponseType.Cancel, 0, 0, charId, Some(0L), name, unk5 = false, Some(None)) + ) + } + } + } + + def DeliverAcceptanceMessages( + squadLeader: Long, + joiningPlayer: Long, + joiningPlayerName: String + ): Unit = { + val msg = SquadResponse.Membership( + SquadResponseType.Accept, + 0, + 0, + joiningPlayer, + Some(squadLeader), + joiningPlayerName, + unk5 = false, + Some(None) + ) + subs.Publish(squadLeader, msg) + subs.Publish(joiningPlayer, msg.copy(unk5 = true)) + } + + def notLimitedByEnrollmentInSquad(squadOpt: Option[SquadFeatures], charId: Long): Boolean = { + squadOpt match { + case Some(features) if features.Squad.Membership.exists { _.CharId == charId } => + EnsureEmptySquad(features) + case Some(_) => + false + case None => + true + } + } + + def canEnrollInSquad(features: SquadFeatures, charId: Long): Boolean = { + !features.Squad.Membership.exists { _.CharId == charId } + } + + def SquadMembershipAcceptInviteAction(invitingPlayer: Player, player: Player, invitedPlayer: Long): Unit = { + //originally, we were invited by someone into a new squad they would form + val invitingPlayerCharId = invitingPlayer.CharId + if (invitingPlayerCharId != invitedPlayer) { + //generate a new squad, with invitingPlayer as the leader + val result = ask(parent, SquadService.PerformStartSquad(invitingPlayer)) + result.onComplete { + case Success(FinishStartSquad(features)) => + HandleVacancyInvite(features, player.CharId, invitingPlayerCharId, player) match { + case Some((_, line)) => + subs.Publish( + invitedPlayer, + SquadResponse.Membership( + SquadResponseType.Accept, + 0, + 0, + invitedPlayer, + Some(invitingPlayerCharId), + "", + unk5=true, + Some(None) + ) + ) + subs.Publish( + invitingPlayerCharId, + SquadResponse.Membership( + SquadResponseType.Accept, + 0, + 0, + invitingPlayerCharId, + Some(invitedPlayer), + player.Name, + unk5=false, + Some(None) + ) + ) + JoinSquad(player, features, line) + CleanUpQueuedInvites(player.CharId) + case _ => ; + } + case _ => ; + } + } + } + + def handleProximityInvite(zone: Zone, invitingPlayer: Long, features: SquadFeatures): Unit = { + val squad = features.Squad + val sguid = squad.GUID + val origSearchForRole = features.SearchForRole + var newRecruitment = List[Long]() + val positionsToRecruitFor = (origSearchForRole match { + case Some(-1) => + //we've already issued a proximity invitation; no need to do another + log.debug("ProximityInvite: wait for existing proximity invitations to clear") + Array[(Member, Int)]() + case Some(position) => + //already searching for the given position; retain the key if the invite is active, or clear if if not + features.SearchForRole = -1 + invites.find { + case (_, LookingForSquadRoleInvite(_, features, role)) => + features.Squad.GUID == sguid && role == position + case _ => + false + } match { + case Some((key, _)) => + newRecruitment = newRecruitment :+ key + squad.Membership.zipWithIndex.filterNot { case (_, index) => index == position } + case None => + CleanUpQueuedInvitesForSquadAndPosition(features, position) + squad.Membership.zipWithIndex + } + case _ => + features.SearchForRole = -1 + squad.Membership.zipWithIndex + }) + .collect { case (member, index) if member.CharId == 0 && squad.Availability(index) => (member, index) } + .sortBy({ _._1.Requirements.foldLeft(0)(_ + _.value) })(Ordering.Int.reverse) + //find recruits + val players = zone.LivePlayers.map { _.avatar } + if (positionsToRecruitFor.nonEmpty && players.nonEmpty) { + //does this do anything? + subs.Publish( + invitingPlayer, + SquadResponse.Membership( + SquadResponseType.ProximityInvite, + 19, + 0, + invitingPlayer, + None, + "", + unk5=true, + Some(None) + ) + ) + positionsToRecruitFor.foreach { case (_, position) => + FindSoldiersWithinScopeAndInvite( + squad.Leader, + features, + position, + players, + features.ProxyInvites ++ newRecruitment, + ProximityEnvelope + ) match { + case None => ; + case Some(id) => + newRecruitment = newRecruitment :+ id + } + } + } + if (newRecruitment.isEmpty) { + features.SearchForRole = origSearchForRole + } else { + features.ProxyInvites = features.ProxyInvites ++ newRecruitment + //if searching for a position originally, convert the active invite to proximity invite, or remove it + val key = newRecruitment.head + (origSearchForRole, invites.get(key)) match { + case (Some(-1), _) => ; + case (Some(position), Some(LookingForSquadRoleInvite(member, _, _))) => + invites(key) = ProximityInvite(member, features, position) + case _ => ; + } + } + } + + def handleAcceptance(player: Player, charId: Long, squadOpt: Option[SquadFeatures]): Unit = { + SquadActionMembershipAcceptInvite(player, charId, RemoveInvite(charId), squadOpt) + NextInviteAndRespond(charId) + } + + def handleRejection( + tplayer: Player, + rejectingPlayer: Long, + squadsToLeaders: List[(PlanetSideGUID, Long)] + ): Unit = { + val rejectedBid = RemoveInvite(rejectingPlayer) + (rejectedBid match { + case Some(SpontaneousInvite(leader)) => + //rejectingPlayer is the would-be squad member; the would-be squad leader sent the request and was rejected + val invitingPlayerCharId = leader.CharId + Refused(rejectingPlayer, invitingPlayerCharId) + (Some(rejectingPlayer), Some(invitingPlayerCharId)) + + case Some(VacancyInvite(leader, _, features)) + if notLeaderOfThisSquad(squadsToLeaders, features.Squad.GUID, rejectingPlayer) => + //rejectingPlayer is the would-be squad member; the squad leader sent the request and was rejected + Refused(rejectingPlayer, leader) + (Some(rejectingPlayer), Some(leader)) + + case Some(ProximityInvite(_, features, position)) + if notLeaderOfThisSquad(squadsToLeaders, features.Squad.GUID, rejectingPlayer) => + //rejectingPlayer is the would-be squad member; the squad leader sent the request and was rejected + ReloadProximityInvite( + tplayer.Zone.Players, + rejectingPlayer, + features, + position + ) + (Some(rejectingPlayer), None) + + case Some(LookingForSquadRoleInvite(member, guid, position)) + if member.CharId != rejectingPlayer => + val leaderCharId = member.CharId + //rejectingPlayer is the would-be squad member; the squad leader sent the request and was rejected + ReloadSearchForRoleInvite( + LivePlayerList.WorldPopulation { _ => true }, + rejectingPlayer, + guid, + position + ) + (Some(rejectingPlayer), Some(leaderCharId)) + + case Some(RequestRole(rejected, features, _)) + if notLeaderOfThisSquad(squadsToLeaders, features.Squad.GUID, rejectingPlayer) => + //rejected is the would-be squad member; rejectingPlayer is the squad leader who rejected the request + features.DeniedPlayers(rejected.CharId) + (Some(rejectingPlayer), None) + + case _ => ; //TODO IndirectInvite, etc., but how to handle them? + (None, None) + }) match { + case (Some(rejected), Some(invited)) => + subs.Publish( + rejected, + SquadResponse.Membership(SquadResponseType.Reject, 0, 0, rejected, Some(invited), "", unk5=true, Some(None)) + ) + subs.Publish( + invited, + SquadResponse.Membership(SquadResponseType.Reject, 0, 0, invited, Some(rejected), tplayer.Name, unk5=false, Some(None)) + ) + case (Some(rejected), None) => + subs.Publish( + rejected, + SquadResponse.Membership(SquadResponseType.Reject, 0, 0, rejected, Some(rejected), "", unk5=true, Some(None)) + ) + case _ => ; + } + NextInviteAndRespond(rejectingPlayer) + } + + def notLeaderOfThisSquad(squadsToLeaders: List[(PlanetSideGUID, Long)], guid: PlanetSideGUID, charId: Long): Boolean = { + squadsToLeaders.find { case (squadGuid, _) => squadGuid == guid } match { + case Some((_, leaderId)) => leaderId != charId + case None => false + } + } + + def ReloadSearchForRoleInvite( + scope: List[Avatar], + rejectingPlayer: Long, + features: SquadFeatures, + position: Int + ) : Unit = { + //rejectingPlayer is the would-be squad member; the squad leader rejected the request + val squadLeader = features.Squad.Leader + Refused(rejectingPlayer, squadLeader.CharId) + features.ProxyInvites = features.ProxyInvites.filterNot { _ == rejectingPlayer } + FindSoldiersWithinScopeAndInvite( + squadLeader, + features, + position, + scope, + features.ProxyInvites, + LookingForSquadRoleEnvelope + ) match { + case None => + if (features.SearchForRole.contains(position) && features.ProxyInvites.isEmpty) { + features.SearchForRole = None + //TODO message the squadLeader.CharId to indicate that there are no more candidates for this position + } + case Some(id) => + features.ProxyInvites = features.ProxyInvites :+ id + } + if (features.ProxyInvites.isEmpty) { + features.SearchForRole = None + } + } + + def ReloadProximityInvite( + scope: List[Avatar], + rejectingPlayer: Long, + features: SquadFeatures, + position: Int + ): Unit = { + //rejectingPlayer is the would-be squad member; the squad leader rejected the request + val squadLeader = features.Squad.Leader + Refused(rejectingPlayer, squadLeader.CharId) + features.ProxyInvites = features.ProxyInvites.filterNot { _ == rejectingPlayer } + FindSoldiersWithinScopeAndInvite( + squadLeader, + features, + position, + scope, + features.ProxyInvites, + ProximityEnvelope + ) match { + case None => + if (features.SearchForRole.contains(-1) && features.ProxyInvites.isEmpty) { + features.SearchForRole = None + //TODO message the squadLeader.CharId to indicate that there are no more candidates for this position + } + case Some(id) => + features.ProxyInvites = features.ProxyInvites :+ id + } + } + + def handleDisbanding(features: SquadFeatures): Unit = { + CleanUpAllInvitesToSquad(features) + } + + def handleCancelling(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 + CleanUpAllProximityInvites(cancellingPlayer) + } + + def handlePromotion( + sponsoringPlayer: Long, + promotedPlayer: Long, + ): Unit = { + ShiftInvitesToPromotedSquadLeader(sponsoringPlayer, promotedPlayer) + } + + def handleDefinitionAction( + player: Player, + action: SquadRequestAction, + features: SquadFeatures + ): Unit = { + import SquadRequestAction._ + action match { + //the following actions cause changes with the squad composition or with invitations + case AutoApproveInvitationRequests(_) => + SquadActionDefinitionAutoApproveInvitationRequests(player, features) + case FindLfsSoldiersForRole(position) => + SquadActionDefinitionFindLfsSoldiersForRole(player, position, features) + case CancelFind() => + SquadActionDefinitionCancelFind(None) + case SelectRoleForYourself(position) => + SquadActionDefinitionSelectRoleForYourselfAsInvite(player, features, position) + case _: CancelSelectRoleForYourself => + SquadActionDefinitionCancelSelectRoleForYourself(player, features) + case _ => ; + } + } + + def SquadActionDefinitionAutoApproveInvitationRequests( + tplayer: Player, + features: SquadFeatures + ): Unit = { + //allowed auto-approval - resolve the requests (only) + val charId = tplayer.CharId + val (requests, others) = + (invites.get(charId) match { + case Some(invite) => invite +: queuedInvites.getOrElse(charId, Nil) + case None => queuedInvites.getOrElse(charId, Nil) + }) + .partition({ case _: RequestRole => true; case _ => false }) + invites.remove(charId) + queuedInvites.remove(charId) + previousInvites.remove(charId) + //RequestRole invitations that still have to be handled + val squad = features.Squad + var remainingRequests = requests.collect { + case request: RequestRole => (request, request.player) + } + var unfulfilled = List[Player]() + //give roles to people who requested specific positions + (1 to 9).foreach { position => + val (discovered, remainder) = remainingRequests.partition { + case (request: RequestRole, _) => request.position == position + case _ => false + } + unfulfilled ++= (discovered + .find { case (_, player) => JoinSquad(player, features, position) } match { + case Some((_, player)) => + remainingRequests = remainder.filterNot { case (_, p) => p.CharId == player.CharId } + discovered.filterNot { case (_, p) => p.CharId == player.CharId } + case None => + remainingRequests = remainder + discovered + }).map { _._2 } + } + //fill any incomplete role by trying to match all sorts of unfulfilled invitations + var otherInvites = unfulfilled ++ + others.collect { + case invite: SpontaneousInvite => invite.player + case invite: IndirectInvite => invite.player + } + .distinctBy { _.CharId } + (1 to 9).foreach { position => + if (squad.Availability(position)) { + otherInvites.zipWithIndex.find { case (invitedPlayer, _) => + JoinSquad(invitedPlayer, features, position) + } match { + case Some((_, index)) => + otherInvites = otherInvites.take(index) ++ otherInvites.drop(index+1) + case None => ; + } + } + } + //cleanup searches by squad leader + features.SearchForRole match { + case Some(-1) => CleanUpAllProximityInvites(charId) + case Some(_) => SquadActionDefinitionCancelFind(Some(features)) + case None => ; + } + } + + def SquadActionDefinitionFindLfsSoldiersForRole( + tplayer: Player, + position: Int, + features: SquadFeatures + ): Unit = { + val squad = features.Squad + val sguid = squad.GUID + (features.SearchForRole match { + case None => + Some(Nil) + case Some(-1) => + //a proximity invitation has not yet cleared; nothing will be gained by trying to invite for a specific role + log.debug("FindLfsSoldiersForRole: waiting for proximity invitations to clear") + None + case Some(pos) if pos == position => + //already recruiting for this specific position in the squad? do nothing + log.debug("FindLfsSoldiersForRole: already recruiting for this position; client-server mismatch?") + None + case Some(pos) => + //some other role is undergoing recruitment; cancel and redirect efforts for new position + features.SearchForRole = None + CleanUpQueuedInvitesForSquadAndPosition(features, pos) + Some( + invites.filter { + case (_, LookingForSquadRoleInvite(_, _features, role)) => _features.Squad.GUID == sguid && role == pos + case _ => false + }.keys.toList + ) + }) match { + case None => + features.SearchForRole = None + case Some(list) => + //this will update the role entry in the GUI to visually indicate being searched for; only one will be displayed at a time + subs.Publish( + tplayer.CharId, + SquadResponse.Detail( + sguid, + SquadDetail().Members( + List(SquadPositionEntry(position, SquadPositionDetail().CharId(char_id = 0L).Name(name = ""))) + ) + ) + ) + //search! + FindSoldiersWithinScopeAndInvite( + squad.Leader, + features, + position, + LivePlayerList.WorldPopulation { _ => true }, + list, + LookingForSquadRoleEnvelope + ) match { + case None => ; + case Some(id) => + features.ProxyInvites = List(id) + features.SearchForRole = position + } + } + } + + def SquadActionDefinitionCancelFind(lSquadOpt: Option[SquadFeatures]): Unit = { + lSquadOpt match { + case Some(features) => + features.SearchForRole match { + case Some(position) if position > -1 => + val squad = features.Squad + val sguid = squad.GUID + features.SearchForRole = None + //remove active invites + invites + .filter { + case (_, LookingForSquadRoleInvite(_, _features, _)) => _features.Squad.GUID == sguid + case _ => false + } + .keys + .foreach { charId => + RemoveInvite(charId) + } + //remove queued invites + queuedInvites.foreach { + case (charId, queue) => + val filtered = queue.filterNot { + case LookingForSquadRoleInvite(_, _features, _) => _features.Squad.GUID == sguid + case _ => false + } + queuedInvites += charId -> filtered + if (filtered.isEmpty) { + queuedInvites.remove(charId) + } + } + //remove yet-to-be invitedPlayers + features.ProxyInvites = Nil + case _ => ; + } + case _ => ; + } + } + + /** the following action can be performed by an unaffiliated player */ + def SquadActionDefinitionSelectRoleForYourselfAsInvite( + tplayer: Player, + features: SquadFeatures, + position: Int + ): Unit = { + //not a member of any squad, but we might become a member of this one + val squad = features.Squad + if (squad.isAvailable(position, tplayer.avatar.certifications)) { + //we could join directly but we need permission from the squad leader first + if (features.AutoApproveInvitationRequests) { + SquadActionMembershipAcceptInvite( + tplayer, + squad.Leader.CharId, + Some(RequestRole(tplayer, features, position)), + None + ) + } else { + //circumvent tests in AddInviteAndRespond + val requestRole = RequestRole(tplayer, features, position) + val charId = squad.Leader.CharId + InviteResponseTemplate(indirectInviteResp)( + requestRole, + AddInvite(charId, requestRole), + charId, + invitingPlayer = 0L, //we ourselves technically are ... + tplayer.Name + ) + } + } + } + + /** the following action can be performed by anyone who has tried to join a squad */ + def SquadActionDefinitionCancelSelectRoleForYourself( + tplayer: Player, + features: SquadFeatures + ): Unit = { + val cancellingPlayer = tplayer.CharId + //assumption: a player who is cancelling will rarely end up with their invite queued + val squad = features.Squad + 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) + subs.Publish( + leaderCharId, + SquadResponse.Membership( + SquadResponseType.Cancel, + 0, + 0, + cancellingPlayer, + None, + entry.player.Name, + unk5=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] + subs.Publish( + leaderCharId, + SquadResponse.Membership( + SquadResponseType.Cancel, + 0, + 0, + cancellingPlayer, + None, + entry.player.Name, + unk5=false, + Some(None) + ) + ) + queuedInvites.remove(leaderCharId) + Some(true) + case (list, index) => + val entry = list(index).asInstanceOf[RequestRole] + subs.Publish( + leaderCharId, + SquadResponse.Membership( + SquadResponseType.Cancel, + 0, + 0, + cancellingPlayer, + None, + entry.player.Name, + unk5=false, + Some(None) + ) + ) + queuedInvites(leaderCharId) = list.take(index) ++ list.drop(index + 1) + Some(true) + } + ) + } + + def handleClosingSquad(features: SquadFeatures): Unit = { + CleanUpAllInvitesToSquad(features) + } + + def handleCleanup(charId: Long): Unit = { + CleanUpAllInvitesWithPlayer(charId) + } + + def handleLeave(charId: Long): Unit = { + refused.remove(charId) + CleanUpAllInvitesWithPlayer(charId) + } + + def resendActiveInvite(charId: Long): Unit = { + invites.get(charId) match { + case Some(invite) => + RespondToInvite(charId, invite) + case None => ; + } + } + + def ShiftInvitesToPromotedSquadLeader( + sponsoringPlayer: Long, + promotedPlayer: Long + ): Unit = { + val leaderInvite = invites.remove(sponsoringPlayer) + val leaderQueuedInvites = queuedInvites.remove(sponsoringPlayer).toList.flatten + val (invitesToConvert, invitesToAppend) = (invites.remove(promotedPlayer).orElse(previousInvites.get(promotedPlayer)), leaderInvite) match { + case (Some(activePromotedInvite), Some(outLeaderInvite)) => + //the promoted player has an active invite; queue these + val promotedQueuedInvites = queuedInvites.remove(promotedPlayer).toList.flatten + NextInvite(promotedPlayer) + NextInvite(sponsoringPlayer) + (activePromotedInvite +: (outLeaderInvite +: leaderQueuedInvites), promotedQueuedInvites) + + case (Some(activePromotedInvite), None) => + //the promoted player has an active invite; queue these + val promotedQueuedInvites = queuedInvites.remove(promotedPlayer).toList.flatten + NextInvite(promotedPlayer) + (activePromotedInvite :: leaderQueuedInvites, promotedQueuedInvites) + + case (None, Some(outLeaderInvite)) => + //no active invite for the promoted player, but the leader had an active invite; trade the queued invites + NextInvite(sponsoringPlayer) + (outLeaderInvite +: leaderQueuedInvites, queuedInvites.remove(promotedPlayer).toList.flatten) + + case (None, None) => + //no active invites for anyone; assign the first queued invite from the promoting player, if available, and queue the rest + (leaderQueuedInvites, queuedInvites.remove(promotedPlayer).toList.flatten) + } + moveOverPromotedInvites(promotedPlayer, invitesToConvert, invitesToAppend) + } + + def moveOverPromotedInvites( + targetPlayer: Long, + convertableInvites: List[Invitation], + otherInvitations: List[Invitation] + ): Unit = { + convertableInvites ++ otherInvitations match { + case Nil => ; + case x :: Nil => + AddInviteAndRespond(targetPlayer, x, x.InviterCharId, x.InviterName) + case x :: xs => + AddInviteAndRespond(targetPlayer, x, x.InviterCharId, x.InviterName) + queuedInvites += targetPlayer -> xs + } + } + + /** + * Enqueue a newly-submitted invitation object + * either as the active position or into the inactive positions + * and dispatch a response for any invitation object that is discovered. + * Implementation of a workflow. + * + * @see `AddInvite` + * @see `indirectInviteResp` + * @param invitedPlayer the unique character identifier for the player being invited; + * in actuality, represents the player who will address the invitation object + * @param targetInvite a comparison invitation object + * @param invitingPlayer the unique character identifier for the player who invited the former + * @param name a name to be used in message composition + */ + def AddInviteAndRespond( + invitedPlayer: Long, + targetInvite: Invitation, + invitingPlayer: Long, + name: String, + autoApprove: Boolean = false + ): Unit = { + val (player, approval) = targetInvite match { + case IndirectInvite(_player, _) => (_player, autoApprove) + case RequestRole(_player, _, _) => (_player, autoApprove) + case _ => (null, false) + } + if (approval) { + SquadActionMembershipAcceptInvite(player, invitingPlayer, Some(targetInvite), None) + } else { + InviteResponseTemplate(indirectInviteResp)( + targetInvite, + AddInvite(invitedPlayer, targetInvite), + invitedPlayer, + invitingPlayer, + name + ) + } + } + + /** + * Enqueue a newly-submitted invitation object + * either as the active position or into the inactive positions + * and dispatch a response for any invitation object that is discovered. + * Implementation of a workflow. + * + * @see `AddInvite` + * @see `altIndirectInviteResp` + * @param targetInvite a comparison invitation object + * @param invitedPlayer the unique character identifier for the player being invited + * @param invitingPlayer the unique character identifier for the player who invited the former + * @param name a name to be used in message composition + */ + def AltAddInviteAndRespond( + invitedPlayer: Long, + targetInvite: Invitation, + invitingPlayer: Long, + name: String, + autoApprove: Boolean = false + ): Unit = { + val (player, approval) = targetInvite match { + case IndirectInvite(_player, _) => (_player, autoApprove) + case RequestRole(_player, _, _) => (_player, autoApprove) + case _ => (null, false) + } + if (approval) { + SquadActionMembershipAcceptInvite(player, invitingPlayer, Some(targetInvite), None) + } else { + InviteResponseTemplate(altIndirectInviteResp)( + targetInvite, + AddInvite(invitedPlayer, targetInvite), + invitedPlayer, + invitingPlayer, + name + ) + } + } + + /** + * Component method used for the response behavior for processing the invitation object as an `IndirectInvite` object. + * + * @see `HandleRequestRole` + * @param invite the original invitation object that started this process + * @param player the target of the response and invitation + * @param invitedPlayer the unique character identifier for the player being invited; + * in actuality, represents the player who will address the invitation object; + * not useful here + * @param invitingPlayer the unique character identifier for the player who invited the former; + * not useful here + * @param name a name to be used in message composition; + * not useful here + * @return na + */ + def indirectInviteResp( + invite: IndirectInvite, + player: Player, + invitedPlayer: Long, + invitingPlayer: Long, + name: String + ): Boolean = { + HandleRequestRole(player, invite) + } + + /** + * Component method used for the response behavior for processing the invitation object as an `IndirectInvite` object. + * + * @see `HandleRequestRole` + * @param invite the original invitation object that started this process + * @param player the target of the response and invitation + * @param invitedPlayer the unique character identifier for the player being invited + * in actuality, represents the player who will address the invitation object + * @param invitingPlayer the unique character identifier for the player who invited the former + * @param name a name to be used in message composition + * @return na + */ + def altIndirectInviteResp( + invite: IndirectInvite, + player: Player, + invitedPlayer: Long, + invitingPlayer: Long, + name: String + ): Boolean = { + subs.Publish( + invitingPlayer, + SquadResponse.Membership( + SquadResponseType.Accept, + 0, + 0, + invitingPlayer, + Some(invitedPlayer), + player.Name, + unk5 = false, + Some(None) + ) + ) + HandleRequestRole(player, invite) + } + + /** + * A branched response for processing (new) invitation objects that have been submitted to the system.
+ *
+ * A comparison is performed between the original invitation object and an invitation object + * that represents the potential modification or redirection of the current active invitation obect. + * Any further action is only performed when an "is equal" comparison is `true`. + * When passing, the system publishes up to two messages + * to users that would anticipate being informed of squad join activity. + * + * @param indirectInviteFunc the method that cans the responding behavior should an `IndirectInvite` object being consumed + * @param targetInvite a comparison invitation object; + * represents the unmodified, unadjusted invite + * @param actualInvite a comparaison invitation object; + * proper use of this field should be the output of another process upon the following `actualInvite` + * @param invitedPlayer the unique character identifier for the player being invited + * in actuality, represents the player who will address the invitation object + * @param invitingPlayer the unique character identifier for the player who invited the former + * @param name a name to be used in message composition + */ + def InviteResponseTemplate(indirectInviteFunc: (IndirectInvite, Player, Long, Long, String) => Boolean)( + targetInvite: Invitation, + actualInvite: Option[Invitation], + invitedPlayer: Long, + invitingPlayer: Long, + name: String + ): Unit = { + if (actualInvite.contains(targetInvite)) { + //immediately respond + targetInvite match { + case VacancyInvite(charId, _name, _) => + subs.Publish( + invitedPlayer, + SquadResponse.Membership( + SquadResponseType.Invite, + 0, + 0, + charId, + Some(invitedPlayer), + _name, + unk5 = false, + Some(None) + ) + ) + subs.Publish( + charId, + SquadResponse.Membership( + SquadResponseType.Invite, + 0, + 0, + invitedPlayer, + Some(charId), + _name, + unk5 = true, + Some(None) + ) + ) + + case _bid@IndirectInvite(player, _) => + indirectInviteFunc(_bid, player, invitedPlayer, invitingPlayer, name) + + case _bid@SpontaneousInvite(player) => + val bidInvitingPlayer = _bid.InviterCharId + subs.Publish( + invitedPlayer, + SquadResponse.Membership( + SquadResponseType.Invite, + 0, + 0, + bidInvitingPlayer, + Some(invitedPlayer), + player.Name, + unk5 = false, + Some(None) + ) + ) + subs.Publish( + bidInvitingPlayer, + SquadResponse.Membership( + SquadResponseType.Invite, + 0, + 0, + invitedPlayer, + Some(bidInvitingPlayer), + player.Name, + unk5 = true, + Some(None) + ) + ) + + case _bid@RequestRole(player, _, _) => + HandleRequestRole(player, _bid) + + case LookingForSquadRoleInvite(member, _, _) => + subs.Publish( + invitedPlayer, + SquadResponse.Membership( + SquadResponseType.Invite, + 0, + 0, + invitedPlayer, + Some(member.CharId), + member.Name, + unk5 = false, + Some(None) + ) + ) + + case ProximityInvite(member, _, _) => + subs.Publish( + invitedPlayer, + SquadResponse.Membership( + SquadResponseType.Invite, + 0, + 0, + invitedPlayer, + Some(member.CharId), + member.Name, + unk5 = false, + Some(None) + ) + ) + + case _ => + log.warn(s"AddInviteAndRespond: can not parse discovered unhandled invitation type - $targetInvite") + } + } + } + + /** + * Assign a provided invitation object to either the active or inactive position for a player.
+ *
+ * The determination for the active position is whether or not something is currently in the active position + * or whether some mechanism tried to shift invitation object into the active position + * but found nothing to shift. + * If an invitation object originating from the reported player already exists, + * a new one is not appended to the inactive queue. + * This method should always be used as the entry point for the active and inactive invitation options + * or as a part of the entry point for the aforesaid options. + * + * @see `AddInviteAndRespond` + * @see `AltAddInviteAndRespond` + * @param invitedPlayer the unique character identifier for the player being invited; + * in actuality, represents the player who will address the invitation object + * @param invite the "new" invitation envelop object + * @return an optional invite; + * the invitation object in the active invite position; + * `None`, if it is not added to either the active option or inactive position + */ + def AddInvite(invitedPlayer: Long, invite: Invitation): Option[Invitation] = { + invites.get(invitedPlayer).orElse(previousInvites.get(invitedPlayer)) match { + case Some(_bid) => + //the new invite may not interact with the active invite; add to queued invites + queuedInvites.get(invitedPlayer) match { + case Some(bidList) => + //ensure that new invite does not interact with the queue's invites by invitingPlayer info + val inviteInviterCharId = invite.InviterCharId + if ( + _bid.InviterCharId != inviteInviterCharId && !bidList.exists { eachBid => + eachBid.InviterCharId == inviteInviterCharId + } + ) { + queuedInvites(invitedPlayer) = invite match { + case _: RequestRole => + //RequestRole is to be expedited + val (normals, others) = bidList.partition(_.isInstanceOf[RequestRole]) + (normals :+ invite) ++ others + case _ => + bidList :+ invite + } + Some(_bid) + } else { + None + } + case None => + if (_bid.InviterCharId != invite.InviterCharId) { + queuedInvites(invitedPlayer) = List(invite) + Some(_bid) + } else { + None + } + } + + case None => + invites(invitedPlayer) = invite + Some(invite) + } + } + + /** + * Select the next invitation object to be shifted into the active position.
+ *
+ * The determination for the active position is whether or not something is currently in the active position + * or whether some mechanism tried to shift invitation object into the active position + * but found nothing to shift. + * After handling of the previous invitation object has completed or finished, + * the temporary block on adding new invitations is removed + * and any queued inactive invitation on the head of the inactive queue is shifted into the active position. + * @see `NextInviteAndRespond` + * @param invitedPlayer the unique character identifier for the player being invited; + * in actuality, represents the player who will address the invitation object + * @return an optional invite; + * the invitation object in the active invite position; + * `None`, if not shifted into the active position + */ + def NextInvite(invitedPlayer: Long): Option[Invitation] = { + previousInvites.remove(invitedPlayer) + invites.get(invitedPlayer) match { + case None => + queuedInvites.get(invitedPlayer) match { + case Some(list) => + list match { + case Nil => + None + case x :: Nil => + invites(invitedPlayer) = x + queuedInvites.remove(invitedPlayer) + Some(x) + case x :: xs => + invites(invitedPlayer) = x + queuedInvites(invitedPlayer) = xs + Some(x) + } + + case None => + None + } + case Some(_) => + None + } + } + + /** + * Select the next invitation object to be shifted into the active position + * and dispatch a response for any invitation object that is discovered. + * @see `NextInvite` + * @see `RespondToInvite` + * @param invitedPlayer the unique character identifier for the player being invited; + * in actuality, represents the player who will address the invitation object + */ + def NextInviteAndRespond(invitedPlayer: Long): Unit = { + NextInvite(invitedPlayer) match { + case Some(invite) => + RespondToInvite(invitedPlayer, invite) + case None => ; + } + } + + /** + * Compose the response to an invitation. + * Use standard handling methods for `IndirectInvite` invitation envelops. + * @see `InviteResponseTemplate` + * @param invitedPlayer the unique character identifier for the player being invited; + * in actuality, represents the player who will address the invitation object + * @param invite the invitation envelope used to recover information about the action being taken + */ + def RespondToInvite(invitedPlayer: Long, invite: Invitation): Unit = { + InviteResponseTemplate(indirectInviteResp)( + invite, + Some(invite), + invitedPlayer, + invite.InviterCharId, + invite.InviterName + ) + } + + /** + * Remove any invitation object from the active position. + * Flag the temporary field to indicate that the active position, while technically available, + * should not yet have a new invitation object shifted into it yet. + * This is the "proper" way to demote invitation objects from the active position + * whether or not they are to be handled + * except in cases of manipulative cleanup. + * @see `NextInvite` + * @see `NextInviteAndRespond` + * @param invitedPlayer the unique character identifier for the player being invited; + * in actuality, represents the player who will address the invitation object + * @return an optional invite; + * the invitation object formerly in the active invite position; + * `None`, if no invitation was in the active position + */ + def RemoveInvite(invitedPlayer: Long): Option[Invitation] = { + invites.remove(invitedPlayer) match { + case out @ Some(invite) => + previousInvites += invitedPlayer -> invite + out + case None => + None + } + } + + /** + * Resolve an invitation to a general, not guaranteed, position in someone else's squad.
+ *
+ * Originally, the instigating type of invitation object was a "`VacancyInvite`" + * which indicated a type of undirected invitation extended from the squad leader to another player + * but the resolution is generalized enough to suffice for a number of invitation objects. + * First, an actual position is determined; + * then, the squad is tested for recruitment conditions, + * including whether the person who solicited the would-be member is still the squad leader. + * If the recruitment is manual and the squad leader is not the same as the recruiting player, + * then the real squad leader is sent an indirect query regarding the player's eligibility. + * These `IndirectInvite` invitation objects also are handled by calls to `HandleVacancyInvite`. + * @see `AltAddInviteAndRespond` + * @see `IndirectInvite` + * @see `SquadFeatures::AutoApproveInvitationRequests` + * @see `VacancyInvite` + * @param features the squad + * @param invitedPlayer the unique character identifier for the player being invited + * @param invitingPlayer the unique character identifier for the player who invited the former + * @param recruit the player being invited + * @return the squad object and a role position index, if properly invited; + * `None`, otherwise + */ + def HandleVacancyInvite( + features: SquadFeatures, + invitedPlayer: Long, + invitingPlayer: Long, + recruit: Player + ): Option[(SquadFeatures, Int)] = { + //accepted an invitation to join an existing squad + val squad = features.Squad + squad.Membership.zipWithIndex.find({ + case (_, index) => + squad.isAvailable(index, recruit.avatar.certifications) + }) match { + case Some((_, line)) => + //position in squad found + if (!features.AutoApproveInvitationRequests && squad.Leader.CharId != invitingPlayer) { + //the inviting player was not the squad leader and this decision should be bounced off the squad leader + AltAddInviteAndRespond( + squad.Leader.CharId, + IndirectInvite(recruit, features), + invitingPlayer, + name = "" + ) + log.debug(s"HandleVacancyInvite: ${recruit.Name} must await an invitation from the leader of squad ${squad.Task}") + None + } else { + Some((features, line)) + } + case _ => + None + } + } + + /** + * An overloaded entry point to the functionality for handling one player requesting a specific squad role. + * + * @param player the player who wants to join the squad + * @param bid a specific kind of `Invitation` object + * @return `true`, if the player is not denied the possibility of joining the squad; + * `false`, otherwise, of it the squad does not exist + */ + def HandleRequestRole(player: Player, bid: RequestRole): Boolean = { + HandleRequestRole(player, bid.features, bid) + } + + /** + * An overloaded entry point to the functionality for handling indirection when messaging the squad leader about an invite. + * + * @param player the player who wants to join the squad + * @param bid a specific kind of `Invitation` object + * @return `true`, if the player is not denied the possibility of joining the squad; + * `false`, otherwise, of it the squad does not exist + */ + def HandleRequestRole(player: Player, bid: IndirectInvite): Boolean = { + HandleRequestRole(player, bid.features, bid) + } + + /** + * The functionality for handling indirection + * for handling one player requesting a specific squad role + * or when messaging the squad leader about an invite.
+ *
+ * At this point in the squad join process, the only consent required is that of the squad leader. + * An automatic consent flag exists on the squad; + * but, if that is not set, then the squad leader must be asked whether or not to accept or to reject the recruit. + * If the squad leader changes in the middle or the latter half of the process, + * the invitation may still fail even if the old squad leader accepts. + * If the squad leader changes in the middle of the latter half of the process, + * the inquiry might be posed again of the new squad leader, of whether to accept or to reject the recruit. + * + * @param player the player who wants to join the squad + * @param features the squad + * @param bid the `Invitation` object that was the target of this request + * @return `true`, if the player is not denied the possibility of joining the squad; + * `false`, otherwise, of it the squad does not exist + */ + def HandleRequestRole(player: Player, features: SquadFeatures, bid: Invitation): Boolean = { + val leaderCharId = features.Squad.Leader.CharId + subs.Publish(leaderCharId, SquadResponse.WantsSquadPosition(leaderCharId, player.Name)) + true + } + + /** + * Determine whether a player is sufficiently unemployed + * and has no grand delusions of being a squad leader. + * @see `CloseSquad` + * @param features an optional squad + * @return `true`, if the target player possesses no squad or the squad is nonexistent; + * `false`, otherwise + */ + def EnsureEmptySquad(features: Option[SquadFeatures]): Boolean = { + features match { + case Some(squad) => EnsureEmptySquad(squad) + case None => true + } + } + + /** + * Determine whether a player is sufficiently unemployed + * and has no grand delusions of being a squad leader. + * @see `CloseSquad` + * @param features the squad + * @return `true`, if the target player possesses no squad or a squad that is suitably nonexistent; + * `false`, otherwise + */ + def EnsureEmptySquad(features: SquadFeatures): Boolean = { + val ensuredEmpty = features.Squad.Size <= 1 + if (ensuredEmpty) { + CleanUpAllInvitesToSquad(features) + } + ensuredEmpty + } + + /** + * Behaviors and exchanges necessary to complete the fulfilled recruitment process for the squad role.
+ *
+ * This operation is fairly safe to call whenever a player is to be inducted into a squad. + * The aforementioned player must have a callback retained in `subs.UserEvents` + * and conditions imposed by both the role and the player must be satisfied. + * @see `CleanUpAllInvitesWithPlayer` + * @see `Squad.isAvailable` + * @see `Squad.Switchboard` + * @see `SquadSubscriptionEntity.MonitorSquadDetails` + * @see `SquadSubscriptionEntity.Publish` + * @see `SquadSubscriptionEntity.Join` + * @see `SquadSubscriptionEntity.UserEvents` + * @param player the new squad member; + * this player is NOT the squad leader + * @param features the squad the player is joining + * @param position the squad member role that the player will be filling + * @return `true`, if the player joined the squad in some capacity; + * `false`, if the player did not join the squad or is already a squad member + */ + def JoinSquad(player: Player, features: SquadFeatures, position: Int): Boolean = { + CleanUpAllInvitesWithPlayer(player.CharId) + parent ! SquadService.PerformJoinSquad(player, features, position) + true + } + + /** + * This player has been refused to join squads by these players, or to form squads with these players. + * @param charId the player who refused other players + * @return the list of other players who have refused this player + */ + def Refused(charId: Long): List[Long] = refused.getOrElse(charId, Nil) + + /** + * This player has been refused to join squads by this squad leaders, or to form squads with this other player. + * @param charId the player who is being refused + * @param refusedCharId the player who refused + * @return the list of other players who have refused this player + */ + def Refused(charId: Long, refusedCharId: Long): List[Long] = { + if (charId != refusedCharId) { + Refused(charId, List(refusedCharId)) + } else { + Nil + } + } + + /** + * This player has been refused to join squads by these squad leaders, or to form squads with these other players. + * @param charId the player who is being refused + * @param list the players who refused + * @return the list of other players who have refused this player + */ + def Refused(charId: Long, list: List[Long]): List[Long] = { + refused.get(charId) match { + case Some(refusedList) => + refused(charId) = list ++ refusedList + Refused(charId) + case None => + Nil + } + } + + /** + * This player has been refused to join squads by this squad leaders, or to form squads with this other player. + * They are now allowed. + * @param charId the player who is being refused + * @param permittedCharId the player who was previously refused + * @return the list of other players who have refused this player + */ + def Allowed(charId: Long, permittedCharId: Long): List[Long] = { + if (charId != permittedCharId) { + Allowed(charId, List(permittedCharId)) + } else { + Nil + } + } + + /** + * This player has been refused to join squads by these squad leaders, or to form squads with these other players. + * They are now allowed. + * @param charId the player who is being refused + * @param list the players who was previously refused + * @return the list of other players who have refused this player + */ + def Allowed(charId: Long, list: List[Long]): List[Long] = { + refused.get(charId) match { + case Some(refusedList) => + refused(charId) = refusedList.filterNot(list.contains) + Refused(charId) + case None => + Nil + } + } + + /** + * Remove all inactive invites associated with this player. + * @param charId the unique character identifier for the player being invited; + * in actuality, represents the player who will address the invitation object + * @return a list of the removed inactive invitation objects + */ + def CleanUpQueuedInvites(charId: Long): Unit = { + val allSquadGuids = queuedInvites.remove(charId) match { + case Some(bidList) => + bidList.collect { + case VacancyInvite(_, _, guid) => guid + case IndirectInvite(_, guid) => guid + case LookingForSquadRoleInvite(_, guid, _) => guid + case ProximityInvite(_, guid, _) => guid + case RequestRole(_, guid, _) => guid + } + case None => + Nil + } + val list = List(charId) + allSquadGuids.foreach { CleanUpSquadFeatures(list, _, position = -1) } + } + + def CleanUpSquadFeatures(removed: List[Long], features: SquadFeatures, position: Int): Unit = { + features.ProxyInvites = features.ProxyInvites.filterNot(removed.contains) + if (features.ProxyInvites.isEmpty) { + features.SearchForRole = None + } + } + + /** + * Remove all invitation objects + * that are related to the particular squad and the particular role in the squad. + * Specifically used to safely disarm obsolete invitation objects related to the specific criteria. + * Affects only certain invitation object types + * including "player requesting role" and "leader requesting recruiting role". + * @see `RemoveActiveInvitesForSquadAndPosition` + * @see `RemoveQueuedInvitesForSquadAndPosition` + * @param features the squad + * @param position the role position index + */ + def CleanUpInvitesForSquadAndPosition(features: SquadFeatures, position: Int): Unit = { + val guid = features.Squad.GUID + CleanUpSquadFeatures( + RemoveActiveInvitesForSquadAndPosition(guid, position) ++ RemoveQueuedInvitesForSquadAndPosition(guid, position), + features, + position + ) + } + + /** + * Remove all inactive invitation objects that are related to the particular squad and the particular role in the squad. + * Specifically used to safely disarm obsolete invitation objects by specific criteria. + * Affects only certain invitation object types. + * @see `RequestRole` + * @see `LookingForSquadRoleInvite` + * @see `CleanUpInvitesForSquadAndPosition` + * @param features the squa + * @param position the role position index + */ + def CleanUpQueuedInvitesForSquadAndPosition(features: SquadFeatures, position: Int): Unit = { + CleanUpSquadFeatures( + RemoveQueuedInvitesForSquadAndPosition(features.Squad.GUID, position), + features, + position + ) + } + + /** + * Remove all invitation objects that are related to the particular squad. + * Specifically used to safely disarm obsolete invitation objects by specific criteria. + * Affects all invitation object types and all data structures that deal with the squad. + * @see `RequestRole` + * @see `IndirectInvite` + * @see `LookingForSquadRoleInvite` + * @see `ProximityInvite` + * @see `RemoveInvite` + * @see `VacancyInvite` + * @param features the squad identifier + */ + def CleanUpAllInvitesToSquad(features: SquadFeatures): Unit = { + val guid = features.Squad.GUID + //clean up invites + val activeInviteIds = { + val keys = invites.keys.toSeq + invites.values.zipWithIndex + .collect { + case (VacancyInvite(_, _, _squad), index) if _squad.Squad.GUID == guid => index + case (IndirectInvite(_, _squad), index) if _squad.Squad.GUID == guid => index + case (LookingForSquadRoleInvite(_, _squad, _), index) if _squad.Squad.GUID == guid => index + case (ProximityInvite(_, _squad, _), index) if _squad.Squad.GUID == guid => index + case (RequestRole(_, _squad, _), index) if _squad.Squad.GUID == guid => index + } + .map { index => + val key = keys(index) + RemoveInvite(key) + key + } + .toList + } + //tidy the queued invitations + val queuedInviteIds = { + val keys = queuedInvites.keys.toSeq + queuedInvites.values.zipWithIndex + .collect { + case (queue, index) => + val key = keys(index) + val (targets, retained) = queue.partition { + case VacancyInvite(_, _, _squad) => _squad.Squad.GUID == guid + case IndirectInvite(_, _squad) => _squad.Squad.GUID == guid + case LookingForSquadRoleInvite(_, _squad, _) => _squad.Squad.GUID == guid + case ProximityInvite(_, _squad, _) => _squad.Squad.GUID == guid + case RequestRole(_, _squad, _) => _squad.Squad.GUID == guid + case _ => false + } + if (retained.isEmpty) { + queuedInvites.remove(key) + } else { + queuedInvites += key -> retained + } + if (targets.nonEmpty) { + Some(key) + } else { + None + } + } + .flatten + .toList + } + CleanUpSquadFeatures(activeInviteIds ++ queuedInviteIds, features, position = -1) + } + + /** + * Remove all active and inactive invitation objects that are related to the particular player. + * Specifically used to safely disarm obsolete invitation objects by specific criteria. + * Affects all invitation object types and all data structures that deal with the player. + * @see `RequestRole` + * @see `IndirectInvite` + * @see `LookingForSquadRoleInvite` + * @see `RemoveInvite` + * @see `CleanUpAllProximityInvites` + * @see `VacancyInvite` + * @param charId the player's unique identifier number + */ + def CleanUpAllInvitesWithPlayer(charId: Long): Unit = { + //clean up our active invitation + val charIdInviteSquadGuid = RemoveInvite(charId) match { + case Some(VacancyInvite(_, _, guid)) => Some(guid) + case Some(IndirectInvite(_, guid)) => Some(guid) + case Some(LookingForSquadRoleInvite(_, guid, _)) => Some(guid) + case Some(ProximityInvite(_, guid, _)) => Some(guid) + case Some(RequestRole(_, guid, _)) => Some(guid) + case _ => None + } + //clean up invites + val (activeInviteIds, activeSquadGuids) = { + val keys = invites.keys.toSeq + invites.values.zipWithIndex + .collect { + case (SpontaneousInvite(player), index) if player.CharId == charId => (index, None) + case (VacancyInvite(player, _, guid), index) if player == charId => (index, Some(guid)) + case (IndirectInvite(player, guid), index) if player.CharId == charId => (index, Some(guid)) + case (LookingForSquadRoleInvite(member, guid, _), index) if member.CharId == charId => (index, Some(guid)) + case (ProximityInvite(member, guid, _), index) if member.CharId == charId => (index, Some(guid)) + case (RequestRole(player, guid, _), index) if player.CharId == charId => (index, Some(guid)) + } + .map { case (index, guid) => + val key = keys(index) + RemoveInvite(key) + (key, guid) + } + .unzip + } + //tidy the queued invitations + val (queuedInviteIds, queuedSquadGuids) = { + val keys = queuedInvites.keys.toSeq + queuedInvites.values.zipWithIndex + .collect { + case (queue, index) => + val key = keys(index) + val (targets, retained) = if(key != charId) { + queue.partition { + case SpontaneousInvite(player) => player.CharId == charId + case VacancyInvite(player, _, _) => player == charId + case IndirectInvite(player, _) => player.CharId == charId + case LookingForSquadRoleInvite(member, _, _) => member.CharId == charId + case ProximityInvite(member, _, _) => member.CharId == charId + case RequestRole(player, _, _) => player.CharId == charId + case _ => false + } + } else { + (queue, Nil) + } + if (retained.isEmpty) { + queuedInvites.remove(key) + } else { + queuedInvites += key -> retained + } + if (targets.nonEmpty) { + Some(( + key, + targets.collect { + case VacancyInvite(_, _, guid) => guid + case IndirectInvite(_, guid) => guid + case LookingForSquadRoleInvite(_, guid, _) => guid + case ProximityInvite(_, guid, _) => guid + case RequestRole(_, guid, _) => guid + } + )) + } else { + None + } + } + .flatten + .toList + .unzip + } + val allInvites = (activeInviteIds ++ queuedInviteIds).toList.distinct + ((activeSquadGuids.toSeq :+ charIdInviteSquadGuid) ++ queuedSquadGuids) + .flatten + .distinct + .foreach { guid => CleanUpSquadFeatures(allInvites, guid, position = -1) } + } + + /** + * Remove all active and inactive proximity squad invites. + * This is related to recruitment from the perspective of the recruiter. + * @param charId the player + */ + def CleanUpAllProximityInvites(charId: Long): Unit = { + //clean up invites + val (activeInviteIds, activeSquadGuids) = { + val keys = invites.keys.toSeq + invites.values.zipWithIndex + .collect { case (ProximityInvite(member, guid, _), index) if member.CharId == charId => (index, Some(guid)) } + .map { case (index, guid) => + val key = keys(index) + RemoveInvite(key) + (key, guid) + } + .unzip + } + //tidy the queued invitations + val (queuedInviteIds, queuedSquadGuids) = { + val keys = queuedInvites.keys.toSeq + queuedInvites.values.zipWithIndex + .collect { + case (queue, index) => + val key = keys(index) + val (targets, retained) = queue.partition { + case ProximityInvite(member, _, _) => member.CharId == charId + case _ => false + } + if (retained.isEmpty) { + queuedInvites.remove(key) + } else { + queuedInvites += key -> retained + } + if (targets.nonEmpty) { + Some((key, targets.collect { case ProximityInvite(_, guid, _) => guid } )) + } else { + None + } + } + .flatten + .toList + .unzip + } + val allInvites = (activeInviteIds ++ queuedInviteIds).toList.distinct + (activeSquadGuids.toSeq ++ queuedSquadGuids) + .flatten + .distinct + .foreach { guid => CleanUpSquadFeatures(allInvites, guid, position = -1) } + } + + /** + * Remove all active and inactive proximity squad invites for a specific squad. + * @param features the squad + */ + def CleanUpProximityInvites(features: SquadFeatures): Unit = { + val squadGuid = features.Squad.GUID + //clean up invites + val activeInviteIds = { + val keys = invites.keys.toSeq + invites.values.zipWithIndex + .collect { + case (ProximityInvite(_, _squad, _), index) if _squad.Squad.GUID == squadGuid => index + } + .map { index => + val key = keys(index) + RemoveInvite(key) + key + } + } + //tidy the queued invitations + val queuedInviteIds = { + val keys = queuedInvites.keys.toSeq + queuedInvites.values.zipWithIndex + .collect { + case (queue, index) => + val key = keys(index) + val (targets, retained) = queue.partition { + case ProximityInvite(_, _squad, _) => _squad.Squad.GUID == squadGuid + case _ => false + } + if (retained.isEmpty) { + queuedInvites.remove(key) + } else { + queuedInvites += key -> retained + } + if (targets.nonEmpty) { + keys.lift(index) + } else { + None + } + } + .flatten + .toList + } + CleanUpSquadFeatures((activeInviteIds ++ queuedInviteIds).toList.distinct, features, position = -1) + } + + /** + * Remove all active invitation objects + * that are related to the particular squad and the particular role in the squad. + * Specifically used to safely disarm obsolete invitation objects related to the specific criteria. + * Affects only certain invitation object types + * including "player requesting role" and "leader requesting recruiting role". + * @see `RequestRole` + * @see `LookingForSquadRoleInvite` + * @see `ProximityInvite` + * @see `RemoveInvite` + * @param guid the squad identifier + * @param position the role position index + * @return the character ids of all players whose invites were removed + */ + def RemoveActiveInvitesForSquadAndPosition(guid: PlanetSideGUID, position: Int): List[Long] = { + val keys = invites.keys.toSeq + invites.values.zipWithIndex + .collect { + case (LookingForSquadRoleInvite(_, _squad, pos), index) if _squad.Squad.GUID == guid && pos == position => index + case (ProximityInvite(_, _squad, pos), index) if _squad.Squad.GUID == guid && pos == position => index + case (RequestRole(_, _squad, pos), index) if _squad.Squad.GUID == guid && pos == position => index + } + .map { index => + val key = keys(index) + RemoveInvite(key) + key + } + .toList + } + + /** + * Remove all inactive invitation objects that are related to the particular squad and the particular role in the squad. + * Specifically used to safely disarm obsolete invitation objects by specific criteria. + * Affects only certain invitation object types. + * @see `RequestRole` + * @see `LookingForSquadRoleInvite` + * @see `CleanUpInvitesForSquadAndPosition` + * @param guid the squad identifier + * @param position the role position index + * @return the character ids of all players whose invites were removed + */ + def RemoveQueuedInvitesForSquadAndPosition(guid: PlanetSideGUID, position: Int): List[Long] = { + val keys = queuedInvites.keys.toSeq + queuedInvites.values.zipWithIndex + .collect { + case (queue, index) => + val key = keys(index) + val (targets, retained) = queue.partition { + case LookingForSquadRoleInvite(_, _squad, pos) => _squad.Squad.GUID == guid && pos == position + case ProximityInvite(_, _squad, pos) => _squad.Squad.GUID == guid && pos == position + case RequestRole(_, _squad, pos) => _squad.Squad.GUID == guid && pos == position + case _ => false + } + if (retained.isEmpty) { + queuedInvites.remove(key) + } else { + queuedInvites += key -> retained + } + if (targets.nonEmpty) { + Some(key) + } else { + None + } + } + .flatten + .toList + } + + def FindSoldiersWithinScopeAndInvite( + invitingPlayer: Member, + features: SquadFeatures, + position: Int, + scope: List[Avatar], + excluded: List[Long], + invitationEnvelopFunc: (Member, SquadFeatures, Int) => Invitation + ): Option[Long] = { + val invitingPlayerCharId = invitingPlayer.CharId + val invitingPlayerName = invitingPlayer.Name + val squad = features.Squad + val faction = squad.Faction + val squadLeader = squad.Leader.CharId + val deniedAndExcluded = features.DeniedPlayers() ++ excluded + val requirementsToMeet = squad.Membership(position).Requirements + //find a player who is of the same faction as the squad, is LFS, and is eligible for the squad position + scope + .find { avatar => + val charId = avatar.id + faction == avatar.faction && + avatar.lookingForSquad && + !deniedAndExcluded.contains(charId) && + !refused(charId).contains(squadLeader) && + requirementsToMeet.intersect(avatar.certifications) == requirementsToMeet + } match { + case None => + None + case Some(invitedPlayer) => + //add invitation for position in squad + val invite = invitationEnvelopFunc(invitingPlayer, features, position) + val id = invitedPlayer.id + AddInviteAndRespond(id, invite, invitingPlayerCharId, invitingPlayerName) + Some(id) + } + } +} + +object SquadInvitationManager { + final case class Join(charId: Long) + + /** + * The base of all objects that exist for the purpose of communicating invitation from one player to the next. + * @param char_id the inviting player's unique identifier number + * @param name the inviting player's name + */ + sealed abstract class Invitation(char_id: Long, name: String) { + def InviterCharId: Long = char_id + def InviterName: String = name + } + + /** + * Utilized when one player attempts to join an existing squad in a specific role. + * Accessed by the joining player from the squad detail window. + * This invitation is handled by the squad leader. + * @param player the player who requested the role + * @param features the squad with the role + * @param position the index of the role + */ + private case class RequestRole(player: Player, features: SquadFeatures, position: Int) + extends Invitation(player.CharId, player.Name) + + /** + * Utilized when one squad member issues an invite for some other player. + * Accessed by an existing squad member using the "Invite" menu option on another player. + * This invitation is handled by the player who would join the squad. + * @param char_id the unique character identifier of the player who sent the invite + * @param name the name the player who sent the invite + * @param features the squad + */ + private case class VacancyInvite(char_id: Long, name: String, features: SquadFeatures) + extends Invitation(char_id, name) + + /** + * Utilized to redirect an (accepted) invitation request to the proper squad leader. + * No direct action causes this message. + * Depending on the situation, either the squad leader or the player who would join the squad handle this invitation. + * @param player the player who would be joining the squad; + * may or may not have actually requested it in the first place + * @param features the squad + */ + private case class IndirectInvite(player: Player, features: SquadFeatures) + extends Invitation(player.CharId, player.Name) + + /** + * Utilized in conjunction with an external queuing data structure + * to search for and submit requests to other players + * for the purposes of fill out unoccupied squad roles. + * This invitation is handled by the player who would be joining the squad. + * @param leader the squad leader + * @param features the squad + * @param position the index of a role + */ + private case class ProximityInvite(leader: Member, features: SquadFeatures, position: Int) + extends Invitation(leader.CharId, leader.Name) + + /** + * Utilized in conjunction with an external queuing data structure + * to search for and submit requests to other players + * for the purposes of fill out an unoccupied squad role. + * This invitation is handled by the player who would be joining the squad. + * @param leader the squad leader + * @param features the squad with the role + * @param position the index of the role + */ + private case class LookingForSquadRoleInvite(leader: Member, features: SquadFeatures, position: Int) + extends Invitation(leader.CharId, leader.Name) + + /** + * Utilized when one player issues an invite for some other player for a squad that does not yet exist. + * This invitation is handled by the player who would be joining the squad. + * @param player the player who wishes to become the leader of a squad + */ + private case class SpontaneousInvite(player: Player) extends Invitation(player.CharId, player.Name) + + /** + * na + * @param invitingPlayer na + * @param features na + * @param position na + * @return na + */ + private def ProximityEnvelope( + invitingPlayer: Member, + features: SquadFeatures, + position: Int + ): Invitation = { + ProximityInvite(invitingPlayer, features, position) + } + + /** + * na + * @param invitingPlayer na + * @param features na + * @param position na + * @return na + */ + private def LookingForSquadRoleEnvelope( + invitingPlayer: Member, + features: SquadFeatures, + position: Int + ): Invitation = { + LookingForSquadRoleInvite(invitingPlayer, features, position) + } + + final case class FinishStartSquad(features: SquadFeatures) +} diff --git a/src/main/scala/net/psforever/services/teamwork/SquadService.scala b/src/main/scala/net/psforever/services/teamwork/SquadService.scala index 024452ed..b8810dee 100644 --- a/src/main/scala/net/psforever/services/teamwork/SquadService.scala +++ b/src/main/scala/net/psforever/services/teamwork/SquadService.scala @@ -1,21 +1,20 @@ -// Copyright (c) 2019 PSForever +// Copyright (c) 2019-2022 PSForever package net.psforever.services.teamwork -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.{Default, LivePlayerList, Player} -import net.psforever.packet.game.{PlanetSideZoneID, SquadDetail, SquadInfo, SquadPositionDetail, SquadPositionEntry, WaypointEventAction, WaypointInfo, SquadAction => SquadRequestAction} -import net.psforever.services.{GenericEventBus, Service} -import net.psforever.types._ -import akka.actor.{Actor, ActorRef, Cancellable, Terminated} +import akka.actor.{Actor, ActorRef, Terminated} import java.io.{PrintWriter, StringWriter} - -import scala.concurrent.duration._ import scala.collection.concurrent.TrieMap import scala.collection.mutable +// +import net.psforever.objects.{Default, LivePlayerList, Player} +import net.psforever.objects.teamwork.{Member, Squad, SquadFeatures} +import net.psforever.objects.avatar.{Avatar, Certification} +import net.psforever.objects.definition.converter.StatConverter +import net.psforever.objects.zones.Zone +import net.psforever.packet.game.SquadAction._ +import net.psforever.packet.game.{PlanetSideZoneID, SquadDetail, SquadInfo, SquadPositionDetail, SquadPositionEntry, SquadAction => SquadRequestAction} +import net.psforever.services.Service +import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, SquadRequestType, SquadResponseType} class SquadService extends Actor { import SquadService._ @@ -23,7 +22,7 @@ class SquadService extends Actor { /** * The current unique squad identifier, to be wrapped in a `PlanetSideGUID` object later. * The count always starts at 1, even when reset. - * A squad of `PlanetSideGUID(0)` indicates both a nonexistent squad and the default no-squad for clients. + * A squad of `PlanetSideGUID(0)` indicates both a nonexistent squad and the service itself to clients. */ private var sid: Int = 1 @@ -31,21 +30,12 @@ class SquadService extends Actor { * All squads.
* key - squad unique number; value - the squad wrapped around its attributes object */ - private var squadFeatures: TrieMap[PlanetSideGUID, SquadFeatures] = new TrieMap[PlanetSideGUID, SquadFeatures]() - - /** - * key - unique char id; value - the POSIX time after which it is cleared - */ - private var lazeIndices: Seq[LazeWaypointData] = Seq.empty - /** - * The periodic clearing of laze pointer waypoints. - */ - private var lazeIndexBlanking: Cancellable = Default.Cancellable + private val squadFeatures: TrieMap[PlanetSideGUID, SquadFeatures] = new TrieMap[PlanetSideGUID, SquadFeatures]() /** * The list of squads that each of the factions see for the purposes of keeping track of changes to the list. * These squads are considered public "listed" squads - - * all the players of a certain faction can see them in the squad list + * all the players of a certain faction can see those squads in the squad list * and may have limited interaction with their squad definition windows.
* key - squad unique number; value - the squad's unique identifier number */ @@ -59,83 +49,30 @@ class SquadService extends Actor { /** * key - a unique character identifier number; value - the squad to which this player is a member */ - private var memberToSquad: mutable.LongMap[Squad] = mutable.LongMap[Squad]() + private val memberToSquad: mutable.LongMap[PlanetSideGUID] = mutable.LongMap[PlanetSideGUID]() /** - * key - a unique character identifier number; value - the active invitation object + * Information relating to player searches to reconstruct the results. + * The field of criteria involved includes details like the name and the certification requirements of the role. + * key - a list of unique character identifier numbers; value - the information to compare squad member positions against */ - private val invites: mutable.LongMap[Invitation] = mutable.LongMap[Invitation]() + private val searchData: mutable.LongMap[SquadService.SearchCriteria] = + mutable.LongMap[SquadService.SearchCriteria]() /** - * key - a unique character identifier number; value - a list of inactive invitation objects waiting to be resolved + * A separate register to keep track of players to their client reference. */ - private val queuedInvites: mutable.LongMap[List[Invitation]] = mutable.LongMap[List[Invitation]]() + private implicit val subs: SquadSubscriptionEntity = new SquadSubscriptionEntity() - /** - * The given player has refused participation into this other player's squad.
- * key - a unique character identifier number; value - a list of unique character identifier numbers - */ - private val refused: mutable.LongMap[List[Long]] = mutable.LongMap[List[Long]]() + private val invitations = new SquadInvitationManager(subs, self) - /** - * Players who are interested in updated details regarding a certain squad though they may not be a member of the squad.
- * key - unique character identifier number; value - a squad identifier number - */ - private val continueToMonitorDetails: mutable.LongMap[PlanetSideGUID] = mutable.LongMap[PlanetSideGUID]() - - /** - * A placeholder for an absent active invite that has not (yet) been accepted or rejected, - * equal to the then-current active invite. - * Created when removing an active invite. - * Checked when trying to add a new invite (if found, the new invite is queued). - * Cleared when the next queued invite becomes active.
- * key - unique character identifier number; value, unique character identifier number - */ - private val previousInvites: mutable.LongMap[Invitation] = mutable.LongMap[Invitation]() - - /** - * This is a formal `ActorEventBus` object that is reserved for faction-wide messages and squad-specific messages. - * When the user joins the `SquadService` with a `Service.Join` message - * that includes a confirmed faction affiliation identifier, - * the origin `ActorRef` is added as a subscription. - * Squad channels are produced when a squad is created, - * and are subscribed to as users join the squad, - * and unsubscribed from as users leave the squad.
- * key - a `PlanetSideEmpire` value; value - `ActorRef` reference
- * key - a consistent squad channel name; value - `ActorRef` reference - * @see `CloseSquad` - * @see `JoinSquad` - * @see `LeaveSquad` - * @see `Service.Join` - * @see `Service.Leave` - */ - private val SquadEvents = new GenericEventBus[SquadServiceResponse] - - /** - * This collection contains the message-sending contact reference for individuals. - * When the user joins the `SquadService` with a `Service.Join` message - * that includes their unique character identifier, - * the origin `ActorRef` is added as a subscription. - * It is maintained until they disconnect entirely. - * The subscription is anticipated to belong to an instance of `SessionActor`.
- * key - unique character identifier number; value - `ActorRef` reference for that character - * @see `Service.Join` - */ - private val UserEvents: mutable.LongMap[ActorRef] = mutable.LongMap[ActorRef]() - - private[this] val log = org.log4s.getLogger + private val log = org.log4s.getLogger private def info(msg: String): Unit = log.info(msg) private def debug(msg: String): Unit = log.debug(msg) override def postStop(): Unit = { - //invitations - invites.clear() - queuedInvites.clear() - previousInvites.clear() - refused.clear() - continueToMonitorDetails.clear() //squads and members (users) squadFeatures.foreach { case (_, features) => @@ -143,13 +80,10 @@ class SquadService extends Actor { } memberToSquad.clear() publishedLists.clear() - UserEvents.foreach { - case (_, actor) => - SquadEvents.unsubscribe(actor) - } - UserEvents.clear() //misc - lazeIndices = Nil + searchData.clear() + subs.postStop() + invitations.postStop() } /** @@ -161,8 +95,8 @@ class SquadService extends Actor { */ def GetNextSquadId(): PlanetSideGUID = { val out = sid - val j = sid + 1 - if (j == 65536) { + val j = sid + 2 + if (j == 65535) { sid = 1 } else { sid = j @@ -171,11 +105,11 @@ class SquadService extends Actor { } /** - * Set the unique squad identifier back to the start (1) if no squads are active and no players are logged on. + * Set the unique squad identifier back to the start if no squads are active. * @return `true`, if the identifier is reset; `false`, otherwise */ def TryResetSquadId(): Boolean = { - if (UserEvents.isEmpty && squadFeatures.isEmpty) { + if (squadFeatures.isEmpty) { sid = 1 true } else { @@ -188,28 +122,26 @@ class SquadService extends Actor { * @param id the squad unique identifier number * @return the discovered squad, or `None` */ - def GetSquad(id: PlanetSideGUID): Option[Squad] = - squadFeatures.get(id) match { - case Some(features) => Some(features.Squad) - case None => None - } + def GetSquad(id: PlanetSideGUID): Option[SquadFeatures] = { + squadFeatures.get(id) + } /** * If this player is a member of any squad, discover that squad. * @param player the potential member * @return the discovered squad, or `None` */ - def GetParticipatingSquad(player: Player): Option[Squad] = GetParticipatingSquad(player.CharId) + def GetParticipatingSquad(player: Player): Option[SquadFeatures] = GetParticipatingSquad(player.CharId) /** * If the player associated with this unique character identifier number is a member of any squad, discover that squad. * @param charId the potential member identifier * @return the discovered squad, or `None` */ - def GetParticipatingSquad(charId: Long): Option[Squad] = + def GetParticipatingSquad(charId: Long): Option[SquadFeatures] = memberToSquad.get(charId) match { - case opt @ Some(_) => - opt + case Some(id) => + squadFeatures.get(id) case None => None } @@ -223,7 +155,7 @@ class SquadService extends Actor { * the expectation is that the provided squad is a known participating squad * @return the discovered squad, or `None` */ - def GetLeadingSquad(player: Player, opt: Option[Squad]): Option[Squad] = GetLeadingSquad(player.CharId, opt) + def GetLeadingSquad(player: Player, opt: Option[SquadFeatures]): Option[SquadFeatures] = GetLeadingSquad(player.CharId, opt) /** * If the player associated with this unique character identifier number is the leader of any squad, discover that squad. @@ -234,11 +166,11 @@ class SquadService extends Actor { * the expectation is that the provided squad is a known participating squad * @return the discovered squad, or `None` */ - def GetLeadingSquad(charId: Long, opt: Option[Squad]): Option[Squad] = + def GetLeadingSquad(charId: Long, opt: Option[SquadFeatures]): Option[SquadFeatures] = opt.orElse(GetParticipatingSquad(charId)) match { - case Some(squad) => - if (squad.Leader.CharId == charId) { - Some(squad) + case Some(features) => + if (features.Squad.Leader.CharId == charId) { + Some(features) } else { None } @@ -246,138 +178,6 @@ class SquadService extends Actor { None } - /** - * Overloaded message-sending operation. - * The `Actor` version wraps around the expected `!` functionality. - * @param to an `ActorRef` which to send the message - * @param msg a message that can be stored in a `SquadServiceResponse` object - */ - def Publish(to: ActorRef, msg: SquadResponse.Response): Unit = { - Publish(to, msg, Nil) - } - - /** - * Overloaded message-sending operation. - * The `Actor` version wraps around the expected `!` functionality. - * @param to an `ActorRef` which to send the message - * @param msg a message that can be stored in a `SquadServiceResponse` object - * @param excluded a group of character identifier numbers who should not receive the message - * (resolved at destination) - */ - def Publish(to: ActorRef, msg: SquadResponse.Response, excluded: Iterable[Long]): Unit = { - to ! SquadServiceResponse("", excluded, msg) - } - - /** - * Overloaded message-sending operation. - * Always publishes on the `SquadEvents` object. - * @param to a faction affiliation used as the channel for the message - * @param msg a message that can be stored in a `SquadServiceResponse` object - */ - def Publish(to: PlanetSideEmpire.Type, msg: SquadResponse.Response): Unit = { - Publish(to, msg, Nil) - } - - /** - * Overloaded message-sending operation. - * Always publishes on the `SquadEvents` object. - * @param to a faction affiliation used as the channel for the message - * @param msg a message that can be stored in a `SquadServiceResponse` object - * @param excluded a group of character identifier numbers who should not receive the message - * (resolved at destination) - */ - def Publish(to: PlanetSideEmpire.Type, msg: SquadResponse.Response, excluded: Iterable[Long]): Unit = { - SquadEvents.publish(SquadServiceResponse(s"/$to/Squad", excluded, msg)) - } - - /** - * Overloaded message-sending operation. - * Strings come in three accepted patterns. - * The first resolves into a faction name, as determined by `PlanetSideEmpire` when transformed into a string. - * The second resolves into a squad's dedicated channel, a name that is formulaic. - * The third resolves as a unique character identifier number. - * @param to a string used as the channel for the message - * @param msg a message that can be stored in a `SquadServiceResponse` object - */ - def Publish(to: String, msg: SquadResponse.Response): Unit = { - Publish(to, msg, Nil) - } - - /** - * Overloaded message-sending operation. - * Strings come in three accepted patterns. - * The first resolves into a faction name, as determined by `PlanetSideEmpire` when transformed into a string. - * The second resolves into a squad's dedicated channel, a name that is formulaic. - * The third resolves as a unique character identifier number. - * @param to a string used as the channel for the message - * @param msg a message that can be stored in a `SquadServiceResponse` object - * @param excluded a group of character identifier numbers who should not receive the message - * (resolved at destination, usually) - */ - def Publish(to: String, msg: SquadResponse.Response, excluded: Iterable[Long]): Unit = { - to match { - case str if "TRNCVS".indexOf(str) > -1 || str.matches("(TR|NC|VS)-Squad\\d+") => - SquadEvents.publish(SquadServiceResponse(s"/$str/Squad", excluded, msg)) - case str if str.matches("\\d+") => - Publish(to.toLong, msg, excluded) - case _ => - log.warn(s"Publish(String): subscriber information is an unhandled format - $to") - } - } - - /** - * Overloaded message-sending operation. - * Always publishes on the `ActorRef` objects retained by the `UserEvents` object. - * @param to a unique character identifier used as the channel for the message - * @param msg a message that can be stored in a `SquadServiceResponse` object - */ - def Publish(to: Long, msg: SquadResponse.Response): Unit = { - UserEvents.get(to) match { - case Some(user) => - user ! SquadServiceResponse("", msg) - case None => - log.warn(s"Publish(Long): subscriber information can not be found - $to") - } - } - - /** - * Overloaded message-sending operation. - * Always publishes on the `ActorRef` objects retained by the `UserEvents` object. - * @param to a unique character identifier used as the channel for the message - * @param msg a message that can be stored in a `SquadServiceResponse` object - * @param excluded a group of character identifier numbers who should not receive the message - */ - def Publish(to: Long, msg: SquadResponse.Response, excluded: Iterable[Long]): Unit = { - if (!excluded.exists(_ == to)) { - Publish(to, msg) - } - } - - /** - * Overloaded message-sending operation. - * No message can be sent using this distinction. - * Log a warning. - * @param to something that was expected to be used as the channel for the message - * but is not handled as such - * @param msg a message that can be stored in a `SquadServiceResponse` object - */ - def Publish[ANY >: Any](to: ANY, msg: SquadResponse.Response): Unit = { - log.warn(s"Publish(Any): subscriber information is an unhandled format - $to") - } - - /** - * Overloaded message-sending operation. - * No message can be sent using this distinction. - * Log a warning. - * @param to something that was expected to be used as the channel for the message - * but is not handled as such - * @param msg a message that can be stored in a `SquadServiceResponse` object - * @param excluded a group of character identifier numbers who should not receive the message - */ - def Publish[ANY >: Any](to: ANY, msg: SquadResponse.Response, excluded: Iterable[Long]): Unit = { - log.warn(s"Publish(Any): subscriber information is an unhandled format - $to") - } - 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 => @@ -399,81 +199,47 @@ class SquadService extends Actor { case Terminated(actorRef) => TerminatedBy(actorRef) - case SquadServiceMessage(tplayer, zone, squad_action) => + case message @ SquadServiceMessage(tplayer, zone, squad_action) => squad_action match { case SquadAction.InitSquadList() => - Publish(sender(), SquadResponse.InitList(PublishedLists(tplayer.Faction))) //send initial squad catalog + SquadActionInitSquadList(tplayer, sender()) case SquadAction.InitCharId() => SquadActionInitCharId(tplayer) - case SquadAction.Membership(SquadRequestType.Invite, invitingPlayer, Some(_invitedPlayer), invitedName, _) => - SquadActionMembershipInvite(tplayer, invitingPlayer, _invitedPlayer, invitedName) + case SquadAction.ReloadDecoration() => + ApplySquadDecorationToEntriesForUser(tplayer.Faction, tplayer.CharId) - case SquadAction.Membership(SquadRequestType.ProximityInvite, invitingPlayer, _, _, _) => - SquadActionMembershipProximityInvite(tplayer, zone, invitingPlayer) + case _: SquadAction.Membership => + SquadActionMembership(tplayer, zone, squad_action) - case SquadAction.Membership(SquadRequestType.Accept, invitedPlayer, _, _, _) => - SquadActionMembershipAccept(tplayer, invitedPlayer) + case sqd: SquadAction.Definition => + SquadActionDefinition(message, sqd.action, sqd.guid) - case SquadAction.Membership(SquadRequestType.Leave, actingPlayer, _leavingPlayer, name, _) => - SquadActionMembershipLeave(tplayer, actingPlayer, _leavingPlayer, name) + case _: SquadAction.Waypoint => + SquadActionWaypoint(message, tplayer) - 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 _: SquadAction.Update => //try to avoid using; use the squad actor itself for updates + SquadActionUpdate(message, tplayer.CharId, sender()) case msg => log.warn(s"Unhandled action $msg from ${sender()}") } - case SquadService.BlankLazeWaypoints() => - lazeIndexBlanking.cancel() - val curr = System.currentTimeMillis() - val blank = lazeIndices.takeWhile { data => curr >= data.endTime } - lazeIndices = lazeIndices.drop(blank.size) - blank.foreach { data => - GetParticipatingSquad(data.charId) match { - case Some(squad) => - Publish( - squadFeatures(squad.GUID).ToChannel, - SquadResponse.WaypointEvent(WaypointEventAction.Remove, data.charId, SquadWaypoint(data.waypointType), None, None, 0), - Seq() - ) - case None => ; - } - } - //retime - lazeIndices match { - case Nil => ; - case x :: _ => - import scala.concurrent.ExecutionContext.Implicits.global - lazeIndexBlanking = context.system.scheduler.scheduleOnce( - math.min(0, x.endTime - curr).milliseconds, - self, - SquadService.BlankLazeWaypoints() - ) - } + case SquadService.PerformStartSquad(invitingPlayer) => + performStartSquad(sender(), invitingPlayer) + + case SquadService.PerformJoinSquad(player, features, position) => + JoinSquad(player, features, position) + + case SquadService.UpdateSquadList(features, changes) => + UpdateSquadList(features, changes) + + case SquadService.UpdateSquadListWhenListed(features, changes) => + UpdateSquadListWhenListed(features, changes) + + case SquadService.ResendActiveInvite(charId) => + invitations.resendActiveInvite(charId) case msg => log.warn(s"Unhandled message $msg from ${sender()}") @@ -482,7 +248,7 @@ class SquadService extends Actor { def JoinByFaction(faction: String, sender: ActorRef): Unit = { val path = s"/$faction/Squad" log.trace(s"$sender has joined $path") - SquadEvents.subscribe(sender, path) + subs.SquadEvents.subscribe(sender, path) } def JoinByCharacterId(charId: String, sender: ActorRef): Unit = { @@ -491,8 +257,8 @@ class SquadService extends Actor { val path = s"/$charId/Squad" log.trace(s"$sender has joined $path") context.watch(sender) - UserEvents += longCharId -> sender - refused(longCharId) = Nil + subs.UserEvents += longCharId -> sender + invitations.handleJoin(longCharId) } catch { case _: ClassCastException => log.warn(s"Service.Join: tried $charId as a unique character identifier, but it could not be casted") @@ -507,7 +273,7 @@ class SquadService extends Actor { def LeaveByFaction(faction: String, sender: ActorRef): Unit = { val path = s"/$faction/Squad" log.trace(s"$sender has left $path") - SquadEvents.unsubscribe(sender, path) + subs.SquadEvents.unsubscribe(sender, path) } def LeaveByCharacterId(charId: String, sender: ActorRef): Unit = { @@ -525,7 +291,7 @@ class SquadService extends Actor { } def LeaveInGeneral(sender: ActorRef): Unit = { - UserEvents find { case (_, subscription) => subscription.path.equals(sender.path) } match { + subs.UserEvents find { case (_, subscription) => subscription.path.equals(sender.path) } match { case Some((to, _)) => LeaveService(to, sender) case _ => ; @@ -534,26 +300,89 @@ class SquadService extends Actor { def TerminatedBy(requestee: ActorRef): Unit = { context.unwatch(requestee) - UserEvents find { case (_, subscription) => subscription eq requestee } match { + subs.UserEvents find { case (_, subscription) => subscription eq requestee } match { case Some((to, _)) => LeaveService(to, requestee) case _ => ; } } + def performStartSquad(sender: ActorRef, player: Player): Unit = { + val invitingPlayerCharId = player.CharId + if (EnsureEmptySquad(invitingPlayerCharId)) { + GetParticipatingSquad(player) 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 features = StartSquad(player) + val squad = features.Squad + squad.Task = s"${player.Name}'s Squad" + subs.Publish(invitingPlayerCharId, SquadResponse.IdentifyAsSquadLeader(squad.GUID)) + sender.tell(SquadInvitationManager.FinishStartSquad(features), self) + Some(features) + } + } + } + + def SquadActionInitSquadList( + tplayer: Player, + sender: ActorRef + ): Unit = { + //send initial squad catalog + val faction = tplayer.Faction + val squads = PublishedLists(faction) + subs.Publish(sender, SquadResponse.InitList(squads)) + squads.foreach { squad => + val guid = squad.squad_guid.get + subs.Publish(tplayer.CharId, SquadResponse.SquadDecoration(guid, squadFeatures(guid).Squad)) + } + } + def SquadActionInitCharId(tplayer: Player): Unit = { val charId = tplayer.CharId - memberToSquad.get(charId) match { + GetParticipatingSquad(charId) match { case None => ; - case Some(squad) => - val guid = squad.GUID - val toChannel = s"/${squadFeatures(guid).ToChannel}/Squad" - val indices = - squad.Membership.zipWithIndex.collect({ case (member, index) if member.CharId != 0 => index }).toList - Publish(charId, SquadResponse.AssociateWithSquad(guid)) - Publish(charId, SquadResponse.Join(squad, indices, toChannel)) - InitSquadDetail(guid, Seq(charId), squad) - InitWaypoints(charId, guid) + case Some(features) => + features.Switchboard ! SquadSwitchboard.Join(tplayer, 0, sender()) + } + } + + def SquadServiceReloadSquadDecoration(faction: PlanetSideEmpire.Value, to: Long): Unit = { + ApplySquadDecorationToEntriesForUser(faction, to) + } + + def SquadActionMembership(tplayer: Player, zone: Zone, action: Any): Unit = { + action match { + case SquadAction.Membership(SquadRequestType.Invite, invitingPlayer, Some(_invitedPlayer), invitedName, _) => + SquadActionMembershipInvite(tplayer, invitingPlayer, _invitedPlayer, invitedName) + + case SquadAction.Membership(SquadRequestType.ProximityInvite, invitingPlayer, _, _, _) => + SquadActionMembershipProximityInvite(zone, invitingPlayer) + + case SquadAction.Membership(SquadRequestType.Accept, invitedPlayer, _, _, _) => + SquadActionMembershipAccept(tplayer, invitedPlayer) + + 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, SquadServiceMessage(tplayer, zone, action), sender()) + + case SquadAction.Membership(event, _, _, _, _) => + debug(s"SquadAction.Membership: $event is not yet supported") + + case _ => ; } } @@ -567,116 +396,57 @@ class SquadService extends Actor { (if (invitedName.nonEmpty) { //validate player with name exists LivePlayerList - .WorldPopulation({ - case (_, a: Avatar) => a.name.equalsIgnoreCase(invitedName) && a.faction == tplayer.Faction - }) + .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 + case Some(a) => subs.UserEvents.keys.find(_ == a.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 Some(_) => 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 => + (GetParticipatingSquad(invitingPlayer), GetParticipatingSquad(invitedPlayer)) match { + case (Some(features1), Some(features2)) + if features1.Squad.GUID == features2.Squad.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 => + case (Some(invitersFeatures), Some(invitedFeatures)) if { + val squad1 = invitersFeatures.Squad + val squad2 = invitedFeatures.Squad + 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 (squad1.Size == squad1.Capacity) { - debug(s"$invitingPlayer tried to invite $invitedPlayer to a squad without available positions") - } else if (!Refused(invitedPlayer).contains(invitingPlayer)) { - val charId = tplayer.CharId - AddInviteAndRespond( - invitedPlayer, - VacancyInvite(charId, tplayer.Name, squad1.GUID), - charId, - tplayer.Name - ) - } else { - debug(s"$invitedPlayer repeated a previous refusal to $invitingPlayer's invitation offer") - } + case (Some(invitersFeatures), Some(invitedFeatures)) + if invitedFeatures.Squad.Size == 1 => + //both players belong to squads, but the invitedPlayer's squad (invitedFeatures) is underutilized + //treat the same as "the classic situation" using invitersFeatures + invitations.createVacancyInvite(tplayer, invitedPlayer, invitersFeatures) - case (Some(squad1), Some(squad2)) if squad1.Size == 1 => + case (Some(invitersFeatures), Some(invitedFeatures)) + if invitersFeatures.Squad.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 (squad2.Size == squad2.Capacity) { - debug(s"$invitingPlayer's invitation got reversed to $invitedPlayer's squad, but the squad has no available positions") - } else if (Refused(invitingPlayer).contains(invitedPlayer)) { - debug(s"$invitingPlayer's invitation got reversed to $invitedPlayer's squad, but $invitedPlayer repeated a previous refusal to $invitingPlayer's invitation offer") - } else if (Refused(invitingPlayer).contains(leader)) { - debug(s"$invitingPlayer's invitation got reversed to $invitedPlayer's squad, but $leader repeated a previous refusal to $invitingPlayer's invitation offer") - } else { - AddInviteAndRespond( - leader, - IndirectInvite(tplayer, squad2.GUID), - invitingPlayer, - tplayer.Name - ) - } + invitations.createIndirectInvite(tplayer, invitedPlayer, invitedFeatures) - case (Some(squad), None) => + case (Some(features), None) => //the classic situation - if (squad.Size == squad.Capacity) { - debug(s"$invitingPlayer tried to invite $invitedPlayer to a squad without available positions") - } else 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") - } + invitations.createVacancyInvite(tplayer, invitedPlayer, features) - case (None, Some(squad)) => + case (None, Some(features)) => //indirection; we're trying to invite ourselves to someone else's squad - val leader = squad.Leader.CharId - if (squad.Size == squad.Capacity) { - debug(s"$invitingPlayer tried to invite to $invitedPlayer's squad, but the squad has no available positions") - } else if (Refused(invitingPlayer).contains(invitedPlayer)) { - debug(s"invitingPlayer tried to invite to $invitedPlayer's squad, but $invitedPlayer repeated a previous refusal to $invitingPlayer's invitation offer") - } else if (Refused(invitingPlayer).contains(leader)) { - debug(s"invitingPlayer tried to invite to $invitedPlayer's squad, but $leader repeated a previous refusal to $invitingPlayer's invitation offer") - } else { - AddInviteAndRespond( - squad.Leader.CharId, - IndirectInvite(tplayer, squad.GUID), - invitingPlayer, - tplayer.Name - ) - } + invitations.createIndirectInvite(tplayer, invitedPlayer, features) 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 - ) - } + invitations.createSpontaneousInvite(tplayer, invitedPlayer) case _ => ; } @@ -684,376 +454,48 @@ class SquadService extends Actor { } } - def SquadActionMembershipProximityInvite(tplayer: Player, zone: Zone, invitingPlayer: Long): Unit = { + def SquadActionMembershipProximityInvite(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 } - if (positions.nonEmpty) { - /* - 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.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, - name - ) - } - case None => ; - } - } - } - - case None => + case Some(features) => + invitations.handleProximityInvite(zone, invitingPlayer, features) + case _ => ; } } 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, - 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) => - //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(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 _ => - //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) + invitations.handleAcceptance(tplayer, invitedPlayer, GetParticipatingSquad(tplayer)) } def SquadActionMembershipLeave(tplayer: Player, actingPlayer: Long, _leavingPlayer: Option[Long], name: String): Unit = { GetParticipatingSquad(actingPlayer) match { - case Some(squad) => + case Some(features) => + val squad = features.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 Some(a) => subs.UserEvents.keys.find(_ == a.id) case None => None } } else { //validate player with id _leavingPlayer match { - case Some(id) => UserEvents.keys.find(_ == id) + case Some(id) => subs.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 + case _ @ Some(leavingPlayer) + if GetParticipatingSquad(leavingPlayer).contains(features) => //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) + DisbandSquad(features) } else { //kicked by the squad leader - Publish( + subs.Publish( leavingPlayer, SquadResponse.Membership( SquadResponseType.Leave, @@ -1062,11 +504,11 @@ class SquadService extends Actor { leavingPlayer, Some(leader), tplayer.Name, - false, + unk5=false, Some(None) ) ) - Publish( + subs.Publish( leader, SquadResponse.Membership( SquadResponseType.Leave, @@ -1075,20 +517,19 @@ class SquadService extends Actor { leader, Some(leavingPlayer), "", - true, + unk5=true, Some(None) ) ) - squadFeatures(squad.GUID).Refuse = leavingPlayer - LeaveSquad(leavingPlayer, squad) + LeaveSquad(leavingPlayer, features) } } 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) + DisbandSquad(features) } else { //leaving the squad of own accord - LeaveSquad(actingPlayer, squad) + LeaveSquad(actingPlayer, features) } } @@ -1099,394 +540,190 @@ class SquadService extends Actor { } 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(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(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(LookingForSquadRoleInvite(leader, _, guid, _)) - if squadFeatures.get(guid).nonEmpty && squadFeatures(guid).Squad.Leader.CharId != rejectingPlayer => - //rejectingPlayer is the would-be squad member; the squad leader's request was rejected - Refused(rejectingPlayer, leader) - val features = squadFeatures(guid) - features.Refuse = rejectingPlayer - if ((features.ProxyInvites = features.ProxyInvites.filterNot(_ == rejectingPlayer)).isEmpty) { - features.SearchForRole = None - } - (None, None) - - case Some(RequestRole(_, 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) + invitations.handleRejection( + tplayer, + rejectingPlayer, + squadFeatures.map { case (guid, features) => (guid, features.Squad.Leader.CharId) }.toList + ) } def SquadActionMembershipDisband(charId: Long): Unit = { GetLeadingSquad(charId, None) match { - case Some(squad) => - DisbandSquad(squad) + case Some(features) => + DisbandSquad(features) 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) + invitations.handleCancelling(cancellingPlayer) } - def SquadActionMembershipPromote(promotingPlayer: Long, _promotedPlayer: Long, promotedName: String): Unit = { - val promotedPlayer = (if (promotedName.nonEmpty) { - //validate player with name exists + def SquadActionMembershipPromote( + sponsoringPlayer: Long, + promotionCandidatePlayer: Long, + promotionCandidateName: String, + msg: SquadServiceMessage, + ref: ActorRef + ): Unit = { + val promotedPlayer: Long = subs.UserEvents.keys.find(_ == promotionCandidatePlayer).orElse({ LivePlayerList - .WorldPopulation({ case (_, a: Avatar) => a.name == promotedName }) + .WorldPopulation({ case (_, a: Avatar) => a.name.equalsIgnoreCase(promotionCandidateName) }) .headOption match { - case Some(player) => UserEvents.keys.find(_ == player.id) - case None => Some(_promotedPlayer) + case Some(a) => Some(a.id) + case None => None } - } else { - Some(_promotedPlayer) }) match { - case Some(player) => player - case None => -1L + case Some(player: Long) => player + case _ => -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) - } - } - info(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)) - ) - ) - ) + //sponsorPlayer should be squad leader + (GetLeadingSquad(sponsoringPlayer, None), GetParticipatingSquad(promotedPlayer)) match { + case (Some(features), Some(features2)) if features.Squad.GUID == features2.Squad.GUID => + SquadActionMembershipPromote(sponsoringPlayer, promotedPlayer, features, msg, ref) case _ => ; } } - def SquadActionWaypoint(tplayer: Player, waypointType: SquadWaypoint, info: Option[WaypointInfo]): Unit = { - val playerCharId = tplayer.CharId - (if (waypointType.subtype == WaypointSubtype.Laze) { - //laze rally can be updated by any squad member - GetParticipatingSquad(tplayer) match { - case Some(squad) => - info match { - case Some(winfo) => - //the laze-indicated target waypoint is not retained - val curr = System.currentTimeMillis() - val clippedLazes = { - val index = lazeIndices.indexWhere { _.charId == playerCharId } - if (index > -1) { - lazeIndices.take(index) ++ lazeIndices.drop(index + 1) - } else { - lazeIndices - } - } - if (lazeIndices.isEmpty || clippedLazes.headOption != lazeIndices.headOption) { - //reason to retime blanking - lazeIndexBlanking.cancel() - import scala.concurrent.ExecutionContext.Implicits.global - lazeIndexBlanking = lazeIndices.headOption match { - case Some(data) => - context.system.scheduler.scheduleOnce(math.min(0, data.endTime - curr).milliseconds, self, SquadService.BlankLazeWaypoints()) - case None => - context.system.scheduler.scheduleOnce(15.seconds, self, SquadService.BlankLazeWaypoints()) - } - } - lazeIndices = clippedLazes :+ LazeWaypointData(playerCharId, waypointType.value, curr + 15000) - (Some(squad), Some(WaypointData(winfo.zone_number, winfo.pos))) - case None => - (Some(squad), None) - } - case None => - (None, None) - } - } else { - //only the squad leader may update other squad waypoints - 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, None) - } - }) match { - case (Some(squad), Some(_)) => - //waypoint added or updated - Publish( - squadFeatures(squad.GUID).ToChannel, - SquadResponse.WaypointEvent(WaypointEventAction.Add, playerCharId, waypointType, None, info, 1), - Seq(playerCharId) - ) - case (Some(squad), None) => - //waypoint removed - Publish( - squadFeatures(squad.GUID).ToChannel, - SquadResponse.WaypointEvent(WaypointEventAction.Remove, playerCharId, waypointType, None, None, 0), - Seq(playerCharId) - ) + def SquadActionMembershipPromote( + sponsoringPlayer: Long, + promotedPlayer: Long, + features: SquadFeatures, + msg: SquadServiceMessage, + ref: ActorRef + ): Unit = { + features.Switchboard.tell(msg, ref) + invitations.handlePromotion(sponsoringPlayer, promotedPlayer) + } - case msg => - log.warn(s"Unsupported squad waypoint behavior: $msg") + def SquadActionWaypoint( + message: SquadServiceMessage, + tplayer: Player + ): Unit = { + GetParticipatingSquad(tplayer) match { + case Some(features) => + features.Switchboard.tell(message, sender()) + case None => + log.warn(s"Unsupported squad waypoint behavior: $message") } } def SquadActionDefinition( - tplayer: Player, - zone: Zone, - guid: PlanetSideGUID, - line: Int, + message: SquadServiceMessage, 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, pSquadOpt, lSquadOpt, purpose) - - case ChangeSquadZone(zone_id) => - SquadActionDefinitionChangeSquadZone(tplayer, pSquadOpt, lSquadOpt, zone_id, sendTo) - - case CloseSquadMemberPosition(position) => - SquadActionDefinitionCloseSquadMemberPosition(tplayer, pSquadOpt, lSquadOpt, position) - - case AddSquadMemberPosition(position) => - SquadActionDefinitionAddSquadMemberPosition(tplayer, pSquadOpt, lSquadOpt, position) - - case ChangeSquadMemberRequirementsRole(position, role) => - SquadActionDefinitionChangeSquadMemberRequirementsRole(tplayer, pSquadOpt, lSquadOpt, position, role) - - case ChangeSquadMemberRequirementsDetailedOrders(position, orders) => - SquadActionDefinitionChangeSquadMemberRequirementsDetailedOrders(tplayer, pSquadOpt, lSquadOpt, position, orders) - - case ChangeSquadMemberRequirementsCertifications(position, certs) => - SquadActionDefinitionChangeSquadMemberRequirementsCertifications(tplayer, pSquadOpt, lSquadOpt, position, certs) - - case LocationFollowsSquadLead(state) => - SquadActionDefinitionLocationFollowsSquadLead(tplayer, pSquadOpt, lSquadOpt, state) - - case AutoApproveInvitationRequests(state) => - SquadActionDefinitionAutoApproveInvitationRequests(tplayer, pSquadOpt, lSquadOpt, state) - - case FindLfsSoldiersForRole(position) => - SquadActionDefinitionFindLfsSoldiersForRole(tplayer, zone, lSquadOpt, position) - - case CancelFind() => - SquadActionDefinitionCancelFind(lSquadOpt) - - case RequestListSquad() => - SquadActionDefinitionRequestListSquad(tplayer, pSquadOpt, 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") + guid: PlanetSideGUID + ): Unit = { + val tplayer = message.tplayer + (action match { + //the following actions only perform an action upon the squad + case _: ChangeSquadPurpose => GetOrCreateSquadOnlyIfLeader(tplayer) + case _: ChangeSquadZone => GetOrCreateSquadOnlyIfLeader(tplayer) + case _: AddSquadMemberPosition => GetOrCreateSquadOnlyIfLeader(tplayer) + case _: ChangeSquadMemberRequirementsRole => GetOrCreateSquadOnlyIfLeader(tplayer) + case _: ChangeSquadMemberRequirementsDetailedOrders => GetOrCreateSquadOnlyIfLeader(tplayer) + case _: ChangeSquadMemberRequirementsCertifications => GetOrCreateSquadOnlyIfLeader(tplayer) + case _: LocationFollowsSquadLead => GetOrCreateSquadOnlyIfLeader(tplayer) + case _: RequestListSquad => GetOrCreateSquadOnlyIfLeader(tplayer) + case _: StopListSquad => GetLeadingSquad(tplayer, None) + //the following actions cause changes with the squad composition or with invitations + case AutoApproveInvitationRequests(_) => + GetOrCreateSquadOnlyIfLeader(tplayer) match { + case out @ Some(features) => + invitations.handleDefinitionAction(tplayer, action, features) + out + case None => + None } + case CloseSquadMemberPosition(position) => + GetOrCreateSquadOnlyIfLeader(tplayer) match { + case out @ Some(features) + if features.Squad.Membership(position).CharId > 0 => + val squad = features.Squad + LeaveSquad(squad.Membership(position).CharId, features) + out + case None => + None + } + case FindLfsSoldiersForRole(_) => + GetLeadingSquad(tplayer, None) match { + case Some(features) => + invitations.handleDefinitionAction(tplayer, action, features) + case _ => ; + } + None + case CancelFind() => + GetLeadingSquad(tplayer, None) match { + case Some(features) => + invitations.handleDefinitionAction(tplayer, action, features) + case _ => ; + } + None + case SelectRoleForYourself(_) => + GetParticipatingSquad(tplayer) match { + case out @ Some(features) => + if (features.Squad.GUID == guid) { + out + } else { + //this isn't the squad we're looking for by GUID; as a precaution, reload all of the published squad list + val faction = tplayer.Faction + subs.Publish(faction, SquadResponse.InitList(PublishedLists(tplayer.Faction))) + None + } + case _ => + GetSquad(guid) match { + case Some(features) => + invitations.handleDefinitionAction(tplayer, action, features) + case _ => ; + } + None + } + case _: CancelSelectRoleForYourself => + GetSquad(guid) match { + case Some(features) => + invitations.handleDefinitionAction(tplayer, action, features) + case _ => ; + } + None + case search: SearchForSquadsWithParticularRole => + SquadActionDefinitionSearchForSquadsWithParticularRole(tplayer, search) + None + case _: CancelSquadSearch => + SquadActionDefinitionCancelSquadSearch(tplayer.CharId) + None + case _: DisplaySquad => + GetSquad(guid) match { + case out @ Some(_) => + SquadActionDefinitionDisplaySquad(tplayer, guid) + out + case None => + None + } + case _: SquadInitializationIssue => + SquadActionDefinitionSquadInitializationIssue(tplayer, guid) + None + case _ => + GetSquad(guid) + }) match { + case Some(features) => features.Switchboard.tell(message, sender()) + case None => ; } } - def GetOrCreateSquadOnlyIfLeader( - player: Player, - participatingSquadOpt: Option[Squad], - leadingSquadOpt: Option[Squad] - ): Option[Squad] = { + def SquadActionUpdate( + message: SquadServiceMessage, + char_id: Long, + replyTo: ActorRef, + ): Unit = { + GetParticipatingSquad(char_id) match { + case Some(features) => features.Switchboard.tell(message, replyTo) + case None => ; + } + } + + def GetOrCreateSquadOnlyIfLeader(player: Player): Option[SquadFeatures] = { + val participatingSquadOpt = GetParticipatingSquad(player) + val leadingSquadOpt = GetLeadingSquad(player, participatingSquadOpt) if (participatingSquadOpt.isEmpty) { Some(StartSquad(player)) } else if (participatingSquadOpt == leadingSquadOpt) { @@ -1496,1492 +733,112 @@ class SquadService extends Actor { } } - def SquadActionDefinitionSaveSquadFavorite( - tplayer: Player, - line: Int, - lSquadOpt: Option[Squad], - sendTo: ActorRef - ): Unit = { - lSquadOpt match { - case Some(squad) if squad.Task.nonEmpty && squad.ZoneId > 0 => - tplayer.squadLoadouts.SaveLoadout(squad, squad.Task, line) - Publish(sendTo, SquadResponse.ListSquadFavorite(line, squad.Task)) - case _ => ; - } - } - - def SquadActionDefinitionLoadSquadFavorite( - tplayer: Player, - line: Int, - pSquadOpt: Option[Squad], - lSquadOpt: Option[Squad], - sendTo: ActorRef - ): Unit = { - //TODO seems all wrong - pSquadOpt match { - case Some(squad) => - 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 _ => - } - case _ => ; - } - } - - def SquadActionDefinitionDeleteSquadFavorite(tplayer: Player, line: Int, sendTo: ActorRef): Unit = { - tplayer.squadLoadouts.DeleteLoadout(line) - Publish(sendTo, SquadResponse.ListSquadFavorite(line, "")) - } - - def SquadActionDefinitionChangeSquadPurpose( - tplayer: Player, - pSquadOpt: Option[Squad], - lSquadOpt: Option[Squad], - purpose: String - ): Unit = { - GetOrCreateSquadOnlyIfLeader(tplayer, pSquadOpt, lSquadOpt) match { - case Some(squad) => - squad.Task = purpose - UpdateSquadListWhenListed(squadFeatures(squad.GUID), SquadInfo().Task(purpose)) - UpdateSquadDetail(squad.GUID, SquadDetail().Task(purpose)) - case None => ; - } - } - - def SquadActionDefinitionChangeSquadZone( - tplayer: Player, - pSquadOpt: Option[Squad], - lSquadOpt: Option[Squad], - zone_id: PlanetSideZoneID, - sendTo: ActorRef - ): Unit = { - GetOrCreateSquadOnlyIfLeader(tplayer, pSquadOpt, lSquadOpt) match { - case Some(squad) => - 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) - ) - case None => ; - } - } - - def SquadActionDefinitionCloseSquadMemberPosition( - tplayer: Player, - pSquadOpt: Option[Squad], - lSquadOpt: Option[Squad], - position: Int - ): Unit = { - GetOrCreateSquadOnlyIfLeader(tplayer, pSquadOpt, lSquadOpt) match { - case Some(squad) => - 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 None => ; - } - } - - def SquadActionDefinitionAddSquadMemberPosition( - tplayer: Player, - pSquadOpt: Option[Squad], - lSquadOpt: Option[Squad], - position: Int - ): Unit = { - GetOrCreateSquadOnlyIfLeader(tplayer, pSquadOpt, lSquadOpt) match { - case Some(squad) => - 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 None => ; - } - } - - def SquadActionDefinitionChangeSquadMemberRequirementsRole( + def SquadActionDefinitionSearchForSquadsWithParticularRole( tplayer: Player, - pSquadOpt: Option[Squad], - lSquadOpt: Option[Squad], - position: Int, - role: String - ): Unit ={ - GetOrCreateSquadOnlyIfLeader(tplayer, pSquadOpt, lSquadOpt) match { - case Some(squad) => - 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 None => ; - } - } - - def SquadActionDefinitionChangeSquadMemberRequirementsDetailedOrders( - tplayer: Player, - pSquadOpt: Option[Squad], - lSquadOpt: Option[Squad], - position: Int, - orders: String - ): Unit = { - GetOrCreateSquadOnlyIfLeader(tplayer, pSquadOpt, lSquadOpt) match { - case Some(squad) => - 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 None => ; - } - } - - def SquadActionDefinitionChangeSquadMemberRequirementsCertifications( - tplayer: Player, - pSquadOpt: Option[Squad], - lSquadOpt: Option[Squad], - position: Int, - certs: Set[Certification] - ): Unit = { - GetOrCreateSquadOnlyIfLeader(tplayer, pSquadOpt, lSquadOpt) match { - case Some(squad) => - 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 None => ; - } - } - - def SquadActionDefinitionLocationFollowsSquadLead( - tplayer: Player, - pSquadOpt: Option[Squad], - lSquadOpt: Option[Squad], - state: Boolean - ): Unit = { - GetOrCreateSquadOnlyIfLeader(tplayer, pSquadOpt, lSquadOpt) match { - case Some(squad) => - val features = squadFeatures(squad.GUID) - features.LocationFollowsSquadLead = state - case None => ; - } - } - - def SquadActionDefinitionAutoApproveInvitationRequests( - tplayer: Player, - pSquadOpt: Option[Squad], - lSquadOpt: Option[Squad], - state: Boolean - ): Unit = { - GetOrCreateSquadOnlyIfLeader(tplayer, pSquadOpt, lSquadOpt) match { - case Some(squad) => - val features = squadFeatures(squad.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 None => ; - } - } - - 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, - pSquadOpt: Option[Squad], - lSquadOpt: Option[Squad], - sendTo: ActorRef - ): Unit = { - GetOrCreateSquadOnlyIfLeader(tplayer, pSquadOpt, lSquadOpt) match { - case Some(squad) => - 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) - } - case None => ; - } - } - - def SquadActionDefinitionStopListSquad(tplayer: Player, lSquadOpt: Option[Squad], sendTo: ActorRef): Unit = { - lSquadOpt match { - case Some(squad) => - val features = squadFeatures(squad.GUID) - if (features.Listed) { - features.Listed = false - Publish(sendTo, SquadResponse.SetListSquad(PlanetSideGUID(0))) - UpdateSquadList(squad, None) - } - case None => ; - //how did we get into this situation? - } - } - - 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( - 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 ??? */ - 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) - squadFeatures.get(guid) match { - case Some(features) => - Publish(features.ToChannel, SquadResponse.AssignMember(squad, fromPosition, position)) - case None => - //we might be in trouble; we might not ... - } - 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 performed by anyone */ - def SquadActionDefinitionDisplaySquad(tplayer: Player, guid: PlanetSideGUID, sendTo: ActorRef): Unit = { + criteria: SearchForSquadsWithParticularRole + ): 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 - } - .map { member => - SquadAction - .Update(member.CharId, member.Health, 0, member.Armor, 0, member.Position, member.ZoneId) - } - .toList - ) - ) - case _ => ; - } - - case None => ; - } - } - - /** - * This player has refused to join squad leader's squads or some other players's offers to form a squad. - * @param charId the player who refused other players - * @return the list of other players who have been refused - */ - def Refused(charId: Long): List[Long] = refused.getOrElse(charId, Nil) - - /** - * This player has refused to join squad leader's squads or some other players's offers to form a squad. - * @param charId the player who is doing the refusal - * @param refusedCharId the player who is refused - * @return the list of other players who have been refused - */ - def Refused(charId: Long, refusedCharId: Long): List[Long] = { - if (charId != refusedCharId) { - Refused(charId, List(refusedCharId)) - } else { - Nil - } - } - - /** - * This player has refused to join squad leader's squads or some other players's offers to form a squad. - * @param charId the player who is doing the refusal - * @param list the players who are refused - * @return the list of other players who have been refused - */ - def Refused(charId: Long, list: List[Long]): List[Long] = { - refused.get(charId) match { - case Some(refusedList) => - refused(charId) = list ++ refusedList - Refused(charId) + searchData.get(charId) match { + case Some(_) => ; + //already searching, so do nothing(?) case None => + val data = SquadService.SearchCriteria(tplayer.Faction, criteria) + searchData.put(charId, data) + SquadActionDefinitionSearchForSquadsUsingCriteria(charId, data) + } + } + + private def SquadActionDefinitionSearchForSquadsUsingCriteria( + charId: Long, + criteria: SquadService.SearchCriteria + ): Unit = { + subs.Publish( + charId, + SquadResponse.SquadSearchResults(SearchForSquadsResults(criteria)) + ) + } + + private def SearchForSquadsResults(criteria: SquadService.SearchCriteria): List[PlanetSideGUID] = { + publishedLists.get(criteria.faction) match { + case Some(squads) if squads.nonEmpty => + squads.flatMap { guid => SearchForSquadsResults(criteria, guid) }.toList + case _ => Nil } } - /** - * Assign a provided invitation object to either the active or inactive position for a player.
- *
- * The determination for the active position is whether or not something is currently in the active position - * or whether some mechanism tried to shift invitation object into the active position - * but found nothing to shift. - * If an invitation object originating from the reported player already exists, - * a new one is not appended to the inactive queue. - * This method should always be used as the entry point for the active and inactive invitation options - * or as a part of the entry point for the aforesaid options. - * @see `AddInviteAndRespond` - * @see `AltAddInviteAndRespond` - * @param invitedPlayer the unique character identifier for the player being invited; - * in actuality, represents the player who will address the invitation object - * @param invite the "new" invitation envelop object - * @return an optional invite; - * the invitation object in the active invite position; - * `None`, if it is not added to either the active option or inactive position - */ - def AddInvite(invitedPlayer: Long, invite: Invitation): Option[Invitation] = { - invites.get(invitedPlayer).orElse(previousInvites.get(invitedPlayer)) match { - case Some(_bid) => - //the active invite does not interact with the given invite; add to queued invites - queuedInvites.get(invitedPlayer) match { - case Some(bidList) => - //ensure that new invite does not interact with the queue's invites by invitingPlayer info - if ( - _bid.InviterCharId != invite.InviterCharId && !bidList.exists { eachBid => - eachBid.InviterCharId == invite.InviterCharId - } - ) { - queuedInvites(invitedPlayer) = invite match { - case _: RequestRole => - val (normals, others) = bidList.partition(_.isInstanceOf[RequestRole]) - (normals :+ invite) ++ others - case _ => - bidList :+ invite - } - Some(_bid) - } else { - None - } - case None => - if (_bid.InviterCharId != invite.InviterCharId) { - queuedInvites(invitedPlayer) = List[Invitation](invite) - Some(_bid) - } else { - None - } - } - - case None => - invites(invitedPlayer) = invite - Some(invite) - } - } - - /** - * Component method used for the response behavior for processing the invitation object as an `IndirectInvite` object. - * @see `HandleRequestRole` - * @param invite the original invitation object that started this process - * @param player the target of the response and invitation - * @param invitedPlayer the unique character identifier for the player being invited; - * in actuality, represents the player who will address the invitation object; - * not useful here - * @param invitingPlayer the unique character identifier for the player who invited the former; - * not useful here - * @param name a name to be used in message composition; - * not useful here - * @return na - */ - def indirectInviteResp( - invite: IndirectInvite, - player: Player, - invitedPlayer: Long, - invitingPlayer: Long, - name: String - ): Boolean = { - HandleRequestRole(invite, player) - } - - /** - * Component method used for the response behavior for processing the invitation object as an `IndirectInvite` object. - * @see `HandleRequestRole` - * @param invite the original invitation object that started this process - * @param player the target of the response and invitation - * @param invitedPlayer the unique character identifier for the player being invited - * in actuality, represents the player who will address the invitation object - * @param invitingPlayer the unique character identifier for the player who invited the former - * @param name a name to be used in message composition - * @return na - */ - def altIndirectInviteResp( - invite: IndirectInvite, - player: Player, - invitedPlayer: Long, - invitingPlayer: Long, - name: String - ): Boolean = { - Publish( - invitingPlayer, - SquadResponse.Membership( - SquadResponseType.Accept, - 0, - 0, - invitingPlayer, - Some(invitedPlayer), - player.Name, - false, - Some(None) - ) - ) - HandleRequestRole(invite, player) - } - - /** - * A branched response for processing (new) invitation objects that have been submitted to the system.
- *
- * A comparison is performed between the original invitation object and an invitation object - * that represents the potential modification or redirection of the current active invitation obect. - * Any further action is only performed when an "is equal" comparison is `true`. - * When passing, the system publishes up to two messages - * to users that would anticipate being informed of squad join activity. - * @param indirectVacancyFunc the method that cans the respondign behavior should an `IndirectVacancy` object being consumed - * @param targetInvite a comparison invitation object; - * represents the unmodified, unadjusted invite - * @param actualInvite a comparaison invitation object; - * proper use of this field should be the output of another process upon the following `actualInvite` - * @param invitedPlayer the unique character identifier for the player being invited - * in actuality, represents the player who will address the invitation object - * @param invitingPlayer the unique character identifier for the player who invited the former - * @param name a name to be used in message composition - */ - def InviteResponseTemplate(indirectVacancyFunc: (IndirectInvite, Player, Long, Long, String) => Boolean)( - targetInvite: Invitation, - actualInvite: Option[Invitation], - invitedPlayer: Long, - invitingPlayer: Long, - name: String - ): Unit = { - if (actualInvite.contains(targetInvite)) { - //immediately respond - targetInvite match { - case VacancyInvite(charId, _name, _) => - Publish( - invitedPlayer, - SquadResponse.Membership( - SquadResponseType.Invite, - 0, - 0, - charId, - Some(invitedPlayer), - _name, - false, - Some(None) - ) - ) - Publish( - charId, - SquadResponse.Membership( - SquadResponseType.Invite, - 0, - 0, - invitedPlayer, - Some(charId), - _name, - true, - Some(None) - ) - ) - - case _bid @ IndirectInvite(player, _) => - indirectVacancyFunc(_bid, player, invitedPlayer, invitingPlayer, name) - - case _bid @ SpontaneousInvite(player) => - val bidInvitingPlayer = _bid.InviterCharId - Publish( - invitedPlayer, - SquadResponse.Membership( - SquadResponseType.Invite, - 0, - 0, - bidInvitingPlayer, - Some(invitedPlayer), - player.Name, - false, - Some(None) - ) - ) - Publish( - bidInvitingPlayer, - SquadResponse.Membership( - SquadResponseType.Invite, - 0, - 0, - invitedPlayer, - Some(bidInvitingPlayer), - player.Name, - true, - Some(None) - ) - ) - - case _bid @ RequestRole(player, _, _) => - HandleRequestRole(_bid, player) - - case LookingForSquadRoleInvite(charId, _name, _, _) => - Publish( - invitedPlayer, - SquadResponse.Membership( - SquadResponseType.Invite, - 0, - 0, - invitedPlayer, - Some(charId), - _name, - false, - Some(None) - ) - ) - - case ProximityInvite(charId, _name, _) => - Publish( - invitedPlayer, - SquadResponse.Membership( - SquadResponseType.Invite, - 0, - 0, - invitedPlayer, - Some(charId), - _name, - false, - Some(None) - ) - ) - - case _ => - log.warn(s"AddInviteAndRespond: can not parse discovered unhandled invitation type - $targetInvite") - } - } - } - - /** - * Enqueue a newly-submitted invitation object - * either as the active position or into the inactive positions - * and dispatch a response for any invitation object that is discovered. - * Implementation of a workflow. - * @see `AddInvite` - * @see `indirectInviteResp` - * @param targetInvite a comparison invitation object - * @param invitedPlayer the unique character identifier for the player being invited; - * in actuality, represents the player who will address the invitation object - * @param invitingPlayer the unique character identifier for the player who invited the former - * @param name a name to be used in message composition - */ - def AddInviteAndRespond(invitedPlayer: Long, targetInvite: Invitation, invitingPlayer: Long, name: String): Unit = { - InviteResponseTemplate(indirectInviteResp)( - targetInvite, - AddInvite(invitedPlayer, targetInvite), - invitedPlayer, - invitingPlayer, - name - ) - } - - /** - * Enqueue a newly-submitted invitation object - * either as the active position or into the inactive positions - * and dispatch a response for any invitation object that is discovered. - * Implementation of a workflow. - * @see `AddInvite` - * @see `altIndirectInviteResp` - * @param targetInvite a comparison invitation object - * @param invitedPlayer the unique character identifier for the player being invited - * @param invitingPlayer the unique character identifier for the player who invited the former - * @param name a name to be used in message composition - */ - def AltAddInviteAndRespond( - invitedPlayer: Long, - targetInvite: Invitation, - invitingPlayer: Long, - name: String - ): Unit = { - InviteResponseTemplate(altIndirectInviteResp)( - targetInvite, - AddInvite(invitedPlayer, targetInvite), - invitedPlayer, - invitingPlayer, - name - ) - } - - /** - * Select the next invitation object to be shifted into the active position.
- *
- * The determination for the active position is whether or not something is currently in the active position - * or whether some mechanism tried to shift invitation object into the active position - * but found nothing to shift. - * After handling of the previous invitation object has completed or finished, - * the temporary block on adding new invitations is removed - * and any queued inactive invitation on the head of the inactive queue is shifted into the active position. - * @see `NextInviteAndRespond` - * @param invitedPlayer the unique character identifier for the player being invited; - * in actuality, represents the player who will address the invitation object - * @return an optional invite; - * the invitation object in the active invite position; - * `None`, if not shifted into the active position - */ - def NextInvite(invitedPlayer: Long): Option[Invitation] = { - previousInvites.remove(invitedPlayer) - invites.get(invitedPlayer) match { - case None => - queuedInvites.get(invitedPlayer) match { - case Some(list) => - list match { - case Nil => - None - case x :: Nil => - invites(invitedPlayer) = x - queuedInvites.remove(invitedPlayer) - Some(x) - case x :: xs => - invites(invitedPlayer) = x - queuedInvites(invitedPlayer) = xs - Some(x) - } - - case None => - None - } - case Some(_) => - None - } - } - - /** - * Select the next invitation object to be shifted into the active position - * and dispatch a response for any invitation object that is discovered. - * @see `InviteResponseTemplate` - * @see `NextInvite` - * @param invitedPlayer the unique character identifier for the player being invited; - * in actuality, represents the player who will address the invitation object - * @return an optional invite; - * the invitation object in the active invite position; - * `None`, if not shifted into the active position - */ - def NextInviteAndRespond(invitedPlayer: Long): Unit = { - NextInvite(invitedPlayer) match { - case Some(invite) => - InviteResponseTemplate(indirectInviteResp)( - invite, - Some(invite), - invitedPlayer, - invite.InviterCharId, - invite.InviterName - ) + def SquadActionDefinitionCancelSquadSearch(charId: Long): Unit = { + searchData.remove(charId) match { case None => ; - } - } - - /** - * Remove any invitation object from the active position. - * Flag the temporary field to indicate that the active position, while technically available, - * should not yet have a new invitation object shifted into it yet. - * This is the "proper" way to demote invitation objects from the active position - * whether or not they are to be handled. - * @see `NextInvite` - * @see `NextInviteAndRespond` - * @param invitedPlayer the unique character identifier for the player being invited; - * in actuality, represents the player who will address the invitation object - * @return an optional invite; - * the invitation object formerly in the active invite position; - * `None`, if no invitation was in the active position - */ - def RemoveInvite(invitedPlayer: Long): Option[Invitation] = { - invites.remove(invitedPlayer) match { - case out @ Some(invite) => - previousInvites += invitedPlayer -> invite - out - case None => - None - } - } - - /** - * Remove all inactive invites. - * @param invitedPlayer the unique character identifier for the player being invited; - * in actuality, represents the player who will address the invitation object - * @return a list of the removed inactive invitation objects - */ - def RemoveQueuedInvites(invitedPlayer: Long): List[Invitation] = { - queuedInvites.remove(invitedPlayer) match { - case Some(_bidList) => _bidList - case None => Nil - } - } - - /** - * Remove all active invitation objects that are related to the particular squad and the particular role in the squad. - * Specifically used to safely disarm obsolete invitation objects related to the specific criteria. - * Affects only certain invitation object types. - * @see `RequestRole` - * @see `LookingForSquadRoleInvite` - * @see `RemoveInvite` - * @see `RemoveQueuedInvitesForSquadAndPosition` - * @param guid the squad identifier - * @param position the role position index - */ - def RemoveInvitesForSquadAndPosition(guid: PlanetSideGUID, position: Int): Unit = { - //eliminate active invites for this role - invites.collect { - case (charId, LookingForSquadRoleInvite(_, _, sguid, pos)) if sguid == guid && pos == position => - RemoveInvite(charId) - case (charId, RequestRole(_, sguid, pos)) if sguid == guid && pos == position => - RemoveInvite(charId) - } - RemoveQueuedInvitesForSquadAndPosition(guid, position) - } - - /** - * Remove all inactive invitation objects that are related to the particular squad and the particular role in the squad. - * Specifically used to safely disarm obsolete invitation objects by specific criteria. - * Affects only certain invitation object types. - * @see `RequestRole` - * @see `LookingForSquadRoleInvite` - * @see `RemoveInvitesForSquadAndPosition` - * @param guid the squad identifier - * @param position the role position index - */ - def RemoveQueuedInvitesForSquadAndPosition(guid: PlanetSideGUID, position: Int): Unit = { - //eliminate other invites for this role - queuedInvites.foreach { - case (charId, queue) => - val filtered = queue.filterNot { - case LookingForSquadRoleInvite(_, _, sguid, pos) => sguid == guid && pos == position - case RequestRole(_, sguid, pos) => sguid == guid && pos == position - case _ => false - } - if (filtered.isEmpty) { - queuedInvites.remove(charId) - } else if (queue.size != filtered.size) { - queuedInvites += charId -> filtered + case Some(data) => + SearchForSquadsResults(data).foreach { guid => + subs.Publish(charId, SquadResponse.SquadDecoration(guid, squadFeatures(guid).Squad)) } } } - /** - * Remove all active and inactive invitation objects that are related to the particular squad. - * Specifically used to safely disarm obsolete invitation objects by specific criteria. - * Affects all invitation object types and all data structures that deal with the squad. - * @see `RequestRole` - * @see `IndirectInvite` - * @see `LookingForSquadRoleInvite` - * @see `ProximityInvite` - * @see `RemoveInvite` - * @see `VacancyInvite` - * @param sguid the squad identifier - */ - def RemoveAllInvitesToSquad(sguid: PlanetSideGUID): Unit = { - //clean up invites - invites.collect { - case (id, VacancyInvite(_, _, guid)) if sguid == guid => RemoveInvite(id) - case (id, IndirectInvite(_, guid)) if sguid == guid => RemoveInvite(id) - case (id, LookingForSquadRoleInvite(_, _, guid, _)) if sguid == guid => RemoveInvite(id) - case (id, RequestRole(_, guid, _)) if sguid == guid => RemoveInvite(id) - case (id, ProximityInvite(_, _, guid)) if sguid == guid => RemoveInvite(id) + private def SearchForSquadsResults( + criteria: SearchCriteria, + guid: PlanetSideGUID + ): Option[PlanetSideGUID] = { + val squad = squadFeatures(guid).Squad + val positions = if (criteria.mode == SquadRequestAction.SearchMode.AnyPositions) { + //includes occupied positions and closed positions that retain assignment information + squad.Membership + } else { + squad.Membership.zipWithIndex.filter { case (_, b) => squad.Availability(b) }.map { _._1 } } - //tidy the queued invitations - queuedInvites.foreach { - case (id, queue) => - val filteredQueue = queue.filterNot { - case VacancyInvite(_, _, guid) => sguid == guid - case IndirectInvite(_, guid) => sguid == guid - case LookingForSquadRoleInvite(_, _, guid, _) => sguid == guid - case RequestRole(_, guid, _) => sguid == guid - case ProximityInvite(_, _, guid) => sguid == guid - case _ => false - } - if (filteredQueue.isEmpty) { - queuedInvites.remove(id) - } else if (filteredQueue.size != queue.size) { - queuedInvites.update(id, filteredQueue) - } - } - squadFeatures(sguid).ProxyInvites = Nil - squadFeatures(sguid).SearchForRole match { - case None => ; - case Some(_) => - squadFeatures(sguid).SearchForRole = None - } - continueToMonitorDetails.collect { - case (charId, guid) if sguid == guid => - continueToMonitorDetails.remove(charId) - } - } - - /** - * Remove all active and inactive invitation objects that are related to the particular player. - * Specifically used to safely disarm obsolete invitation objects by specific criteria. - * Affects all invitation object types and all data structures that deal with the player. - * @see `RequestRole` - * @see `IndirectInvite` - * @see `LookingForSquadRoleInvite` - * @see `RemoveInvite` - * @see `RemoveProximityInvites` - * @see `VacancyInvite` - * @param charId the player's unique identifier number - */ - def RemoveAllInvitesWithPlayer(charId: Long): Unit = { - RemoveInvite(charId) - invites.collect { - case (id, SpontaneousInvite(player)) if player.CharId == charId => RemoveInvite(id) - case (id, VacancyInvite(_charId, _, _)) if _charId == charId => RemoveInvite(id) - case (id, IndirectInvite(player, _)) if player.CharId == charId => RemoveInvite(id) - case (id, LookingForSquadRoleInvite(_charId, _, _, _)) if _charId == charId => RemoveInvite(id) - case (id, RequestRole(player, _, _)) if player.CharId == charId => RemoveInvite(id) - } - //tidy the queued invitations - queuedInvites.remove(charId) - queuedInvites.foreach { - case (id, queue) => - val filteredQueue = queue.filterNot { - case SpontaneousInvite(player) => player.CharId == charId - case VacancyInvite(player, _, _) => player == charId - case IndirectInvite(player, _) => player.CharId == charId - case LookingForSquadRoleInvite(player, _, _, _) => player == charId - case RequestRole(player, _, _) => player.CharId == charId - case _ => false - } - if (filteredQueue.isEmpty) { - queuedInvites.remove(id) - } else if (filteredQueue.size != queue.size) { - queuedInvites.update(id, filteredQueue) - } - } - continueToMonitorDetails.remove(charId) - RemoveProximityInvites(charId) - } - - /** - * Remove all active and inactive proximity squad invites related to the recruiter. - * @see `RemoveProximityInvites(Iterable[(Long, PlanetSideGUID)])` - * @param invitingPlayer the player who did the recruiting - * @return a list of all players (unique character identifier number and name) who had active proximity invitations - */ - def RemoveProximityInvites(invitingPlayer: Long): Iterable[(Long, String)] = { - //invites - val (removedInvites, out): (Iterable[(Long, PlanetSideGUID)], Iterable[(Long, String)]) = invites.collect { - case (id, ProximityInvite(inviterCharId, inviterName, squadGUID)) if inviterCharId == invitingPlayer => - RemoveInvite(id) - ((id, squadGUID), (id, inviterName)) - }.unzip - RemoveProximityInvites(removedInvites) - //queued - RemoveProximityInvites(queuedInvites.flatMap { - case (id: Long, inviteList: List[Invitation]) => - val (outList, inList) = inviteList.partition { - case ProximityInvite(inviterCharId, _, _) if inviterCharId == invitingPlayer => true - case _ => false - } - if (inList.isEmpty) { - queuedInvites.remove(id) - } else { - queuedInvites(id) = inList - } - outList.collect { case ProximityInvite(_, _, sguid: PlanetSideGUID) => id -> sguid } - }) - out.toSeq.distinct - } - - /** - * Remove all queued proximity squad invite information retained by the squad object. - * @see `RemoveProximityInvites(Long)` - * @see `SquadFeatures.ProxyInvites` - * @see `SquadFeatures.SearchForRole` - * @param list a list of players to squads with expected entry redundancy - */ - def RemoveProximityInvites(list: Iterable[(Long, PlanetSideGUID)]): Unit = { - val (_, squads) = list.unzip - squads.toSeq.distinct.foreach { squad => - squadFeatures.get(squad) match { - case Some(features) => - val out = list.collect { case (id, sguid) if sguid == squad => id }.toSeq - if ((features.ProxyInvites = features.ProxyInvites filterNot out.contains) isEmpty) { - features.SearchForRole = None + if ( + positions.nonEmpty && + (criteria.zoneId == 0 || criteria.zoneId == squad.ZoneId) && + (criteria.role.isEmpty || positions.exists(_.Role.equalsIgnoreCase(criteria.role))) && + (criteria.requirements.isEmpty || positions.exists { p => + val results = p.Requirements.intersect(criteria.requirements) + if (criteria.mode == SquadRequestAction.SearchMode.SomeCertifications) { + results.size > 1 + } else { + results == criteria.requirements } - case _ => ; - } + }) + ) { + Some(guid) + } else { + None } } - /** - * Remove all active and inactive proximity squad invites for a specific squad. - * @param guid the squad - * @return a list of all players (unique character identifier number and name) who had active proximity invitations - */ - def RemoveProximityInvites(guid: PlanetSideGUID): Iterable[(Long, String)] = { - //invites - val (removedInvites, out): (Iterable[PlanetSideGUID], Iterable[(Long, String)]) = invites.collect { - case (id, ProximityInvite(_, inviterName, squadGUID)) if squadGUID == guid => - RemoveInvite(id) - (squadGUID, (id, inviterName)) - }.unzip - removedInvites.foreach { sguid => - squadFeatures(sguid).ProxyInvites = Nil - squadFeatures(sguid).SearchForRole = None - } - //queued - queuedInvites.flatMap { - case (id: Long, inviteList: List[Invitation]) => - val (outList, inList) = inviteList.partition { - case ProximityInvite(_, _, squadGUID) if squadGUID == guid => true - case _ => false - } - if (inList.isEmpty) { - queuedInvites.remove(id) - } else { - queuedInvites(id) = inList - } - outList.collect { - case ProximityInvite(_, _, sguid: PlanetSideGUID) => - squadFeatures(sguid).ProxyInvites = Nil - squadFeatures(sguid).SearchForRole = None - } - } - out.toSeq.distinct + /** the following action can be performed by anyone */ + def SquadActionDefinitionDisplaySquad(tplayer: Player, guid: PlanetSideGUID): Unit = { + subs.MonitorSquadDetails += tplayer.CharId -> SquadSubscriptionEntity.MonitorEntry(guid) } - /** - * Resolve an invitation to a general, not guaranteed, position in someone else's squad. - * For the moment, just validate the provided parameters and confirm the eligibility of the user. - * @see `VacancyInvite` - * @param squad_guid the unique squad identifier number - * @param invitedPlayer the unique character identifier for the player being invited - * @param invitingPlayer the unique character identifier for the player who invited the former - * @param recruit the player being invited - * @return the squad object and a role position index, if properly invited; - * `None`, otherwise - */ - def HandleVacancyInvite( - squad_guid: PlanetSideGUID, - invitedPlayer: Long, - invitingPlayer: Long, - recruit: Player - ): Option[(Squad, Int)] = { - squadFeatures.get(squad_guid) match { + def SquadActionDefinitionSquadInitializationIssue(tplayer: Player, guid: PlanetSideGUID): Unit = { + //the following message is feedback from a specific client, awaiting proper initialization + //this tends to happen when the client receives listing details about a squad it has never known before + val reason = GetSquad(guid) match { case Some(features) => val squad = features.Squad - memberToSquad.get(invitedPlayer) match { - case Some(enrolledSquad) => - if (enrolledSquad eq squad) { - log.warn(s"HandleVacancyInvite: ${recruit.Name} is already a member of squad ${squad.Task}") - } else { - log.warn( - s"HandleVacancyInvite: ${recruit.Name} is a member of squad ${enrolledSquad.Task} and can not join squad ${squad.Task}" - ) - } - None - case _ => - HandleVacancyInvite(squad, invitedPlayer, invitingPlayer, recruit) - } - - case _ => - log.warn(s"HandleVacancyInvite: the squad #${squad_guid.guid} no longer exists") - None - } - } - - /** - * Resolve an invitation to a general, not guaranteed, position in someone else's squad.
- *
- * Originally, the instigating type of invitation object was a "`VacancyInvite`" - * which indicated a type of undirected invitation extended from the squad leader to another player - * but the resolution is generalized enough to suffice for a number of invitation objects. - * First, an actual position is determined; - * then, the squad is tested for recruitment conditions, - * including whether the person who solicited the would-be member is still the squad leader. - * If the recruitment is manual and the squad leader is not the same as the recruiting player, - * then the real squad leader is sent an indirect query regarding the player's eligibility. - * These `IndirectInvite` invitation objects also are handled by calls to `HandleVacancyInvite`. - * @see `AltAddInviteAndRespond` - * @see `IndirectInvite` - * @see `SquadFeatures::AutoApproveInvitationRequests` - * @see `VacancyInvite` - * @param squad the squad - * @param invitedPlayer the unique character identifier for the player being invited - * @param invitingPlayer the unique character identifier for the player who invited the former - * @param recruit the player being invited - * @return the squad object and a role position index, if properly invited; - * `None`, otherwise - */ - def HandleVacancyInvite( - squad: Squad, - invitedPlayer: Long, - invitingPlayer: Long, - recruit: Player - ): Option[(Squad, Int)] = { - //accepted an invitation to join an existing squad - squad.Membership.zipWithIndex.find({ - case (_, index) => - squad.isAvailable(index, recruit.avatar.certifications) - }) match { - case Some((_, line)) => - //position in squad found - val sguid = squad.GUID - val features = squadFeatures(sguid) - if (!features.AutoApproveInvitationRequests && squad.Leader.CharId != invitingPlayer) { - //the inviting player was not the squad leader and this decision should be bounced off the squad leader - AltAddInviteAndRespond( - squad.Leader.CharId, - IndirectInvite(recruit, sguid), - invitingPlayer, - name = "" - ) - debug(s"HandleVacancyInvite: ${recruit.Name} must await an invitation from the leader of squad ${squad.Task}") - None + if (squad.Faction != tplayer.Faction) { + s"about an enemy ${squad.Faction} squad" + } else if(!features.Listed) { + s"about a squad that may not yet be listed - ${squad.Task}" } else { - Some((squad, line)) + "for an unknown reason" } - case _ => - None + case None => + s"about a squad that does not exist - ${guid.guid}" } + log.warn(s"${tplayer.Name} has a potential squad issue; might be exchanging information $reason") } - /** - * An overloaded entry point to the functionality for handling one player requesting a specific squad role. - * @param bid a specific kind of `Invitation` object - * @param player the player who wants to join the squad - * @return `true`, if the player is not denied the possibility of joining the squad; - * `false`, otherwise, of it the squad does not exist - */ - def HandleRequestRole(bid: RequestRole, player: Player): Boolean = { - HandleRequestRole(bid, bid.squad_guid, player) - } - - /** - * An overloaded entry point to the functionality for handling indirection when messaging the squad leader about an invite. - * @param bid a specific kind of `Invitation` object - * @param player the player who wants to join the squad - * @return `true`, if the player is not denied the possibility of joining the squad; - * `false`, otherwise, of it the squad does not exist - */ - def HandleRequestRole(bid: IndirectInvite, player: Player): Boolean = { - HandleRequestRole(bid, bid.squad_guid, player) - } - - /** - * The functionality for handling indirection - * for handling one player requesting a specific squad role - * or when messaging the squad leader about an invite.
- *
- * At this point in the squad join process, the only consent required is that of the squad leader. - * An automatic consent flag exists on the squad; - * but, if that is not set, then the squad leader must be asked whether or not to accept or to reject the recruit. - * If the squad leader changes in the middle or the latter half of the process, - * the invitation may still fail even if the old squad leader accepts. - * If the squad leader changes in the middle of the latter half of the process, - * the inquiry might be posed again of the new squad leader, of whether to accept or to reject the recruit. - * @param bid the `Invitation` object that was the target of this request - * @param guid the unique squad identifier number - * @param player the player who wants to join the squad - * @return `true`, if the player is not denied the possibility of joining the squad; - * `false`, otherwise, of it the squad does not exist - */ - def HandleRequestRole(bid: Invitation, guid: PlanetSideGUID, player: Player): Boolean = { - squadFeatures.get(guid) match { + def CleanUpSquadFeatures(removed: List[Long], guid: PlanetSideGUID, position: Int): Unit = { + GetSquad(guid) match { case Some(features) => - val leaderCharId = features.Squad.Leader.CharId - if (features.AutoApproveInvitationRequests) { - self ! SquadServiceMessage( - player, - Zone.Nowhere, - SquadAction.Membership(SquadRequestType.Accept, leaderCharId, None, "", None) - ) - } else { - Publish(leaderCharId, SquadResponse.WantsSquadPosition(leaderCharId, player.Name)) + features.ProxyInvites = features.ProxyInvites.filterNot(removed.contains) + if (features.ProxyInvites.isEmpty) { + features.SearchForRole = None } - true - case _ => - //squad is missing - log.error(s"Attempted to process ${bid.InviterName}'s bid for a position in a squad that does not exist") - false - } - } - - /** - * Pertains to the original message of squad synchronicity sent to the squad leader by the server under specific conditions. - * The initial formation of a squad of two players is the most common expected situation. - * While the underlying flag is normally only set once, its state can be reset and triggered anew if necessary. - * @see `Publish` - * @see ``ResetAll - * @see `SquadResponse.AssociateWithSquad` - * @see `SquadResponse.Detail` - * @see `SquadService.PublishFullDetails` - * @param squad the squad - */ - def InitialAssociation(squad: Squad): Unit = { - val guid = squad.GUID - if (squadFeatures(guid).InitialAssociation) { - squadFeatures(guid).InitialAssociation = false - val charId = squad.Leader.CharId - Publish(charId, SquadResponse.AssociateWithSquad(guid)) - Publish(charId, SquadResponse.Detail(guid, SquadService.PublishFullDetails(squad))) + case None => ; } } @@ -3000,10 +857,11 @@ class SquadService extends Actor { * @param player the player who would become the squad leader * @return the squad that has been created */ - def StartSquad(player: Player): Squad = { + def StartSquad(player: Player): SquadFeatures = { val faction = player.Faction val name = player.Name - val squad = new Squad(GetNextSquadId(), faction) + val sguid = GetNextSquadId() + val squad = new Squad(sguid, faction) val leadPosition = squad.Membership(0) leadPosition.Name = name leadPosition.CharId = player.CharId @@ -3011,108 +869,46 @@ class SquadService extends Actor { leadPosition.Armor = StatConverter.Health(player.Armor, player.MaxArmor, min = 1, max = 64) leadPosition.Position = player.Position leadPosition.ZoneId = 1 - squadFeatures += squad.GUID -> new SquadFeatures(squad).Start - memberToSquad += squad.Leader.CharId -> squad - info(s"$name-$faction has created a new squad (#${squad.GUID.guid})") - squad + leadPosition.Certifications = player.avatar.certifications + val features = new SquadFeatures(squad).Start + squadFeatures += sguid -> features + memberToSquad += squad.Leader.CharId -> sguid + info(s"$name-$faction has created a new squad (#${sguid.guid})") + features } /** * Behaviors and exchanges necessary to complete the fulfilled recruitment process for the squad role.
*
* This operation is fairly safe to call whenever a player is to be inducted into a squad. - * The aforementioned player must have a callback retained in `UserEvents` + * The aforementioned player must have a callback retained in `subs.UserEvents` * and conditions imposed by both the role and the player must be satisfied. - * @see `InitialAssociation` - * @see `InitSquadDetail` - * @see `InitWaypoints` - * @see `Publish` - * @see `RemoveAllInvitesWithPlayer` - * @see `SquadDetail` - * @see `SquadInfo` - * @see `SquadPositionDetail` - * @see `SquadPositionEntry` - * @see `SquadResponse.Join` - * @see `StatConverter.Health` - * @see `UpdateSquadListWhenListed` + * @see `CleanUpAllInvitesWithPlayer` + * @see `Squad.isAvailable` + * @see `Squad.Switchboard` + * @see `SquadSubscriptionEntity.MonitorSquadDetails` + * @see `SquadSubscriptionEntity.Publish` + * @see `SquadSubscriptionEntity.Join` + * @see `SquadSubscriptionEntity.UserEvents` * @param player the new squad member; * this player is NOT the squad leader - * @param squad the squad the player is joining + * @param features the squad the player is joining * @param position the squad member role that the player will be filling * @return `true`, if the player joined the squad in some capacity; * `false`, if the player did not join the squad or is already a squad member */ - def JoinSquad(player: Player, squad: Squad, position: Int): Boolean = { + def JoinSquad(player: Player, features: SquadFeatures, position: Int): Boolean = { val charId = player.CharId - val role = squad.Membership(position) - UserEvents.get(charId) match { - case Some(events) if squad.Leader.CharId != charId && squad.isAvailable(position, player.avatar.certifications) => - info(s"${player.Name}-${player.Faction} joins position ${position+1} of squad #${squad.GUID.guid} - ${squad.Task}") - role.Name = player.Name - role.CharId = charId - role.Health = StatConverter.Health(player.Health, player.MaxHealth, min = 1, max = 64) - role.Armor = StatConverter.Health(player.Armor, player.MaxArmor, min = 1, max = 64) - role.Position = player.Position - role.ZoneId = 1 - memberToSquad(charId) = squad - - continueToMonitorDetails.remove(charId) - RemoveAllInvitesWithPlayer(charId) - InitialAssociation(squad) - Publish(charId, SquadResponse.AssociateWithSquad(squad.GUID)) - val features = squadFeatures(squad.GUID) - val size = squad.Size - if (size == 2) { - //first squad member after leader; both members fully initialize - val (memberCharIds, indices) = squad.Membership.zipWithIndex - .filterNot { case (member, _) => member.CharId == 0 } - .toList - .unzip { case (member, index) => (member.CharId, index) } - val toChannel = features.ToChannel - memberCharIds - .map { id => (id, UserEvents.get(id)) } - .collect { case (id, Some(sub)) => - SquadEvents.subscribe(sub, s"/$toChannel/Squad") - Publish( - id, - SquadResponse.Join( - squad, - indices.filterNot(_ == position) :+ position, - toChannel - ) - ) - InitWaypoints(id, squad.GUID) - } - //fully update for all users - InitSquadDetail(squad) - } else { - //joining an active squad; everybody updates differently - val toChannel = features.ToChannel - //new member gets full squad UI updates - Publish( - charId, - SquadResponse.Join( - squad, - position +: squad.Membership.zipWithIndex - .collect({ case (member, index) if member.CharId > 0 => index }) - .filterNot(_ == position) - .toList, - toChannel - ) - ) - //other squad members see new member joining the squad - Publish(toChannel, SquadResponse.Join(squad, List(position), "")) - InitWaypoints(charId, squad.GUID) - InitSquadDetail(squad.GUID, Seq(charId), squad) - UpdateSquadDetail( - squad.GUID, - SquadDetail().Members( - List(SquadPositionEntry(position, SquadPositionDetail().CharId(charId).Name(player.Name))) - ) - ) - SquadEvents.subscribe(events, s"/$toChannel/Squad") - } - UpdateSquadListWhenListed(features, SquadInfo().Size(size)) + val squad = features.Squad + subs.UserEvents.get(charId) match { + case Some(events) + if !memberToSquad.contains(charId) && + squad.Leader.CharId != charId && + squad.isAvailable(position, player.avatar.certifications) => + memberToSquad(charId) = squad.GUID + subs.MonitorSquadDetails.subtractOne(charId) + invitations.handleCleanup(charId) + features.Switchboard ! SquadSwitchboard.Join(player, position, events) true case _ => false @@ -3128,11 +924,11 @@ class SquadService extends Actor { * `false`, otherwise */ def EnsureEmptySquad(charId: Long): Boolean = { - memberToSquad.get(charId) match { + GetParticipatingSquad(charId) match { case None => true - case Some(squad) if squad.Size == 1 => - CloseSquad(squad) + case Some(features) if features.Squad.Size == 1 => + CloseSquad(features.Squad) true case _ => log.warn("EnsureEmptySquad: the invited player is already a member of a squad and can not join a second one") @@ -3143,78 +939,19 @@ class SquadService extends Actor { /** * Behaviors and exchanges necessary to undo the recruitment process for the squad role. * @see `PanicLeaveSquad` - * @see `Publish` + * @see `SquadSubscriptionEntity.Publish` * @param charId the player - * @param squad the squad + * @param features the squad * @return `true`, if the player, formerly a normal member of the squad, has been ejected from the squad; * `false`, otherwise */ - def LeaveSquad(charId: Long, squad: Squad): Boolean = { + def LeaveSquad(charId: Long, features: SquadFeatures): Boolean = { + val squad = features.Squad val membership = squad.Membership.zipWithIndex membership.find { case (_member, _) => _member.CharId == charId } match { - case data @ Some((us, index)) if squad.Leader.CharId != charId => - PanicLeaveSquad(charId, squad, data) - //member leaves the squad completely (see PanicSquadLeave) - Publish( - charId, - SquadResponse.Leave( - squad, - (charId, index) +: membership.collect { - case (_member, _index) if _member.CharId > 0 && _member.CharId != charId => (_member.CharId, _index) - }.toList - ) - ) - SquadEvents.unsubscribe(UserEvents(charId), s"/${squadFeatures(squad.GUID).ToChannel}/Squad") - info(s"${us.Name} has left squad #${squad.GUID.guid}") - true - case _ => - false - } - } - - /** - * Behaviors and exchanges necessary to undo the recruitment process for the squad role.
- *
- * The complement of the prior `LeaveSquad` method. - * This method deals entirely with other squad members observing the given squad member leaving the squad - * while the other method handles messaging only for the squad member who is leaving. - * The distinction is useful when unsubscribing from this service, - * as the `ActorRef` object used to message the player's client is no longer reliable - * and has probably ceased to exist. - * @see `LeaveSquad` - * @see `Publish` - * @see `SquadDetail` - * @see `SquadInfo` - * @see `SquadPositionDetail` - * @see `SquadPositionEntry` - * @see `SquadResponse.Leave` - * @see `UpdateSquadDetail` - * @see `UpdateSquadListWhenListed` - * @param charId the player - * @param squad the squad - * @param entry a paired membership role with its index in the squad - * @return if a role/index pair is provided - */ - def PanicLeaveSquad(charId: Long, squad: Squad, entry: Option[(Member, Int)]): Boolean = { - entry match { - case Some((member, index)) => - info(s"${member.Name}-${squad.Faction} has left squad #${squad.GUID.guid} - ${squad.Task}") - val entry = (charId, index) - //member leaves the squad completely + case Some(_) if squad.Leader.CharId != charId => memberToSquad.remove(charId) - member.Name = "" - member.CharId = 0 - //other squad members see the member leaving - squadFeatures.get(squad.GUID) match { - case Some(features) => - Publish(features.ToChannel, SquadResponse.Leave(squad, List(entry)), Seq(charId)) - UpdateSquadListWhenListed(features, SquadInfo().Size(squad.Size)) - case None => ; - } - UpdateSquadDetail( - squad.GUID, - SquadDetail().Members(List(SquadPositionEntry(index, SquadPositionDetail().Player(char_id = 0, name = "")))) - ) + features.Switchboard ! SquadSwitchboard.Leave(charId) true case _ => false @@ -3227,28 +964,28 @@ class SquadService extends Actor { * will still leave the squad, but will not attempt to send feedback to the said unreachable client. * If the player is in the process of unsubscribing from the service, * the no-messaging pathway is useful to avoid accumulating dead letters. - * @see `Publish` - * @see `RemoveAllInvitesToSquad` + * @see `CleanUpAllInvitesToSquad` * @see `SquadDetail` + * @see `SquadSubscriptionEntity.Publish` * @see `TryResetSquadId` * @see `UpdateSquadList` * @param squad the squad */ def CloseSquad(squad: Squad): Unit = { val guid = squad.GUID - RemoveAllInvitesToSquad(guid) val membership = squad.Membership.zipWithIndex val (updateMembers, updateIndices) = membership.collect { case (member, index) if member.CharId > 0 => - ((member, member.CharId, index, UserEvents.get(member.CharId)), (member.CharId, index)) + ((member, member.CharId, index, subs.UserEvents.get(member.CharId)), (member.CharId, index)) }.unzip val updateIndicesList = updateIndices.toList val completelyBlankSquadDetail = SquadDetail().Complete val features = squadFeatures(guid) val channel = s"/${features.ToChannel}/Squad" if (features.Listed) { - Publish(squad.Leader.CharId, SquadResponse.SetListSquad(PlanetSideGUID(0))) + subs.Publish(squad.Leader.CharId, SquadResponse.SetListSquad(PlanetSideGUID(0))) } + invitations.handleClosingSquad(features) updateMembers .foreach { case (member, charId, _, None) => @@ -3259,8 +996,8 @@ class SquadService extends Actor { memberToSquad.remove(charId) member.Name = "" member.CharId = 0L - SquadEvents.unsubscribe(actor, channel) - Publish( + subs.SquadEvents.unsubscribe(actor, channel) + subs.Publish( charId, SquadResponse.Leave( squad, @@ -3269,13 +1006,10 @@ class SquadService extends Actor { } :+ (charId, index) //we need to be last ) ) - Publish(charId, SquadResponse.AssociateWithSquad(PlanetSideGUID(0))) - Publish(charId, SquadResponse.Detail(PlanetSideGUID(0), completelyBlankSquadDetail)) + subs.Publish(charId, SquadResponse.IdentifyAsSquadLeader(PlanetSideGUID(0))) + subs.Publish(charId, SquadResponse.Detail(PlanetSideGUID(0), completelyBlankSquadDetail)) } - UpdateSquadListWhenListed( - squadFeatures.remove(guid).get.Stop, - None - ) + UpdateSquadListWhenListed(features.Stop, None) } /** @@ -3283,19 +1017,20 @@ class SquadService extends Actor { * Essentially, perform the same operations as `CloseSquad` * but treat the process as if the squad is being disbanded in terms of messaging. * @see `PanicDisbandSquad` - * @see `Publish` * @see `SquadResponse.Membership` - * @param squad the squad + * @see `SquadSubscriptionEntity.Publish` + * @param features the squad */ - def DisbandSquad(squad: Squad): Unit = { + def DisbandSquad(features: SquadFeatures): Unit = { + val squad = features.Squad val leader = squad.Leader.CharId PanicDisbandSquad( - squad, + features, squad.Membership.collect { case member if member.CharId > 0 && member.CharId != leader => member.CharId } ) //the squad is being disbanded, the squad events channel is also going away; use cached character ids info(s"Squad #${squad.GUID.guid} has been disbanded.") - Publish(leader, SquadResponse.Membership(SquadResponseType.Disband, 0, 0, leader, None, "", true, Some(None))) + subs.Publish(leader, SquadResponse.Membership(SquadResponseType.Disband, 0, 0, leader, None, "", unk5=true, Some(None))) } /** @@ -3308,136 +1043,23 @@ class SquadService extends Actor { * and has probably ceased to exist. * @see `CloseSquad` * @see `DisbandSquad` - * @see `Publish` * @see `SquadResponse.Membership` - * @param squad the squad + * @see `SquadResponseType` + * @see `SquadSubscriptionEntity.Publish` + * @param features the squad * @param membership the unique character identifier numbers of the other squad members * @return if a role/index pair is provided */ - def PanicDisbandSquad(squad: Squad, membership: Iterable[Long]): Unit = { + def PanicDisbandSquad(features: SquadFeatures, membership: Iterable[Long]): Unit = { + val squad = features.Squad + val leader = squad.Leader.CharId CloseSquad(squad) - membership.foreach { charId => - Publish(charId, SquadResponse.Membership(SquadResponseType.Disband, 0, 0, charId, None, "", false, Some(None))) - } - lazeIndices = lazeIndices.filterNot { data => membership.toSeq.contains(data.charId) } - } - - /** - * Move one player into one squad role and, - * if encountering a player already recruited to the destination role, - * swap that other player into the first player's position. - * If no encounter, just blank the original role. - * @see `AssignSquadMemberToRole` - * @see `SelectRoleForYourself` - * @param toMember the squad role where the player is being placed - * @param fromMember the squad role where the player is being encountered; - * if a conflicting player is discovered, swap that player into `fromMember` - */ - def SwapMemberPosition(toMember: Member, fromMember: Member): Unit = { - val (name, charId, zoneId, pos, health, armor) = - (fromMember.Name, fromMember.CharId, fromMember.ZoneId, fromMember.Position, fromMember.Health, fromMember.Armor) - if (toMember.CharId > 0) { - fromMember.Name = toMember.Name - fromMember.CharId = toMember.CharId - fromMember.ZoneId = toMember.ZoneId - fromMember.Position = toMember.Position - fromMember.Health = toMember.Health - fromMember.Armor = toMember.Armor - } else { - fromMember.Name = "" - fromMember.CharId = 0L - } - toMember.Name = name - toMember.CharId = charId - toMember.ZoneId = zoneId - toMember.Position = pos - toMember.Health = health - toMember.Armor = armor - } - - /** - * Display the indicated waypoint.
- *
- * Despite the name, no waypoints are actually "added." - * All of the waypoints constantly exist as long as the squad to which they are attached exists. - * They are merely "activated" and "deactivated." - * No waypoint is ever remembered for the laze-indicated target. - * @see `SquadWaypointRequest` - * @see `WaypointInfo` - * @param guid the squad's unique identifier - * @param waypointType the type of the waypoint - * @param info information about the waypoint, as was reported by the client's packet - * @return the waypoint data, if the waypoint type is changed - */ - def AddWaypoint( - guid: PlanetSideGUID, - waypointType: SquadWaypoint, - info: WaypointInfo - ): Option[WaypointData] = { - squadFeatures.get(guid) match { - case Some(features) => - features.Waypoints.lift(waypointType.value) match { - case Some(point) => - point.zone_number = info.zone_number - point.pos = info.pos - Some(point) - case None => - log.error(s"no squad waypoint $waypointType found") - None - } - case None => - log.error(s"no squad waypoint $waypointType found") - None - } - } - - /** - * Hide the indicated waypoint. - * Unused waypoints are marked by having a non-zero z-coordinate.
- *
- * Despite the name, no waypoints are actually "removed." - * All of the waypoints constantly exist as long as the squad to which they are attached exists. - * They are merely "activated" and "deactivated." - * @param guid the squad's unique identifier - * @param waypointType the type of the waypoint - */ - def RemoveWaypoint(guid: PlanetSideGUID, waypointType: SquadWaypoint): Unit = { - squadFeatures.get(guid) match { - case Some(features) => - features.Waypoints.lift(waypointType.value) match { - case Some(point) => - point.pos = Vector3.z(1) - case None => - log.warn(s"no squad waypoint $waypointType found") - } - case _ => - log.warn(s"no squad #$guid found") - } - } - - /** - * Dispatch all of the information about a given squad's waypoints to a user. - * @param toCharId the player to whom the waypoint data will be dispatched - * @param guid the squad's unique identifier - */ - def InitWaypoints(toCharId: Long, guid: PlanetSideGUID): Unit = { - squadFeatures.get(guid) match { - case Some(features) => - val squad = features.Squad - val vz1 = Vector3.z(value = 1) - val list = features.Waypoints - Publish( - toCharId, - SquadResponse.InitWaypoints( - squad.Leader.CharId, - list.zipWithIndex.collect { - case (point, index) if point.pos != vz1 => - (SquadWaypoint(index), WaypointInfo(point.zone_number, point.pos), 1) - } - ) - ) - case None => ; - } + //alert former members and anyone watching this squad for updates + (membership.filterNot(_ == leader) ++ subs.PublishToMonitorTargets(squad.GUID, Nil)) + .toSet + .foreach { charId : Long => + subs.Publish(charId, SquadResponse.Membership(SquadResponseType.Disband, 0, 0, charId, None, "", unk5=false, Some(None))) + } } /** @@ -3455,45 +1077,65 @@ class SquadService extends Actor { * @param sender the `ActorRef` associated with this character */ def LeaveService(charId: Long, sender: ActorRef): Unit = { - refused.remove(charId) - continueToMonitorDetails.remove(charId) - RemoveAllInvitesWithPlayer(charId) + subs.MonitorSquadDetails.subtractOne(charId) + invitations.handleLeave(charId) val pSquadOpt = GetParticipatingSquad(charId) pSquadOpt match { //member of the squad; leave the squad - case Some(squad) => + case Some(features) => + val squad = features.Squad val size = squad.Size - UserEvents.remove(charId) match { + subs.UserEvents.remove(charId) match { case Some(events) => - SquadEvents.unsubscribe(events, s"/${squadFeatures(squad.GUID).ToChannel}/Squad") + subs.SquadEvents.unsubscribe(events, s"/${features.ToChannel}/Squad") case _ => ; } - GetLeadingSquad(charId, pSquadOpt) match { - case Some(_) => - //leader of a squad; the squad will be disbanded - PanicDisbandSquad( - squad, - squad.Membership.collect { case member if member.CharId > 0 && member.CharId != charId => member.CharId } - ) - case None if size == 2 => - //one of the last two members of a squad; the squad will be disbanded - PanicDisbandSquad( - squad, - squad.Membership.collect { case member if member.CharId > 0 && member.CharId != charId => member.CharId } - ) - case None => - //not the leader of the squad; tell other members that we are leaving - PanicLeaveSquad( - charId, - squad, - squad.Membership.zipWithIndex.find { case (_member, _) => _member.CharId == charId } - ) + if (size > 2) { + GetLeadingSquad(charId, pSquadOpt) match { + case Some(_) => + //leader of a squad; search for a suitable substitute leader + squad.Membership.drop(1).find { _.CharId > 0 } match { + case Some(member) => + //leader was shifted into a subordinate position and will retire from duty + SquadActionMembershipPromote( + charId, + member.CharId, + features, + SquadServiceMessage(null, null, SquadAction.Membership(SquadRequestType.Promote, charId, Some(member.CharId), "", None)), + Default.Actor + ) + LeaveSquad(charId, features) + case _ => + //the squad will be disbanded + PanicDisbandSquad( + features, + squad.Membership.collect { case member if member.CharId > 0 && member.CharId != charId => member.CharId } + ) + } + case None => + //not the leader of a full squad; tell other members that we are leaving + SquadSwitchboard.PanicLeaveSquad( + charId, + features, + squad.Membership.zipWithIndex.find { case (_member, _) => _member.CharId == charId }, + subs, + self, + log + ) + } + } else { + //with only two members before our leave, the squad will be disbanded + PanicDisbandSquad( + features, + squad.Membership.collect { case member if member.CharId > 0 && member.CharId != charId => member.CharId } + ) } case None => - //not a member of any squad; nothing to do here - UserEvents.remove(charId) + //not a member of any squad; nothing really to do here + subs.UserEvents.remove(charId) } - SquadEvents.unsubscribe(sender) //just to make certain + subs.SquadEvents.unsubscribe(sender) //just to make certain + searchData.remove(charId) TryResetSquadId() } @@ -3521,9 +1163,8 @@ class SquadService extends Actor { * these "changes" do not have to reflect the actual squad but are related to the contents of the message */ private def UpdateSquadListWhenListed(features: SquadFeatures, changes: Option[SquadInfo]): Unit = { - val squad = features.Squad if (features.Listed) { - UpdateSquadList(squad, changes) + UpdateSquadList(features, changes) } } @@ -3554,11 +1195,12 @@ class SquadService extends Actor { * @see `SquadResponse.InitList` * @see `SquadResponse.UpdateList` * @see `SquadService.SquadList.Publish` - * @param squad the squad + * @param features the squad * @param changes the optional highlighted aspects of the squad; * these "changes" do not have to reflect the actual squad but are related to the contents of the message */ - def UpdateSquadList(squad: Squad, changes: Option[SquadInfo]): Unit = { + def UpdateSquadList(features: SquadFeatures, changes: Option[SquadInfo]): Unit = { + val squad = features.Squad val guid = squad.GUID val faction = squad.Faction val factionListings = publishedLists(faction) @@ -3568,244 +1210,160 @@ class SquadService extends Actor { changes match { case Some(changedFields) => //squad information update - Publish(faction, SquadResponse.UpdateList(Seq((index, changedFields)))) + subs.Publish(faction, SquadResponse.UpdateList(Seq((index, changedFields)))) + ApplySquadDecorationToEntry(faction, guid, squad) case None => //remove squad from listing factionListings.remove(index) - //Publish(faction, SquadResponse.RemoveFromList(Seq(index))) - Publish(faction, SquadResponse.InitList(PublishedLists(factionListings))) + subs.Publish(faction, SquadResponse.InitList(PublishedLists(factionListings.flatMap { GetSquad }))) } case None => //first time being published factionListings += guid - Publish(faction, SquadResponse.InitList(PublishedLists(factionListings))) + subs.Publish(faction, SquadResponse.InitList(PublishedLists(factionListings.flatMap { GetSquad }))) + ApplySquadDecorationToEntry(faction, guid, squad) } } - /** - * Dispatch a message entailing the composition of this squad. - * This is considered the first time this information will be dispatched to any relevant observers - * so the details of the squad will be updated in full and be sent to all relevant observers, - * namely, all the occupants of the squad. - * External observers are ignored. - * @see `InitSquadDetail(PlanetSideGUID, Iterable[Long], Squad)` - * @param squad the squad - */ - def InitSquadDetail(squad: Squad): Unit = { - InitSquadDetail( - squad.GUID, - squad.Membership.collect { case member if member.CharId > 0 => member.CharId }, - squad - ) - } - - /** - * 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.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.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.PublishFullDetails` - * @see `UpdateSquadDetail(PlanetSideGUID, PlanetSideGUID, List[Long], SquadDetail)` - * @param squad the squad - */ - def UpdateSquadDetail(squad: Squad): Unit = { - UpdateSquadDetail( - squad.GUID, - squad.GUID, - Nil, - SquadService.PublishFullDetails(squad) - ) - } - - /** - * Send a message entailing the strategic information and the composition of the squad to the existing members of the squad. - * Rather than using the squad's existing unique identifier number, - * a meaningful substitute identifier will be employed in the message. - * The "meaningful substitute" is usually `PlanetSideGUID(0)` - * which indicates the local non-squad squad data on the client of a squad leader. - * @see `SquadService.PublishFullDetails` - * @see `UpdateSquadDetail(PlanetSideGUID, PlanetSideGUID, List[Long], SquadDetail)` - * @param squad the squad - */ - def UpdateSquadDetail(guid: PlanetSideGUID, squad: Squad): Unit = { - UpdateSquadDetail( - guid, - squad.GUID, - Nil, - SquadService.PublishFullDetails(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 - * @param details the squad details to be included in the message - */ - def UpdateSquadDetail(guid: PlanetSideGUID, details: SquadDetail): Unit = { - UpdateSquadDetail( - guid, - guid, - Nil, - details - ) - } - - /** - * Send a message entailing some of the strategic information and the composition to the existing members of the squad. - * Also send the same information to any users who are watching the squad, potentially for want to join it. - * The squad-specific message is contingent on finding the squad's features using the unique identifier number - * and, from that, reporting to the specific squad's messaging channel. - * Anyone watching the squad will always be updated the given details. - * @see `DisplaySquad` - * @see `Publish` - * @see `SquadDetail` - * @see `SquadResponse.Detail` - * @param guid the unique squad identifier number to be used for the squad detail message - * @param toGuid the unique squad identifier number indicating the squad broadcast channel name - * @param excluding the explicit unique character identifier numbers of individuals who should not receive the message - * @param details the squad details to be included in the message - */ - def UpdateSquadDetail( - guid: PlanetSideGUID, - toGuid: PlanetSideGUID, - excluding: Iterable[Long], - details: SquadDetail - ): Unit = { - val output = SquadResponse.Detail(guid, details) - squadFeatures.get(toGuid) match { - case Some(features) => - Publish(features.ToChannel, output, excluding) - case _ => ; - } - continueToMonitorDetails - .collect { - case (charId: Long, sguid: PlanetSideGUID) if sguid == guid && !excluding.exists(_ == charId) => - Publish(charId, output, Nil) - } - } - /** * Transform a list of squad unique identifiers into a list of `SquadInfo` objects for updating the squad list window. * @param faction the faction to which the squads belong * @return a `Vector` of transformed squad data */ def PublishedLists(faction: PlanetSideEmpire.Type): Vector[SquadInfo] = { - PublishedLists(publishedLists(faction)) + PublishedLists(publishedLists(faction).flatMap { GetSquad }) } /** * Transform a list of squad unique identifiers into a list of `SquadInfo` objects for updating the squad list window. - * @param guids the list of squad unique identifier numbers + * @param squads the list of squads * @return a `Vector` of transformed squad data */ - def PublishedLists(guids: Iterable[PlanetSideGUID]): Vector[SquadInfo] = { - guids.map { guid => SquadService.PublishFullListing(squadFeatures(guid).Squad) }.toVector + def PublishedLists(squads: Iterable[SquadFeatures]): Vector[SquadInfo] = { + squads.map { features => SquadService.PublishFullListing(features.Squad) }.toVector + } + + /** + * Squad decoration are the colors applied to entries in the squad listing based on individual assessments. + * Apply these colors to one squad at a time. + * This sends out the least amount of messages - + * one for the whole faction and one message for each search for which this squad is a positive result. + * @param faction empire whose squad is being decorated + * @param guid the squad's identifier + * @param squad the squad + */ + def ApplySquadDecorationToEntry( + faction: PlanetSideEmpire.Value, + guid: PlanetSideGUID, + squad: Squad + ): Unit = { + //search result decoration (per user) + val result = SquadResponse.SquadSearchResults(List(guid)) + val excluded = searchData.collect { + case (charId: Long, data: SearchCriteria) + if data.faction == faction && SearchForSquadsResults(data, guid).nonEmpty => + subs.Publish(charId, result) + (charId, charId) + }.keys.toList + //normal decoration (whole faction, later excluding the former users) + subs.Publish(faction, SquadResponse.SquadDecoration(guid, squad), excluded) + } + + def ApplySquadDecorationToEntriesForUser( + faction: PlanetSideEmpire.Value, + targetCharId: Long + ): Unit = { + publishedLists(faction) + .flatMap { GetSquad } + .foreach { features => + val squad = features.Squad + val guid = squad.GUID + val result = SquadResponse.SquadSearchResults(List(guid)) + if (searchData.get(targetCharId) match { + case Some(data) + if data.faction == faction && SearchForSquadsResults(data, guid).nonEmpty => + subs.Publish(targetCharId, result) + false + case _ => + true + }) { + subs.Publish(targetCharId, SquadResponse.SquadDecoration(guid, squad)) + } + } } } object SquadService { - private case class BlankLazeWaypoints() - - private case class LazeWaypointData(charId: Long, waypointType: Int, endTime: Long) - - /** - * Information necessary to display a specific map marker. - */ - class WaypointData() { - var zone_number: Int = 1 - var pos: Vector3 = Vector3.z(1) //a waypoint with a non-zero z-coordinate will flag as not getting drawn - } + final case class PerformStartSquad(player: Player) - object WaypointData{ - def apply(zone_number: Int, pos: Vector3): WaypointData = { - val data = new WaypointData() - data.zone_number = zone_number - data.pos = pos - data + final case class PerformJoinSquad(player: Player, features: SquadFeatures, position: Int) + + final case class ResendActiveInvite(charId: Long) + + /** + * A message to indicate that the squad list needs to update for the clients. + * @param features the squad + * @param changes optional changes to the squad details + */ + final case class UpdateSquadList(features: SquadFeatures, changes: Option[SquadInfo]) + /** + * A message to indicate that the squad list needs to update for the clients, + * but only if that squad is already listed. + * @param features the squad + * @param changes the changes to the squad details + */ + final case class UpdateSquadListWhenListed(features: SquadFeatures, changes: SquadInfo) + + private case class SearchCriteria( + faction: PlanetSideEmpire.Value, + zoneId: Int, + role: String, + requirements: Set[Certification], + mode: SquadRequestAction.SearchMode.Value + ) + private object SearchCriteria { + def apply( + faction: PlanetSideEmpire.Value, + criteria: SquadRequestAction.SearchForSquadsWithParticularRole + ): SearchCriteria = { + SearchCriteria(faction, criteria.zone_id, criteria.role, criteria.requirements, criteria.mode) } } /** - * The base of all objects that exist for the purpose of communicating invitation from one player to the next. - * @param char_id the inviting player's unique identifier number - * @param name the inviting player's name + * Move one player into one squad role and, + * if encountering a player already recruited to the destination role, + * swap that other player into the first player's position. + * If no encounter, just blank the original role. + * Certification requirements for the role are not respected. + * @see `AssignSquadMemberToRole` + * @see `SelectRoleForYourself` + * @param toMember the squad role where the player is being placed + * @param fromMember the squad role where the player is being encountered; + * if a conflicting player is discovered, swap that player into `fromMember` */ - abstract class Invitation(char_id: Long, name: String) { - def InviterCharId: Long = char_id - def InviterName: String = name + def SwapMemberPosition(toMember: Member, fromMember: Member): Unit = { + val (name, charId, zoneId, pos, health, armor) = + (fromMember.Name, fromMember.CharId, fromMember.ZoneId, fromMember.Position, fromMember.Health, fromMember.Armor) + if (toMember.CharId > 0) { + fromMember.Name = toMember.Name + fromMember.CharId = toMember.CharId + fromMember.ZoneId = toMember.ZoneId + fromMember.Position = toMember.Position + fromMember.Health = toMember.Health + fromMember.Armor = toMember.Armor + } else { + fromMember.Name = "" + fromMember.CharId = 0L + } + toMember.Name = name + toMember.CharId = charId + toMember.ZoneId = zoneId + toMember.Position = pos + toMember.Health = health + toMember.Armor = armor } - /** - * Utilized when one player attempts to join an existing squad in a specific role. - * Accessed by the joining player from the squad detail window. - * @param player the player who requested the role - * @param squad_guid the squad with the role - * @param position the index of the role - */ - final case class RequestRole(player: Player, squad_guid: PlanetSideGUID, position: Int) - extends Invitation(player.CharId, player.Name) - - /** - * Utilized when one squad member issues an invite for some other player. - * Accessed by an existing squad member using the "Invite" menu option on another player. - * @param char_id the unique character identifier of the player who sent the invite - * @param name the name the player who sent the invite - * @param squad_guid the squad - */ - final case class VacancyInvite(char_id: Long, name: String, squad_guid: PlanetSideGUID) - extends Invitation(char_id, name) - - /** - * Utilized to redirect an (accepted) invitation request to the proper squad leader. - * No direct action causes this message. - * @param player the player who would be joining the squad; - * may or may not have actually requested it in the first place - * @param squad_guid the squad - */ - final case class IndirectInvite(player: Player, squad_guid: PlanetSideGUID) - extends Invitation(player.CharId, player.Name) - - /** - * Utilized in conjunction with an external queuing data structure - * to search for and submit requests to other players - * for the purposes of fill out unoccupied squad roles. - * @param char_id the unique character identifier of the squad leader - * @param name the name of the squad leader - * @param squad_guid the squad - */ - final case class ProximityInvite(char_id: Long, name: String, squad_guid: PlanetSideGUID) - extends Invitation(char_id, name) - - /** - * Utilized in conjunction with an external queuing data structure - * to search for and submit requests to other players - * for the purposes of fill out an unoccupied squad role. - * @param char_id the unique character identifier of the squad leader - * @param name the name of the squad leader - * @param squad_guid the squad with the role - * @param position the index of the role - */ - final case class LookingForSquadRoleInvite(char_id: Long, name: String, squad_guid: PlanetSideGUID, position: Int) - extends Invitation(char_id, name) - - /** - * Utilized when one player issues an invite for some other player for a squad that does not yet exist. - * @param player na - */ - final case class SpontaneousInvite(player: Player) extends Invitation(player.CharId, player.Name) - /** * Produce complete squad information. * @see `SquadInfo` @@ -3831,7 +1389,7 @@ object SquadService { */ def PublishFullDetails(squad: Squad): SquadDetail = { SquadDetail() - .Field1(squad.GUID.guid) + .Guid(squad.GUID.guid) .LeaderCharId(squad.Leader.CharId) .LeaderName(squad.Leader.Name) .Task(squad.Task) @@ -3853,27 +1411,4 @@ object SquadService { ) .Complete } - - /** - * Clear the current detail about a squad's membership and replace it with a previously stored details. - * @param squad the squad - * @param favorite the loadout object - */ - def LoadSquadDefinition(squad: Squad, favorite: SquadLoadout): Unit = { - squad.Task = favorite.task - squad.ZoneId = favorite.zone_id.getOrElse(squad.ZoneId) - squad.Availability.indices.foreach { index => squad.Availability.update(index, false) } - squad.Membership.foreach { position => - position.Role = "" - position.Orders = "" - position.Requirements = Set() - } - favorite.members.foreach { position => - squad.Availability.update(position.index, true) - val member = squad.Membership(position.index) - member.Role = position.role - member.Orders = position.orders - member.Requirements = position.requirements - } - } } diff --git a/src/main/scala/net/psforever/services/teamwork/SquadServiceMessage.scala b/src/main/scala/net/psforever/services/teamwork/SquadServiceMessage.scala index 9b0f8d81..f99942df 100644 --- a/src/main/scala/net/psforever/services/teamwork/SquadServiceMessage.scala +++ b/src/main/scala/net/psforever/services/teamwork/SquadServiceMessage.scala @@ -2,6 +2,7 @@ package net.psforever.services.teamwork import net.psforever.objects.Player +import net.psforever.objects.avatar.Certification import net.psforever.objects.zones.Zone import net.psforever.packet.game.{WaypointEventAction, WaypointInfo, SquadAction => PacketSquadAction} import net.psforever.types.{PlanetSideGUID, SquadRequestType, SquadWaypoint, Vector3} @@ -16,8 +17,9 @@ object SquadServiceMessage { object SquadAction { sealed trait Action - final case class InitSquadList() extends Action - final case class InitCharId() extends Action + final case class InitSquadList() extends Action + final case class InitCharId() extends Action + final case class ReloadDecoration() extends Action final case class Definition(guid: PlanetSideGUID, line: Int, action: PacketSquadAction) extends Action final case class Membership( @@ -35,10 +37,12 @@ object SquadAction { ) extends Action final case class Update( char_id: Long, + guid: PlanetSideGUID, health: Int, max_health: Int, armor: Int, max_armor: Int, + certifications: Set[Certification], pos: Vector3, zone_number: Int ) extends Action diff --git a/src/main/scala/net/psforever/services/teamwork/SquadServiceResponse.scala b/src/main/scala/net/psforever/services/teamwork/SquadServiceResponse.scala index 24861139..367127f4 100644 --- a/src/main/scala/net/psforever/services/teamwork/SquadServiceResponse.scala +++ b/src/main/scala/net/psforever/services/teamwork/SquadServiceResponse.scala @@ -1,6 +1,8 @@ // Copyright (c) 2019 PSForever package net.psforever.services.teamwork +import akka.actor.ActorRef +import net.psforever.objects.avatar.Certification import net.psforever.objects.teamwork.Squad import net.psforever.packet.game.{SquadDetail, SquadInfo, WaypointEventAction, WaypointInfo} import net.psforever.types.{PlanetSideGUID, SquadResponseType, SquadWaypoint} @@ -26,7 +28,7 @@ object SquadResponse { final case class UpdateList(infos: Iterable[(Int, SquadInfo)]) extends Response final case class RemoveFromList(infos: Iterable[Int]) extends Response - final case class AssociateWithSquad(squad_guid: PlanetSideGUID) extends Response + final case class IdentifyAsSquadLeader(squad_guid: PlanetSideGUID) extends Response final case class SetListSquad(squad_guid: PlanetSideGUID) extends Response final case class Membership( @@ -40,11 +42,11 @@ object SquadResponse { unk6: Option[Option[String]] ) extends Response //see SquadMembershipResponse final case class WantsSquadPosition(leader_char_id: Long, bid_name: String) extends Response - final case class Join(squad: Squad, positionsToUpdate: List[Int], channel: String) extends Response + final case class Join(squad: Squad, positionsToUpdate: List[Int], channel: String, ref: ActorRef) extends Response final case class Leave(squad: Squad, positionsToUpdate: List[(Long, Int)]) extends Response final case class UpdateMembers(squad: Squad, update_info: List[SquadAction.Update]) extends Response final case class AssignMember(squad: Squad, from_index: Int, to_index: Int) extends Response - final case class PromoteMember(squad: Squad, char_id: Long, from_index: Int, to_index: Int) extends Response + final case class PromoteMember(squad: Squad, char_id: Long, from_index: Int) extends Response final case class Detail(guid: PlanetSideGUID, squad_detail: SquadDetail) extends Response @@ -59,5 +61,16 @@ object SquadResponse { unk: Int ) extends Response - final case class SquadSearchResults() extends Response + final case class SquadDecoration(guid: PlanetSideGUID, squad: Squad) extends Response + + final case class SquadSearchResults(results: List[PlanetSideGUID]) extends Response + + final case class CharacterKnowledge( + id: Long, + name: String, + certs: Set[Certification], + unk1: Int, + unk2: Int, + zoneNumber: Int + ) extends Response } diff --git a/src/main/scala/net/psforever/services/teamwork/SquadSubscriptionEntity.scala b/src/main/scala/net/psforever/services/teamwork/SquadSubscriptionEntity.scala new file mode 100644 index 00000000..427d6c85 --- /dev/null +++ b/src/main/scala/net/psforever/services/teamwork/SquadSubscriptionEntity.scala @@ -0,0 +1,316 @@ +// Copyright (c) 2022 PSForever +package net.psforever.services.teamwork + +import akka.actor.ActorRef +import scala.collection.mutable + +import net.psforever.objects.teamwork.{Squad, SquadFeatures} +import net.psforever.packet.game.SquadDetail +import net.psforever.services.GenericEventBus +import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID} + +class SquadSubscriptionEntity { + private[this] val log = org.log4s.getLogger(name="SquadService") + + /** + * This is a formal `ActorEventBus` object that is reserved for faction-wide messages and squad-specific messages. + * When the user joins the `SquadService` with a `Service.Join` message + * that includes a confirmed faction affiliation identifier, + * the origin `ActorRef` is added as a subscription. + * Squad channels are produced when a squad is created, + * and are subscribed to as users join the squad, + * and unsubscribed from as users leave the squad.
+ * key - a `PlanetSideEmpire` value; value - `ActorRef` reference
+ * key - a consistent squad channel name; value - `ActorRef` reference + * @see `CloseSquad` + * @see `JoinSquad` + * @see `LeaveSquad` + * @see `Service.Join` + * @see `Service.Leave` + */ + val SquadEvents = new GenericEventBus[SquadServiceResponse] + + /** + * 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, + * 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 `SessionActor`.
+ * key - unique character identifier number; value - `ActorRef` reference for that character + * @see `Service.Join` + */ + val UserEvents: mutable.LongMap[ActorRef] = mutable.LongMap[ActorRef]() + + /** + * Players who are interested in updated details regarding a certain squad + * though they may not be a member of the squad.
+ * key - unique character identifier number; value - a squad identifier number + */ + val MonitorSquadDetails: mutable.LongMap[SquadSubscriptionEntity.MonitorEntry] = mutable.LongMap[SquadSubscriptionEntity.MonitorEntry]() + + def postStop(): Unit = { + MonitorSquadDetails.clear() + UserEvents.foreach { + case (_, actor) => + SquadEvents.unsubscribe(actor) + } + UserEvents.clear() + } + + /** + * Overloaded message-sending operation. + * The `Actor` version wraps around the expected `!` functionality. + * @param to an `ActorRef` which to send the message + * @param msg a message that can be stored in a `SquadServiceResponse` object + */ + def Publish(to: ActorRef, msg: SquadResponse.Response): Unit = { + Publish(to, msg, Nil) + } + + /** + * Overloaded message-sending operation. + * The `Actor` version wraps around the expected `!` functionality. + * @param to an `ActorRef` which to send the message + * @param msg a message that can be stored in a `SquadServiceResponse` object + * @param excluded a group of character identifier numbers who should not receive the message + * (resolved at destination) + */ + def Publish(to: ActorRef, msg: SquadResponse.Response, excluded: Iterable[Long]): Unit = { + to ! SquadServiceResponse("", excluded, msg) + } + + /** + * Overloaded message-sending operation. + * Always publishes on the `SquadEvents` object. + * @param to a faction affiliation used as the channel for the message + * @param msg a message that can be stored in a `SquadServiceResponse` object + */ + def Publish(to: PlanetSideEmpire.Type, msg: SquadResponse.Response): Unit = { + Publish(to, msg, Nil) + } + + /** + * Overloaded message-sending operation. + * Always publishes on the `SquadEvents` object. + * @param to a faction affiliation used as the channel for the message + * @param msg a message that can be stored in a `SquadServiceResponse` object + * @param excluded a group of character identifier numbers who should not receive the message + * (resolved at destination) + */ + def Publish(to: PlanetSideEmpire.Type, msg: SquadResponse.Response, excluded: Iterable[Long]): Unit = { + SquadEvents.publish(SquadServiceResponse(s"/$to/Squad", excluded, msg)) + } + + /** + * Overloaded message-sending operation. + * Strings come in three accepted patterns. + * The first resolves into a faction name, as determined by `PlanetSideEmpire` when transformed into a string. + * The second resolves into a squad's dedicated channel, a name that is formulaic. + * The third resolves as a unique character identifier number. + * @param to a string used as the channel for the message + * @param msg a message that can be stored in a `SquadServiceResponse` object + */ + def Publish(to: String, msg: SquadResponse.Response): Unit = { + Publish(to, msg, Nil) + } + + /** + * Overloaded message-sending operation. + * Strings come in three accepted patterns. + * The first resolves into a faction name, as determined by `PlanetSideEmpire` when transformed into a string. + * The second resolves into a squad's dedicated channel, a name that is formulaic. + * The third resolves as a unique character identifier number. + * @param to a string used as the channel for the message + * @param msg a message that can be stored in a `SquadServiceResponse` object + * @param excluded a group of character identifier numbers who should not receive the message + * (resolved at destination, usually) + */ + def Publish(to: String, msg: SquadResponse.Response, excluded: Iterable[Long]): Unit = { + to match { + case str if "TRNCVS".indexOf(str) > -1 || str.matches("(TR|NC|VS)-Squad\\d+") => + SquadEvents.publish(SquadServiceResponse(s"/$str/Squad", excluded, msg)) + case str if str.matches("\\d+") => + Publish(to.toLong, msg, excluded) + case _ => + log.warn(s"Publish(String): subscriber information is an unhandled format - $to") + } + } + + /** + * Overloaded message-sending operation. + * Always publishes on the `ActorRef` objects retained by the `UserEvents` object. + * @param to a unique character identifier used as the channel for the message + * @param msg a message that can be stored in a `SquadServiceResponse` object + */ + def Publish(to: Long, msg: SquadResponse.Response): Unit = { + UserEvents.get(to) match { + case Some(user) => + user ! SquadServiceResponse("", msg) + case None => + log.warn(s"Publish(Long): subscriber information can not be found - $to") + } + } + + /** + * Overloaded message-sending operation. + * Always publishes on the `ActorRef` objects retained by the `UserEvents` object. + * @param to a unique character identifier used as the channel for the message + * @param msg a message that can be stored in a `SquadServiceResponse` object + * @param excluded a group of character identifier numbers who should not receive the message + */ + def Publish(to: Long, msg: SquadResponse.Response, excluded: Iterable[Long]): Unit = { + if (!excluded.exists(_ == to)) { + Publish(to, msg) + } + } + + /** + * Overloaded message-sending operation. + * No message can be sent using this distinction. + * Log a warning. + * @param to something that was expected to be used as the channel for the message + * but is not handled as such + * @param msg a message that can be stored in a `SquadServiceResponse` object + */ + def Publish[ANY >: Any](to: ANY, msg: SquadResponse.Response): Unit = { + log.warn(s"Publish(Any): subscriber information is an unhandled format - $to") + } + + /** + * Overloaded message-sending operation. + * No message can be sent using this distinction. + * Log a warning. + * @param to something that was expected to be used as the channel for the message + * but is not handled as such + * @param msg a message that can be stored in a `SquadServiceResponse` object + * @param excluded a group of character identifier numbers who should not receive the message + */ + def Publish[ANY >: Any](to: ANY, msg: SquadResponse.Response, excluded: Iterable[Long]): Unit = { + log.warn(s"Publish(Any): subscriber information is an unhandled format - $to") + } + + /* The following functions are related to common communications of squad information, mainly detail. */ + + /** + * Dispatch a message entailing the composition of this squad. + * This is considered the first time this information will be dispatched to any relevant observers + * so the details of the squad will be updated in full and be sent to all relevant observers, + * namely, all the occupants of the squad. + * External observers are ignored. + * @see `InitSquadDetail(PlanetSideGUID, Iterable[Long], Squad)` + * @param features the squad + */ + def InitSquadDetail(features: SquadFeatures): Unit = { + val squad = features.Squad + InitSquadDetail( + squad.GUID, + squad.Membership.collect { case member if member.CharId > 0 => member.CharId }, + squad + ) + } + + /** + * 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.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.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.PublishFullDetails` + * @see `UpdateSquadDetail(PlanetSideGUID, PlanetSideGUID, List[Long], SquadDetail)` + * @param features the squad + */ + def UpdateSquadDetail(features: SquadFeatures): Unit = { + val squad = features.Squad + UpdateSquadDetail( + squad.GUID, + features.ToChannel, + Nil, + SquadService.PublishFullDetails(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 features information about the squad + * @param details the squad details to be included in the message + */ + def UpdateSquadDetail(features: SquadFeatures, details: SquadDetail): Unit = { + UpdateSquadDetail( + features.Squad.GUID, + features.ToChannel, + Nil, + details + ) + } + + /** + * Send a message entailing some of the strategic information and the composition to the existing members of the squad. + * Also send the same information to any users who are watching the squad, potentially for want to join it. + * The squad-specific message is contingent on finding the squad's features using the unique identifier number + * and, from that, reporting to the specific squad's messaging channel. + * Anyone watching the squad will always be updated the given details. + * @see `DisplaySquad` + * @see `Publish` + * @see `SquadDetail` + * @see `SquadResponse.Detail` + * @param guid the unique squad identifier number to be used for the squad detail message + * @param toChannel the squad broadcast channel name + * @param excluding the explicit unique character identifier numbers of individuals who should not receive the message + * @param details the squad details to be included in the message + */ + def UpdateSquadDetail( + guid: PlanetSideGUID, + toChannel: String, + excluding: Iterable[Long], + details: SquadDetail + ): Unit = { + val output = SquadResponse.Detail(guid, details) + Publish(toChannel, output, excluding) + PublishToMonitorTargets(guid, excluding).foreach { charId => Publish(charId, output, Nil) } + } + + /** + * na + * @see `LongMap.subtractOne` + * @see `SquadSubscriptionEntity.MonitorEntry` + * @param guid the unique squad identifier number to be used for the squad detail message + * @param excluding the explicit unique character identifier numbers of individuals who should not receive the message + */ + def PublishToMonitorTargets( + guid: PlanetSideGUID, + excluding: Iterable[Long] + ): Iterable[Long] = { + val curr = System.currentTimeMillis() + MonitorSquadDetails + .toSeq + .collect { + case out @ (charId: Long, entry: SquadSubscriptionEntity.MonitorEntry) + if entry.squadGuid == guid && !excluding.exists(_ == charId) => + if (curr - entry.time < 300000L) { + Some(out._1) + } else { + MonitorSquadDetails.subtractOne(charId) + None + } + } + .flatten + } +} + +object SquadSubscriptionEntity { + private[teamwork] case class MonitorEntry(squadGuid: PlanetSideGUID) { + val time: Long = System.currentTimeMillis() + } +} diff --git a/src/main/scala/net/psforever/services/teamwork/SquadSwitchboard.scala b/src/main/scala/net/psforever/services/teamwork/SquadSwitchboard.scala index 339e06dd..40d35a3c 100644 --- a/src/main/scala/net/psforever/services/teamwork/SquadSwitchboard.scala +++ b/src/main/scala/net/psforever/services/teamwork/SquadSwitchboard.scala @@ -1,149 +1,1034 @@ -// Copyright (c) 2019 PSForever +// Copyright (c) 2019-2022 PSForever package net.psforever.services.teamwork -import akka.actor.{Actor, ActorRef, Terminated} - -import scala.collection.mutable +import akka.actor.{Actor, ActorRef, Cancellable} +import org.log4s.Logger +import scala.concurrent.duration._ +// +import net.psforever.objects.{Default, Player} +import net.psforever.objects.avatar.Certification +import net.psforever.objects.definition.converter.StatConverter +import net.psforever.objects.loadouts.SquadLoadout +import net.psforever.objects.teamwork.{Member, Squad, SquadFeatures, WaypointData} +import net.psforever.packet.game.{PlanetSideZoneID, SquadDetail, SquadInfo, SquadPositionDetail, SquadPositionEntry, WaypointEventAction, WaypointInfo, SquadAction => SquadRequestAction} +import net.psforever.types.{PlanetSideGUID, SquadRequestType, SquadWaypoint, Vector3, WaypointSubtype} /** * The dedicated messaging switchboard for members and observers of a given squad. - * It almost always dispatches messages to `WorldSessionActor` instances, much like any other `Service`. + * It almost always dispatches messages to `SessionActor` instances, much like any other `Service`. * The sole purpose of this `ActorBus` container is to manage a subscription model * that can involuntarily drop subscribers without informing them explicitly * or can just vanish without having to properly clean itself up. + * @param features squad and associated information about the squad + * @param subscriptions individually-connected subscription service */ -class SquadSwitchboard extends Actor { +class SquadSwitchboard( + features: SquadFeatures, + subscriptions: SquadSubscriptionEntity + ) extends Actor { + private[this] val log = org.log4s.getLogger(context.self.path.name) + + private var lazeWaypoints: Seq[SquadSwitchboard.LazeWaypointData] = Seq.empty /** - * This collection contains the message-sending contact reference for squad members. - * Users are added to this collection via the `SquadSwitchboard.Join` message, or a - * combination of the `SquadSwitchboard.DelayJoin` message followed by a - * `SquadSwitchboard.Join` message with or without an `ActorRef` hook. - * The message `SquadSwitchboard.Leave` removes the user from this collection. - * key - unique character id; value - `Actor` reference for that character + * The periodic clearing of laze pointer waypoints. */ - val UserActorMap: mutable.LongMap[ActorRef] = mutable.LongMap[ActorRef]() + private var lazeIndexBlanking: Cancellable = Default.Cancellable - /** - * This collection contains the message-sending contact information for would-be squad members. - * Users are added to this collection via the `SquadSwitchboard.DelayJoin` message - * and are promoted to an actual squad member through a `SquadSwitchboard.Join` message. - * The message `SquadSwitchboard.Leave` removes the user from this collection. - * key - unique character id; value - `Actor` reference for that character - */ - val DelayedJoin: mutable.LongMap[ActorRef] = mutable.LongMap[ActorRef]() - - /** - * This collection contains the message-sending contact information for squad observers. - * Squad observers only get "details" messages as opposed to the sort of messages squad members receive. - * Squad observers are promoted to an actual squad member through a `SquadSwitchboard.Watch` message. - * The message `SquadSwitchboard.Leave` removes the user from this collection. - * The message `SquadSwitchboard.Unwatch` also removes the user from this collection. - * key - unique character id; value - `Actor` reference for that character - */ - val Watchers: mutable.LongMap[ActorRef] = mutable.LongMap[ActorRef]() - - override def postStop(): Unit = { - UserActorMap.clear() - DelayedJoin.clear() - Watchers.clear() + override def postStop() : Unit = { + lazeIndexBlanking.cancel() + lazeIndexBlanking = Default.Cancellable + lazeWaypoints = Nil } def receive: Receive = { - case SquadSwitchboard.Join(char_id, Some(actor)) => - UserActorMap(char_id) = DelayedJoin.remove(char_id).orElse(Watchers.remove(char_id)) match { - case Some(_actor) => - context.watch(_actor) - _actor - case None => - context.watch(actor) - actor + case SquadSwitchboard.Join(player, position, sendTo) => + val charId = player.CharId + if (!features.Squad.Membership.exists { _.CharId == charId }) { + //joining this squad for the first time + JoinSquad(player, position, sendTo) + } else { + //potential relog + val squad = features.Squad + val guid = squad.GUID + val toChannel = s"/${features.ToChannel}/Squad" + val indices = squad.Membership + .zipWithIndex + .collect { case (member, index) if member.CharId != 0 => index } + .toList + subscriptions.Publish(charId, SquadResponse.Join(squad, indices, toChannel, self)) + if (squad.Leader.CharId == charId) { + subscriptions.Publish(charId, SquadResponse.IdentifyAsSquadLeader(guid)) + } + InitWaypoints(charId, features) + subscriptions.InitSquadDetail(guid, Seq(charId), squad) } - case SquadSwitchboard.Join(char_id, None) => - DelayedJoin.remove(char_id).orElse(Watchers.remove(char_id)) match { - case Some(actor) => - UserActorMap(char_id) = actor - case None => ; - } - - case SquadSwitchboard.DelayJoin(char_id, actor) => - context.watch(actor) - DelayedJoin(char_id) = actor - case SquadSwitchboard.Leave(char_id) => - UserActorMap - .find { case (charId, _) => charId == char_id } - .orElse(DelayedJoin.find { case (charId, _) => charId == char_id }) - .orElse(Watchers.find { case (charId, _) => charId == char_id }) match { - case Some((member, actor)) => - context.unwatch(actor) - UserActorMap.remove(member) - DelayedJoin.remove(member) - Watchers.remove(member) - case None => ; - } - - case SquadSwitchboard.Watch(char_id, actor) => - context.watch(actor) - Watchers(char_id) = actor - - case SquadSwitchboard.Unwatch(char_id) => - Watchers.remove(char_id) + LeaveSquad(char_id) case SquadSwitchboard.To(member, msg) => - UserActorMap.find { case (char_id, _) => char_id == member } match { - case Some((_, actor)) => - actor ! msg - case None => ; + if (features.Squad.Membership.exists(_.CharId == member)) { + subscriptions.UserEvents.get(member) match { + case Some(actor) => + actor ! msg + case None => ; + } } case SquadSwitchboard.ToAll(msg) => - UserActorMap - .foreach { - case (_, actor) => - actor ! msg + features.Squad.Membership + .map { member => subscriptions.UserEvents(member.CharId) } + .foreach { actor => + actor ! msg } case SquadSwitchboard.Except(excluded, msg) => - UserActorMap - .filterNot { case (char_id, _) => char_id == excluded } - .foreach { - case (_, actor) => - actor ! msg + features.Squad.Membership + .collect { case member if member.CharId != excluded => subscriptions.UserEvents(member.CharId) } + .foreach { actor => + actor ! msg } - case Terminated(actorRef) => - UserActorMap - .find { case (_, ref) => ref == actorRef } - .orElse(DelayedJoin.find { case (_, ref) => ref == actorRef }) - .orElse(Watchers.find { case (_, ref) => ref == actorRef }) match { - case Some((member, actor)) => - context.unwatch(actor) - UserActorMap.remove(member) - DelayedJoin.remove(member) - Watchers.remove(member) - case None => ; + case SquadServiceMessage(tplayer, _, squad_action) => + squad_action match { + case SquadAction.Definition(_, line, action) => + SquadActionDefinition(tplayer, line, action, sender()) + + case _: SquadAction.Membership => + SquadActionMembership(squad_action) + + case SquadAction.Waypoint(_, wtype, _, info) => + SquadActionWaypoint(tplayer, wtype, info) + + case SquadAction.Update(char_id, guid, health, max_health, armor, max_armor, certs, pos, zone_number) => + SquadActionUpdate(char_id, guid, health, max_health, armor, max_armor, certs, pos, zone_number, tplayer, sender()) + + case _ => ; } - case _ => ; + case SquadSwitchboard.BlankLazeWaypoints => + TryBlankLazeWaypoints() + + case msg => + log.warn(s"Unhandled message $msg from ${sender()}") + } + + /** + * Behaviors and exchanges necessary to complete the fulfilled recruitment process for the squad role.
+ *
+ * This operation is fairly safe to call whenever a player is to be inducted into a squad. + * The aforementioned player must have a callback retained in `subs.UserEvents` + * and conditions imposed by both the role and the player must be satisfied. + * @see `InitialAssociation` + * @see `InitSquadDetail` + * @see `InitWaypoints` + * @see `Publish` + * @see `CleanUpAllInvitesWithPlayer` + * @see `SquadDetail` + * @see `SquadInfo` + * @see `SquadPositionDetail` + * @see `SquadPositionEntry` + * @see `SquadResponse.Join` + * @see `StatConverter.Health` + * @see `UpdateSquadListWhenListed` + * @param player the new squad member; + * this player is NOT the squad leader + * @param position the squad member role that the player will be filling + * @param sendTo a specific client callback + */ + def JoinSquad(player: Player, position: Int, sendTo: ActorRef): Unit = { + val charId = player.CharId + val squad = features.Squad + val role = squad.Membership(position) + log.info(s"${player.Name}-${player.Faction} joins position ${position+1} of squad #${squad.GUID.guid} - ${squad.Task}") + role.Name = player.Name + role.CharId = charId + role.Health = StatConverter.Health(player.Health, player.MaxHealth, min = 1, max = 64) + role.Armor = StatConverter.Health(player.Armor, player.MaxArmor, min = 1, max = 64) + role.Position = player.Position + role.ZoneId = player.Zone.Number + role.Certifications = player.avatar.certifications + val toChannel = features.ToChannel + val size = squad.Size + val leaderId = squad.Leader.CharId + val membership = squad.Membership + if (size == 2) { + //first squad member after leader; both members fully initialize + val memberAndIndex = membership + .zipWithIndex + .collect { case (member, index) if member.CharId > 0 => + (member.CharId, index, subscriptions.UserEvents.get(member.CharId)) + } + .toList + val indices = memberAndIndex.unzip { case (_, b, _) => (b, b) } ._2 + memberAndIndex + .collect { case (id, _, Some(sub)) => + subscriptions.Publish(sub, SquadResponse.Join(squad, indices, toChannel, self)) + InitWaypoints(id, features) + subscriptions.SquadEvents.subscribe(sub, s"/$toChannel/Squad") + } + //update for leader + features.InitialAssociation = false + subscriptions.Publish(leaderId, SquadResponse.IdentifyAsSquadLeader(squad.GUID)) + subscriptions.Publish(leaderId, SquadResponse.CharacterKnowledge(charId, role.Name, role.Certifications, 40, 5, role.ZoneId)) + //everyone + subscriptions.InitSquadDetail(features) + } else { + //joining an active squad; different people update differently + //new member gets full squad UI updates + subscriptions.InitSquadDetail(squad.GUID, Seq(charId), squad) + subscriptions.Publish( + charId, + SquadResponse.Join( + squad, + membership.zipWithIndex + .collect { case (member, index) if member.CharId > 0 => index } + .toList, + toChannel, + self + ) + ) + InitWaypoints(charId, features) + //other squad members see new member joining the squad + subscriptions.UpdateSquadDetail( + squad.GUID, + toChannel, + Seq(charId), + SquadDetail().Members( + List(SquadPositionEntry(position, SquadPositionDetail().Player(charId, player.Name))) + ) + ) + subscriptions.Publish(toChannel, SquadResponse.Join(squad, List(position), "", self), Seq(charId)) + //update for leader + subscriptions.Publish(leaderId, SquadResponse.CharacterKnowledge(charId, role.Name, role.Certifications, 40, 5, role.ZoneId)) + subscriptions.SquadEvents.subscribe(sendTo, s"/$toChannel/Squad") + } + context.parent ! SquadService.UpdateSquadListWhenListed( + features, + SquadInfo().Leader(squad.Leader.Name).Size(size) + ) + } + + /** + * Behaviors and exchanges necessary to undo the recruitment process for the squad role. + * @see `PanicLeaveSquad` + * @see `Publish` + * @param charId the player + * @return `true`, if the player, formerly a normal member of the squad, has been ejected from the squad; + * `false`, otherwise + */ + def LeaveSquad(charId: Long): Boolean = { + val squad = features.Squad + val membership = squad.Membership.zipWithIndex + membership.find { case (_member, _) => _member.CharId == charId } match { + case data @ Some((us, index)) if squad.Leader.CharId != charId => + SquadSwitchboard.PanicLeaveSquad(charId, features, data, subscriptions, context.parent, log) + //member leaves the squad completely (see PanicSquadLeave) + subscriptions.Publish( + charId, + SquadResponse.Leave( + squad, + (charId, index) +: membership.collect { + case (_member, _index) if _member.CharId > 0 && _member.CharId != charId => (_member.CharId, _index) + }.toList + ) + ) + subscriptions.UserEvents.get(charId) match { + case Some(events) => + subscriptions.SquadEvents.unsubscribe(events, s"/${features.ToChannel}/Squad") + case None => ; + } + log.info(s"${us.Name} has left squad #${squad.GUID.guid} - ${squad.Task}") + true + case _ => + false + } + } + + def SquadActionDefinition( + tplayer: Player, + line: Int, + action: SquadRequestAction, + sendTo: ActorRef + ): Unit = { + import net.psforever.packet.game.SquadAction._ + //the following actions can only be performed by a squad's leader + action match { + case SaveSquadFavorite() => + SquadActionDefinitionSaveSquadFavorite(tplayer, line, sendTo) + + case LoadSquadFavorite() => + SquadActionDefinitionLoadSquadFavorite(tplayer, line, sendTo) + + case DeleteSquadFavorite() => + SquadActionDefinitionDeleteSquadFavorite(tplayer, line, sendTo) + + case ChangeSquadPurpose(purpose) => + SquadActionDefinitionChangeSquadPurpose(tplayer, purpose) + + case ChangeSquadZone(zone_id) => + SquadActionDefinitionChangeSquadZone(tplayer, zone_id, sendTo) + + case CloseSquadMemberPosition(position) => + SquadActionDefinitionCloseSquadMemberPosition(tplayer, position) + + case AddSquadMemberPosition(position) => + SquadActionDefinitionAddSquadMemberPosition(tplayer, position) + + case ChangeSquadMemberRequirementsRole(position, role) => + SquadActionDefinitionChangeSquadMemberRequirementsRole(tplayer, position, role) + + case ChangeSquadMemberRequirementsDetailedOrders(position, orders) => + SquadActionDefinitionChangeSquadMemberRequirementsDetailedOrders(tplayer, position, orders) + + case ChangeSquadMemberRequirementsCertifications(position, certs) => + SquadActionDefinitionChangeSquadMemberRequirementsCertifications(tplayer, position, certs) + + case LocationFollowsSquadLead(state) => + SquadActionDefinitionLocationFollowsSquadLead(tplayer, state) + + case AutoApproveInvitationRequests(state) => + SquadActionDefinitionAutoApproveInvitationRequests(tplayer, state) + + case RequestListSquad() => + SquadActionDefinitionRequestListSquad(tplayer, sendTo) + + case StopListSquad() => + SquadActionDefinitionStopListSquad(tplayer, sendTo) + + case ResetAll() => + SquadActionDefinitionResetAll(tplayer) + + //the following action can be performed by the squad leader and maybe an unaffiliated player + case SelectRoleForYourself(position) => + SquadActionDefinitionSelectRoleForYourself(tplayer, position) + + case AssignSquadMemberToRole(position, char_id) => + SquadActionDefinitionAssignSquadMemberToRole(char_id, position) + + case DisplaySquad() => + SquadActionDefinitionDisplaySquad(sendTo) + + case msg => + log.warn(s"Unsupported squad definition behavior: $msg") + } + } + + def SquadActionDefinitionSaveSquadFavorite( + tplayer: Player, + line: Int, + sendTo: ActorRef + ): Unit = { + val squad = features.Squad + if (squad.Leader.CharId == tplayer.CharId && squad.Task.nonEmpty && squad.ZoneId > 0) { + val squad = features.Squad + tplayer.squadLoadouts.SaveLoadout(squad, squad.Task, line) + subscriptions.Publish(sendTo, SquadResponse.ListSquadFavorite(line, squad.Task)) + } + } + + def SquadActionDefinitionLoadSquadFavorite( + tplayer: Player, + line: Int, + sendTo: ActorRef + ): Unit = { + //TODO seems all wrong + val squad = features.Squad + tplayer.squadLoadouts.LoadLoadout(line) match { + case Some(loadout: SquadLoadout) if squad.Size == 1 => + SquadSwitchboard.LoadSquadDefinition(squad, loadout) + context.parent ! SquadService.UpdateSquadListWhenListed(features, SquadService.PublishFullListing(squad)) + subscriptions.Publish(sendTo, SquadResponse.IdentifyAsSquadLeader(PlanetSideGUID(0))) + subscriptions.InitSquadDetail(PlanetSideGUID(0), Seq(tplayer.CharId), squad) + subscriptions.UpdateSquadDetail(features) + subscriptions.Publish(sendTo, SquadResponse.IdentifyAsSquadLeader(squad.GUID)) + case _ => ; + } + } + + def SquadActionDefinitionDeleteSquadFavorite(tplayer: Player, line: Int, sendTo: ActorRef): Unit = { + tplayer.squadLoadouts.DeleteLoadout(line) + subscriptions.Publish(sendTo, SquadResponse.ListSquadFavorite(line, "")) + } + + def SquadActionDefinitionChangeSquadPurpose( + tplayer: Player, + purpose: String + ): Unit = { + val squad = features.Squad + if (squad.Leader.CharId == tplayer.CharId) { + val squad = features.Squad + squad.Task = purpose + if (features.Listed) { + context.parent ! SquadService.UpdateSquadList(features, Some(SquadInfo().Task(purpose))) + subscriptions.UpdateSquadDetail( + squad.GUID, + features.ToChannel, + Seq(squad.Leader.CharId), + SquadDetail().Task(purpose) + ) + } + } + } + + def SquadActionDefinitionChangeSquadZone( + tplayer: Player, + zone_id: PlanetSideZoneID, + sendTo: ActorRef + ): Unit = { + val squad = features.Squad + if (squad.Leader.CharId == tplayer.CharId) { + squad.ZoneId = zone_id.zoneId.toInt + if (features.Listed) { + context.parent ! SquadService.UpdateSquadList(features, Some(SquadInfo().ZoneId(zone_id))) + subscriptions.UpdateSquadDetail( + squad.GUID, + features.ToChannel, + Seq(squad.Leader.CharId), + SquadDetail().ZoneId(zone_id) + ) + } + } + } + + def SquadActionDefinitionCloseSquadMemberPosition( + tplayer: Player, + position: Int + ): Unit = { + val squad = features.Squad + if (squad.Leader.CharId == tplayer.CharId) { + squad.Availability.lift(position) match { + case Some(true) if position > 0 => //do not close squad leader position; undefined behavior + squad.Availability.update(position, false) + if (features.Listed) { + context.parent ! SquadService.UpdateSquadList(features, Some(SquadInfo().Capacity(squad.Capacity))) + subscriptions.UpdateSquadDetail( + features, + SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail.Closed))) + ) + } + case Some(_) | None => ; + } + } + } + + def SquadActionDefinitionAddSquadMemberPosition( + tplayer: Player, + position: Int + ): Unit = { + val squad = features.Squad + if (squad.Leader.CharId == tplayer.CharId) { + squad.Availability.lift(position) match { + case Some(false) => + squad.Availability.update(position, true) + if (features.Listed) { + context.parent ! SquadService.UpdateSquadList(features, Some(SquadInfo().Capacity(squad.Capacity))) + subscriptions.UpdateSquadDetail( + features, + SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail.Open))) + ) + } + case Some(true) | None => ; + } + } + } + + def SquadActionDefinitionChangeSquadMemberRequirementsRole( + tplayer: Player, + position: Int, + role: String + ): Unit = { + val squad = features.Squad + if (squad.Leader.CharId == tplayer.CharId) { + squad.Availability.lift(position) match { + case Some(true) => + squad.Membership(position).Role = role + if (features.Listed) { + subscriptions.UpdateSquadDetail( + features, + SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail().Role(role)))) + ) + } + case Some(false) | None => ; + } + } + } + + def SquadActionDefinitionChangeSquadMemberRequirementsDetailedOrders( + tplayer: Player, + position: Int, + orders: String + ): Unit = { + val squad = features.Squad + if (squad.Leader.CharId == tplayer.CharId) { + squad.Availability.lift(position) match { + case Some(true) => + squad.Membership(position).Orders = orders + if (features.Listed) { + subscriptions.UpdateSquadDetail( + features, + SquadDetail().Members( + List(SquadPositionEntry(position, SquadPositionDetail().DetailedOrders(orders))) + ) + ) + } + case Some(false) | None => ; + } + } + } + + def SquadActionDefinitionChangeSquadMemberRequirementsCertifications( + tplayer: Player, + position: Int, + certs: Set[Certification] + ): Unit = { + val squad = features.Squad + if (squad.Leader.CharId == tplayer.CharId) { + squad.Availability.lift(position) match { + case Some(true) => + squad.Membership(position).Requirements = certs + if (features.Listed) { + subscriptions.UpdateSquadDetail( + features, + SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail().Requirements(certs)))) + ) + } + case Some(false) | None => ; + } + } + } + + def SquadActionDefinitionLocationFollowsSquadLead( + tplayer: Player, + state: Boolean + ): Unit = { + val squad = features.Squad + if (squad.Leader.CharId == tplayer.CharId) { + features.LocationFollowsSquadLead = state + } + } + + def SquadActionDefinitionAutoApproveInvitationRequests( + tplayer: Player, + state: Boolean + ): Unit = { + val squad = features.Squad + if (squad.Leader.CharId == tplayer.CharId) { + features.AutoApproveInvitationRequests = state + } + } + + def SquadActionDefinitionRequestListSquad( + tplayer: Player, + sendTo: ActorRef + ): Unit = { + val squad = features.Squad + if (squad.Leader.CharId == tplayer.CharId) { + if (!features.Listed && squad.Task.nonEmpty && squad.ZoneId > 0) { + features.Listed = true + features.InitialAssociation = false + val guid = squad.GUID + val charId = squad.Leader.CharId + subscriptions.Publish(charId, SquadResponse.IdentifyAsSquadLeader(guid)) + subscriptions.Publish(sendTo, SquadResponse.SetListSquad(guid)) + context.parent ! SquadService.UpdateSquadList(features, None) + } + } + } + + def SquadActionDefinitionStopListSquad( + tplayer: Player, + sendTo: ActorRef + ): Unit = { + val squad = features.Squad + if (squad.Leader.CharId == tplayer.CharId) { + if (features.Listed) { + features.Listed = false + subscriptions.Publish(sendTo, SquadResponse.SetListSquad(PlanetSideGUID(0))) + context.parent ! SquadService.UpdateSquadList(features, None) + } + } + } + + def SquadActionDefinitionResetAll(tplayer: Player): Unit = { + val squad = features.Squad + if (squad.Leader.CharId == tplayer.CharId) { + 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() + }) + features.LocationFollowsSquadLead = true + features.AutoApproveInvitationRequests = true + if (features.Listed) { + //unlist the squad + features.Listed = false + subscriptions.Publish(features.ToChannel, SquadResponse.SetListSquad(PlanetSideGUID(0))) + context.parent ! SquadService.UpdateSquadList(features, None) + } + subscriptions.UpdateSquadDetail(features) + val guid = squad.GUID + val charId = squad.Leader.CharId + subscriptions.Publish(charId, SquadResponse.IdentifyAsSquadLeader(guid)) + features.InitialAssociation = true + } + } + + def SquadActionDefinitionSelectRoleForYourself( + tplayer: Player, + position: Int + ): Unit = { + //no swap to squad leader position, ever + val squad = features.Squad + if (position != 0 && position < squad.Size && squad.Availability(position)) { + //this may give someone a position that they might not otherwise be able to fulfill for free + val toMember: Member = squad.Membership(position) + squad.Membership.indexWhere { p => p.CharId == tplayer.CharId } match { + case -1 => + //no swap + case 0 => + //the squad leader must stay the squad leader, position 0 + //don't swap between the actual positions, just swap details of the positions + val fromMember = squad.Membership.head + val fromMemberPositionRole = fromMember.Role + val fromMemberPositionOrders = fromMember.Orders + val fromMemberPositionReq = fromMember.Requirements + fromMember.Role = toMember.Role + fromMember.Orders = toMember.Orders + fromMember.Requirements = toMember.Requirements + toMember.Role = fromMemberPositionRole + toMember.Orders = fromMemberPositionOrders + toMember.Requirements = fromMemberPositionReq + subscriptions.UpdateSquadDetail( + features, + SquadDetail().Members( + List( + SquadPositionEntry(0, SquadPositionDetail() + .Role(fromMemberPositionRole) + .DetailedOrders(fromMemberPositionOrders) + .Requirements(fromMemberPositionReq) + ), + SquadPositionEntry(position, SquadPositionDetail() + .Role(toMember.Role) + .DetailedOrders(toMember.Orders) + .Requirements(toMember.Requirements) + ) + ) + ) + ) + case index + if index != position && squad.isAvailable(position, tplayer.avatar.certifications) => + //only validate if the requesting player is qualified to swap between these positions + val fromMember = squad.Membership(index) + SquadService.SwapMemberPosition(toMember, fromMember) + subscriptions.Publish( + features.ToChannel, + SquadResponse.AssignMember(squad, index, position) + ) + subscriptions.UpdateSquadDetail( + features, + SquadDetail().Members(List( + SquadPositionEntry(index, SquadPositionDetail().Player(fromMember.CharId, fromMember.Name)), + SquadPositionEntry(position, SquadPositionDetail().Player(toMember.CharId, toMember.Name)) + )) + ) + case _ => ; + //no swap + } + } + } + + def SquadActionDefinitionAssignSquadMemberToRole( + char_id: Long, + position: Int + ): Unit = { + val squad = features.Squad + val membership = squad.Membership + if (squad.Leader.CharId == char_id) { + membership.lift(position) match { + case Some(toMember) => + SquadActionMembershipPromote(char_id, toMember.CharId) + case _ => ; + } + } else { + (membership.zipWithIndex.find({ case (member, _) => member.CharId == char_id }), membership.lift(position)) match { + case (Some((fromMember, fromPosition)), Some(toMember)) => + val name = fromMember.Name + SquadService.SwapMemberPosition(toMember, fromMember) + subscriptions.Publish(features.ToChannel, SquadResponse.AssignMember(squad, fromPosition, position)) + subscriptions.UpdateSquadDetail( + features, + SquadDetail().Members( + List( + SquadPositionEntry(position, SquadPositionDetail().Player(fromMember.CharId, fromMember.Name)), + SquadPositionEntry(fromPosition, SquadPositionDetail().Player(char_id, name)) + ) + ) + ) + case _ => ; + } + } + } + + def SquadActionDefinitionDisplaySquad(sendTo: ActorRef): Unit = { + val squad = features.Squad + subscriptions.Publish(sendTo, SquadResponse.Detail(squad.GUID, SquadService.PublishFullDetails(squad))) + } + + def SquadActionMembership(action: Any): Unit = { + action match { + case SquadAction.Membership(SquadRequestType.Promote, promotingPlayer, Some(promotedPlayer), _, _) => + SquadActionMembershipPromote(promotingPlayer, promotedPlayer) + + case SquadAction.Membership(event, _, _, _, _) => + log.debug(s"SquadAction.Membership: $event is not supported here") + + case _ => ; + } + } + + def SquadActionMembershipPromote(sponsoringPlayer: Long, promotedPlayer: Long): Unit = { + val squad = features.Squad + val leader = squad.Leader + if (squad.Leader.CharId == sponsoringPlayer) { + val guid = squad.GUID + val membership = squad.Membership + val membershipIndexed = membership.zipWithIndex + val (member, index) = membershipIndexed.find { + case (_member, _) => _member.CharId == promotedPlayer + }.get + log.info(s"Promoting player ${member.Name} to be the leader of ${squad.Task}") + val memberName = member.Name + val detail = SquadDetail() + .LeaderCharId(promotedPlayer) + .LeaderName(memberName) + .Members( + List( + SquadPositionEntry(0, SquadPositionDetail().Player(promotedPlayer, memberName)), + SquadPositionEntry(index, SquadPositionDetail().Player(sponsoringPlayer, leader.Name)) + ) + ) + .Complete + SquadService.SwapMemberPosition(leader, member) + subscriptions.Publish(features.ToChannel, SquadResponse.PromoteMember(squad, promotedPlayer, index)) + //to the new squad leader + subscriptions.UpdateSquadDetail( + guid, + toChannel = s"$promotedPlayer", + Nil, + detail.Guid(guid.guid).Task(squad.Task).ZoneId(PlanetSideZoneID(squad.ZoneId)) + ) + membership + .filterNot { + _.CharId == promotedPlayer + } + .foreach { member => + subscriptions.Publish(promotedPlayer, SquadResponse.CharacterKnowledge(member.CharId, member.Name, member.Certifications, 40, 5, member.ZoneId)) + } + //to old and to new squad leader + if (features.Listed) { + context.parent ! SquadService.UpdateSquadList(features, Some(SquadInfo().Leader(memberName))) + subscriptions.Publish(sponsoringPlayer, SquadResponse.SetListSquad(PlanetSideGUID(0))) + subscriptions.Publish(promotedPlayer, SquadResponse.SetListSquad(squad.GUID)) + } + //to old squad leader and rest of squad + subscriptions.UpdateSquadDetail( + guid, + features.ToChannel, + List(promotedPlayer), + detail + ) + } + } + + def SquadActionWaypoint(tplayer: Player, waypointType: SquadWaypoint, info: Option[WaypointInfo]): Unit = { + SquadActionWaypoint(tplayer.CharId, waypointType, info) + } + + def SquadActionWaypoint(playerCharId: Long, waypointType: SquadWaypoint, info: Option[WaypointInfo]): Unit = { + (if (waypointType.subtype == WaypointSubtype.Laze) { + info match { + case Some(winfo) => + //laze rally can be updated by any squad member + //the laze-indicated target waypoint is not retained + val curr = System.currentTimeMillis() + val clippedLazes = { + val index = lazeWaypoints.indexWhere { _.charId == playerCharId } + if (index > -1) { + lazeWaypoints.take(index) ++ lazeWaypoints.drop(index + 1) + } + else { + lazeWaypoints + } + } + if (lazeWaypoints.isEmpty || clippedLazes.headOption != lazeWaypoints.headOption) { + //reason to retime blanking + lazeIndexBlanking.cancel() + import scala.concurrent.ExecutionContext.Implicits.global + lazeIndexBlanking = lazeWaypoints.headOption match { + case Some(data) => + context.system.scheduler.scheduleOnce(math.min(0, data.endTime - curr).milliseconds, self, SquadSwitchboard.BlankLazeWaypoints) + case None => + context.system.scheduler.scheduleOnce(15.seconds, self, SquadSwitchboard.BlankLazeWaypoints) + } + } + lazeWaypoints = clippedLazes :+ SquadSwitchboard.LazeWaypointData(playerCharId, waypointType.value, curr + 15000) + Some(WaypointData(winfo.zone_number, winfo.pos)) + case _ => + None + } + } else if (playerCharId == features.Squad.Leader.CharId) { + //only the squad leader may update other squad waypoints + info match { + case Some(winfo) => + features.AddWaypoint(features.Squad.GUID, waypointType, winfo) + case _ => + features.RemoveWaypoint(features.Squad.GUID, waypointType) + None + } + }) match { + case Some(_) => + //waypoint added or updated + subscriptions.Publish( + features.ToChannel, + SquadResponse.WaypointEvent(WaypointEventAction.Add, playerCharId, waypointType, None, info, 1) + ) + case None => + //waypoint removed + subscriptions.Publish( + features.ToChannel, + SquadResponse.WaypointEvent(WaypointEventAction.Remove, playerCharId, waypointType, None, None, 0) + ) + } + } + + def TryBlankLazeWaypoints(): Unit = { + lazeIndexBlanking.cancel() + val curr = System.currentTimeMillis() + val blank = lazeWaypoints.takeWhile { data => curr >= data.endTime } + lazeWaypoints = lazeWaypoints.drop(blank.size) + blank.foreach { data => + subscriptions.Publish( + features.ToChannel, + SquadResponse.WaypointEvent(WaypointEventAction.Remove, data.charId, SquadWaypoint(data.waypointType), None, None, 0), + Seq() + ) + } + //retime + lazeWaypoints match { + case Nil => ; + case x :: _ => + import scala.concurrent.ExecutionContext.Implicits.global + lazeIndexBlanking = context.system.scheduler.scheduleOnce( + math.min(0, x.endTime - curr).milliseconds, + self, + SquadSwitchboard.BlankLazeWaypoints + ) + } + } + + /** + * Dispatch all of the information about a given squad's waypoints to a user. + * @param toCharId the player to whom the waypoint data will be dispatched + * @param features the squad + */ + def InitWaypoints(toCharId: Long, features: SquadFeatures): Unit = { + val squad = features.Squad + val vz1 = Vector3.z(value = 1) + val list = features.Waypoints + subscriptions.Publish( + toCharId, + SquadResponse.InitWaypoints( + squad.Leader.CharId, + list.zipWithIndex.collect { + case (point, index) if point.pos != vz1 => + (SquadWaypoint(index), WaypointInfo(point.zone_number, point.pos), 1) + } + ) + ) + } + + def SquadActionUpdate( + charId: Long, + guid: PlanetSideGUID, + health: Int, + maxHealth: Int, + armor: Int, + maxArmor: Int, + certifications: Set[Certification], + pos: Vector3, + zoneNumber: Int, + player: Player, + sendTo: ActorRef + ): Unit = { + //squad members + val squad = features.Squad + squad.Membership.find(_.CharId == charId) match { + case Some(member) => + val healthBefore = member.Health + val healthAfter = StatConverter.Health(health, maxHealth, min = 1, max = 64) + val beforeZone = member.ZoneId + val certsBefore = member.Certifications + val zoneBefore = member.ZoneId + member.GUID = guid + member.Health = healthAfter + member.Armor = StatConverter.Health(armor, maxArmor, min = 1, max = 64) + member.Certifications = certifications + member.Position = pos + member.ZoneId = zoneNumber + subscriptions.Publish( + sendTo, + SquadResponse.UpdateMembers( + squad, + squad.Membership + .filterNot { _.CharId == 0 } + .map { member => + SquadAction.Update(member.CharId, PlanetSideGUID(0), member.Health, 0, member.Armor, 0, member.Certifications, member.Position, member.ZoneId) + } + .toList + ) + ) + if ((healthBefore == 0 && healthAfter > 0) || beforeZone != zoneNumber) { + //resend the active invite + context.parent.tell(SquadService.ResendActiveInvite(charId), sendTo) + } + val leader = squad.Leader + val leaderCharId = leader.CharId + if (!certsBefore.equals(certifications)) { + if (leaderCharId != charId) { + subscriptions.Publish( + leaderCharId, + SquadResponse.CharacterKnowledge(charId, member.Name, certifications, 40, 5, zoneNumber) + ) + } + context.parent ! SquadServiceMessage(player, player.Zone, SquadAction.ReloadDecoration()) + } else if (zoneBefore != zoneNumber && leaderCharId != charId) { + subscriptions.Publish( + leaderCharId, + SquadResponse.CharacterKnowledge(charId, member.Name, certifications, 40, 5, 0) + ) + subscriptions.Publish( + leaderCharId, + SquadResponse.CharacterKnowledge(charId, member.Name, certifications, 40, 5, zoneNumber) + ) + } + if (features.LocationFollowsSquadLead) { + //redraw squad experience waypoint + if (leaderCharId == charId) { + features.AddWaypoint(squad.GUID, SquadWaypoint.ExperienceRally, WaypointInfo(zoneNumber, pos.xy)) + } + subscriptions.Publish( + charId, + SquadResponse.WaypointEvent( + WaypointEventAction.Add, + charId, + SquadWaypoint.ExperienceRally, + None, + Some(WaypointInfo(leader.ZoneId, leader.Position)), + 1 + ) + ) + } + case _ => ; + } } } object SquadSwitchboard { - final case class Join(char_id: Long, actor: Option[ActorRef]) + private case class LazeWaypointData(charId: Long, waypointType: Int, endTime: Long) - final case class DelayJoin(char_id: Long, actor: ActorRef) + private case object BlankLazeWaypoints - final case class Leave(char_id: Long) + final case class Join(player: Player, position: Int, replyTo: ActorRef) - final case class Watch(char_id: Long, actor: ActorRef) + final case class Leave(charId: Long) - final case class Unwatch(char_id: Long) + final case class Promote(candidate: Long) final case class To(member: Long, msg: SquadServiceResponse) final case class ToAll(msg: SquadServiceResponse) final case class Except(excluded_member: Long, msg: SquadServiceResponse) + + /** + * Behaviors and exchanges necessary to undo the recruitment process for the squad role.
+ *
+ * The complement of the prior `LeaveSquad` method. + * This method deals entirely with other squad members observing the given squad member leaving the squad + * while the other method handles messaging only for the squad member who is leaving. + * The distinction is useful when unsubscribing from this service, + * as the `ActorRef` object used to message the player's client is no longer reliable + * and has probably ceased to exist. + * @see `LeaveSquad` + * @see `Publish` + * @see `SquadDetail` + * @see `SquadInfo` + * @see `SquadPositionDetail` + * @see `SquadPositionEntry` + * @see `SquadResponse.Leave` + * @see `UpdateSquadDetail` + * @see `UpdateSquadListWhenListed` + * @param charId the player + * @param features the squad + * @param entry a paired membership role with its index in the squad + * @return if a role/index pair is provided + */ + def PanicLeaveSquad( + charId: Long, + features: SquadFeatures, + entry: Option[(Member, Int)], + subscriptions: SquadSubscriptionEntity, + squadDetailActorRef: ActorRef, + log: Logger + ): Boolean = { + val squad = features.Squad + entry match { + case Some((member, index)) => + log.info(s"${member.Name}-${squad.Faction} has left squad #${squad.GUID.guid} - ${squad.Task}") + val entry = (charId, index) + //member leaves the squad completely + member.Name = "" + member.CharId = 0 + //other squad members see the member leaving + subscriptions.Publish(features.ToChannel, SquadResponse.Leave(squad, List(entry)), Seq(charId)) + subscriptions.UpdateSquadDetail( + features, + SquadDetail().Members(List(SquadPositionEntry(index, SquadPositionDetail().Player(char_id = 0, name = "")))) + ) + squadDetailActorRef ! SquadService.UpdateSquadListWhenListed(features, SquadInfo().Size(squad.Size)) + true + case _ => + false + } + } + + /** + * Clear the current detail about a squad's membership and replace it with a previously stored details. + * @param squad the squad + * @param favorite the loadout object + */ + def LoadSquadDefinition(squad: Squad, favorite: SquadLoadout): Unit = { + squad.Task = favorite.task + squad.ZoneId = favorite.zone_id.getOrElse(squad.ZoneId) + squad.Availability.indices.foreach { index => squad.Availability.update(index, false) } + squad.Membership.foreach { position => + position.Role = "" + position.Orders = "" + position.Requirements = Set() + } + favorite.members.foreach { position => + squad.Availability.update(position.index, true) + val member = squad.Membership(position.index) + member.Role = position.role + member.Orders = position.orders + member.Requirements = position.requirements + } + } } diff --git a/src/main/scala/net/psforever/types/SquadListDecoration.scala b/src/main/scala/net/psforever/types/SquadListDecoration.scala new file mode 100644 index 00000000..e938a5cf --- /dev/null +++ b/src/main/scala/net/psforever/types/SquadListDecoration.scala @@ -0,0 +1,27 @@ +// Copyright (c) 2022 PSForever +package net.psforever.types + +import scodec.codecs.uint + +object SquadListDecoration extends Enumeration { + type Type = Value + + val NotAvailable = Value(0) + val Available = Value(1) + val CertQualified = Value(2) + val SearchResult = Value(3) + + implicit val codec = uint(bits = 3).xmap[SquadListDecoration.Value]( + { + value => + if (value < 4) { + SquadListDecoration(value) + } else if (value < 7) { + SquadListDecoration.Available + } else { + SquadListDecoration.NotAvailable + } + }, + _.id + ) +} diff --git a/src/main/scala/net/psforever/types/SquadResponseType.scala b/src/main/scala/net/psforever/types/SquadResponseType.scala index 17d836e6..51da4d5e 100644 --- a/src/main/scala/net/psforever/types/SquadResponseType.scala +++ b/src/main/scala/net/psforever/types/SquadResponseType.scala @@ -4,9 +4,10 @@ package net.psforever.types import net.psforever.packet.PacketHelpers import scodec.codecs._ +//unk01 is some sort of reply to ProximityInvite object SquadResponseType extends Enumeration { type Type = Value - val Invite, Unk01, Accept, Reject, Cancel, Leave, Disband, PlatoonInvite, PlatoonAccept, PlatoonReject, PlatoonCancel, + val Invite, ProximityInvite, Accept, Reject, Cancel, Leave, Disband, PlatoonInvite, PlatoonAccept, PlatoonReject, PlatoonCancel, PlatoonLeave, PlatoonDisband = Value implicit val codec = PacketHelpers.createEnumerationCodec(this, uint4L) diff --git a/src/main/scala/net/psforever/types/SquadWaypoints.scala b/src/main/scala/net/psforever/types/SquadWaypoints.scala index dc99c5d2..03b031b4 100644 --- a/src/main/scala/net/psforever/types/SquadWaypoints.scala +++ b/src/main/scala/net/psforever/types/SquadWaypoints.scala @@ -37,7 +37,7 @@ sealed abstract class StandardWaypoint(override val value: Int) extends SquadWay * is indicated in the game world, on the proximity map, and on the continental map, * and is designated by the number of the squad member that produced it. * Only one laze waypoint may be made visible from any one squad member at any given time, overwritten when replaced. - * When viewed by a squad member seated in a Flail, the waypoint includes an elevation reticle for aiming purposes. + * When viewed by a squad member seated in a Flail, the waypoint includes an elevation reticule for aiming purposes. * YMMV. * @see `SquadWaypointEvent` * @see `SquadWaypointRequest` diff --git a/src/test/scala/game/CharacterKnowledgeMessageTest.scala b/src/test/scala/game/CharacterKnowledgeMessageTest.scala index 7675446b..d4b199e5 100644 --- a/src/test/scala/game/CharacterKnowledgeMessageTest.scala +++ b/src/test/scala/game/CharacterKnowledgeMessageTest.scala @@ -33,7 +33,7 @@ class CharacterKnowledgeMessageTest extends Specification { ), 15, 0, - PlanetSideGUID(12) + 12 ) case _ => ko @@ -61,7 +61,7 @@ class CharacterKnowledgeMessageTest extends Specification { ), 15, 0, - PlanetSideGUID(12) + 12 ) ) val pkt = PacketCoding.encodePacket(msg).require.toByteVector diff --git a/src/test/scala/game/SquadDefinitionActionMessageTest.scala b/src/test/scala/game/SquadDefinitionActionMessageTest.scala index a2ada267..ff636f46 100644 --- a/src/test/scala/game/SquadDefinitionActionMessageTest.scala +++ b/src/test/scala/game/SquadDefinitionActionMessageTest.scala @@ -6,7 +6,7 @@ import org.specs2.mutable._ import net.psforever.packet._ import net.psforever.packet.game.SquadAction._ import net.psforever.packet.game._ -import net.psforever.types.PlanetSideGUID +import net.psforever.types.{PlanetSideGUID, SquadListDecoration} import scodec.bits._ class SquadDefinitionActionMessageTest extends Specification { @@ -29,6 +29,7 @@ class SquadDefinitionActionMessageTest extends Specification { val string_26 = hex"E7 68 000000" val string_28 = hex"E7 70 000020" //On val string_31 = hex"E7 7c 000020" //On + val string_33 = hex"E7 84 0C0008" val string_34a = hex"E7 88 00002180420061006400610073007300000000000000040000" //"Badass", Solsar, Any matching position val string_34b = @@ -224,6 +225,17 @@ class SquadDefinitionActionMessageTest extends Specification { } } + "decode (33)" in { + PacketCoding.decodePacket(string_33).require match { + case SquadDefinitionActionMessage(unk1, unk2, action) => + unk1 mustEqual PlanetSideGUID(3) + unk2 mustEqual 0 + action mustEqual SquadListDecorator(SquadListDecoration.Available) + case _ => + ko + } + } + "decode (34a)" in { PacketCoding.decodePacket(string_34a).require match { case SquadDefinitionActionMessage(unk1, unk2, action) => @@ -460,6 +472,13 @@ class SquadDefinitionActionMessageTest extends Specification { pkt mustEqual string_31 } + "encode (33)" in { + val msg = SquadDefinitionActionMessage(PlanetSideGUID(3), 0, SquadAction.SquadListDecorator(SquadListDecoration.Available)) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual string_33 + } + "encode (34a)" in { val msg = SquadDefinitionActionMessage( PlanetSideGUID(0), diff --git a/src/test/scala/game/SquadDetailDefinitionUpdateMessageTest.scala b/src/test/scala/game/SquadDetailDefinitionUpdateMessageTest.scala index 6903bf0d..64f7250b 100644 --- a/src/test/scala/game/SquadDetailDefinitionUpdateMessageTest.scala +++ b/src/test/scala/game/SquadDetailDefinitionUpdateMessageTest.scala @@ -651,7 +651,7 @@ class SquadDetailDefinitionUpdateMessageTest extends Specification { val msg = SquadDetailDefinitionUpdateMessage( PlanetSideGUID(3), SquadDetail() - .Field1(0) + .Guid(0) .LeaderCharId(1221560L) .Members( List( @@ -677,7 +677,7 @@ class SquadDetailDefinitionUpdateMessageTest extends Specification { PlanetSideGUID(3), SquadDetail() .Leader(42631712L, "Jaako") - .Field3(556403L) + .OutfitId(556403L) .Members( List( SquadPositionEntry(0, SquadPositionDetail().Player(0L, "")) diff --git a/src/test/scala/game/SquadMembershipResponseTest.scala b/src/test/scala/game/SquadMembershipResponseTest.scala index b1ad3835..6492b4f9 100644 --- a/src/test/scala/game/SquadMembershipResponseTest.scala +++ b/src/test/scala/game/SquadMembershipResponseTest.scala @@ -62,7 +62,7 @@ class SquadMembershipResponseTest extends Specification { "decode (1-1)" in { PacketCoding.decodePacket(string_11).require match { case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) => - unk1 mustEqual SquadResponseType.Unk01 + unk1 mustEqual SquadResponseType.ProximityInvite unk2 mustEqual 19 unk3 mustEqual 0 unk4 mustEqual 41530025L @@ -78,7 +78,7 @@ class SquadMembershipResponseTest extends Specification { "decode (1-2)" in { PacketCoding.decodePacket(string_12).require match { case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) => - unk1 mustEqual SquadResponseType.Unk01 + unk1 mustEqual SquadResponseType.ProximityInvite unk2 mustEqual 18 unk3 mustEqual 0 unk4 mustEqual 41578085L @@ -315,14 +315,14 @@ class SquadMembershipResponseTest extends Specification { } "encode (1-1)" in { - val msg = SquadMembershipResponse(SquadResponseType.Unk01, 19, 0, 41530025L, Some(0L), "", true, Some(None)) + val msg = SquadMembershipResponse(SquadResponseType.ProximityInvite, 19, 0, 41530025L, Some(0L), "", true, Some(None)) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual string_11 } "encode (1-2)" in { - val msg = SquadMembershipResponse(SquadResponseType.Unk01, 18, 0, 41578085L, Some(0L), "", true, Some(None)) + val msg = SquadMembershipResponse(SquadResponseType.ProximityInvite, 18, 0, 41578085L, Some(0L), "", true, Some(None)) val pkt = PacketCoding.encodePacket(msg).require.toByteVector pkt mustEqual string_12 diff --git a/src/test/scala/game/objectcreate/CharacterDataTest.scala b/src/test/scala/game/objectcreate/CharacterDataTest.scala index 81e01df6..e0898627 100644 --- a/src/test/scala/game/objectcreate/CharacterDataTest.scala +++ b/src/test/scala/game/objectcreate/CharacterDataTest.scala @@ -57,6 +57,7 @@ class CharacterDataTest extends Specification { a.unk9 mustEqual 0 a.unkA mustEqual 0 + b.outfit_id mustEqual 316554L b.outfit_name mustEqual "Black Beret Armoured Corps" b.outfit_logo mustEqual 23 b.backpack mustEqual false @@ -67,7 +68,6 @@ class CharacterDataTest extends Specification { b.is_cloaking mustEqual false b.charging_pose mustEqual false b.on_zipline.isEmpty mustEqual true - b.unk0 mustEqual 316554L b.unk1 mustEqual false b.unk2 mustEqual false b.unk3 mustEqual false @@ -174,6 +174,7 @@ class CharacterDataTest extends Specification { a.unk9 mustEqual 0 a.unkA mustEqual 0 + b.outfit_id mustEqual 26L b.outfit_name mustEqual "Black Beret Armoured Corps" b.outfit_logo mustEqual 23 b.backpack mustEqual false @@ -184,7 +185,6 @@ class CharacterDataTest extends Specification { b.is_cloaking mustEqual false b.charging_pose mustEqual false b.on_zipline.isEmpty mustEqual true - b.unk0 mustEqual 26L b.unk1 mustEqual false b.unk2 mustEqual false b.unk3 mustEqual false @@ -243,6 +243,7 @@ class CharacterDataTest extends Specification { a.unk9 mustEqual 0 a.unkA mustEqual 0 + b.outfit_id mustEqual 529687L b.outfit_name mustEqual "Original District" b.outfit_logo mustEqual 23 b.backpack mustEqual true @@ -253,7 +254,6 @@ class CharacterDataTest extends Specification { b.is_cloaking mustEqual false b.charging_pose mustEqual false b.on_zipline.isEmpty mustEqual true - b.unk0 mustEqual 529687L b.unk1 mustEqual false b.unk2 mustEqual false b.unk3 mustEqual false diff --git a/src/test/scala/game/objectcreatedetailed/DetailedCharacterDataTest.scala b/src/test/scala/game/objectcreatedetailed/DetailedCharacterDataTest.scala index ef7dc5df..496f6d59 100644 --- a/src/test/scala/game/objectcreatedetailed/DetailedCharacterDataTest.scala +++ b/src/test/scala/game/objectcreatedetailed/DetailedCharacterDataTest.scala @@ -87,6 +87,7 @@ class DetailedCharacterDataTest extends Specification { a.unk9 mustEqual 0 a.unkA mustEqual 65535 + b.outfit_id mustEqual 0L b.outfit_name mustEqual "" b.outfit_logo mustEqual 0 b.backpack mustEqual false @@ -97,7 +98,6 @@ class DetailedCharacterDataTest extends Specification { b.is_cloaking mustEqual false b.charging_pose mustEqual false b.on_zipline.isEmpty mustEqual true - b.unk0 mustEqual 0L b.unk1 mustEqual false b.unk2 mustEqual false b.unk3 mustEqual false @@ -275,6 +275,7 @@ class DetailedCharacterDataTest extends Specification { a.unk9 mustEqual 0 a.unkA mustEqual 0 + b.outfit_id mustEqual 0L b.outfit_name mustEqual "" b.outfit_logo mustEqual 0 b.backpack mustEqual false @@ -285,7 +286,6 @@ class DetailedCharacterDataTest extends Specification { b.is_cloaking mustEqual false b.charging_pose mustEqual false b.on_zipline.isEmpty mustEqual true - b.unk0 mustEqual 0L b.unk1 mustEqual false b.unk2 mustEqual false b.unk3 mustEqual false @@ -460,6 +460,7 @@ class DetailedCharacterDataTest extends Specification { a.unk9 mustEqual 0 a.unkA mustEqual 65535 + b.outfit_id mustEqual 0L b.outfit_name mustEqual "" b.outfit_logo mustEqual 0 b.backpack mustEqual false @@ -470,7 +471,6 @@ class DetailedCharacterDataTest extends Specification { b.is_cloaking mustEqual false b.charging_pose mustEqual false b.on_zipline.isEmpty mustEqual true - b.unk0 mustEqual 0L b.unk1 mustEqual false b.unk2 mustEqual false b.unk3 mustEqual false @@ -687,6 +687,7 @@ class DetailedCharacterDataTest extends Specification { a.unk9 mustEqual 0 a.unkA mustEqual 0 + b.outfit_id mustEqual 556539L b.outfit_name mustEqual "" b.outfit_logo mustEqual 14 b.backpack mustEqual false @@ -697,7 +698,6 @@ class DetailedCharacterDataTest extends Specification { b.is_cloaking mustEqual false b.charging_pose mustEqual false b.on_zipline.isEmpty mustEqual true - b.unk0 mustEqual 556539L b.unk1 mustEqual false b.unk2 mustEqual false b.unk3 mustEqual false @@ -1208,7 +1208,7 @@ class DetailedCharacterDataTest extends Specification { a.unk9 mustEqual 10 a.unkA mustEqual 1 - b.unk0 mustEqual 25044L + b.outfit_id mustEqual 25044L b.outfit_name mustEqual "Black Armored Reapers" b.outfit_logo mustEqual 15 b.unk1 mustEqual false @@ -1355,7 +1355,7 @@ class DetailedCharacterDataTest extends Specification { a.unk9 mustEqual 0 a.unkA mustEqual 0 - b.unk0 mustEqual 16507L + b.outfit_id mustEqual 16507L b.outfit_name mustEqual "Hooked On Insanity" b.outfit_logo mustEqual 5 b.unk1 mustEqual false diff --git a/src/test/scala/game/objectcreatevehicle/MountedVehiclesTest.scala b/src/test/scala/game/objectcreatevehicle/MountedVehiclesTest.scala index 7153b65b..de95c886 100644 --- a/src/test/scala/game/objectcreatevehicle/MountedVehiclesTest.scala +++ b/src/test/scala/game/objectcreatevehicle/MountedVehiclesTest.scala @@ -75,6 +75,7 @@ class MountedVehiclesTest extends Specification { a.unk9 mustEqual 0 a.unkA mustEqual 0 + b.outfit_id mustEqual 316554L b.outfit_name mustEqual "Black Beret Armoured Corps" b.outfit_logo mustEqual 23 b.backpack mustEqual false @@ -85,7 +86,6 @@ class MountedVehiclesTest extends Specification { b.is_cloaking mustEqual false b.charging_pose mustEqual false b.on_zipline.isEmpty mustEqual true - b.unk0 mustEqual 316554L b.unk1 mustEqual false b.unk2 mustEqual false b.unk3 mustEqual false