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