From f45420b51156d82a9689024461db819b328988f1 Mon Sep 17 00:00:00 2001
From: FateJH <Jason_DiDonato@yahoo.com>
Date: Fri, 17 May 2019 14:26:11 -0400
Subject: [PATCH 01/31] ; moved from brnach squad-req, due to branch
 incompatibilities

---
 .../psforever/packet/GamePacketOpcode.scala   |  2 +-
 .../packet/game/SquadMembershipRequest.scala  | 55 +++++++++++++++
 .../psforever/types/SquadRequestType.scala    | 28 ++++++++
 .../game/SquadMembershipRequestTest.scala     | 67 +++++++++++++++++++
 4 files changed, 151 insertions(+), 1 deletion(-)
 create mode 100644 common/src/main/scala/net/psforever/packet/game/SquadMembershipRequest.scala
 create mode 100644 common/src/main/scala/net/psforever/types/SquadRequestType.scala
 create mode 100644 common/src/test/scala/game/SquadMembershipRequestTest.scala

diff --git a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
index b26a9834..61979bc8 100644
--- a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
+++ b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
@@ -446,7 +446,7 @@ object GamePacketOpcode extends Enumeration {
     case 0x6b => game.TriggerSoundMessage.decode
     case 0x6c => game.LootItemMessage.decode
     case 0x6d => game.VehicleSubStateMessage.decode
-    case 0x6e => noDecoder(SquadMembershipRequest)
+    case 0x6e => game.SquadMembershipRequest.decode
     case 0x6f => noDecoder(SquadMembershipResponse)
 
     // OPCODES 0x70-7f
diff --git a/common/src/main/scala/net/psforever/packet/game/SquadMembershipRequest.scala b/common/src/main/scala/net/psforever/packet/game/SquadMembershipRequest.scala
new file mode 100644
index 00000000..ee55360d
--- /dev/null
+++ b/common/src/main/scala/net/psforever/packet/game/SquadMembershipRequest.scala
@@ -0,0 +1,55 @@
+// Copyright (c) 2019 PSForever
+package net.psforever.packet.game
+
+import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
+import net.psforever.types.SquadRequestType
+import scodec.Codec
+import scodec.codecs._
+
+/**
+  * SquadMembershipRequest is a Client->Server message for manipulating squad and platoon members.
+  * @param request_type na
+  * @param unk2 na
+  * @param unk3 na
+  * @param player_name na
+  * @param unk5 na
+  */
+final case class SquadMembershipRequest(request_type : SquadRequestType.Value,
+                                        unk2 : Long,
+                                        unk3 : Option[Long],
+                                        player_name : String,
+                                        unk5 : Option[Option[String]])
+  extends PlanetSideGamePacket {
+  request_type match {
+    case SquadRequestType.Accept | SquadRequestType.Reject | SquadRequestType.Disband |
+         SquadRequestType.PAccept | SquadRequestType.PReject | SquadRequestType.PDisband =>
+      assert(unk3.isEmpty, s"a $request_type request requires the unk3 field be undefined")
+    case _ =>
+      assert(unk3.nonEmpty, s"a $request_type request requires the unk3 field be defined")
+  }
+  if(request_type == SquadRequestType.Invite) {
+    assert(unk5.nonEmpty, "an Invite request requires the unk5 field be undefined")
+  }
+
+  type Packet = SquadMembershipRequest
+  def opcode = GamePacketOpcode.SquadMembershipRequest
+  def encode = SquadMembershipRequest.encode(this)
+}
+
+object SquadMembershipRequest extends Marshallable[SquadMembershipRequest] {
+  implicit val codec : Codec[SquadMembershipRequest] = (
+    ("request_type" | SquadRequestType.codec) >>:~ { request_type =>
+      ("unk2" | uint32L) ::
+        conditional(request_type != SquadRequestType.Accept &&
+          request_type != SquadRequestType.Reject &&
+          request_type != SquadRequestType.Disband &&
+          request_type != SquadRequestType.PAccept &&
+          request_type != SquadRequestType.PReject &&
+          request_type != SquadRequestType.PDisband, "unk3" | uint32L) ::
+        (("player_name" | PacketHelpers.encodedWideStringAligned(4)) >>:~ { pname =>
+          conditional(request_type == SquadRequestType.Invite,
+            "unk5" | optional(bool, PacketHelpers.encodedWideStringAligned({if(pname.length == 0) 3 else 7}))
+          ).hlist
+        })
+    }).as[SquadMembershipRequest]
+}
diff --git a/common/src/main/scala/net/psforever/types/SquadRequestType.scala b/common/src/main/scala/net/psforever/types/SquadRequestType.scala
new file mode 100644
index 00000000..6b104c68
--- /dev/null
+++ b/common/src/main/scala/net/psforever/types/SquadRequestType.scala
@@ -0,0 +1,28 @@
+// Copyright (c) 2019 PSForever
+package net.psforever.types
+
+import net.psforever.packet.PacketHelpers
+import scodec.codecs._
+
+object SquadRequestType extends Enumeration {
+  type Type = Value
+  val Invite,   //00
+  Unk01,    //01
+  Accept,   //02
+  Reject,   //03
+  Cancel,   //04
+  Leave,    //05
+  Promote,  //06
+  Disband,  //07
+  PInvite,  //08
+  PAccept,  //09
+  PReject,  //10
+  PCancel,  //11
+  PLeave,   //12
+  PDisband, //13
+  Unk14,    //14
+  Unk15     //15
+  = Value
+
+  implicit val codec = PacketHelpers.createEnumerationCodec(this, uint4L)
+}
diff --git a/common/src/test/scala/game/SquadMembershipRequestTest.scala b/common/src/test/scala/game/SquadMembershipRequestTest.scala
new file mode 100644
index 00000000..79cb9319
--- /dev/null
+++ b/common/src/test/scala/game/SquadMembershipRequestTest.scala
@@ -0,0 +1,67 @@
+// Copyright (c) 2019 PSForever
+package game
+
+import net.psforever.packet._
+import net.psforever.packet.game._
+import net.psforever.types.SquadRequestType
+import org.specs2.mutable._
+import scodec.bits._
+
+class SquadMembershipRequestTest extends Specification {
+  //481c897e-b47b-41cc-b7ad-c604606d985e / PSCap-2016-03-18_12-48-12-PM.gcap / Game record 5521 at 662.786844s
+  val string1 = hex"6e015aa7a0224d87a0280000"
+  //... / PSCap-2016-06-29_07-49-26-PM (last).gcap / Game record 77 at 9.732430 (found in MultiPacket)
+  val string2 = hex"6E265DD7A02800"
+  //TODO find example where player_name field is defined
+  //TODO find example where unk field is defined and is a string
+
+  "decode (1)" in {
+    PacketCoding.DecodePacket(string1).require match {
+      case SquadMembershipRequest(req_type, unk2, unk3, p_name, unk5) =>
+        req_type mustEqual SquadRequestType.Invite
+        unk2 mustEqual 41593365L
+        unk3.contains(41605156L) mustEqual true
+        p_name mustEqual ""
+        unk5.contains(None) mustEqual true
+      case _ =>
+        ko
+    }
+  }
+
+  "decode (2)" in {
+    PacketCoding.DecodePacket(string2).require match {
+      case SquadMembershipRequest(req_type, unk2, unk3, p_name, unk5) =>
+        req_type mustEqual SquadRequestType.Accept
+        unk2 mustEqual 41606501L
+        unk3.isEmpty mustEqual true
+        p_name mustEqual ""
+        unk5.isEmpty mustEqual true
+      case _ =>
+        ko
+    }
+  }
+
+  "encode (1)" in {
+    val msg = SquadMembershipRequest(SquadRequestType.Invite, 41593365, Some(41605156L), "", Some(None))
+    val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+    pkt mustEqual string1
+  }
+
+  "encode (1; failure 1)" in {
+    SquadMembershipRequest(SquadRequestType.Invite, 41593365, None, "", Some(None)) must throwA[AssertionError]
+  }
+
+  "encode (1; failure 2)" in {
+    SquadMembershipRequest(SquadRequestType.Invite, 41593365, Some(41605156L), "", None) must throwA[AssertionError]
+  }
+
+  "encode (2)" in {
+    val msg = SquadMembershipRequest(SquadRequestType.Accept, 41606501, None, "", None)
+    val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+    pkt mustEqual string2
+  }
+
+  "encode (2; failure)" in {
+    SquadMembershipRequest(SquadRequestType.Accept, 41606501, Some(41606501), "", None) must throwA[AssertionError]
+  }
+}

From e44d21520a293c8ec4a38b5bf26c3b38133de74e Mon Sep 17 00:00:00 2001
From: FateJH <Jason_DiDonato@yahoo.com>
Date: Sat, 18 May 2019 15:22:46 -0400
Subject: [PATCH 02/31] initial SquadMembershipResponse packet and tests;
 re-naming some SquadRequestType values

---
 .../psforever/packet/GamePacketOpcode.scala   |   2 +-
 .../packet/game/SquadMembershipRequest.scala  |   8 +-
 .../packet/game/SquadMembershipResponse.scala |  52 +++
 .../psforever/types/SquadRequestType.scala    |  15 +-
 .../game/SquadMembershipResponseTest.scala    | 421 ++++++++++++++++++
 5 files changed, 486 insertions(+), 12 deletions(-)
 create mode 100644 common/src/main/scala/net/psforever/packet/game/SquadMembershipResponse.scala
 create mode 100644 common/src/test/scala/game/SquadMembershipResponseTest.scala

diff --git a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
index 61979bc8..3845c4a3 100644
--- a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
+++ b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
@@ -447,7 +447,7 @@ object GamePacketOpcode extends Enumeration {
     case 0x6c => game.LootItemMessage.decode
     case 0x6d => game.VehicleSubStateMessage.decode
     case 0x6e => game.SquadMembershipRequest.decode
-    case 0x6f => noDecoder(SquadMembershipResponse)
+    case 0x6f => game.SquadMembershipResponse.decode
 
     // OPCODES 0x70-7f
     case 0x70 => noDecoder(SquadMemberEvent)
diff --git a/common/src/main/scala/net/psforever/packet/game/SquadMembershipRequest.scala b/common/src/main/scala/net/psforever/packet/game/SquadMembershipRequest.scala
index ee55360d..55d73197 100644
--- a/common/src/main/scala/net/psforever/packet/game/SquadMembershipRequest.scala
+++ b/common/src/main/scala/net/psforever/packet/game/SquadMembershipRequest.scala
@@ -22,7 +22,7 @@ final case class SquadMembershipRequest(request_type : SquadRequestType.Value,
   extends PlanetSideGamePacket {
   request_type match {
     case SquadRequestType.Accept | SquadRequestType.Reject | SquadRequestType.Disband |
-         SquadRequestType.PAccept | SquadRequestType.PReject | SquadRequestType.PDisband =>
+         SquadRequestType.PlatoonAccept | SquadRequestType.PlatoonReject | SquadRequestType.PlatoonDisband =>
       assert(unk3.isEmpty, s"a $request_type request requires the unk3 field be undefined")
     case _ =>
       assert(unk3.nonEmpty, s"a $request_type request requires the unk3 field be defined")
@@ -43,9 +43,9 @@ object SquadMembershipRequest extends Marshallable[SquadMembershipRequest] {
         conditional(request_type != SquadRequestType.Accept &&
           request_type != SquadRequestType.Reject &&
           request_type != SquadRequestType.Disband &&
-          request_type != SquadRequestType.PAccept &&
-          request_type != SquadRequestType.PReject &&
-          request_type != SquadRequestType.PDisband, "unk3" | uint32L) ::
+          request_type != SquadRequestType.PlatoonAccept &&
+          request_type != SquadRequestType.PlatoonReject &&
+          request_type != SquadRequestType.PlatoonDisband, "unk3" | uint32L) ::
         (("player_name" | PacketHelpers.encodedWideStringAligned(4)) >>:~ { pname =>
           conditional(request_type == SquadRequestType.Invite,
             "unk5" | optional(bool, PacketHelpers.encodedWideStringAligned({if(pname.length == 0) 3 else 7}))
diff --git a/common/src/main/scala/net/psforever/packet/game/SquadMembershipResponse.scala b/common/src/main/scala/net/psforever/packet/game/SquadMembershipResponse.scala
new file mode 100644
index 00000000..0d01a938
--- /dev/null
+++ b/common/src/main/scala/net/psforever/packet/game/SquadMembershipResponse.scala
@@ -0,0 +1,52 @@
+// Copyright (c) 2019 PSForever
+package net.psforever.packet.game
+
+import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
+import net.psforever.types.SquadRequestType
+import scodec.Codec
+import scodec.codecs._
+
+/**
+  * na
+  * @param request_type the type of request being answered
+  * @param unk1 na
+  * @param unk2 na
+  * @param unk3 na
+  * @param unk4 na
+  * @param player_name the player being affected, if applicable
+  * @param unk5 na
+  * @param unk6 na
+  */
+final case class SquadMembershipResponse(request_type : SquadRequestType.Value,
+                                         unk1 : Int,
+                                         unk2 : Int,
+                                         unk3 : Long,
+                                         unk4 : Option[Long],
+                                         player_name : String,
+                                         unk5 : Boolean,
+                                         unk6 : Option[Option[String]])
+  extends PlanetSideGamePacket {
+  /*
+    if(response_type != 6 && response_type != 12)
+      assert(unk5.isDefined, "unk5 field required")
+    else
+      assert(!unk5.isDefined, "unk5 defined but unk1 invalid value")
+  */
+  type Packet = SquadMembershipResponse
+  def opcode = GamePacketOpcode.SquadMembershipResponse
+  def encode = SquadMembershipResponse.encode(this)
+}
+
+object SquadMembershipResponse extends Marshallable[SquadMembershipResponse] {
+  implicit val codec : Codec[SquadMembershipResponse] = (
+    "request_type" | SquadRequestType.codec >>:~ { d =>
+      ("unk1" | uint(5)) ::
+        ("unk2" | uint2) ::
+        ("unk3" | uint32L) ::
+        ("unk4" | conditional(d != SquadRequestType.Promote && d != SquadRequestType.PlatoonLeave, uint32L)) ::
+        ("player_name" | PacketHelpers.encodedWideStringAligned(5)) ::
+        ("unk5" | bool) ::
+        conditional(d != SquadRequestType.Invite, optional(bool, "unk6" | PacketHelpers.encodedWideStringAligned(6)))
+    }
+    ).as[SquadMembershipResponse]
+}
diff --git a/common/src/main/scala/net/psforever/types/SquadRequestType.scala b/common/src/main/scala/net/psforever/types/SquadRequestType.scala
index 6b104c68..8d7e2435 100644
--- a/common/src/main/scala/net/psforever/types/SquadRequestType.scala
+++ b/common/src/main/scala/net/psforever/types/SquadRequestType.scala
@@ -6,7 +6,8 @@ import scodec.codecs._
 
 object SquadRequestType extends Enumeration {
   type Type = Value
-  val Invite,   //00
+  val
+  Invite,   //00
   Unk01,    //01
   Accept,   //02
   Reject,   //03
@@ -14,12 +15,12 @@ object SquadRequestType extends Enumeration {
   Leave,    //05
   Promote,  //06
   Disband,  //07
-  PInvite,  //08
-  PAccept,  //09
-  PReject,  //10
-  PCancel,  //11
-  PLeave,   //12
-  PDisband, //13
+  PlatoonInvite,  //08
+  PlatoonAccept,  //09
+  PlatoonReject,  //10
+  PlatoonCancel,  //11
+  PlatoonLeave,   //12
+  PlatoonDisband, //13
   Unk14,    //14
   Unk15     //15
   = Value
diff --git a/common/src/test/scala/game/SquadMembershipResponseTest.scala b/common/src/test/scala/game/SquadMembershipResponseTest.scala
new file mode 100644
index 00000000..c640df04
--- /dev/null
+++ b/common/src/test/scala/game/SquadMembershipResponseTest.scala
@@ -0,0 +1,421 @@
+// Copyright (c) 2019 PSForever
+package game
+
+import net.psforever.packet._
+import net.psforever.packet.game._
+import net.psforever.types.SquadRequestType
+import org.specs2.mutable._
+import scodec.bits._
+
+class SquadMembershipResponseTest extends Specification {
+  val string_01 = hex"6f0 00854518050db2260108048006f006600440000"
+  val string_02 = hex"6f0 0049e8220112aa1e01100530050004f0049004c0045005200530080"
+  val string_11 = hex"6f1 995364f2040000000100080"
+  val string_12 = hex"6f1 90cadcf4040000000100080"
+  val string_21 = hex"6f2 010db2260085451805140560069007200750073004700690076006500720080"
+  val string_22 = hex"6f2 010db22601da03aa03140560069007200750073004700690076006500720080"
+  val string_31 = hex"6f3 07631db202854518050a048004d0046004900430000"
+  val string_32 = hex"6f3 04c34fb402854518050e0440041004e00310031003100310000"
+  val string_41 = hex"6f4 04cadcf405bbbef405140530041007200610069007300560061006e00750000"
+  val string_42 = hex"6f4 05c9c0f405d71aec0516041006900720049006e006a006500630074006f00720000"
+  val string_51 = hex"6f5 0249e8220049e822010e0430043005200490044004500520080"
+  val string_71 = hex"6f7 1049e822000000000100080"
+  val string_72 = hex"6f7 00cadcf4041355ae03100570069007a006b00690064003400350080"
+  val string_81 = hex"6f8 001355ae02cadcf405100570069007a006b00690064003400350000"
+  val string_91 = hex"6f9 008310080115aef40500080"
+  val string_92 = hex"6f9 001355ae02cadcf405100570069007a006b00690064003400350000"
+  val string_b1 = hex"6fb 021355ae02cadcf405140530041007200610069007300560061006e00750000"
+
+  "SquadMembershipResponse" should {
+    "decode (0-1)" in {
+      PacketCoding.DecodePacket(string_01).require match {
+        case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) =>
+          unk1 mustEqual SquadRequestType.Invite
+          unk2 mustEqual 0
+          unk3 mustEqual 0
+          unk4 mustEqual 42771010L
+          unk5.contains(1300870L) mustEqual true
+          unk6 mustEqual "HofD"
+          unk7 mustEqual false
+          unk8.isEmpty mustEqual true
+        case _ =>
+          ko
+      }
+    }
+
+    "decode (0-2)" in {
+      PacketCoding.DecodePacket(string_02).require match {
+        case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) =>
+          unk1 mustEqual SquadRequestType.Invite
+          unk2 mustEqual 0
+          unk3 mustEqual 0
+          unk4 mustEqual 1176612L
+          unk5.contains(1004937L) mustEqual true
+          unk6 mustEqual "SPOILERS"
+          unk7 mustEqual true
+          unk8.isEmpty mustEqual true
+        case _ =>
+          ko
+      }
+    }
+
+    "decode (1-1)" in {
+      PacketCoding.DecodePacket(string_11).require match {
+        case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) =>
+          unk1 mustEqual SquadRequestType.Unk01
+          unk2 mustEqual 19
+          unk3 mustEqual 0
+          unk4 mustEqual 41530025L
+          unk5.contains(0L) mustEqual true
+          unk6 mustEqual ""
+          unk7 mustEqual true
+          unk8.contains(None) mustEqual true
+        case _ =>
+          ko
+      }
+    }
+
+    "decode (1-2)" in {
+      PacketCoding.DecodePacket(string_12).require match {
+        case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) =>
+          unk1 mustEqual SquadRequestType.Unk01
+          unk2 mustEqual 18
+          unk3 mustEqual 0
+          unk4 mustEqual 41578085L
+          unk5.contains(0L) mustEqual true
+          unk6 mustEqual ""
+          unk7 mustEqual true
+          unk8.contains(None) mustEqual true
+        case _ =>
+          ko
+      }
+    }
+
+    "decode (2-1)" in {
+      PacketCoding.DecodePacket(string_21).require match {
+        case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) =>
+          unk1 mustEqual SquadRequestType.Accept
+          unk2 mustEqual 0
+          unk3 mustEqual 0
+          unk4 mustEqual 1300870L
+          unk5.contains(42771010L) mustEqual true
+          unk6 mustEqual "VirusGiver"
+          unk7 mustEqual true
+          unk8.contains(None) mustEqual true
+        case _ =>
+          ko
+      }
+    }
+
+    "decode (2-2)" in {
+      PacketCoding.DecodePacket(string_22).require match {
+        case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) =>
+          unk1 mustEqual SquadRequestType.Accept
+          unk2 mustEqual 0
+          unk3 mustEqual 0
+          unk4 mustEqual 1300870L
+          unk5.contains(30736877L) mustEqual true
+          unk6 mustEqual "VirusGiver"
+          unk7 mustEqual true
+          unk8.contains(None) mustEqual true
+        case _ =>
+          ko
+      }
+    }
+
+    "decode (3-1)" in {
+      PacketCoding.DecodePacket(string_31).require match {
+        case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) =>
+          unk1 mustEqual SquadRequestType.Reject
+          unk2 mustEqual 0
+          unk3 mustEqual 3
+          unk4 mustEqual 31035057L
+          unk5.contains(42771010L) mustEqual true
+          unk6 mustEqual "HMFIC"
+          unk7 mustEqual false
+          unk8.contains(None) mustEqual true
+        case _ =>
+          ko
+      }
+    }
+
+    "decode (3-2)" in {
+      PacketCoding.DecodePacket(string_32).require match {
+        case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) =>
+          unk1 mustEqual SquadRequestType.Reject
+          unk2 mustEqual 0
+          unk3 mustEqual 2
+          unk4 mustEqual 31106913L
+          unk5.contains(42771010L) mustEqual true
+          unk6 mustEqual "DAN1111"
+          unk7 mustEqual false
+          unk8.contains(None) mustEqual true
+        case _ =>
+          ko
+      }
+    }
+
+    "decode (4-1)" in {
+      PacketCoding.DecodePacket(string_41).require match {
+        case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) =>
+          unk1 mustEqual SquadRequestType.Cancel
+          unk2 mustEqual 0
+          unk3 mustEqual 2
+          unk4 mustEqual 41578085L
+          unk5.contains(41607133L) mustEqual true
+          unk6 mustEqual "SAraisVanu"
+          unk7 mustEqual false
+          unk8.contains(None) mustEqual true
+        case _ =>
+          ko
+      }
+    }
+
+    "decode (4-2)" in {
+      PacketCoding.DecodePacket(string_42).require match {
+        case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) =>
+          unk1 mustEqual SquadRequestType.Cancel
+          unk2 mustEqual 0
+          unk3 mustEqual 2
+          unk4 mustEqual 41607396L
+          unk5.contains(41324011L) mustEqual true
+          unk6 mustEqual "AirInjector"
+          unk7 mustEqual false
+          unk8.contains(None) mustEqual true
+        case _ =>
+          ko
+      }
+    }
+
+    "decode (5-1)" in {
+      PacketCoding.DecodePacket(string_51).require match {
+        case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) =>
+          unk1 mustEqual SquadRequestType.Leave
+          unk2 mustEqual 0
+          unk3 mustEqual 1
+          unk4 mustEqual 1176612L
+          unk5.contains(1176612L) mustEqual true
+          unk6 mustEqual "CCRIDER"
+          unk7 mustEqual true
+          unk8.contains(None) mustEqual true
+        case _ =>
+          ko
+      }
+    }
+
+    "decode (7-1)" in {
+      PacketCoding.DecodePacket(string_71).require match {
+        case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) =>
+          unk1 mustEqual SquadRequestType.Disband
+          unk2 mustEqual 2
+          unk3 mustEqual 0
+          unk4 mustEqual 1176612L
+          unk5.contains(0L) mustEqual true
+          unk6 mustEqual ""
+          unk7 mustEqual true
+          unk8.contains(None) mustEqual true
+        case _ =>
+          ko
+      }
+    }
+
+    "decode (7-2)" in {
+      PacketCoding.DecodePacket(string_72).require match {
+        case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) =>
+          unk1 mustEqual SquadRequestType.Disband
+          unk2 mustEqual 0
+          unk3 mustEqual 0
+          unk4 mustEqual 41578085L
+          unk5.contains(30910985L) mustEqual true
+          unk6 mustEqual "Wizkid45"
+          unk7 mustEqual true
+          unk8.contains(None) mustEqual true
+        case _ =>
+          ko
+      }
+    }
+
+    "decode (8-1)" in {
+      PacketCoding.DecodePacket(string_81).require match {
+        case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) =>
+          unk1 mustEqual SquadRequestType.PlatoonInvite
+          unk2 mustEqual 0
+          unk3 mustEqual 0
+          unk4 mustEqual 30910985L
+          unk5.contains(41578085L) mustEqual true
+          unk6 mustEqual "Wizkid45"
+          unk7 mustEqual false
+          unk8.contains(None) mustEqual true
+        case _ =>
+          ko
+      }
+    }
+
+    "decode (9-1)" in {
+      PacketCoding.DecodePacket(string_91).require match {
+        case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) =>
+          unk1 mustEqual SquadRequestType.PlatoonAccept
+          unk2 mustEqual 0
+          unk3 mustEqual 0
+          unk4 mustEqual 297025L
+          unk5.contains(41605002L) mustEqual true
+          unk6 mustEqual ""
+          unk7 mustEqual true
+          unk8.contains(None) mustEqual true
+        case _ =>
+          ko
+      }
+    }
+
+    "decode (9-2)" in {
+      PacketCoding.DecodePacket(string_92).require match {
+        case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) =>
+          unk1 mustEqual SquadRequestType.PlatoonAccept
+          unk2 mustEqual 0
+          unk3 mustEqual 0
+          unk4 mustEqual 30910985L
+          unk5.contains(41578085L) mustEqual true
+          unk6 mustEqual "Wizkid45"
+          unk7 mustEqual false
+          unk8.contains(None) mustEqual true
+        case _ =>
+          ko
+      }
+    }
+
+    "decode (b-1)" in {
+      PacketCoding.DecodePacket(string_b1).require match {
+        case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) =>
+          unk1 mustEqual SquadRequestType.PlatoonCancel
+          unk2 mustEqual 0
+          unk3 mustEqual 1
+          unk4 mustEqual 30910985L
+          unk5.contains(41578085L) mustEqual true
+          unk6 mustEqual "SAraisVanu"
+          unk7 mustEqual false
+          unk8.contains(None) mustEqual true
+        case _ =>
+          ko
+      }
+    }
+
+    "encode (0-1)" in {
+      val msg = SquadMembershipResponse(SquadRequestType.Invite, 0, 0, 42771010L, Some(1300870L), "HofD", false, None)
+      val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+      pkt mustEqual string_01
+    }
+
+    "encode (0-2)" in {
+      val msg = SquadMembershipResponse(SquadRequestType.Invite, 0, 0, 1176612L, Some(1004937L), "SPOILERS", true, None)
+      val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+      pkt mustEqual string_02
+    }
+
+    "encode (1-1)" in {
+      val msg = SquadMembershipResponse(SquadRequestType.Unk01, 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(SquadRequestType.Unk01, 18, 0, 41578085L, Some(0L), "", true, Some(None))
+      val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+      pkt mustEqual string_12
+    }
+
+    "encode (2-1)" in {
+      val msg = SquadMembershipResponse(SquadRequestType.Accept, 0, 0, 1300870L, Some(42771010L), "VirusGiver", true, Some(None))
+      val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+      pkt mustEqual string_21
+    }
+
+    "encode (2-2)" in {
+      val msg = SquadMembershipResponse(SquadRequestType.Accept, 0, 0, 1300870L, Some(30736877L), "VirusGiver", true, Some(None))
+      val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+      pkt mustEqual string_22
+    }
+
+    "encode (3-1)" in {
+      val msg = SquadMembershipResponse(SquadRequestType.Reject, 0, 3, 31035057L, Some(42771010L), "HMFIC", false, Some(None))
+      val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+      pkt mustEqual string_31
+    }
+
+    "encode (3-2)" in {
+      val msg = SquadMembershipResponse(SquadRequestType.Reject, 0, 2, 31106913L, Some(42771010L), "DAN1111", false, Some(None))
+      val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+      pkt mustEqual string_32
+    }
+
+    "encode (4-1)" in {
+      val msg = SquadMembershipResponse(SquadRequestType.Cancel, 0, 2, 41578085L, Some(41607133L), "SAraisVanu", false, Some(None))
+      val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+      pkt mustEqual string_41
+    }
+
+    "encode (4-2)" in {
+      val msg = SquadMembershipResponse(SquadRequestType.Cancel, 0, 2, 41607396L, Some(41324011L), "AirInjector", false, Some(None))
+      val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+      pkt mustEqual string_42
+    }
+
+    "encode (5-1)" in {
+      val msg = SquadMembershipResponse(SquadRequestType.Leave, 0, 1, 1176612L, Some(1176612L), "CCRIDER", true, Some(None))
+      val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+      pkt mustEqual string_51
+    }
+
+    "encode (7-1)" in {
+      val msg = SquadMembershipResponse(SquadRequestType.Disband, 2, 0, 1176612L, Some(0L), "", true, Some(None))
+      val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+      pkt mustEqual string_71
+    }
+
+    "encode (7-2)" in {
+      val msg = SquadMembershipResponse(SquadRequestType.Disband, 0, 0, 41578085L, Some(30910985L), "Wizkid45", true, Some(None))
+      val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+      pkt mustEqual string_72
+    }
+
+    "encode (8-1)" in {
+      val msg = SquadMembershipResponse(SquadRequestType.PlatoonInvite, 0, 0, 30910985L, Some(41578085L), "Wizkid45", false, Some(None))
+      val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+      pkt mustEqual string_81
+    }
+
+    "encode (9-1)" in {
+      val msg = SquadMembershipResponse(SquadRequestType.PlatoonAccept, 0, 0, 297025L, Some(41605002L), "", true, Some(None))
+      val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+      pkt mustEqual string_91
+    }
+
+    "encode (9-2)" in {
+      val msg = SquadMembershipResponse(SquadRequestType.PlatoonAccept, 0, 0, 30910985L, Some(41578085L), "Wizkid45", false, Some(None))
+      val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+      pkt mustEqual string_92
+    }
+
+    "encode (b-1)" in {
+      val msg = SquadMembershipResponse(SquadRequestType.PlatoonCancel, 0, 1, 30910985L, Some(41578085L), "SAraisVanu", false, Some(None))
+      val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+      pkt mustEqual string_b1
+    }
+  }
+}

From 6fdc617a876bf0c7c81cdbbba289892670a16783 Mon Sep 17 00:00:00 2001
From: FateJH <Jason_DiDonato@yahoo.com>
Date: Wed, 22 May 2019 02:17:11 -0400
Subject: [PATCH 03/31] overhaul of ReplicationStreamMessage to make the object
 form of the packet less of a hassle to compose; updated tests for RSM

---
 .../game/ReplicationStreamMessage.scala       | 934 ++++++++++--------
 .../game/ReplicationStreamMessageTest.scala   | 523 ++++------
 .../src/main/scala/WorldSessionActor.scala    |   4 +-
 3 files changed, 698 insertions(+), 763 deletions(-)

diff --git a/common/src/main/scala/net/psforever/packet/game/ReplicationStreamMessage.scala b/common/src/main/scala/net/psforever/packet/game/ReplicationStreamMessage.scala
index 0f4aafc3..233f5a59 100644
--- a/common/src/main/scala/net/psforever/packet/game/ReplicationStreamMessage.scala
+++ b/common/src/main/scala/net/psforever/packet/game/ReplicationStreamMessage.scala
@@ -5,90 +5,82 @@ import net.psforever.newcodecs.newcodecs
 import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
 import scodec.{Attempt, Codec, Err}
 import scodec.codecs._
-import shapeless.{::, HNil}
+import shapeless.HNil //DO NOT IMPORT shapeless.:: HERE; it interferes with required scala.collection.immutable.::
+
+import scala.annotation.tailrec
 
 /**
-  * Maintains squad information changes performed by this listing.
+  * Maintain squad information for a given squad's listing.
   * Only certain information will be transmitted depending on the purpose of the packet.
-  * @param leader the name of the squad leader as a wide character string, or `None` if not applicable
-  * @param task the task the squad is trying to perform as a wide character string, or `None` if not applicable
-  * @param zone_id the continent on which the squad is acting, or `None` if not applicable
-  * @param size the current size of the squad, or `None` if not applicable;
-  *             "can" be greater than `capacity`, though with issues
-  * @param capacity the maximum number of members that the squad can tolerate, or `None` if not applicable;
+  * @param leader the name of the squad leader, usually the first person in the squad member list;
+  *               `None` if not applicable
+  * @param task the task the squad is trying to perform as a wide character string;
+  *             `None` if not applicable
+  * @param zone_id the continent on which the squad is acting;
+  *                `None` if not applicable
+  * @param size the current size of the squad;
+  *             "can" be greater than `capacity`, though with issues;
+  *             `None` if not applicable
+  * @param capacity the maximum number of members that the squad can tolerate;
   *                 normal count is 10;
-  *                 maximum is 15 but naturally can not be assigned that many
-  * @param squad_guid a GUID associated with the squad, used to recover the squad definition, or `None` if not applicable;
-  *                   sometimes it is defined but is still not applicable
+  *                 maximum is 15 but naturally can not be assigned that many;
+  *                 `None` if not applicable
+  * @param squad_guid a GUID associated with the squad, used to recover the squad definition;
+  *                   sometimes it is defined but is still not applicable;
+  *                   `None` if not applicable (rarely applicable)
   */
 final case class SquadInfo(leader : Option[String],
                            task : Option[String],
                            zone_id : Option[PlanetSideZoneID],
                            size : Option[Int],
                            capacity : Option[Int],
-                           squad_guid : Option[PlanetSideGUID] = None)
+                           squad_guid : Option[PlanetSideGUID] = None) {
+  /**
+    * Populate the undefined fields of this object with the populated fields of a second object.
+    * If the field is already defined in this object, the provided object does not contribute new data.
+    * @param info the `SquadInfo` data to be incorporated into this object's data
+    * @return a new `SquadInfo` object, combining with two objects' field data
+    */
+  def And(info : SquadInfo) : SquadInfo = {
+    SquadInfo(
+      leader.orElse(info.leader),
+      task.orElse(info.task),
+      zone_id.orElse(info.zone_id),
+      size.orElse(info.size),
+      capacity.orElse(info.capacity),
+      squad_guid.orElse(info.squad_guid)
+    )
+  }
+}
 
 /**
-  * Define three fields determining the purpose of data in this listing.<br>
-  * <br>
-  * The third field `unk3` is not always be defined and will be supplanted by the squad (definition) GUID during initialization and a full update.<br>
-  * <br>
-  * Actions:<br>
-  * `unk1&nbsp;unk2&nbsp;&nbsp;unk3`<br>
-  * `0&nbsp;&nbsp;&nbsp;&nbsp;true&nbsp;&nbsp;4 -- `Remove a squad from listing<br>
-  * `128&nbsp;&nbsp;true&nbsp;&nbsp;0 -- `Update a squad's leader<br>
-  * `128&nbsp;&nbsp;true&nbsp;&nbsp;1 -- `Update a squad's task or continent<br>
-  * `128&nbsp;&nbsp;true&nbsp;&nbsp;2 -- `Update a squad's size<br>
-  * `129&nbsp;&nbsp;false&nbsp;0 -- `Update a squad's leader or size<br>
-  * `129&nbsp;&nbsp;false&nbsp;1 -- `Update a squad's task and continent<br>
-  * `131&nbsp;&nbsp;false&nbsp;X -- `Add all squads during initialization / update all information pertaining to this squad
-  * @param unk1 na
-  * @param unk2 na
-  * @param unk3 na;
-  *             not always defined
-  * @param info information pertaining to this squad listing
-  */
-//TODO when these unk# values are better understood, transform SquadHeader to streamline the actions to be performed
-final case class SquadHeader(unk1 : Int,
-                             unk2 : Boolean,
-                             unk3 : Option[Int],
-                             info : Option[SquadInfo] = None)
-
-/**
-  * An indexed entry in the listing of squads.<br>
-  * <br>
-  * Squad listing indices are not an arbitrary order.
-  * The server communicates changes to the client by referencing a squad's listing index, defined at the time of list initialization.
-  * Once initialized, each client may organize their squads however they wish, e.g., by leader, by task, etc., without compromising this index.
-  * During the list initialization process, the entries must always follow numerical order, increasing from `0`.
-  * During any other operation, the entries may be prefixed with whichever index is necessary to indicate the squad listing in question.
-  * @param index the index of this listing;
-  *              first entry should be 0, and subsequent valid entries are sequentially incremental;
-  *              last entry is always a placeholder with index 255
-  * @param listing the data for this entry, defining both the actions and the pertinent squad information
+  * An indexed entry in the listing of squads.
+  * @param index the listing entry index for this squad;
+  *              zero-based;
+  *              255 is the maximum index and is reserved to indicate the end of the listings for the packet
+  * @param listing the squad data;
+  *                `None` when the index is 255, or when invoking a "remove" action on any squad at a known index
   */
 final case class SquadListing(index : Int = 255,
-                              listing : Option[SquadHeader] = None)
+                              listing : Option[SquadInfo] = None)
 
 /**
-  * Modify the list of squads available to a given player.
-  * The squad list updates in real time rather than just whenever a player opens the squad information window.<br>
+  * Display the list of squads available to a given player.<br>
   * <br>
-  * The four main operations are: initializing the list, updating entries in the list, removing entries from the list, and clearing the list.
+  * The four main operations are:
+  * initializing the list,
+  * updating entries in the list,
+  * removing entries from the list,
+  * and clearing the list.
   * The process of initializing the list and clearing the list actually are performed by similar behavior.
   * Squads would just not be added after the list clears.
   * Moreover, removing entries from the list overrides the behavior to update entries in the list.
-  * The two-three codes per entry (see `SquadHeader`) are important for determining the effect of a specific entry.
-  * As of the moment, the important details of the behaviors is that they modify how the packet is encoded and padded.<br>
+  * Squad list entries are typically referenced by their line index.<br>
   * <br>
-  * Referring to information in `SquadListing`, entries are identified by their index in the list.
-  * This is followed by a coded section that indicates what action the entry will execute on that squad listing.
-  * After the "coded action" section is the "general information" section where the data for the change is specified.
-  * In this manner, all the entries will have a knowable length.<br>
-  * <br>
-  * The total number of entries in a packet is not known until they have all been parsed.
+  * Though often specified with a global identifier, squads are rarely accessed using that identifier.
+  * Outside of initialization activities, the specific index of the squad listing is referenced.
   * During the list initialization process, the entries must be in ascending order of index.
-  * Otherwise, the specific index of the squad listing is referenced.
+  * The total number of entries in a packet is not known until they have all been parsed.
   * The minimum number of entries is "no entries."
   * The maximum number of entries is supposedly 254.
   * The last item is always the index 255 and this is interpreted as the end of the stream.<br>
@@ -106,9 +98,10 @@ final case class SquadListing(index : Int = 255,
   * `5&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;6&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; `Clear squad list and initialize new squad list<br>
   * `5&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;6&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; `Clear squad list (transitions directly into 255-entry)<br>
   * `6&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;X&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; `Update a squad in the list
-  * @param behavior a code that suggests the primary purpose of the data in this packet
-  * @param behavior2 during initialization, this code is read;
-  *                  it supplements the normal `behavior` and is typically is an "update" code
+  * @param behavior a required code that suggests the operations of the data in this packet
+  * @param behavior2 an optional code that suggests the operations of the data in this packet;
+  *                  during initialization, this code is read;
+  *                  it typically flags an "update" action
   * @param entries a `Vector` of the squad listings
   */
 final case class ReplicationStreamMessage(behavior : Int,
@@ -121,15 +114,20 @@ final case class ReplicationStreamMessage(behavior : Int,
 }
 
 object SquadInfo {
+  /**
+    * An entry where no fields are defined.
+    */
+  final val Blank = SquadInfo(None, None, None, None, None)
+
   /**
     * Alternate constructor for `SquadInfo` that ignores the `Option` requirement for the fields.<br>
     * <br>
     * This constructor is not actually used at the moment.
-    * @param leader the name of the squad leader
-    * @param task the task the squad is trying to perform
+    * @param leader         the name of the squad leader
+    * @param task           the task the squad is trying to perform
     * @param continent_guid the continent on which the squad is acting
-    * @param size the current size of the squad
-    * @param capacity the maximum number of members that the squad can tolerate
+    * @param size           the current size of the squad
+    * @param capacity       the maximum number of members that the squad can tolerate
     * @return a `SquadInfo` object
     */
   def apply(leader : String, task : String, continent_guid : PlanetSideZoneID, size : Int, capacity : Int) : SquadInfo = {
@@ -139,13 +137,13 @@ object SquadInfo {
   /**
     * Alternate constructor for `SquadInfo` that ignores the `Option` requirement for the fields.<br>
     * <br>
-    * This constructor is used by the `initCodec`, `alt_initCodec`, and `allCodec`.
-    * @param leader the name of the squad leader
-    * @param task the task the squad is trying to perform
+    * This constructor is used by the `infoCodec`, `alt_infoCodec`, and `allCodec`.
+    * @param leader         the name of the squad leader
+    * @param task           the task the squad is trying to perform
     * @param continent_guid the continent on which the squad is acting
-    * @param size the current size of the squad
-    * @param capacity the maximum number of members that the squad can tolerate
-    * @param squad_guid a GUID associated with the squad, used to recover the squad definition
+    * @param size           the current size of the squad
+    * @param capacity       the maximum number of members that the squad can tolerate
+    * @param squad_guid     a GUID associated with the squad, used to recover the squad definition
     * @return a `SquadInfo` object
     */
   def apply(leader : String, task : String, continent_guid : PlanetSideZoneID, size : Int, capacity : Int, squad_guid : PlanetSideGUID) : SquadInfo = {
@@ -153,33 +151,23 @@ object SquadInfo {
   }
 
   /**
-    * Alternate constructor for `SquadInfo` that ignores the `Option` requirement for the important field.<br>
-    * <br>
-    * This constructor is used by `leaderCodec`.
-    * Two of the fields normally are `Option[String]`s.
-    * Only the `leader` field in this packet is a `String`, giving the method a distinct signature.
-    * The other field - an `Option[String]` for `task` - can still be set if passed.<br>
-    * <br>
-    * Recommended use: `SquadInfo(leader, None)`
+    * Alternate constructor for `SquadInfo` that ignores the `Option` requirement for the important field.
     * @param leader the name of the squad leader
-    * @param task the task the squad is trying to perform, if not `None`
     * @return a `SquadInfo` object
     */
-  def apply(leader : String, task : Option[String]) : SquadInfo = {
-    SquadInfo(Some(leader), task, None, None, None)
+  def apply(leader : String) : SquadInfo = {
+    SquadInfo(Some(leader), None, None, None, None)
   }
 
   /**
     * Alternate constructor for `SquadInfo` that ignores the `Option` requirement for the important field.<br>
     * <br>
-    * This constructor is used by `taskOrContinentCodec`.
-    * Two of the fields normally are `Option[String]`s.
     * Only the `task` field in this packet is a `String`, giving the method a distinct signature.
     * The other field - an `Option[String]` for `leader` - can still be set if passed.<br>
     * <br>
     * Recommended use: `SquadInfo(None, task)`
     * @param leader the name of the squad leader, if not `None`
-    * @param task the task the squad is trying to perform
+    * @param task   the task the squad is trying to perform
     * @return a `SquadInfo` object
     */
   def apply(leader : Option[String], task : String) : SquadInfo = {
@@ -187,9 +175,7 @@ object SquadInfo {
   }
 
   /**
-    * Alternate constructor for `SquadInfo` that ignores the `Option` requirement for the field.<br>
-    * <br>
-    * This constructor is used by `taskOrContinentCodec`.
+    * Alternate constructor for `SquadInfo` that ignores the `Option` requirement for the field.
     * @param continent_guid the continent on which the squad is acting
     * @return a `SquadInfo` object
     */
@@ -198,463 +184,589 @@ object SquadInfo {
   }
 
   /**
-    * Alternate constructor for `SquadInfo` that ignores the `Option` requirement for the important field.<br>
-    * <br>
-    * This constructor is used by `sizeCodec`.
-    * Two of the fields normally are `Option[Int]`s.
-    * Only the `size` field in this packet is an `Int`, giving the method a distinct signature.<br>
-    * <br>
-    * Recommended use: `SquadInfo(size, None)`<br>
-    * <br>
-    * Exploration:<br>
-    * We do not currently know any `SquadHeader` action codes for adjusting `capacity`.
-    * @param size the current size of the squad
-    * @param capacity the maximum number of members that the squad can tolerate, if not `None`
+    * Alternate constructor for `SquadInfo` that ignores the `Option` requirement for the important field.
+    * @param size     the current size of the squad
     * @return a `SquadInfo` object
     */
-  def apply(size : Int, capacity : Option[Int]) : SquadInfo = {
+  def apply(size : Int) : SquadInfo = {
     SquadInfo(None, None, None, Some(size), None)
   }
 
   /**
     * Alternate constructor for `SquadInfo` that ignores the `Option` requirement for the important field.<br>
     * <br>
-    * This constructor is not actually used at the moment.
     * Two of the fields normally are `Option[Int]`s.
-    * Only the `capacity` field in this packet is an `Int`, giving the method a distinct signature.<br>
+    * Only the `capacity` field in this packet is an `Int`, giving the method a distinct signature.
+    * The other field - an `Option[Int]` for `size` - can still be set if passed.<br>
     * <br>
-    * Recommended use: `SquadInfo(None, capacity)`<br>
-    * <br>
-    * Exploration:<br>
-    * We do not currently know any `SquadHeader` action codes for adjusting `capacity`.
-    * @param size the current size of the squad
+    * Recommended use: `SquadInfo(None, capacity)`
+    * @param size     the current size of the squad
     * @param capacity the maximum number of members that the squad can tolerate, if not `None`
     * @return a `SquadInfo` object
     */
   def apply(size : Option[Int], capacity : Int) : SquadInfo = {
-    SquadInfo(None, None, None, None, Some(capacity))
+    SquadInfo(None, None, None, size, Some(capacity))
   }
 
   /**
-    * Alternate constructor for `SquadInfo` that ignores the `Option` requirement for the fields.<br>
-    * <br>
-    * This constructor is used by `leaderSizeCodec`.
-    * @param leader the name of the squad leader
-    * @param size the current size of the squad
-    * @return a `SquadInfo` object
+    * The codes related to the specific application of different `Codec`s when parsing squad information as different fields.
+    * Hence, "field codes."
+    * These fields are identified when using `SquadInfo` data in `ReplicationStreamMessage`'s "update" format
+    * but are considered absent when performing squad list initialization.
     */
-  def apply(leader : String, size : Int) : SquadInfo = {
-    SquadInfo(Some(leader), None, None, Some(size), None)
-  }
-
-  /**
-    * Alternate constructor for `SquadInfo` that ignores the Option requirement for the fields.<br>
-    * <br>
-    * This constructor is used by `taskAndContinentCodec`.
-    * @param task the task the squad is trying to perform
-    * @param continent_guid the continent on which the squad is acting
-    * @return a `SquadInfo` object
-    */
-  def apply(task : String, continent_guid : PlanetSideZoneID) : SquadInfo = {
-    SquadInfo(None, Some(task), Some(continent_guid), None, None, None)
+  object Field {
+    final val Leader = 1
+    final val Task = 2
+    final val ZoneId = 3
+    final val Size = 4
+    final val Capacity = 5
   }
 }
 
+/**
+  * An object that contains all of the logic necessary to transform between
+  * the various forms of squad information found in formulaic packet data structures
+  * and a singular `SquadInfo` object with only the important data fields that were defined.
+  */
 object SquadHeader {
-  /**
-    * Alternate constructor for SquadInfo that ignores the Option requirement for the `info` field.
-    * @param unk1 na
-    * @param unk2 na
-    * @param unk3 na; not always defined
-    * @param info information pertaining to this squad listing
-    */
-  def apply(unk1 : Int, unk2 : Boolean, unk3 : Option[Int], info : SquadInfo) : SquadHeader = {
-    SquadHeader(unk1, unk2, unk3, Some(info))
-  }
-
-  /**
-    * `squadPattern` completes the fields for the `SquadHeader` class.
-    * It translates an indeterminate number of bit regions into something that can be processed as an `Option[SquadInfo]`.
-    */
-  private type squadPattern = Option[SquadInfo] :: HNil
+  //DO NOT IMPORT shapeless.:: TOP LEVEL TO THIS OBJECT - it interferes with required scala.collection.immutable.::
 
   /**
     * `Codec` for reading `SquadInfo` data from the first entry from a packet with squad list initialization entries.
     */
-  private val initCodec : Codec[squadPattern] = (
-    ("squad_guid" | PlanetSideGUID.codec) ::
+  private val infoCodec : Codec[SquadInfo] = {
+    import shapeless.::
+    (("squad_guid" | PlanetSideGUID.codec) ::
       ("leader" | PacketHelpers.encodedWideString) ::
       ("task" | PacketHelpers.encodedWideString) ::
       ("continent_guid" | PlanetSideZoneID.codec) ::
       ("size" | uint4L) ::
       ("capacity" | uint4L)
-    ).exmap[squadPattern] (
-    {
-      case sguid :: lead :: tsk :: cguid :: sz :: cap :: HNil =>
-        Attempt.successful(Some(SquadInfo(lead, tsk, cguid, sz, cap, sguid)) :: HNil)
-      case _ =>
-        Attempt.failure(Err("failed to decode squad data for adding [A] a squad entry"))
-    },
-    {
-      case Some(SquadInfo(Some(lead), Some(tsk), Some(cguid), Some(sz), Some(cap), Some(sguid))) :: HNil =>
-        Attempt.successful(sguid :: lead :: tsk :: cguid :: sz :: cap :: HNil)
-      case _ =>
-        Attempt.failure(Err("failed to encode squad data for adding [A] a squad entry"))
-    }
-  )
+      ).exmap[SquadInfo](
+      {
+        case sguid :: lead :: tsk :: cguid :: sz :: cap :: HNil =>
+          Attempt.successful(SquadInfo(lead, tsk, cguid, sz, cap, sguid))
+        case _ =>
+          Attempt.failure(Err("failed to decode squad data for adding the initial squad entry"))
+      },
+      {
+        case SquadInfo(Some(lead), Some(tsk), Some(cguid), Some(sz), Some(cap), Some(sguid)) =>
+          Attempt.successful(sguid :: lead :: tsk :: cguid :: sz :: cap :: HNil)
+        case _ =>
+          Attempt.failure(Err("failed to encode squad data for adding the initial squad entry"))
+      }
+    )
+  }
 
   /**
     * `Codec` for reading `SquadInfo` data from all entries other than the first from a packet with squad list initialization entries.
     */
-  private val alt_initCodec : Codec[squadPattern] = (
-    ("squad_guid" | PlanetSideGUID.codec) ::
-      ("leader" | PacketHelpers.encodedWideStringAligned(7)) ::
-      ("task" | PacketHelpers.encodedWideString) ::
-      ("continent_guid" | PlanetSideZoneID.codec) ::
-      ("size" | uint4L) ::
-      ("capacity" | uint4L)
-    ).exmap[squadPattern] (
-    {
-      case sguid :: lead :: tsk :: cguid :: sz :: cap :: HNil =>
-        Attempt.successful(Some(SquadInfo(lead, tsk, cguid, sz, cap, sguid)) :: HNil)
-      case _ =>
-        Attempt.failure(Err("failed to decode squad data for adding [B] a squad entry"))
-    },
-    {
-      case Some(SquadInfo(Some(lead), Some(tsk), Some(cguid), Some(sz), Some(cap), Some(sguid))) :: HNil =>
-        Attempt.successful(sguid :: lead :: tsk :: cguid :: sz :: cap :: HNil)
-      case _ =>
-        Attempt.failure(Err("failed to encode squad data for adding [B] a squad entry"))
-    }
-  )
+  private val alt_infoCodec : Codec[SquadInfo] = {
+    import shapeless.::
+    (
+      ("squad_guid" | PlanetSideGUID.codec) ::
+        ("leader" | PacketHelpers.encodedWideStringAligned(7)) ::
+        ("task" | PacketHelpers.encodedWideString) ::
+        ("continent_guid" | PlanetSideZoneID.codec) ::
+        ("size" | uint4L) ::
+        ("capacity" | uint4L)
+      ).exmap[SquadInfo] (
+      {
+        case sguid :: lead :: tsk :: cguid :: sz :: cap :: HNil =>
+          Attempt.successful(SquadInfo(lead, tsk, cguid, sz, cap, sguid))
+        case _ =>
+          Attempt.failure(Err("failed to decode squad data for adding an additional squad entry"))
+      },
+      {
+        case SquadInfo(Some(lead), Some(tsk), Some(cguid), Some(sz), Some(cap), Some(sguid)) =>
+          Attempt.successful(sguid :: lead :: tsk :: cguid :: sz :: cap :: HNil)
+        case _ =>
+          Attempt.failure(Err("failed to encode squad data for adding an additional squad entry"))
+      }
+    )
+  }
 
   /**
     * `Codec` for reading the `SquadInfo` data in an "update all squad data" entry.
     */
-  private val allCodec : Codec[squadPattern] = (
-    ("squad_guid" | PlanetSideGUID.codec) ::
-      ("leader" | PacketHelpers.encodedWideStringAligned(3)) ::
-      ("task" | PacketHelpers.encodedWideString) ::
-      ("continent_guid" | PlanetSideZoneID.codec) ::
-      ("size" | uint4L) ::
-      ("capacity" | uint4L)
-    ).exmap[squadPattern] (
-    {
-      case sguid :: lead :: tsk :: cguid :: sz :: cap :: HNil =>
-        Attempt.successful(Some(SquadInfo(lead, tsk, cguid, sz, cap, sguid)) :: HNil)
-      case _ =>
-        Attempt.failure(Err("failed to decode squad data for updating a squad entry"))
-    },
-    {
-      case Some(SquadInfo(Some(lead), Some(tsk), Some(cguid), Some(sz), Some(cap), Some(sguid))) :: HNil =>
-        Attempt.successful(sguid :: lead :: tsk :: cguid :: sz :: cap :: HNil)
-      case _ =>
-        Attempt.failure(Err("failed to encode squad data for updating a squad entry"))
+  private val allCodec : Codec[SquadInfo] = {
+    import shapeless.::
+    (
+      ("squad_guid" | PlanetSideGUID.codec) ::
+        ("leader" | PacketHelpers.encodedWideStringAligned(3)) ::
+        ("task" | PacketHelpers.encodedWideString) ::
+        ("continent_guid" | PlanetSideZoneID.codec) ::
+        ("size" | uint4L) ::
+        ("capacity" | uint4L)
+      ).exmap[SquadInfo] (
+      {
+        case sguid :: lead :: tsk :: cguid :: sz :: cap :: HNil =>
+          Attempt.successful(SquadInfo(lead, tsk, cguid, sz, cap, sguid))
+        case _ =>
+          Attempt.failure(Err("failed to decode squad data for updating a squad entry"))
+      },
+      {
+        case SquadInfo(Some(lead), Some(tsk), Some(cguid), Some(sz), Some(cap), Some(sguid)) =>
+          Attempt.successful(sguid :: lead :: tsk :: cguid :: sz :: cap :: HNil)
+        case _ =>
+          Attempt.failure(Err("failed to encode squad data for updating a squad entry"))
+      }
+    )
+  }
+
+  /**
+    * Produces a `Codec` function for byte-aligned, padded Pascal strings encoded through common manipulations.
+    * @see `PacketHelpers.encodedWideStringAligned`
+    * @param over the number of bits past the previous byte-aligned index;
+    *             should be a 0-7 number that gets converted to a 1-7 string padding number
+    * @return the encoded string `Codec`
+    */
+  private def paddedStringMetaCodec(over : Int) : Codec[String] = PacketHelpers.encodedWideStringAligned({
+    val mod8 = over % 8
+    if(mod8 == 0) {
+      0
     }
-  )
+    else {
+      8 - mod8
+    }
+  })
 
   /**
     * `Codec` for reading the `SquadInfo` data in an "update squad leader" entry.
     */
-  private val leaderCodec : Codec[squadPattern] = (
-    bool ::
-      ("leader" | PacketHelpers.encodedWideStringAligned(7))
-    ).exmap[squadPattern] (
+  private def leaderCodec(over : Int) : Codec[SquadInfo] = paddedStringMetaCodec(over).exmap[SquadInfo] (
+    lead => Attempt.successful(SquadInfo(lead)),
     {
-      case true :: lead :: HNil =>
-        Attempt.successful(Some(SquadInfo(lead, None)) :: HNil)
-      case _ =>
-        Attempt.failure(Err("failed to decode squad data for a leader name"))
-    },
-    {
-      case Some(SquadInfo(Some(lead), _, _, _, _, _)) :: HNil =>
-        Attempt.successful(true :: lead :: HNil)
+      case SquadInfo(Some(lead), _, _, _, _, _) =>
+        Attempt.successful(lead)
       case _ =>
         Attempt.failure(Err("failed to encode squad data for a leader name"))
     }
   )
 
   /**
-    * `Codec` for reading the `SquadInfo` data in an "update squad task or continent" entry.
+    * `Codec` for reading the `SquadInfo` data in an "update task text" entry.
     */
-  private val taskOrContinentCodec : Codec[squadPattern] = (
-    bool >>:~ { path =>
-      conditional(path, "continent_guid" | PlanetSideZoneID.codec) ::
-        conditional(!path, "task" | PacketHelpers.encodedWideStringAligned(7))
-    }
-    ).exmap[squadPattern] (
+  private def taskCodec(over : Int) : Codec[SquadInfo] = paddedStringMetaCodec(over).exmap[SquadInfo] (
+    task => Attempt.successful(SquadInfo(None, task)),
     {
-      case true :: Some(cguid) :: _ :: HNil =>
-        Attempt.successful(Some(SquadInfo(cguid)) :: HNil)
-      case true :: None :: _ :: HNil =>
-        Attempt.failure(Err("failed to decode squad data for a task - no continent"))
-      case false :: _ :: Some(tsk) :: HNil =>
-        Attempt.successful(Some(SquadInfo(None, tsk)) :: HNil)
-      case false :: _ :: None :: HNil =>
-        Attempt.failure(Err("failed to decode squad data for a task - no task"))
-    },
-    {
-      case Some(SquadInfo(_, None, Some(cguid), _, _, _)) :: HNil =>
-        Attempt.successful(true :: Some(cguid) :: None :: HNil)
-      case Some(SquadInfo(_, Some(tsk), None, _, _, _)) :: HNil =>
-        Attempt.successful(false :: None :: Some(tsk) :: HNil)
-      case Some(SquadInfo(_, Some(_), Some(_), _, _, _)) :: HNil =>
-        Attempt.failure(Err("failed to encode squad data for either a task or a continent - multiple encodings reachable"))
+      case SquadInfo(_, Some(task), _, _, _, _) =>
+        Attempt.successful(task)
       case _ =>
-        Attempt.failure(Err("failed to encode squad data for either a task or a continent"))
+        Attempt.failure(Err("failed to encode squad data for a task string"))
+    }
+  )
+
+  /**
+    * `Codec` for reading the `SquadInfo` data in an "update squad zone id" entry.
+    * In reality, the "zone's id"  is the zone's server ordinal index.
+    */
+  private val zoneIdCodec : Codec[SquadInfo] = PlanetSideZoneID.codec.exmap[SquadInfo] (
+    cguid => Attempt.successful(SquadInfo(cguid)),
+    {
+      case SquadInfo(_, _, Some(cguid), _, _, _) =>
+        Attempt.successful(cguid)
+      case _ =>
+        Attempt.failure(Err("failed to encode squad data for a continent number"))
     }
   )
 
   /**
     * `Codec` for reading the `SquadInfo` data in an "update squad size" entry.
     */
-  private val sizeCodec : Codec[squadPattern] = (
-    bool ::
-      ("size" | uint4L)
-    ).exmap[squadPattern] (
+  private val sizeCodec : Codec[SquadInfo] = uint4L.exmap[SquadInfo] (
+    sz => Attempt.successful(SquadInfo(sz)),
     {
-      case false :: sz :: HNil =>
-        Attempt.successful(Some(SquadInfo(sz, None)) :: HNil)
+      case SquadInfo(_, _, _, Some(sz), _, _) =>
+        Attempt.successful(sz)
       case _ =>
-        Attempt.failure(Err("failed to decode squad data for a size"))
-    },
-    {
-      case Some(SquadInfo(_, _, _, Some(sz), _, _)) :: HNil =>
-        Attempt.successful(false :: sz :: HNil)
-      case _ =>
-        Attempt.failure(Err("failed to encode squad data for a size"))
+        Attempt.failure(Err("failed to encode squad data for squad size"))
     }
   )
 
   /**
-    * `Codec` for reading the `SquadInfo` data in an "update squad leader and size" entry.
+    * `Codec` for reading the `SquadInfo` data in an "update squad capacity" entry.
     */
-  private val leaderSizeCodec : Codec[squadPattern] = (
-    bool ::
-      ("leader" | PacketHelpers.encodedWideStringAligned(7)) ::
-      uint4L ::
-      ("size" | uint4L)
-    ).exmap[squadPattern] (
+  private val capacityCodec : Codec[SquadInfo] = uint4L.exmap[SquadInfo] (
+    cap => Attempt.successful(SquadInfo(None, cap)),
     {
-      case true :: lead :: 4 :: sz :: HNil =>
-        Attempt.successful(Some(SquadInfo(lead, sz)) :: HNil)
+      case SquadInfo(_, _, _, _, Some(cap), _) =>
+        Attempt.successful(cap)
       case _ =>
-        Attempt.failure(Err("failed to decode squad data for a leader and a size"))
-    },
-    {
-      case Some(SquadInfo(Some(lead), _, _, Some(sz), _, _)) :: HNil =>
-        Attempt.successful(true :: lead :: 4 :: sz :: HNil)
-      case _ =>
-        Attempt.failure(Err("failed to encode squad data for a leader and a size"))
+        Attempt.failure(Err("failed to encode squad data for squad capacity"))
     }
   )
 
   /**
-    * `Codec` for reading the `SquadInfo` data in an "update squad task and continent" entry.
+    * `Codec` for reading the `SquadInfo` data in a "remove squad from list" entry.
+    * While the input has no impact, it always writes the number four to a `3u` field - or `0x100`.
     */
-  private val taskAndContinentCodec : Codec[squadPattern] = (
-    bool ::
-      ("task" | PacketHelpers.encodedWideStringAligned(7)) ::
-      uintL(3) ::
-      bool ::
-      ("continent_guid" | PlanetSideZoneID.codec)
-    ).exmap[squadPattern] (
-    {
-      case false :: tsk :: 1 :: true :: cguid :: HNil =>
-        Attempt.successful(Some(SquadInfo(tsk, cguid)) :: HNil)
-      case _ =>
-        Attempt.failure(Err("failed to decode squad data for a task and a continent"))
-    },
-    {
-      case Some(SquadInfo(_, Some(tsk), Some(cguid), _, _, _)) :: HNil =>
-        Attempt.successful(false :: tsk :: 1 :: true :: cguid :: HNil)
-      case _ =>
-        Attempt.failure(Err("failed to encode squad data for a task and a continent"))
-    }
-  )
-
-  /**
-    * Codec for reading the `SquadInfo` data in a "remove squad from list" entry.
-    * This `Codec` is unique because it is considered a valid `Codec` that does not read any bit data.
-    * The `conditional` will always return `None` because its determining conditional statement is explicitly `false`.
-    */
-  private val removeCodec : Codec[squadPattern] = conditional(false, bool).exmap[squadPattern] (
-    {
-      case None | _ =>
-        Attempt.successful(None :: HNil)
-    },
-    {
-      case None :: HNil | _ =>
-        Attempt.successful(None)
-    }
+  private val removeCodec : Codec[SquadInfo] = uint(3).exmap[SquadInfo] (
+    _ => Attempt.successful(SquadInfo.Blank),
+    _ => Attempt.successful(4)
   )
 
   /**
     * `Codec` for failing to determine a valid `Codec` based on the entry data.
     * This `Codec` is an invalid codec that does not read any bit data.
-    * The `conditional` will always return `None` because its determining conditional statement is explicitly `false`.
+    * The `conditional` will always return `None` because
+    * its determining conditional statement is explicitly `false`
+    * and all cases involving explicit failure.
     */
-  private val failureCodec : Codec[squadPattern] = conditional(false, bool).exmap[squadPattern] (
-    {
-      case None | _ =>
-        Attempt.failure(Err("decoding with unhandled codec"))
-    },
-    {
-      case None :: HNil | _ =>
-        Attempt.failure(Err("encoding with unhandled codec"))
-    }
+  private val failureCodec : Codec[SquadInfo] = conditional(included = false, bool).exmap[SquadInfo] (
+    _ => Attempt.failure(Err("decoding with unhandled codec")),
+    _ => Attempt.failure(Err("encoding with unhandled codec"))
   )
 
   /**
-    * Select the `Codec` to translate bit data in this packet into an `Option[SquadInfo]` using other fields of the same packet.
-    * Refer to comments for the primary `case class` constructor for `SquadHeader` to explain how the conditions in this function path.
-    * @param a na
-    * @param b na
-    * @param c na; may be `None`
-    * @param optionalCodec a to-be-defined `Codec` that is determined by the suggested mood of the packet and listing of the squad;
-    *                      despite the name, actually a required parameter
-    * @return a `Codec` that corresponds to a `squadPattern` translation
+    * An internal class that assists in the process of transforming squad data
+    * encoded in the "update" format of a `ReplicationStreamMessage` packet as index-coded fields
+    * into a singular decoded `SquadInfo` object that is populated with all of the previously-discovered fields.
+    * @param code the field code for the current data
+    * @param info the current squad data
+    * @param next a potential next encoded squad field
     */
-  private def selectCodec(a : Int, b : Boolean, c : Option[Int], optionalCodec : Codec[squadPattern]) : Codec[squadPattern] = {
-    if(c.isDefined) {
-      val cVal = c.get
-      if(a == 0 && b)
-        if(cVal == 4)
-          return removeCodec
-      if(a == 128 && b) {
-        if(cVal == 0)
-          return leaderCodec
-        else if(cVal == 1)
-          return taskOrContinentCodec
-        else if(cVal == 2)
-          return sizeCodec
-      }
-      else if(a == 129 && !b) {
-        if(cVal == 0)
-          return leaderSizeCodec
-        else if(cVal == 1)
-          return taskAndContinentCodec
-      }
+  private final case class LinkedSquadInfo(code : Int, info : SquadInfo, next : Option[LinkedSquadInfo])
+
+  /**
+    * Concatenate a `SquadInfo` object chain into a single `SquadInfo` object.
+    * @param info the chain
+    * @return the concatenated `SquadInfo` object
+    */
+  private def unlinkSquadInfo(info : LinkedSquadInfo) : SquadInfo = unlinkSquadInfo(Some(info))
+
+  /**
+    * Concatenate a `SquadInfo` object chain into a single `SquadInfo` object.
+    * Recursively visits every link in a `SquadInfo` object chain.
+    * @param info the current link in the chain
+    * @param squadInfo the persistent `SquadInfo` concatenation object;
+    *                  defaults to `SquadInfo.Blank`
+    * @return the concatenated `SquadInfo` object
+    */
+  @tailrec
+  private def unlinkSquadInfo(info : Option[LinkedSquadInfo], squadInfo : SquadInfo = SquadInfo.Blank) : SquadInfo = {
+    info match {
+      case None =>
+        squadInfo
+      case Some(sqInfo) =>
+        unlinkSquadInfo(sqInfo.next, squadInfo And sqInfo.info)
+    }
+  }
+
+  /**
+    * Decompose a single `SquadInfo` object into a `SquadInfo` object chain of the original's fields.
+    * The fields as a linked list are explicitly organized "leader", "task", "zone_id", "size", "capacity,"
+    * or as "(leader, (task, (zone_id, (size, (capacity, None)))))" when fully populated and composed.
+    * @param info a `SquadInfo` object that has all relevant fields populated
+    * @return a linked list of `SquadInfo` objects, each with a single field from the input `SquadInfo` object
+    */
+  private def linkSquadInfo(info : SquadInfo) : LinkedSquadInfo = {
+    //import scala.collection.immutable.::
+    Seq(
+      (SquadInfo.Field.Capacity, SquadInfo(None, None, None, None, info.capacity)),
+      (SquadInfo.Field.Size, SquadInfo(None, None, None, info.size, None)),
+      (SquadInfo.Field.ZoneId, SquadInfo(None, None, info.zone_id, None, None)),
+      (SquadInfo.Field.Task, SquadInfo(None, info.task, None, None, None)),
+      (SquadInfo.Field.Leader, SquadInfo(info.leader, None, None, None, None))
+    ) //in reverse order so that the linked list is in the correct order
+      .filterNot { case (_, sqInfo) => sqInfo == SquadInfo.Blank }
+    match {
+      case Nil =>
+        throw new Exception("no linked list squad fields encountered where at least one was expected") //bad end
+      case x :: Nil =>
+        val (code, squadInfo) = x
+        LinkedSquadInfo(code, squadInfo, None)
+      case x :: xs =>
+        val (code, squadInfo) = x
+        linkSquadInfo(xs, LinkedSquadInfo(code, squadInfo, None))
+    }
+  }
+
+  /**
+    * Decompose a single `SquadInfo` object into a `SquadInfo` object chain of the original's fields.
+    * The fields as a linked list are explicitly organized "leader", "task", "zone_id", "size", "capacity,"
+    * or as "(leader, (task, (zone_id, (size, (capacity, None)))))" when fully populated and composed.
+    * @param infoList a series of paired field codes and `SquadInfo` objects with data in the indicated fields
+    * @return a linked list of `SquadInfo` objects, each with a single field from the input `SquadInfo` object
+    */
+  @tailrec
+  private def linkSquadInfo(infoList : Seq[(Int, SquadInfo)], linkedInfo : LinkedSquadInfo) : LinkedSquadInfo = {
+    if(infoList.isEmpty) {
+      linkedInfo
     }
     else {
-      if(a == 131 && !b)
-        return optionalCodec
+      val (code, data) = infoList.head
+      linkSquadInfo(infoList.tail, LinkedSquadInfo(code, data, Some(linkedInfo)))
     }
-    //we've not encountered a valid Codec
-    failureCodec
+  }
+
+  /**
+    * Parse a known number of knowable format data fields for squad info
+    * until no more fields are left for parsing.
+    * @see `selectCodecAction`
+    * @see `modifyPadValue`
+    * @param size the total number of data fields to parse
+    * @param pad the current overflow/padding value
+    * @return a `LinkedSquadInfo` `Codec` object (linked list)
+    */
+  private def listing_codec(size : Int, pad : Int = 1) : Codec[LinkedSquadInfo] = {
+    import shapeless.::
+    (
+      uint4 >>:~ { code =>
+        selectCodecAction(code, pad) ::
+          conditional(size - 1 > 0, listing_codec(size - 1, (pad + modifyPadValue(code, pad)) % 8))
+      }
+      ).xmap[LinkedSquadInfo] (
+      {
+        case code :: entry :: next :: HNil =>
+          LinkedSquadInfo(code, entry, next)
+      },
+      {
+        case LinkedSquadInfo(code, entry, next) =>
+          code :: entry :: next :: HNil
+      }
+    )
+  }
+
+  /**
+    * Given the field code value of the current `SquadInfo` object's data,
+    * select the `Codec` object that is most suitable to parse that data.
+    * @param code the field code number
+    * @param pad  the current overflow/padding value;
+    *             the number of bits past the previous byte-aligned index;
+    *             should be a 0-7 number that gets converted to a 1-7 string padding number
+    * @return a `Codec` object for the specific field's data
+    */
+  private def selectCodecAction(code : Int, pad : Int) : Codec[SquadInfo] = {
+    code match {
+      case SquadInfo.Field.Leader => leaderCodec(pad)
+      case SquadInfo.Field.Task => taskCodec(pad)
+      case SquadInfo.Field.ZoneId => zoneIdCodec
+      case SquadInfo.Field.Size => sizeCodec
+      case SquadInfo.Field.Capacity => capacityCodec
+      case _ => failureCodec
+    }
+  }
+
+  /**
+    * Given the field code value of the current `SquadInfo` object's data,
+    * determine how the inherited overflow/padding value for string data should be adjusted for future entries.
+    * There are three paths: becomes zero, doesn't change, or increases by four units.
+    * The invalid condition leads to an extremely negative number, but this condition should also never be encountered.
+    * @param code the field code number
+    * @param pad  the current overflow/padding value;
+    *             the number of bits past the previous byte-aligned index;
+    *             should be a 0-7 number that gets converted to a 1-7 string padding number
+    * @return the number of units that the current overflow/padding value should be modified, in terms of addition
+    */
+  private def modifyPadValue(code : Int, pad : Int) : Int = {
+    code match {
+      case SquadInfo.Field.Leader => -pad //byte-aligned string; padding zero'd
+      case SquadInfo.Field.Task => -pad //byte-aligned string; padding zero'd
+      case SquadInfo.Field.ZoneId => 4 //4u + 32u = 4u + 8*4u = additional 4u
+      case SquadInfo.Field.Size => 0 //4u + 4u = no change to padding
+      case SquadInfo.Field.Capacity => 0 //4u + 4u = no change to padding
+      case _ => Int.MinValue //wildly incorrect
+    }
+  }
+
+  /**
+    * The framework for transforming data from squad listing entries.
+    * Three paths lead from this position:
+    * processing the data in the course of an entry removal action,
+    * processing the data in the course of a total squad listing initialization action, and
+    * processing the data of a single entry in a piecemeal fashion that parses a coded field-by-field format.
+    * For the second - initialization - another `Codec` object is utilized to determine how the data should be interpreted.
+    * @param providedCodec the `Codec` for processing a `SquadInfo` object during the squad list initialization process
+    * @return a `SquadListing` `Codec` object
+    */
+  private def meta_codec(providedCodec : Codec[SquadInfo]) : Codec[Option[SquadInfo]] = {
+    import shapeless.::
+    (
+      bool >>:~ { unk1 =>
+        uint8 >>:~ { unk2 =>
+          conditional(!unk1 && unk2 == 1, removeCodec) ::
+            conditional(unk1 && unk2 == 6, providedCodec) ::
+            conditional(unk1 && unk2 != 6, listing_codec(unk2))
+        }
+      }).exmap[Option[SquadInfo]] (
+      {
+        case false :: 1 :: Some(SquadInfo.Blank) :: None :: None :: HNil => //'remove' case
+          Attempt.Successful( None )
+
+        case true :: 6 :: None :: Some(info) :: None :: HNil => //handle complete squad info; no field codes
+          Attempt.Successful( Some(info) )
+
+        case true :: _ :: None :: None:: Some(result) :: HNil => //iterable field codes
+          Attempt.Successful( Some(unlinkSquadInfo(result)) )
+
+        case data => //error
+          Attempt.failure(Err(s"$data can not be encoded as a squad header"))
+      },
+      {
+        case None => //'remove' case
+          Attempt.Successful( false :: 1 :: Some(SquadInfo.Blank) :: None :: None :: HNil )
+
+        case info @ Some(SquadInfo(Some(_), Some(_), Some(_), Some(_), Some(_), Some(_))) => //handle complete squad info; no field codes
+          Attempt.Successful( true :: 6 :: None :: info :: None :: HNil )
+
+        case Some(info) => //iterable field codes
+          val linkedInfo = linkSquadInfo(info)
+          var count = 1
+          var linkNext = linkedInfo.next
+          while(linkNext.nonEmpty) {
+            count += 1
+            linkNext = linkNext.get.next
+          }
+          Attempt.Successful( true :: count :: None :: None :: Some(linkSquadInfo(info)) :: HNil )
+
+        case data => //error
+          Attempt.failure(Err(s"$data can not be decoded into a squad header"))
+      }
+    )
   }
 
   /**
     * `Codec` for standard `SquadHeader` entries.
     */
-  val codec : Codec[SquadHeader] = (
-    ("unk1" | uint8L) >>:~ { unk1 =>
-      ("unk2" | bool) >>:~ { unk2 =>
-        conditional(unk1 != 131, "unk3" | uintL(3)) >>:~ { unk3 =>
-          selectCodec(unk1, unk2, unk3, allCodec)
-        }
-      }
-    }).as[SquadHeader]
+  val codec : Codec[Option[SquadInfo]] = meta_codec(allCodec)
 
   /**
     * `Codec` for types of `SquadHeader` initializations.
     */
-  val init_codec : Codec[SquadHeader] = (
-    ("unk1" | uint8L) >>:~ { unk1 =>
-      ("unk2" | bool) >>:~ { unk2 =>
-        conditional(unk1 != 131, "unk3" | uintL(3)) >>:~ { unk3 =>
-          selectCodec(unk1, unk2, unk3, initCodec)
-        }
-      }
-    }).as[SquadHeader]
+  val info_codec : Codec[Option[SquadInfo]] = meta_codec(infoCodec)
 
   /**
     * Alternate `Codec` for types of `SquadHeader` initializations.
     */
-  val alt_init_codec : Codec[SquadHeader] = (
-    ("unk1" | uint8L) >>:~ { unk1 =>
-      ("unk2" | bool) >>:~ { unk2 =>
-        conditional(unk1 != 131, "unk3" | uintL(3)) >>:~ { unk3 =>
-          selectCodec(unk1, unk2, unk3, alt_initCodec)
-        }
-      }
-    }).as[SquadHeader]
+  val alt_info_codec : Codec[Option[SquadInfo]] = meta_codec(alt_infoCodec)
 }
 
 object SquadListing {
+  import shapeless.::
+
   /**
-    * `Codec` for standard `SquadListing` entries.
+    * Overloaded constructor for guaranteed squad information.
+    * @param index the listing entry index for this squad
+    * @param info the squad data
+    * @return a `SquadListing` object
     */
-  val codec : Codec[SquadListing] = (
+  def apply(index : Int, info : SquadInfo) : SquadListing = {
+    SquadListing(index, Some(info))
+  }
+
+  /**
+    * The framework for transforming data from squad listing entries.
+    * @param entryFunc the `Codec` for processing a given `SquadListing` object
+    * @return a `SquadListing` `Codec` object
+    */
+  private def meta_codec(entryFunc : Int=>Codec[Option[SquadInfo]]) : Codec[SquadListing] = (
     ("index" | uint8L) >>:~ { index =>
-      conditional(index < 255, "listing" | SquadHeader.codec) ::
-        conditional(index == 255, bits) //consume n < 8 bits padding the tail entry, else vector will try to operate on invalid data
+      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
     }).xmap[SquadListing] (
     {
-      case ndx :: lstng :: _ :: HNil =>
+      case ndx :: Some(lstng) :: _ :: HNil =>
         SquadListing(ndx, lstng)
+      case ndx :: None :: _ :: HNil =>
+        SquadListing(ndx, None)
     },
     {
       case SquadListing(ndx, lstng) =>
-        ndx :: lstng :: None :: HNil
+        ndx :: Some(lstng) :: None :: HNil
     }
   )
 
+  /**
+    * `Codec` for standard `SquadListing` entries.
+    */
+  val codec : Codec[SquadListing] = meta_codec({ _ => SquadHeader.codec })
+
   /**
     * `Codec` for branching types of `SquadListing` initializations.
     */
-  val init_codec : Codec[SquadListing] = (
-    ("index" | uint8L) >>:~ { index =>
-      conditional(index < 255,
-        newcodecs.binary_choice(index == 0,
-          "listing" | SquadHeader.init_codec,
-          "listing" | SquadHeader.alt_init_codec)
-      ) ::
-        conditional(index == 255, bits) //consume n < 8 bits padding the tail entry, else vector will try to operate on invalid data
-    }).xmap[SquadListing] (
-    {
-      case ndx :: lstng :: _ :: HNil =>
-        SquadListing(ndx, lstng)
-    },
-    {
-      case SquadListing(ndx, lstng) =>
-        ndx :: lstng :: None :: HNil
-    }
-  )
+  val info_codec : Codec[SquadListing] = meta_codec({ index : Int =>
+    newcodecs.binary_choice(index == 0,
+      "listing" | SquadHeader.info_codec,
+      "listing" | SquadHeader.alt_info_codec
+    )
+  })
 }
 
 object ReplicationStreamMessage extends Marshallable[ReplicationStreamMessage] {
+  import shapeless.::
+
+  /**
+    * A shortcut for the squad list initialization format of the packet.
+    * Supplied squad data is given a zero-indexed counter and transformed into formal "`Listing`s" for processing.
+    * @param infos the squad data to be composed into formal list entries
+    * @return a `ReplicationStreamMessage` packet object
+    */
+  def apply(infos : Iterable[SquadInfo]) : ReplicationStreamMessage = {
+    ReplicationStreamMessage(5, Some(6), infos
+      .zipWithIndex
+      .map { case (info, index) => SquadListing(index, Some(info)) }
+      .toVector
+    )
+  }
+
   implicit val codec : Codec[ReplicationStreamMessage] = (
     ("behavior" | uintL(3)) >>:~ { behavior =>
       conditional(behavior == 5, "behavior2" | uintL(3)) ::
         conditional(behavior != 1, bool) ::
         newcodecs.binary_choice(behavior != 5,
           "entries" | vector(SquadListing.codec),
-          "entries" | vector(SquadListing.init_codec)
+          "entries" | vector(SquadListing.info_codec)
         )
     }).xmap[ReplicationStreamMessage] (
     {
       case bhvr :: bhvr2 :: _ :: lst :: HNil =>
-        ReplicationStreamMessage(bhvr, bhvr2, lst)
+        ReplicationStreamMessage(bhvr, bhvr2, ignoreTerminatingEntry(lst))
     },
     {
       case ReplicationStreamMessage(1, bhvr2, lst) =>
-        1 :: bhvr2 :: None :: lst :: HNil
+        1 :: bhvr2 :: None :: ensureTerminatingEntry(lst) :: HNil
       case ReplicationStreamMessage(bhvr, bhvr2, lst) =>
-        bhvr :: bhvr2 :: Some(false) :: lst :: HNil
+        bhvr :: bhvr2 :: Some(false) :: ensureTerminatingEntry(lst) :: HNil
     }
   )
+
+  /**
+    * The last entry in the sequence of squad information listings should be a dummied listing with an index of 255.
+    * Ensure that this terminal entry is located at the end.
+    * @param list the listing of squad information
+    * @return the listing of squad information, with a specific final entry
+    */
+  private def ensureTerminatingEntry(list : Vector[SquadListing]) : Vector[SquadListing] = {
+    list.lastOption match {
+      case Some(SquadListing(255, _)) => list
+      case Some(_) | None => list :+ SquadListing()
+    }
+  }
+
+  /**
+    * The last entry in the sequence of squad information listings should be a dummied listing with an index of 255.
+    * Remove this terminal entry from the end of the list so as not to hassle with it.
+    * @param list the listing of squad information
+    * @return the listing of squad information, with a specific final entry truncated
+    */
+  private def ignoreTerminatingEntry(list : Vector[SquadListing]) : Vector[SquadListing] = {
+    list.lastOption match {
+      case Some(SquadListing(255, _)) => list.init
+      case Some(_) | None => list
+    }
+  }
 }
 
 /*
-                         +-> SquadListing.codec -------> SquadHeader.codec ----------+
-                         |                                                           |
-                         |                                                           |
-ReplicationStream.codec -+                                                           |
-                         |                                                           |
-                         |                           +-> SquadHeader.init_codec -----+-> SquadInfo
-                         |                           |                               |
-                         +-> SquadListing.initCodec -+                               |
-                                                     |                               |
-                                                     +-> SquadHeader.alt_init_codec -+
+                         +-> SquadListing.codec --------> SquadHeader.codec ----------+
+                         |                                                            |
+                         |                                                            |
+ReplicationStream.codec -+                                                            |
+                         |                                                            |
+                         |                            +-> SquadHeader.info_codec -----+-> SquadInfo
+                         |                            |                               |
+                         +-> SquadListing.info_codec -+                               |
+                                                      |                               |
+                                                      +-> SquadHeader.alt_info_codec -+
 */
diff --git a/common/src/test/scala/game/ReplicationStreamMessageTest.scala b/common/src/test/scala/game/ReplicationStreamMessageTest.scala
index d878b937..2d39a2a6 100644
--- a/common/src/test/scala/game/ReplicationStreamMessageTest.scala
+++ b/common/src/test/scala/game/ReplicationStreamMessageTest.scala
@@ -19,17 +19,7 @@ class ReplicationStreamMessageTest extends Specification {
   val stringUpdateLeaderSize = hex"E6 C0 58 10 C3 00 4A0069006D006D0079006E00 43 FF"
   val stringUpdateTaskContinent = hex"E6 C0 58 11 40 80 3200 3 04000000 FF0"
   val stringUpdateAll = hex"E6 C0 78 30 58 0430 6D00610064006D0075006A00 80 040000000A FF"
-  //failing conditions
-  val stringCodecFail = hex"E6 20 A1 19 FE"
-  val stringListOneFail = hex"E6 B8 01 06 01 00 8B 46007200610067004C0041004E00640049004E004300 84 4600720061006700 0A00 00 01 0A FF"
-  val stringListTwoFail = hex"E6 B8 01 06 06 00 8E 470065006E006500720061006C0047006F0072006700750074007A00 A1 46004C0059002C0041006C006C002000770065006C0063006F006D0065002C0063006E0020006C0061007300740020006E0069006700680074002100210021002100 0400 00 00 7A 01 83 02 00 45 80 4B004F004B006B006900610073004D00460043004E00 87 5300710075006100640020003200 0400 00 01 6A FF"
-  val stringUpdateLeaderFail = hex"E6 C0 28 08 44 00 46006100740065004A0048004E004300 FF"
-  val stringUpdateTaskFail = hex"E6 C0 58 09CE00 52004900500020005000530031002C0020007600690073006900740020005000530046006F00720065007600650072002E006E0065007400 FF"
-  val stringUpdateContinentFail = hex"E6 C0 38 09 85000001 7F80"
-  val stringUpdateSizeFail = hex"E6 C0 18 0A B7 F8"
-  val stringUpdateLeaderSizeFail = hex"E6 C0 58 10 43 00 4A0069006D006D0079006E00 43 FF"
-  val stringUpdateTaskContinentFail = hex"E6 C0 58 11 C0 80 3200 3 04000000 FF0"
-  val stringUpdateAllFail = hex"E6 C0 78 30 58 0430 6D00610064006D0075006A00 80 04000001 0A FF"
+  val stringRemoveUpdate = hex"e6 20201801014aff"
 
   "SquadInfo (w/out squad_guid)" in {
     val o = SquadInfo("FragLANdINC", "Frag", PlanetSideZoneID(10), 0, 10)
@@ -56,15 +46,27 @@ class ReplicationStreamMessageTest extends Specification {
     o.capacity.get mustEqual 7
   }
 
+  "SquadInfo (Add)" in {
+    val o1 = SquadInfo(Some("FragLANdINC"), Some("Frag"), Some(PlanetSideZoneID(10)), None, None)
+    val o2 = SquadInfo(Some(7), 10)
+    val o3 = SquadInfo("FragLANdINC", "Frag", PlanetSideZoneID(10), 7, 10)
+    o1.And(o2) mustEqual o3
+  }
+
+  "SquadInfo (Add, with blocked fields)" in {
+    val o1 = SquadInfo(Some("FragLANdINC"), None, Some(PlanetSideZoneID(10)), None, Some(10))
+    val o2 = SquadInfo(Some("Frag"), Some("Frag"), Some(PlanetSideZoneID(15)), Some(7), Some(7))
+    val o3 = SquadInfo("FragLANdINC", "Frag", PlanetSideZoneID(10), 7, 10)
+    o1.And(o2) mustEqual o3
+  }
+
   "decode (clear)" in {
     PacketCoding.DecodePacket(stringListClear).require match {
       case ReplicationStreamMessage(behavior, behavior2, entries) =>
         behavior mustEqual 5
         behavior2.isDefined mustEqual true
         behavior2.get mustEqual 6
-        entries.length mustEqual 1
-        entries.head.index mustEqual 255
-        entries.head.listing.isDefined mustEqual false
+        entries.length mustEqual 0
       case _ =>
         ko
     }
@@ -75,27 +77,21 @@ class ReplicationStreamMessageTest extends Specification {
       case ReplicationStreamMessage(behavior, behavior2, entries) =>
         behavior mustEqual 5
         behavior2.get mustEqual 6
-        entries.length mustEqual 2
+        entries.length mustEqual 1
         entries.head.index mustEqual 0
         entries.head.listing.isDefined mustEqual true
-        entries.head.listing.get.unk1 mustEqual 131
-        entries.head.listing.get.unk2 mustEqual false
-        entries.head.listing.get.unk3.isDefined mustEqual false
-        entries.head.listing.get.info.isDefined mustEqual true
-        entries.head.listing.get.info.get.leader.isDefined mustEqual true
-        entries.head.listing.get.info.get.leader.get mustEqual "FragLANdINC"
-        entries.head.listing.get.info.get.task.isDefined mustEqual true
-        entries.head.listing.get.info.get.task.get mustEqual "Frag"
-        entries.head.listing.get.info.get.zone_id.isDefined mustEqual true
-        entries.head.listing.get.info.get.zone_id.get mustEqual PlanetSideZoneID(10)
-        entries.head.listing.get.info.get.size.isDefined mustEqual true
-        entries.head.listing.get.info.get.size.get mustEqual 0
-        entries.head.listing.get.info.get.capacity.isDefined mustEqual true
-        entries.head.listing.get.info.get.capacity.get mustEqual 10
-        entries.head.listing.get.info.get.squad_guid.isDefined mustEqual true
-        entries.head.listing.get.info.get.squad_guid.get mustEqual PlanetSideGUID(1)
-        entries(1).index mustEqual 255
-        entries(1).listing.isDefined mustEqual false
+        entries.head.listing.get.leader.isDefined mustEqual true
+        entries.head.listing.get.leader.get mustEqual "FragLANdINC"
+        entries.head.listing.get.task.isDefined mustEqual true
+        entries.head.listing.get.task.get mustEqual "Frag"
+        entries.head.listing.get.zone_id.isDefined mustEqual true
+        entries.head.listing.get.zone_id.get mustEqual PlanetSideZoneID(10)
+        entries.head.listing.get.size.isDefined mustEqual true
+        entries.head.listing.get.size.get mustEqual 0
+        entries.head.listing.get.capacity.isDefined mustEqual true
+        entries.head.listing.get.capacity.get mustEqual 10
+        entries.head.listing.get.squad_guid.isDefined mustEqual true
+        entries.head.listing.get.squad_guid.get mustEqual PlanetSideGUID(1)
       case _ =>
         ko
     }
@@ -106,28 +102,21 @@ class ReplicationStreamMessageTest extends Specification {
       case ReplicationStreamMessage(behavior, behavior2, entries) =>
         behavior mustEqual 5
         behavior2.get mustEqual 6
-        entries.length mustEqual 3
+        entries.length mustEqual 2
         entries.head.index mustEqual 0
-        entries.head.listing.get.unk1 mustEqual 131
-        entries.head.listing.get.unk2 mustEqual false
-        entries.head.listing.get.unk3.isDefined mustEqual false
-        entries.head.listing.get.info.get.leader.get mustEqual "GeneralGorgutz"
-        entries.head.listing.get.info.get.task.get mustEqual "FLY,All welcome,cn last night!!!!"
-        entries.head.listing.get.info.get.zone_id.get mustEqual PlanetSideZoneID(4)
-        entries.head.listing.get.info.get.size.get mustEqual 7
-        entries.head.listing.get.info.get.capacity.get mustEqual 10
-        entries.head.listing.get.info.get.squad_guid.get mustEqual PlanetSideGUID(6)
+        entries.head.listing.get.leader.get mustEqual "GeneralGorgutz"
+        entries.head.listing.get.task.get mustEqual "FLY,All welcome,cn last night!!!!"
+        entries.head.listing.get.zone_id.get mustEqual PlanetSideZoneID(4)
+        entries.head.listing.get.size.get mustEqual 7
+        entries.head.listing.get.capacity.get mustEqual 10
+        entries.head.listing.get.squad_guid.get mustEqual PlanetSideGUID(6)
         entries(1).index mustEqual 1
-        entries(1).listing.get.unk1 mustEqual 131
-        entries(1).listing.get.unk2 mustEqual false
-        entries(1).listing.get.unk3.isDefined mustEqual false
-        entries(1).listing.get.info.get.leader.get mustEqual "KOKkiasMFCN"
-        entries(1).listing.get.info.get.task.get mustEqual "Squad 2"
-        entries(1).listing.get.info.get.zone_id.get mustEqual PlanetSideZoneID(4)
-        entries(1).listing.get.info.get.size.get mustEqual 6
-        entries(1).listing.get.info.get.capacity.get mustEqual 10
-        entries(1).listing.get.info.get.squad_guid.get mustEqual PlanetSideGUID(4)
-        entries(2).index mustEqual 255
+        entries(1).listing.get.leader.get mustEqual "KOKkiasMFCN"
+        entries(1).listing.get.task.get mustEqual "Squad 2"
+        entries(1).listing.get.zone_id.get mustEqual PlanetSideZoneID(4)
+        entries(1).listing.get.size.get mustEqual 6
+        entries(1).listing.get.capacity.get mustEqual 10
+        entries(1).listing.get.squad_guid.get mustEqual PlanetSideGUID(4)
       case _ =>
         ko
     }
@@ -138,38 +127,28 @@ class ReplicationStreamMessageTest extends Specification {
       case ReplicationStreamMessage(behavior, behavior2, entries) =>
         behavior mustEqual 5
         behavior2.get mustEqual 6
-        entries.length mustEqual 4
+        entries.length mustEqual 3
         entries.head.index mustEqual 0
-        entries.head.listing.get.unk1 mustEqual 131
-        entries.head.listing.get.unk2 mustEqual false
-        entries.head.listing.get.unk3.isDefined mustEqual false
-        entries.head.listing.get.info.get.leader.get mustEqual "GeneralGorgutz"
-        entries.head.listing.get.info.get.task.get mustEqual "FLY,All welcome,cn last night!!!!"
-        entries.head.listing.get.info.get.zone_id.get mustEqual PlanetSideZoneID(4)
-        entries.head.listing.get.info.get.size.get mustEqual 7
-        entries.head.listing.get.info.get.capacity.get mustEqual 10
-        entries.head.listing.get.info.get.squad_guid.get mustEqual PlanetSideGUID(6)
+        entries.head.listing.get.leader.get mustEqual "GeneralGorgutz"
+        entries.head.listing.get.task.get mustEqual "FLY,All welcome,cn last night!!!!"
+        entries.head.listing.get.zone_id.get mustEqual PlanetSideZoneID(4)
+        entries.head.listing.get.size.get mustEqual 7
+        entries.head.listing.get.capacity.get mustEqual 10
+        entries.head.listing.get.squad_guid.get mustEqual PlanetSideGUID(6)
         entries(1).index mustEqual 1
-        entries(1).listing.get.unk1 mustEqual 131
-        entries(1).listing.get.unk2 mustEqual false
-        entries(1).listing.get.unk3.isDefined mustEqual false
-        entries(1).listing.get.info.get.leader.get mustEqual "NIGHT88RAVEN"
-        entries(1).listing.get.info.get.task.get mustEqual "All Welcome"
-        entries(1).listing.get.info.get.zone_id.get mustEqual PlanetSideZoneID(10)
-        entries(1).listing.get.info.get.size.get mustEqual 4
-        entries(1).listing.get.info.get.capacity.get mustEqual 10
-        entries(1).listing.get.info.get.squad_guid.get mustEqual PlanetSideGUID(3)
+        entries(1).listing.get.leader.get mustEqual "NIGHT88RAVEN"
+        entries(1).listing.get.task.get mustEqual "All Welcome"
+        entries(1).listing.get.zone_id.get mustEqual PlanetSideZoneID(10)
+        entries(1).listing.get.size.get mustEqual 4
+        entries(1).listing.get.capacity.get mustEqual 10
+        entries(1).listing.get.squad_guid.get mustEqual PlanetSideGUID(3)
         entries(2).index mustEqual 2
-        entries(2).listing.get.unk1 mustEqual 131
-        entries(2).listing.get.unk2 mustEqual false
-        entries(2).listing.get.unk3.isDefined mustEqual false
-        entries(2).listing.get.info.get.leader.get mustEqual "KOKkiasMFCN"
-        entries(2).listing.get.info.get.task.get mustEqual "Squad 2"
-        entries(2).listing.get.info.get.zone_id.get mustEqual PlanetSideZoneID(4)
-        entries(2).listing.get.info.get.size.get mustEqual 6
-        entries(2).listing.get.info.get.capacity.get mustEqual 10
-        entries(2).listing.get.info.get.squad_guid.get mustEqual PlanetSideGUID(4)
-        entries(3).index mustEqual 255
+        entries(2).listing.get.leader.get mustEqual "KOKkiasMFCN"
+        entries(2).listing.get.task.get mustEqual "Squad 2"
+        entries(2).listing.get.zone_id.get mustEqual PlanetSideZoneID(4)
+        entries(2).listing.get.size.get mustEqual 6
+        entries(2).listing.get.capacity.get mustEqual 10
+        entries(2).listing.get.squad_guid.get mustEqual PlanetSideGUID(4)
       case _ =>
         ko
     }
@@ -180,15 +159,9 @@ class ReplicationStreamMessageTest extends Specification {
       case ReplicationStreamMessage(behavior, behavior2, entries) =>
         behavior mustEqual 1
         behavior2.isDefined mustEqual false
-        entries.length mustEqual 2
+        entries.length mustEqual 1
         entries.head.index mustEqual 5
-        entries.head.listing.isDefined mustEqual true
-        entries.head.listing.get.unk1 mustEqual 0
-        entries.head.listing.get.unk2 mustEqual true
-        entries.head.listing.get.unk3.isDefined mustEqual true
-        entries.head.listing.get.unk3.get mustEqual 4
-        entries.head.listing.get.info.isDefined mustEqual false
-        entries(1).index mustEqual 255
+        entries.head.listing.isDefined mustEqual false
       case _ =>
         ko
     }
@@ -199,22 +172,16 @@ class ReplicationStreamMessageTest extends Specification {
       case ReplicationStreamMessage(behavior, behavior2, entries) =>
         behavior mustEqual 6
         behavior2.isDefined mustEqual false
-        entries.length mustEqual 2
+        entries.length mustEqual 1
         entries.head.index mustEqual 2
         entries.head.listing.isDefined mustEqual true
-        entries.head.listing.get.unk1 mustEqual 128
-        entries.head.listing.get.unk2 mustEqual true
-        entries.head.listing.get.unk3.isDefined mustEqual true
-        entries.head.listing.get.unk3.get mustEqual 0
-        entries.head.listing.get.info.isDefined mustEqual true
-        entries.head.listing.get.info.get.leader.isDefined mustEqual true
-        entries.head.listing.get.info.get.leader.get mustEqual "FateJHNC"
-        entries.head.listing.get.info.get.task.isDefined mustEqual false
-        entries.head.listing.get.info.get.zone_id.isDefined mustEqual false
-        entries.head.listing.get.info.get.size.isDefined mustEqual false
-        entries.head.listing.get.info.get.capacity.isDefined mustEqual false
-        entries.head.listing.get.info.get.squad_guid.isDefined mustEqual false
-        entries(1).index mustEqual 255
+        entries.head.listing.get.leader.isDefined mustEqual true
+        entries.head.listing.get.leader.get mustEqual "FateJHNC"
+        entries.head.listing.get.task.isDefined mustEqual false
+        entries.head.listing.get.zone_id.isDefined mustEqual false
+        entries.head.listing.get.size.isDefined mustEqual false
+        entries.head.listing.get.capacity.isDefined mustEqual false
+        entries.head.listing.get.squad_guid.isDefined mustEqual false
       case _ =>
         ko
     }
@@ -225,22 +192,16 @@ class ReplicationStreamMessageTest extends Specification {
       case ReplicationStreamMessage(behavior, behavior2, entries) =>
         behavior mustEqual 6
         behavior2.isDefined mustEqual false
-        entries.length mustEqual 2
+        entries.length mustEqual 1
         entries.head.index mustEqual 5
         entries.head.listing.isDefined mustEqual true
-        entries.head.listing.get.unk1 mustEqual 128
-        entries.head.listing.get.unk2 mustEqual true
-        entries.head.listing.get.unk3.isDefined mustEqual true
-        entries.head.listing.get.unk3.get mustEqual 1
-        entries.head.listing.get.info.isDefined mustEqual true
-        entries.head.listing.get.info.get.leader.isDefined mustEqual false
-        entries.head.listing.get.info.get.task.isDefined mustEqual true
-        entries.head.listing.get.info.get.task.get mustEqual "RIP PS1, visit PSForever.net"
-        entries.head.listing.get.info.get.zone_id.isDefined mustEqual false
-        entries.head.listing.get.info.get.size.isDefined mustEqual false
-        entries.head.listing.get.info.get.capacity.isDefined mustEqual false
-        entries.head.listing.get.info.get.squad_guid.isDefined mustEqual false
-        entries(1).index mustEqual 255
+        entries.head.listing.get.leader.isDefined mustEqual false
+        entries.head.listing.get.task.isDefined mustEqual true
+        entries.head.listing.get.task.get mustEqual "RIP PS1, visit PSForever.net"
+        entries.head.listing.get.zone_id.isDefined mustEqual false
+        entries.head.listing.get.size.isDefined mustEqual false
+        entries.head.listing.get.capacity.isDefined mustEqual false
+        entries.head.listing.get.squad_guid.isDefined mustEqual false
       case _ =>
         ko
     }
@@ -251,22 +212,16 @@ class ReplicationStreamMessageTest extends Specification {
       case ReplicationStreamMessage(behavior, behavior2, entries) =>
         behavior mustEqual 6
         behavior2.isDefined mustEqual false
-        entries.length mustEqual 2
+        entries.length mustEqual 1
         entries.head.index mustEqual 3
         entries.head.listing.isDefined mustEqual true
-        entries.head.listing.get.unk1 mustEqual 128
-        entries.head.listing.get.unk2 mustEqual true
-        entries.head.listing.get.unk3.isDefined mustEqual true
-        entries.head.listing.get.unk3.get mustEqual 1
-        entries.head.listing.get.info.isDefined mustEqual true
-        entries.head.listing.get.info.get.leader.isDefined mustEqual false
-        entries.head.listing.get.info.get.task.isDefined mustEqual false
-        entries.head.listing.get.info.get.zone_id.isDefined mustEqual true
-        entries.head.listing.get.info.get.zone_id.get mustEqual PlanetSideZoneID(10)
-        entries.head.listing.get.info.get.size.isDefined mustEqual false
-        entries.head.listing.get.info.get.capacity.isDefined mustEqual false
-        entries.head.listing.get.info.get.squad_guid.isDefined mustEqual false
-        entries(1).index mustEqual 255
+        entries.head.listing.get.leader.isDefined mustEqual false
+        entries.head.listing.get.task.isDefined mustEqual false
+        entries.head.listing.get.zone_id.isDefined mustEqual true
+        entries.head.listing.get.zone_id.get mustEqual PlanetSideZoneID(10)
+        entries.head.listing.get.size.isDefined mustEqual false
+        entries.head.listing.get.capacity.isDefined mustEqual false
+        entries.head.listing.get.squad_guid.isDefined mustEqual false
       case _ =>
         ko
     }
@@ -277,22 +232,16 @@ class ReplicationStreamMessageTest extends Specification {
       case ReplicationStreamMessage(behavior, behavior2, entries) =>
         behavior mustEqual 6
         behavior2.isDefined mustEqual false
-        entries.length mustEqual 2
+        entries.length mustEqual 1
         entries.head.index mustEqual 1
         entries.head.listing.isDefined mustEqual true
-        entries.head.listing.get.unk1 mustEqual 128
-        entries.head.listing.get.unk2 mustEqual true
-        entries.head.listing.get.unk3.isDefined mustEqual true
-        entries.head.listing.get.unk3.get mustEqual 2
-        entries.head.listing.get.info.isDefined mustEqual true
-        entries.head.listing.get.info.get.leader.isDefined mustEqual false
-        entries.head.listing.get.info.get.task.isDefined mustEqual false
-        entries.head.listing.get.info.get.zone_id.isDefined mustEqual false
-        entries.head.listing.get.info.get.size.isDefined mustEqual true
-        entries.head.listing.get.info.get.size.get mustEqual 6
-        entries.head.listing.get.info.get.capacity.isDefined mustEqual false
-        entries.head.listing.get.info.get.squad_guid.isDefined mustEqual false
-        entries(1).index mustEqual 255
+        entries.head.listing.get.leader.isDefined mustEqual false
+        entries.head.listing.get.task.isDefined mustEqual false
+        entries.head.listing.get.zone_id.isDefined mustEqual false
+        entries.head.listing.get.size.isDefined mustEqual true
+        entries.head.listing.get.size.get mustEqual 6
+        entries.head.listing.get.capacity.isDefined mustEqual false
+        entries.head.listing.get.squad_guid.isDefined mustEqual false
       case _ =>
         ko
     }
@@ -303,23 +252,17 @@ class ReplicationStreamMessageTest extends Specification {
       case ReplicationStreamMessage(behavior, behavior2, entries) =>
         behavior mustEqual 6
         behavior2.isDefined mustEqual false
-        entries.length mustEqual 2
+        entries.length mustEqual 1
         entries.head.index mustEqual 5
         entries.head.listing.isDefined mustEqual true
-        entries.head.listing.get.unk1 mustEqual 129
-        entries.head.listing.get.unk2 mustEqual false
-        entries.head.listing.get.unk3.isDefined mustEqual true
-        entries.head.listing.get.unk3.get mustEqual 0
-        entries.head.listing.get.info.isDefined mustEqual true
-        entries.head.listing.get.info.get.leader.isDefined mustEqual true
-        entries.head.listing.get.info.get.leader.get mustEqual "Jimmyn"
-        entries.head.listing.get.info.get.task.isDefined mustEqual false
-        entries.head.listing.get.info.get.zone_id.isDefined mustEqual false
-        entries.head.listing.get.info.get.size.isDefined mustEqual true
-        entries.head.listing.get.info.get.size.get mustEqual 3
-        entries.head.listing.get.info.get.capacity.isDefined mustEqual false
-        entries.head.listing.get.info.get.squad_guid.isDefined mustEqual false
-        entries(1).index mustEqual 255
+        entries.head.listing.get.leader.isDefined mustEqual true
+        entries.head.listing.get.leader.get mustEqual "Jimmyn"
+        entries.head.listing.get.task.isDefined mustEqual false
+        entries.head.listing.get.zone_id.isDefined mustEqual false
+        entries.head.listing.get.size.isDefined mustEqual true
+        entries.head.listing.get.size.get mustEqual 3
+        entries.head.listing.get.capacity.isDefined mustEqual false
+        entries.head.listing.get.squad_guid.isDefined mustEqual false
       case _ =>
         ko
     }
@@ -330,23 +273,17 @@ class ReplicationStreamMessageTest extends Specification {
       case ReplicationStreamMessage(behavior, behavior2, entries) =>
         behavior mustEqual 6
         behavior2.isDefined mustEqual false
-        entries.length mustEqual 2
+        entries.length mustEqual 1
         entries.head.index mustEqual 5
         entries.head.listing.isDefined mustEqual true
-        entries.head.listing.get.unk1 mustEqual 129
-        entries.head.listing.get.unk2 mustEqual false
-        entries.head.listing.get.unk3.isDefined mustEqual true
-        entries.head.listing.get.unk3.get mustEqual 1
-        entries.head.listing.get.info.isDefined mustEqual true
-        entries.head.listing.get.info.get.leader.isDefined mustEqual false
-        entries.head.listing.get.info.get.task.isDefined mustEqual true
-        entries.head.listing.get.info.get.task.get mustEqual "2"
-        entries.head.listing.get.info.get.zone_id.isDefined mustEqual true
-        entries.head.listing.get.info.get.zone_id.get mustEqual PlanetSideZoneID(4)
-        entries.head.listing.get.info.get.size.isDefined mustEqual false
-        entries.head.listing.get.info.get.capacity.isDefined mustEqual false
-        entries.head.listing.get.info.get.squad_guid.isDefined mustEqual false
-        entries(1).index mustEqual 255
+        entries.head.listing.get.leader.isDefined mustEqual false
+        entries.head.listing.get.task.isDefined mustEqual true
+        entries.head.listing.get.task.get mustEqual "2"
+        entries.head.listing.get.zone_id.isDefined mustEqual true
+        entries.head.listing.get.zone_id.get mustEqual PlanetSideZoneID(4)
+        entries.head.listing.get.size.isDefined mustEqual false
+        entries.head.listing.get.capacity.isDefined mustEqual false
+        entries.head.listing.get.squad_guid.isDefined mustEqual false
       case _ =>
         ko
     }
@@ -357,60 +294,57 @@ class ReplicationStreamMessageTest extends Specification {
       case ReplicationStreamMessage(behavior, behavior2, entries) =>
         behavior mustEqual 6
         behavior2.isDefined mustEqual false
-        entries.length mustEqual 2
+        entries.length mustEqual 1
         entries.head.index mustEqual 7
         entries.head.listing.isDefined mustEqual true
-        entries.head.listing.get.unk1 mustEqual 131
-        entries.head.listing.get.unk2 mustEqual false
-        entries.head.listing.get.unk3.isDefined mustEqual false
-        entries.head.listing.get.info.isDefined mustEqual true
-        entries.head.listing.get.info.get.leader.isDefined mustEqual true
-        entries.head.listing.get.info.get.leader.get mustEqual "madmuj"
-        entries.head.listing.get.info.get.task.isDefined mustEqual true
-        entries.head.listing.get.info.get.task.get mustEqual ""
-        entries.head.listing.get.info.get.zone_id.isDefined mustEqual true
-        entries.head.listing.get.info.get.zone_id.get mustEqual PlanetSideZoneID(4)
-        entries.head.listing.get.info.get.size.isDefined mustEqual true
-        entries.head.listing.get.info.get.size.get mustEqual 0
-        entries.head.listing.get.info.get.capacity.isDefined mustEqual true
-        entries.head.listing.get.info.get.capacity.get mustEqual 10
-        entries.head.listing.get.info.get.squad_guid.isDefined mustEqual true
-        entries.head.listing.get.info.get.squad_guid.get mustEqual PlanetSideGUID(11)
-        entries(1).index mustEqual 255
+        entries.head.listing.get.leader.isDefined mustEqual true
+        entries.head.listing.get.leader.get mustEqual "madmuj"
+        entries.head.listing.get.task.isDefined mustEqual true
+        entries.head.listing.get.task.get mustEqual ""
+        entries.head.listing.get.zone_id.isDefined mustEqual true
+        entries.head.listing.get.zone_id.get mustEqual PlanetSideZoneID(4)
+        entries.head.listing.get.size.isDefined mustEqual true
+        entries.head.listing.get.size.get mustEqual 0
+        entries.head.listing.get.capacity.isDefined mustEqual true
+        entries.head.listing.get.capacity.get mustEqual 10
+        entries.head.listing.get.squad_guid.isDefined mustEqual true
+        entries.head.listing.get.squad_guid.get mustEqual PlanetSideGUID(11)
       case _ =>
         ko
     }
   }
 
-  "decode (fails)" in {
-    PacketCoding.DecodePacket(stringCodecFail).isFailure mustEqual true
-    //PacketCoding.DecodePacket(stringListOneFail).isFailure mustEqual true -> used to fail
-    //PacketCoding.DecodePacket(stringListTwoFail).isFailure mustEqual true -> used to fail
-    PacketCoding.DecodePacket(stringUpdateLeaderFail).isFailure mustEqual true
-    PacketCoding.DecodePacket(stringUpdateTaskFail).isFailure mustEqual true
-    //PacketCoding.DecodePacket(stringUpdateContinentFail).isFailure mustEqual true -> used to fail
-    PacketCoding.DecodePacket(stringUpdateSizeFail).isFailure mustEqual true
-    PacketCoding.DecodePacket(stringUpdateLeaderSizeFail).isFailure mustEqual true
-    PacketCoding.DecodePacket(stringUpdateTaskContinentFail).isFailure mustEqual true
-    //PacketCoding.DecodePacket(stringUpdateAllFail).isFailure mustEqual true -> used to fail
+  "decode (remove 1 and update 0)" in {
+    PacketCoding.DecodePacket(stringRemoveUpdate).require match {
+      case ReplicationStreamMessage(behavior, behavior2, entries) =>
+        behavior mustEqual 1
+        behavior2.isDefined mustEqual false
+        entries.length mustEqual 2
+        entries.head.index mustEqual 1
+        entries.head.listing.isDefined mustEqual false
+        entries(1).listing.get.leader.isDefined mustEqual false
+        entries(1).listing.get.task.isDefined mustEqual false
+        entries(1).listing.get.zone_id.isDefined mustEqual false
+        entries(1).listing.get.size.isDefined mustEqual true
+        entries(1).listing.get.size.get mustEqual 10
+        entries(1).listing.get.capacity.isDefined mustEqual false
+        entries(1).listing.get.squad_guid.isDefined mustEqual false
+      case _ =>
+        ko
+    }
   }
 
   "encode (clear)" in {
-    val msg = ReplicationStreamMessage(5, Some(6),
-      Vector(
-        SquadListing(255)
-      )
-    )
+    val msg = ReplicationStreamMessage(5, Some(6), Vector.empty)
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual stringListClear
   }
 
   "encode (one)" in {
-    val msg = ReplicationStreamMessage(5, Some(6),
-      Vector(
-        SquadListing(0, Some(SquadHeader(131, false, None, SquadInfo("FragLANdINC", "Frag", PlanetSideZoneID(10), 0, 10, PlanetSideGUID(1))))),
-        SquadListing(255)
+    val msg = ReplicationStreamMessage(
+      Seq(
+        SquadInfo("FragLANdINC", "Frag", PlanetSideZoneID(10), 0, 10, PlanetSideGUID(1))
       )
     )
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
@@ -419,11 +353,10 @@ class ReplicationStreamMessageTest extends Specification {
   }
 
   "encode (two)" in {
-    val msg = ReplicationStreamMessage(5, Some(6),
-      Vector(
-        SquadListing(0, Some(SquadHeader(131, false, None, SquadInfo("GeneralGorgutz", "FLY,All welcome,cn last night!!!!", PlanetSideZoneID(4), 7, 10, PlanetSideGUID(6))))),
-        SquadListing(1, Some(SquadHeader(131, false, None, SquadInfo("KOKkiasMFCN", "Squad 2", PlanetSideZoneID(4), 6, 10, PlanetSideGUID(4))))),
-        SquadListing(255)
+    val msg = ReplicationStreamMessage(
+      Seq(
+        SquadInfo("GeneralGorgutz", "FLY,All welcome,cn last night!!!!", PlanetSideZoneID(4), 7, 10, PlanetSideGUID(6)),
+        SquadInfo("KOKkiasMFCN", "Squad 2", PlanetSideZoneID(4), 6, 10, PlanetSideGUID(4))
       )
     )
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
@@ -432,12 +365,11 @@ class ReplicationStreamMessageTest extends Specification {
   }
 
   "encode (three)" in {
-    val msg = ReplicationStreamMessage(5, Some(6),
-      Vector(
-        SquadListing(0, Some(SquadHeader(131, false, None, SquadInfo("GeneralGorgutz", "FLY,All welcome,cn last night!!!!", PlanetSideZoneID(4), 7, 10, PlanetSideGUID(6))))),
-        SquadListing(1, Some(SquadHeader(131, false, None, SquadInfo("NIGHT88RAVEN", "All Welcome", PlanetSideZoneID(10), 4, 10, PlanetSideGUID(3))))),
-        SquadListing(2, Some(SquadHeader(131, false, None, SquadInfo("KOKkiasMFCN", "Squad 2", PlanetSideZoneID(4), 6, 10, PlanetSideGUID(4))))),
-        SquadListing(255)
+    val msg = ReplicationStreamMessage(
+      Seq(
+        SquadInfo("GeneralGorgutz", "FLY,All welcome,cn last night!!!!", PlanetSideZoneID(4), 7, 10, PlanetSideGUID(6)),
+        SquadInfo("NIGHT88RAVEN", "All Welcome", PlanetSideZoneID(10), 4, 10, PlanetSideGUID(3)),
+        SquadInfo("KOKkiasMFCN", "Squad 2", PlanetSideZoneID(4), 6, 10, PlanetSideGUID(4))
       )
     )
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
@@ -448,8 +380,7 @@ class ReplicationStreamMessageTest extends Specification {
   "encode (remove)" in {
     val msg = ReplicationStreamMessage(1, None,
       Vector(
-        SquadListing(5, Some(SquadHeader(0, true, Some(4)))),
-        SquadListing(255)
+        SquadListing(5, None)
       )
     )
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
@@ -460,8 +391,7 @@ class ReplicationStreamMessageTest extends Specification {
   "encode (update leader)" in {
     val msg = ReplicationStreamMessage(6, None,
       Vector(
-        SquadListing(2, Some(SquadHeader(128, true, Some(0), SquadInfo("FateJHNC", None)))),
-        SquadListing(255)
+        SquadListing(2, SquadInfo("FateJHNC"))
       )
     )
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
@@ -472,8 +402,7 @@ class ReplicationStreamMessageTest extends Specification {
   "encode (update task)" in {
     val msg = ReplicationStreamMessage(6, None,
       Vector(
-        SquadListing(5, Some(SquadHeader(128, true, Some(1), SquadInfo(None, "RIP PS1, visit PSForever.net")))),
-        SquadListing(255)
+        SquadListing(5, SquadInfo(None, "RIP PS1, visit PSForever.net"))
       )
     )
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
@@ -484,8 +413,7 @@ class ReplicationStreamMessageTest extends Specification {
   "encode (update continent)" in {
     val msg = ReplicationStreamMessage(6, None,
       Vector(
-        SquadListing(3, Some(SquadHeader(128, true, Some(1), SquadInfo(PlanetSideZoneID(10))))),
-        SquadListing(255)
+        SquadListing(3, SquadInfo(PlanetSideZoneID(10)))
       )
     )
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
@@ -496,8 +424,7 @@ class ReplicationStreamMessageTest extends Specification {
   "encode (update size)" in {
     val msg = ReplicationStreamMessage(6, None,
       Vector(
-        SquadListing(1, Some(SquadHeader(128, true, Some(2), SquadInfo(6, None)))),
-        SquadListing(255)
+        SquadListing(1, SquadInfo(6))
       )
     )
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
@@ -508,8 +435,7 @@ class ReplicationStreamMessageTest extends Specification {
   "encode (update leader and size)" in {
     val msg = ReplicationStreamMessage(6, None,
       Vector(
-        SquadListing(5, Some(SquadHeader(129, false, Some(0), SquadInfo("Jimmyn", 3)))),
-        SquadListing(255)
+        SquadListing(5, SquadInfo("Jimmyn").And(SquadInfo(3)))
       )
     )
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
@@ -520,8 +446,7 @@ class ReplicationStreamMessageTest extends Specification {
   "encode (update task and continent)" in {
     val msg = ReplicationStreamMessage(6, None,
       Vector(
-        SquadListing(5, Some(SquadHeader(129, false, Some(1), SquadInfo("2", PlanetSideZoneID(4))))),
-        SquadListing(255)
+        SquadListing(5, SquadInfo(None, "2").And(SquadInfo(PlanetSideZoneID(4))))
       )
     )
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
@@ -532,8 +457,7 @@ class ReplicationStreamMessageTest extends Specification {
   "encode (update all)" in {
     val msg = ReplicationStreamMessage(6, None,
       Vector(
-        SquadListing(7, Some(SquadHeader(131, false, None, SquadInfo("madmuj", "", PlanetSideZoneID(4), 0, 10, PlanetSideGUID(11))))),
-        SquadListing(255)
+        SquadListing(7, SquadInfo("madmuj", "", PlanetSideZoneID(4), 0, 10, PlanetSideGUID(11)))
       )
     )
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
@@ -541,116 +465,15 @@ class ReplicationStreamMessageTest extends Specification {
     pkt mustEqual stringUpdateAll
   }
 
-  "encode (fails)" in {
-    //encode codec fail
-    PacketCoding.EncodePacket(
-      ReplicationStreamMessage(1, None,
-        Vector(
-          SquadListing(5, Some(SquadHeader(0, false, Some(4)))),
-          SquadListing(255)
-        )
+  "encode (remove 1 and update 0)" in {
+    val msg = ReplicationStreamMessage(1, None,
+      Vector(
+        SquadListing(1, None),
+        SquadListing(0, SquadInfo(10))
       )
-    ).isFailure mustEqual true
+    )
+    val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
-    //encode one
-    PacketCoding.EncodePacket(
-      ReplicationStreamMessage(5, Some(6),
-        Vector(
-          SquadListing(0, Some(SquadHeader(131, false, None, Some(SquadInfo(Some("FragLANdINC"), Some("Frag"), None, Some(0),Some(10), Some(PlanetSideGUID(1))))))),
-          SquadListing(255)
-        )
-      )
-    ).isFailure mustEqual true
-
-    //encode two
-    PacketCoding.EncodePacket(
-      ReplicationStreamMessage(5, Some(6),
-        Vector(
-          SquadListing(0, Some(SquadHeader(131, false, None, SquadInfo("GeneralGorgutz", "FLY,All welcome,cn last night!!!!", PlanetSideZoneID(4), 7, 10, PlanetSideGUID(6))))),
-          SquadListing(1, Some(SquadHeader(131, false, None, Some(SquadInfo(Some("KOKkiasMFCN"), Some("Squad 2"), None, Some(6), Some(10), Some(PlanetSideGUID(4))))))),
-          SquadListing(255)
-        )
-      )
-    ).isFailure mustEqual true
-
-    //encode leader
-    PacketCoding.EncodePacket(
-      ReplicationStreamMessage(6, None,
-        Vector(
-          SquadListing(2, Some(SquadHeader(128, true, Some(0), Some(SquadInfo(None, None, None, None, None, None))))),
-          SquadListing(255)
-        )
-      )
-    ).isFailure mustEqual true
-
-    //encode task
-    PacketCoding.EncodePacket(
-      ReplicationStreamMessage(6, None,
-        Vector(
-          SquadListing(5, Some(SquadHeader(128, true, Some(1), Some(SquadInfo(None, None, None, None, None, None))))),
-          SquadListing(255)
-        )
-      )
-    ).isFailure mustEqual true
-
-    //encode continent
-    PacketCoding.EncodePacket(
-      ReplicationStreamMessage(6, None,
-        Vector(
-          SquadListing(3, Some(SquadHeader(128, true, Some(1), Some(SquadInfo(None, None, None, None, None, None))))),
-          SquadListing(255)
-        )
-      )
-    ).isFailure mustEqual true
-
-    //encode task or continent
-    PacketCoding.EncodePacket(
-      ReplicationStreamMessage(6, None,
-        Vector(
-          SquadListing(3, Some(SquadHeader(128, true, Some(1), Some(SquadInfo(None, Some(""), Some(PlanetSideZoneID(10)), None, None, None))))),
-          SquadListing(255)
-        )
-      )
-    ).isFailure mustEqual true
-
-    //encode size
-    PacketCoding.EncodePacket(
-      ReplicationStreamMessage(6, None,
-        Vector(
-          SquadListing(1, Some(SquadHeader(128, true, Some(2), Some(SquadInfo(None, None, None, None, None, None))))),
-          SquadListing(255)
-        )
-      )
-    ).isFailure mustEqual true
-
-    //encode leader and size
-    PacketCoding.EncodePacket(
-      ReplicationStreamMessage(6, None,
-        Vector(
-          SquadListing(5, Some(SquadHeader(129, false, Some(0), Some(SquadInfo(None, None, None, None, None, None))))),
-          SquadListing(255)
-        )
-      )
-    ).isFailure mustEqual true
-
-    //encode task and continent
-    PacketCoding.EncodePacket(
-      ReplicationStreamMessage(6, None,
-        Vector(
-          SquadListing(5, Some(SquadHeader(129, false, Some(1), Some(SquadInfo(None, None, None, None, None, None))))),
-          SquadListing(255)
-        )
-      )
-    ).isFailure mustEqual true
-
-    //encode all
-    PacketCoding.EncodePacket(
-      ReplicationStreamMessage(6, None,
-        Vector(
-          SquadListing(7, Some(SquadHeader(131, false, None, Some(SquadInfo(None, None, None, None, None, None))))),
-          SquadListing(255)
-        )
-      )
-    ).isFailure mustEqual true
+    pkt mustEqual stringRemoveUpdate
   }
 }
diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala
index 8c3424ce..c33ba93a 100644
--- a/pslogin/src/main/scala/WorldSessionActor.scala
+++ b/pslogin/src/main/scala/WorldSessionActor.scala
@@ -798,7 +798,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
       traveler = new Traveler(self, continent.Id)
       //PropertyOverrideMessage
       sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(0), 112, 0)) // disable festive backpacks
-      sendResponse(ReplicationStreamMessage(5, Some(6), Vector(SquadListing()))) //clear squad list
+      sendResponse(ReplicationStreamMessage(5, Some(6), Vector.empty)) //clear squad list
       sendResponse(FriendsResponse(FriendAction.InitializeFriendList, 0, true, true, Nil))
       sendResponse(FriendsResponse(FriendAction.InitializeIgnoreList, 0, true, true, Nil))
       avatarService ! Service.Join(avatar.name) //channel will be player.Name
@@ -3003,7 +3003,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
       sendResponse(TimeOfDayMessage(1191182336))
       //custom
       sendResponse(ContinentalLockUpdateMessage(13, PlanetSideEmpire.VS)) // "The VS have captured the VS Sanctuary."
-      sendResponse(ReplicationStreamMessage(5, Some(6), Vector(SquadListing()))) //clear squad list
+      sendResponse(ReplicationStreamMessage(5, Some(6), Vector.empty)) //clear squad list
       sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(0), 112, 0)) // disable festive backpacks
       //(0 to 255).foreach(i => { sendResponse(SetEmpireMessage(PlanetSideGUID(i), PlanetSideEmpire.VS)) })
 

From afb10f57c377046df57e0272653cefa2c9866719 Mon Sep 17 00:00:00 2001
From: FateJH <Jason_DiDonato@yahoo.com>
Date: Sat, 25 May 2019 15:49:51 -0400
Subject: [PATCH 04/31] restored SquadDefinitionActionMessage to the past
 upgrade form that was lost; preliminary objects for squad infromation and
 squad member position information

---
 .../psforever/objects/teamwork/Member.scala   |  65 ++
 .../psforever/objects/teamwork/Squad.scala    |  51 ++
 .../game/SquadDefinitionActionMessage.scala   | 554 +++++++++---------
 .../SquadDefinitionActionMessageTest.scala    | 312 +++-------
 4 files changed, 477 insertions(+), 505 deletions(-)
 create mode 100644 common/src/main/scala/net/psforever/objects/teamwork/Member.scala
 create mode 100644 common/src/main/scala/net/psforever/objects/teamwork/Squad.scala

diff --git a/common/src/main/scala/net/psforever/objects/teamwork/Member.scala b/common/src/main/scala/net/psforever/objects/teamwork/Member.scala
new file mode 100644
index 00000000..fc1988e3
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/teamwork/Member.scala
@@ -0,0 +1,65 @@
+// Copyright (c) 2019 PSForever
+package net.psforever.objects.teamwork
+
+import net.psforever.types.{CertificationType, Vector3}
+
+class Member {
+  //about the position to be filled
+  private var role : String = ""
+  private var restrictions : Set[CertificationType.Value] = Set()
+  //about the individual filling the position
+  private var name : String = ""
+  private var health : Int = 0
+  private var armor : Int = 0
+  private var zoneId : String = "Nowhere"
+  private var position : Vector3 = Vector3.Zero
+
+  def Role : String = role
+
+  def Role_=(title : String) : String = {
+    role = title
+    Role
+  }
+
+  def Restrictions : Set[CertificationType.Value] = restrictions
+
+  def Restrictions_=(requirements : Set[CertificationType.Value]) = {
+    restrictions = requirements
+    Restrictions
+  }
+
+  def Name : String = name
+
+  def Name_=(moniker : String) : String = {
+    name = moniker
+    Name
+  }
+
+  def Health : Int = health
+
+  def Health_=(red : Int) : Int = {
+    health = red
+    Health
+  }
+
+  def Armor : Int = armor
+
+  def Armor_=(blue : Int) : Int = {
+    armor = blue
+    Armor
+  }
+
+  def ZoneId : String = zoneId
+
+  def ZoneId_=(id : String) : String = {
+    zoneId = id
+    ZoneId
+  }
+
+  def Position : Vector3 = position
+
+  def Position_=(pos : Vector3) : Vector3 = {
+    position = pos
+    Position
+  }
+}
diff --git a/common/src/main/scala/net/psforever/objects/teamwork/Squad.scala b/common/src/main/scala/net/psforever/objects/teamwork/Squad.scala
new file mode 100644
index 00000000..23a96a7a
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/teamwork/Squad.scala
@@ -0,0 +1,51 @@
+// Copyright (c) 2019 PSForever
+package net.psforever.objects.teamwork
+
+import net.psforever.objects.entity.IdentifiableEntity
+import net.psforever.packet.game.PlanetSideGUID
+import net.psforever.types.PlanetSideEmpire
+
+class Squad(squadId : PlanetSideGUID, alignment : PlanetSideEmpire.Value) extends IdentifiableEntity {
+  super.GUID_=(squadId)
+  private val faction : PlanetSideEmpire.Value = alignment //does not change
+  private val zoneId : Option[String] = None
+  private var task : String = ""
+  private var description : String = ""
+  private val membership : Array[Member] = Array.fill[Member](10)(new Member)
+  private val availability : Array[Boolean] = Array.fill[Boolean](10)(true)
+
+  override def GUID_=(d : PlanetSideGUID) : PlanetSideGUID = GUID
+
+  def Faction : PlanetSideEmpire.Value = faction
+
+  def ZoneId : String = zoneId.getOrElse({
+    membership.headOption match {
+      case Some(leader) =>
+        leader.ZoneId
+      case _ =>
+        "Nowhere"
+    }
+  })
+
+  def Task : String = task
+
+  def Task_=(assignment : String) : String = {
+    task = assignment
+    Task
+  }
+
+  def Description : String = description
+
+  def Description_=(desc : String) : String = {
+    description = desc
+    Description
+  }
+
+  def Membership : Array[Member] = membership
+
+  def Availability : Array[Boolean] = availability
+
+  def Size : Int = membership.count(member => !member.Name.equals(""))
+
+  def Capacity : Int = availability.count(open => open)
+}
diff --git a/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala b/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala
index 052c0e55..0baf8518 100644
--- a/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala
+++ b/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala
@@ -2,21 +2,239 @@
 package net.psforever.packet.game
 
 import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
+import scodec.bits.BitVector
 import scodec.{Attempt, Codec, Err}
 import scodec.codecs._
 import shapeless.{::, HNil}
 
+/**
+  * The generic superclass of a specific behavior for this type of squad definition action.
+  * All behaviors have a "code" that indicates how the rest of the data is parsed.
+  * @param code the action behavior code
+  */
+protected abstract class SquadAction(val code : Int)
+
+object SquadAction{
+  final case class SaveSquadDefinition() extends SquadAction(3)
+
+  final case class ListSquad() extends SquadAction(8)
+
+  final case class SelectRoleForYourself(state : Int) extends SquadAction(10)
+
+  final case class ChangeSquadPurpose(purpose : String) extends SquadAction(19)
+
+  final case class ChangeSquadZone(zone : PlanetSideZoneID) extends SquadAction(20)
+
+  final case class CloseSquadMemberPosition(position : Int) extends SquadAction(21)
+
+  final case class AddSquadMemberPosition(position : Int) extends SquadAction(22)
+
+  final case class ChangeSquadMemberRequirementsRole(u1 : Int, role : String) extends SquadAction(23)
+
+  final case class ChangeSquadMemberRequirementsDetailedOrders(u1 : Int, orders : String) extends SquadAction(24)
+
+  final case class ChangeSquadMemberRequirementsWeapons(u1 : Int, u2 : Long) extends SquadAction(25)
+
+  final case class ResetAll() extends SquadAction(26)
+
+  final case class AutoApproveInvitationRequests(state : Boolean) extends SquadAction(28)
+
+  final case class LocationFollowsSquadLead(state : Boolean) extends SquadAction(31)
+
+  final case class SearchForSquadsWithParticularRole(u1: String, u2 : Long, u3: Int, u4 : Int) extends SquadAction(34)
+
+  final case class CancelSquadSearch() extends SquadAction(35)
+
+  final case class FindLfsSoldiersForRole(state : Int) extends SquadAction(40)
+
+  final case class CancelFind() extends SquadAction(41)
+
+  final case class Unknown(badCode : Int, data : BitVector) extends SquadAction(badCode)
+
+  /**
+    * The `Codec`s used to transform the input stream into the context of a specific action
+    * and extract the field data from that stream.
+    */
+  object Codecs {
+    private val everFailCondition = conditional(included = false, bool)
+
+    val saveSquadDefinitionCodec = everFailCondition.xmap[SaveSquadDefinition] (
+      _ => SaveSquadDefinition(),
+      {
+        case SaveSquadDefinition() => None
+      }
+    )
+
+    val listSquadCodec = everFailCondition.xmap[ListSquad] (
+      _ => ListSquad(),
+      {
+        case ListSquad() => None
+      }
+    )
+
+    val selectRoleForYourselfCodec = uint4.xmap[SelectRoleForYourself] (
+      value => SelectRoleForYourself(value),
+      {
+        case SelectRoleForYourself(value) => value
+      }
+    )
+
+    val changeSquadPurposeCodec = PacketHelpers.encodedWideStringAligned(6).xmap[ChangeSquadPurpose] (
+      purpose => ChangeSquadPurpose(purpose),
+      {
+        case ChangeSquadPurpose(purpose) => purpose
+      }
+    )
+
+    val changeSquadZoneCodec = uint16L.xmap[ChangeSquadZone] (
+      value => ChangeSquadZone(PlanetSideZoneID(value)),
+      {
+        case ChangeSquadZone(value) => value.zoneId.toInt
+      }
+    )
+
+    val closeSquadMemberPositionCodec = uint4.xmap[CloseSquadMemberPosition] (
+      position => CloseSquadMemberPosition(position),
+      {
+        case CloseSquadMemberPosition(position) => position
+      }
+    )
+
+    val addSquadMemberPositionCodec = uint4.xmap[AddSquadMemberPosition] (
+      position => AddSquadMemberPosition(position),
+      {
+        case AddSquadMemberPosition(position) => position
+      }
+    )
+
+    val changeSquadMemberRequirementsRoleCodec = (uint4L :: PacketHelpers.encodedWideStringAligned(2)).xmap[ChangeSquadMemberRequirementsRole] (
+      {
+        case u1 :: role :: HNil => ChangeSquadMemberRequirementsRole(u1, role)
+      },
+      {
+        case ChangeSquadMemberRequirementsRole(u1, role) => u1 :: role :: HNil
+      }
+    )
+
+    val changeSquadMemberRequirementsDetailedOrdersCodec = (uint4L :: PacketHelpers.encodedWideStringAligned(2)).xmap[ChangeSquadMemberRequirementsDetailedOrders] (
+      {
+        case u1 :: role :: HNil => ChangeSquadMemberRequirementsDetailedOrders(u1, role)
+      },
+      {
+        case ChangeSquadMemberRequirementsDetailedOrders(u1, role) => u1 :: role :: HNil
+      }
+    )
+
+    val changeSquadMemberRequirementsWeaponsCodec = (uint4 :: ulongL(46)).xmap[ChangeSquadMemberRequirementsWeapons] (
+      {
+        case u1 :: u2 :: HNil => ChangeSquadMemberRequirementsWeapons(u1, u2)
+      },
+      {
+        case ChangeSquadMemberRequirementsWeapons(u1, u2) => u1 :: u2 :: HNil
+      }
+    )
+
+    val resetAllCodec = everFailCondition.xmap[ResetAll] (
+      _ => ResetAll(),
+      {
+        case ResetAll() => None
+      }
+    )
+
+    val autoApproveInvitationRequestsCodec = bool.xmap[AutoApproveInvitationRequests] (
+      state => AutoApproveInvitationRequests(state),
+      {
+        case AutoApproveInvitationRequests(state) => state
+      }
+    )
+
+    val locationFollowsSquadLeadCodec = bool.xmap[LocationFollowsSquadLead] (
+      state => LocationFollowsSquadLead(state),
+      {
+        case LocationFollowsSquadLead(state) => state
+      }
+    )
+
+    val searchForSquadsWithParticularRoleCodec = (
+      PacketHelpers.encodedWideStringAligned(6) ::
+        ulongL(46) ::
+        uint16L ::
+        uintL(3)).xmap[SearchForSquadsWithParticularRole] (
+      {
+        case u1 :: u2 :: u3 :: u4 :: HNil => SearchForSquadsWithParticularRole(u1, u2, u3, u4)
+      },
+      {
+        case SearchForSquadsWithParticularRole(u1, u2, u3, u4) => u1 :: u2 :: u3 :: u4 :: HNil
+      }
+    )
+
+    val cancelSquadSearchCodec = everFailCondition.xmap[CancelSquadSearch] (
+      _ => CancelSquadSearch(),
+      {
+        case CancelSquadSearch() => None
+      }
+    )
+
+    val findLfsSoldiersForRoleCodec = uint4.xmap[FindLfsSoldiersForRole] (
+      state => FindLfsSoldiersForRole(state),
+      {
+        case FindLfsSoldiersForRole(state) => state
+      }
+    )
+
+    val cancelFindCodec = everFailCondition.xmap[CancelFind] (
+      _ => CancelFind(),
+      {
+        case CancelFind() => None
+      }
+    )
+
+    /**
+      * A common form for known action code indexes with an unknown purpose and transformation is an "Unknown" object.
+      * @param action the action behavior code
+      * @return a transformation between the action code and the unknown bit data
+      */
+    def unknownCodec(action : Int) = bits.xmap[Unknown] (
+     data => Unknown(action, data),
+      {
+        case Unknown(_, data) => data
+      }
+    )
+
+    /**
+      * The action code was completely unanticipated!
+      * @param action the action behavior code
+      * @return nothing; always fail
+      */
+    def failureCodec(action : Int)= everFailCondition.exmap[SquadAction] (
+      _ => Attempt.failure(Err(s"can not match a codec pattern for decoding $action")),
+      _ => Attempt.failure(Err(s"can not match a codec pattern for encoding $action"))
+    )
+  }
+}
+
 /**
   * Manage composition and details of a player's current squad, or the currently-viewed squad.<br>
   * <br>
   * The `action` code indicates the format of the remainder data in the packet.
   * The following formats are translated; their purposes are listed:<br>
   * &nbsp;&nbsp;`(None)`<br>
-  * &nbsp;&nbsp;&nbsp;&nbsp;`3 ` - Save Squad Definition
-  * &nbsp;&nbsp;&nbsp;&nbsp;`8 ` - List Squad
-  * &nbsp;&nbsp;&nbsp;&nbsp;`26` - Reset All
-  * &nbsp;&nbsp;&nbsp;&nbsp;`35` - Cancel Squad Search
-  * &nbsp;&nbsp;&nbsp;&nbsp;`41` - Cancel Find
+  * &nbsp;&nbsp;&nbsp;&nbsp;`0 ` - UNKNOWN<br>
+  * &nbsp;&nbsp;&nbsp;&nbsp;`1 ` - UNKNOWN<br>
+  * &nbsp;&nbsp;&nbsp;&nbsp;`2 ` - UNKNOWN<br>
+  * &nbsp;&nbsp;&nbsp;&nbsp;`3 ` - Save Squad Definition<br>
+  * &nbsp;&nbsp;&nbsp;&nbsp;`4 ` - UNKNOWN<br>
+  * &nbsp;&nbsp;&nbsp;&nbsp;`6 ` - UNKNOWN<br>
+  * &nbsp;&nbsp;&nbsp;&nbsp;`8 ` - List Squad<br>
+  * &nbsp;&nbsp;&nbsp;&nbsp;`9 ` - UNKNOWN<br>
+  * &nbsp;&nbsp;&nbsp;&nbsp;`16` - UNKNOWN<br>
+  * &nbsp;&nbsp;&nbsp;&nbsp;`17` - UNKNOWN<br>
+  * &nbsp;&nbsp;&nbsp;&nbsp;`18` - UNKNOWN<br>
+  * &nbsp;&nbsp;&nbsp;&nbsp;`26` - Reset All<br>
+  * &nbsp;&nbsp;&nbsp;&nbsp;`35` - Cancel Squad Search<br>
+  * &nbsp;&nbsp;&nbsp;&nbsp;`41` - Cancel Find<br>
+  * &nbsp;&nbsp;&nbsp;&nbsp;`42` - UNKNOWN<br>
+  * &nbsp;&nbsp;&nbsp;&nbsp;`43` - UNKNOWN<br>
   * &nbsp;&nbsp;`Boolean`<br>
   * &nbsp;&nbsp;&nbsp;&nbsp;`28` - Auto-approve Requests for Invitation<br>
   * &nbsp;&nbsp;&nbsp;&nbsp;`29` - UNKNOWN<br>
@@ -48,34 +266,15 @@ import shapeless.{::, HNil}
   * &nbsp;&nbsp;`Long :: Long`<br>
   * &nbsp;&nbsp;&nbsp;&nbsp;`36` - UNKNOWN<br>
   * &nbsp;&nbsp;`String :: Long :: Int :: Int`<br>
-  * &nbsp;&nbsp;&nbsp;&nbsp;`34` - Search for Squads with a Particular Role<br>
-  * <br>
-  * Exploration:<br>
-  * Some notes regarding the full list of action codes follows after this packet.
-  * Asides from codes whose behaviors are unknown, some codes also have unknown data format.
-  * No information for codes 1, 5, 9, 27, or 35 has been found yet.
-  * @param action the purpose of this packet;
-  *               also decides the content of the parameter fields
+  * &nbsp;&nbsp;&nbsp;&nbsp;`34` - Search for Squads with a Particular Role
   * @param unk1 na
   * @param unk2 na
-  * @param string_opt the optional `String` parameter
-  * @param int1_opt the first optional `Int` parameter;
-  *                 will not necessarily conform to a single bit length
-  * @param int2_opt the second optional `Int` parameter
-  * @param long1_opt the first optional `Long` parameter;
-  *                 will not necessarily conform to a single bit length
-  * @param long2_opt the second optional `Long` parameter
-  * @param bool_opt the optional `Boolean` parameter
+  * @param action the purpose of this packet;
+  *               also decides the content of the parameter fields
   */
-final case class SquadDefinitionActionMessage(action : Int,
-                                              unk1 : Int,
+final case class SquadDefinitionActionMessage(unk1 : Int,
                                               unk2 : Int,
-                                              string_opt : Option[String],
-                                              int1_opt : Option[Int],
-                                              int2_opt : Option[Int],
-                                              long1_opt : Option[Long],
-                                              long2_opt : Option[Long],
-                                              bool_opt : Option[Boolean])
+                                              action : SquadAction)
   extends PlanetSideGamePacket {
   type Packet = SquadDefinitionActionMessage
   def opcode = GamePacketOpcode.SquadDefinitionActionMessage
@@ -84,266 +283,57 @@ final case class SquadDefinitionActionMessage(action : Int,
 
 object SquadDefinitionActionMessage extends Marshallable[SquadDefinitionActionMessage] {
   /**
-    * Common pattern for the parameters, with enough fields to support all possible outputs.
-    * All fields are `Option`al purposefully.
+    * Use the action code to transform between
+    * the specific action object and its field data
+    * and the stream of bits of the original packet.
+    * @param code the action behavior code
+    * @return the `SquadAction` `Codec` to use for the given `code`
     */
-  private type allPattern = Option[String] :: Option[Int] :: Option[Int] :: Option[Long] :: Option[Long] :: Option[Boolean] :: HNil
-
-  /**
-    * `Codec` for reading nothing from the remainder of the stream data.
-    * @return a filled-out `allPattern` if successful
-    */
-  def noneCodec : Codec[allPattern] = ignore(0).xmap[allPattern] (
-    {
-      case () =>
-        None :: None :: None :: None :: None :: None :: HNil
-    },
-    {
-      case _ :: _ :: _ :: _ :: _ :: _ :: HNil =>
-        ()
-    }
-  )
-
-  /**
-    * `Codec` for reading a single `Boolean` from remaining stream data.
-    * @return a filled-out `allPattern` if successful
-    */
-  def boolCodec : Codec[allPattern] = bool.hlist.exmap[allPattern] (
-    {
-      case n :: HNil =>
-        Attempt.successful(None :: None :: None :: None :: None :: Some(n) :: HNil)
-    },
-    {
-      case _ :: _ :: _ :: _ :: _ :: None :: HNil =>
-        Attempt.failure(Err("expected a boolean value but found nothing"))
-      case _ :: _ :: _ :: _ :: _ :: Some(n) :: HNil =>
-        Attempt.successful(n :: HNil)
-    }
-  )
-
-  /**
-    * `Codec` for reading a single `Int` from remaining stream data.
-    * Multiple bit lengths can be processed from this reading.
-    * @param icodec the `Codec[Int]` read by this method
-    * @return a filled-out `allPattern` if successful
-    */
-  def intCodec(icodec : Codec[Int]) : Codec[allPattern] = icodec.hlist.exmap[allPattern] (
-    {
-      case n :: HNil =>
-        Attempt.successful(None :: Some(n) :: None :: None :: None :: None :: HNil)
-    },
-    {
-      case _ :: None :: _ :: _ :: _ :: _ :: HNil =>
-        Attempt.failure(Err("expected an integer value but found nothing"))
-      case _ :: Some(n) :: _ :: _ :: _ :: _ :: HNil =>
-        Attempt.successful(n :: HNil)
-    }
-  )
-
-  /**
-    * `Codec` for reading a single `Long` from remaining stream data.
-    * @return a filled-out `allPattern` if successful
-    */
-  def longCodec : Codec[allPattern] = uint32L.hlist.exmap[allPattern] (
-    {
-      case n :: HNil =>
-        Attempt.successful(None :: None :: None :: Some(n) :: None :: None :: HNil)
-    },
-    {
-      case _ :: _ :: _ :: None :: _ :: _ :: HNil =>
-        Attempt.failure(Err("expected a long value but found nothing"))
-      case _ :: _ :: _ :: Some(n) :: _ :: _ :: HNil =>
-        Attempt.successful(n :: HNil)
-    }
-  )
-
-  /**
-    * `Codec` for reading a `String` from remaining stream data.
-    * All `String`s processed by this reading are wide character and are padded by six.
-    * @return a filled-out `allPattern` if successful
-    */
-  def stringCodec : Codec[allPattern] = PacketHelpers.encodedWideStringAligned(6).hlist.exmap[allPattern] (
-    {
-      case a :: HNil =>
-        Attempt.successful(Some(a) :: None :: None :: None :: None :: None :: HNil)
-    },
-    {
-      case None:: _ :: _ :: _ :: _ :: _ :: HNil =>
-        Attempt.failure(Err("expected a string value but found nothing"))
-      case Some(a) :: _ :: _ :: _ :: _ :: _ :: HNil =>
-        Attempt.successful(a :: HNil)
-    }
-  )
-
-  /**
-    * `Codec` for reading an `Int` followed by a `Long` from remaining stream data.
-    * Multiple bit lengths can be processed for the `Long1` value from this reading.
-    * @param lcodec the `Codec[Long]` read by this method
-    * @return a filled-out `allPattern` if successful
-    */
-  def intLongCodec(lcodec : Codec[Long]) : Codec[allPattern] = (
-    uint4L ::
-      lcodec
-    ).exmap[allPattern] (
-    {
-      case a :: b :: HNil =>
-        Attempt.successful(None :: Some(a) :: None :: Some(b) :: None :: None :: HNil)
-    },
-    {
-      case _ :: None :: _ :: _ :: _ :: _ :: HNil =>
-        Attempt.failure(Err("expected a integer value but found nothing"))
-      case _ :: _ :: _ :: None :: _ :: _ :: HNil =>
-        Attempt.failure(Err("expected a long value but found nothing"))
-      case _ :: Some(a) :: _ :: Some(b) :: _ :: _ :: HNil =>
-        Attempt.successful(a :: b :: HNil)
-    }
-  )
-
-  /**
-    * `Codec` for reading an `Int` followed by a `String` from remaining stream data.
-    * All `String`s processed by this reading are wide character and are padded by two.
-    * @return a filled-out `allPattern` if successful
-    */
-  def intStringCodec : Codec[allPattern] = (
-    uint4L ::
-      PacketHelpers.encodedWideStringAligned(2)
-    ).exmap[allPattern] (
-    {
-      case a :: b :: HNil =>
-        Attempt.successful(Some(b) :: Some(a) :: None :: None :: None :: None :: HNil)
-    },
-    {
-      case None:: _ :: _ :: _ :: _ :: _ :: HNil =>
-        Attempt.failure(Err("expected a string value but found nothing"))
-      case _ :: None :: _ :: _ :: _ :: _ :: HNil =>
-        Attempt.failure(Err("expected an integer value but found nothing"))
-      case Some(b) :: Some(a) :: _ :: _ :: _ :: _ :: HNil =>
-        Attempt.successful(a :: b :: HNil)
-    }
-  )
-
-  /**
-    * `Codec` for reading two `Long`s from remaining stream data.
-    * @return a filled-out `allPattern` if successful
-    */
-  def longLongCodec : Codec[allPattern] = (
-    ulongL(46) ::
-      uint32L
-    ).exmap[allPattern] (
-    {
-      case a :: b :: HNil =>
-        Attempt.successful(None :: None :: None :: Some(a) :: Some(b) :: None :: HNil)
-    },
-    {
-      case (_ :: _ :: _ :: None :: _ :: _ :: HNil) | (_ :: _ :: _ :: _ :: None :: _ :: HNil) =>
-        Attempt.failure(Err("expected two long values but found one"))
-      case _ :: _ :: _ :: Some(a) :: Some(b) :: _ :: HNil =>
-        Attempt.successful(a :: b :: HNil)
-    }
-  )
-
-  /**
-    * `Codec` for reading a `String`, a `Long`, and two `Int`s from remaining stream data.
-    * All `String`s processed by this reading are wide character and are padded by six.
-    * @return a filled-out `allPattern` if successful
-    */
-  def complexCodec : Codec[allPattern] = (
-    PacketHelpers.encodedWideStringAligned(6) ::
-      ulongL(46) ::
-      uint16L ::
-      uintL(3)
-    ).exmap[allPattern] (
-    {
-      case a :: b :: c :: d :: HNil =>
-        Attempt.successful(Some(a) :: Some(c) :: Some(d) :: Some(b) :: None :: None :: HNil)
-    },
-    {
-      case None:: _ :: _ :: _ :: _ :: _ :: HNil =>
-        Attempt.failure(Err("expected a string value but found nothing"))
-      case _ :: _ :: _ :: None :: _ :: _ :: HNil =>
-        Attempt.failure(Err("expected a long value but found nothing"))
-      case (_ :: None :: _ :: _ :: _ :: _ :: HNil) | (_ :: _ :: None :: _ :: _ :: _ :: HNil) =>
-        Attempt.failure(Err("expected two integer values but found one"))
-      case Some(a) :: Some(c) :: Some(d) :: Some(b) :: _ :: _ :: HNil =>
-        Attempt.successful(a :: b :: c :: d :: HNil)
-    }
-  )
-
-  import scala.annotation.switch
-
-  /**
-    * Select the `Codec` to translate bit data in this packet with an `allPattern` format.
-    * @param action the purpose of this packet;
-    *               also decides the content of the parameter fields
-    * @return an `allPattern` `Codec` that parses the appropriate data
-    */
-  def selectCodec(action : Int) : Codec[allPattern] = (action : @switch) match {
-    case 3 | 8 | 26 | 35 | 41 => //TODO double check these
-      noneCodec
-
-    case 28 | 29 | 30 | 31  =>
-      boolCodec
-
-    case 33 =>
-      intCodec(uintL(3))
-    case 10 | 11 | 21 | 22 | 40 =>
-      intCodec(uint4L)
-    case 20 =>
-      intCodec(uint16L)
-
-    case 13 | 14 | 15 | 37 =>
-      longCodec
-
-    case 7 | 19 =>
-      stringCodec
-
-    case 12 | 38 =>
-      intLongCodec(uint32L)
-    case 25 =>
-      intLongCodec(ulongL(46))
-
-    case 23 | 24 =>
-      intStringCodec
-
-    case 36 =>
-      longLongCodec
-
-    case 34 =>
-      complexCodec
-
-    case _ =>
-      //TODO for debugging purposes only; normal failure condition below
-      bits.hlist.exmap[allPattern] (
-        {
-          case x :: HNil =>
-            org.log4s.getLogger.warn(s"can not match a codec pattern for decoding $action")
-            Attempt.successful(Some(x.toString) :: None :: None :: None :: None :: None :: HNil)
-        },
-        {
-          case Some(x) :: None :: None :: None :: None :: None :: HNil =>
-            org.log4s.getLogger.warn(s"can not match a codec pattern for encoding $action")
-            Attempt.successful(scodec.bits.BitVector.fromValidBin(x) :: HNil)
-        }
-      )
-//      ignore(0).exmap[allPattern] (
-//        {
-//          case () =>
-//            Attempt.failure(Err(s"can not match a codec pattern for decoding $action"))
-//        },
-//        {
-//          case _ :: _ :: _ :: _ :: _ :: _ :: HNil =>
-//            Attempt.failure(Err(s"can not match a codec pattern for encoding $action"))
-//        }
-//      )
+  def selectFromActionCode(code : Int) : Codec[SquadAction] = {
+    import SquadAction.Codecs._
+    import scala.annotation.switch
+    ((code : @switch) match {
+      case 3 => saveSquadDefinitionCodec
+      case 8 => listSquadCodec
+      case 10 => selectRoleForYourselfCodec
+      case 19 => changeSquadPurposeCodec
+      case 20 => changeSquadZoneCodec
+      case 21 => closeSquadMemberPositionCodec
+      case 22 => addSquadMemberPositionCodec
+      case 23 => changeSquadMemberRequirementsRoleCodec
+      case 24 => changeSquadMemberRequirementsDetailedOrdersCodec
+      case 25 => changeSquadMemberRequirementsWeaponsCodec
+      case 26 => resetAllCodec
+      case 28 => autoApproveInvitationRequestsCodec
+      case 31 => locationFollowsSquadLeadCodec
+      case 34 => searchForSquadsWithParticularRoleCodec
+      case 35 => cancelSquadSearchCodec
+      case 40 => findLfsSoldiersForRoleCodec
+      case 41 => cancelFindCodec
+      case 0 | 1 | 2 | 4 | 6 | 7 | 9 |
+           11 | 12 | 13 | 14 | 15 | 16 |
+           17 | 18 | 29 | 30 | 33 | 36 |
+           37 | 38 | 42 | 43 => unknownCodec(code)
+      case _ => failureCodec(code)
+    }).asInstanceOf[Codec[SquadAction]]
   }
 
   implicit val codec : Codec[SquadDefinitionActionMessage] = (
-    ("action" | uintL(6)) >>:~ { action =>
+    uintL(6) >>:~ { code =>
       ("unk1" | uint16L) ::
         ("unk2" | uint4L) ::
-        selectCodec(action)
+        ("action" | selectFromActionCode(code))
     }
-    ).as[SquadDefinitionActionMessage]
+    ).xmap[SquadDefinitionActionMessage] (
+    {
+      case _ :: u1 :: u2 :: action :: HNil =>
+        SquadDefinitionActionMessage(u1, u2, action)
+    },
+    {
+      case SquadDefinitionActionMessage(u1, u2, action) =>
+        action.code :: u1 :: u2 :: action :: HNil
+    }
+  )
 }
 
 /*
diff --git a/common/src/test/scala/game/SquadDefinitionActionMessageTest.scala b/common/src/test/scala/game/SquadDefinitionActionMessageTest.scala
index a9657dec..934422a7 100644
--- a/common/src/test/scala/game/SquadDefinitionActionMessageTest.scala
+++ b/common/src/test/scala/game/SquadDefinitionActionMessageTest.scala
@@ -3,6 +3,7 @@ package game
 
 import org.specs2.mutable._
 import net.psforever.packet._
+import net.psforever.packet.game.SquadAction._
 import net.psforever.packet.game._
 import scodec.bits._
 
@@ -30,18 +31,15 @@ class SquadDefinitionActionMessageTest extends Specification {
   val string_40 = hex"E7 a0 000004" //index: 1
   val string_41 = hex"E7 a4 000000"
 
+  val string_43 = hex"e7 ac 000000"
+  val string_failure = hex"E7 ff"
+
   "decode (03)" in {
     PacketCoding.DecodePacket(string_03).require match {
-      case SquadDefinitionActionMessage(action, unk1, unk2, str, int1, int2, long1, long2, bool) =>
-        action mustEqual 3
+      case SquadDefinitionActionMessage(unk1, unk2, action) =>
         unk1 mustEqual 0
         unk2 mustEqual 3
-        str.isDefined mustEqual false
-        int1.isDefined mustEqual false
-        int2.isDefined mustEqual false
-        long1.isDefined mustEqual false
-        long2.isDefined mustEqual false
-        bool.isDefined mustEqual false
+        action mustEqual SaveSquadDefinition()
       case _ =>
         ko
     }
@@ -49,16 +47,10 @@ class SquadDefinitionActionMessageTest extends Specification {
 
   "decode (08)" in {
     PacketCoding.DecodePacket(string_08).require match {
-      case SquadDefinitionActionMessage(action, unk1, unk2, str, int1, int2, long1, long2, bool) =>
-        action mustEqual 8
+      case SquadDefinitionActionMessage(unk1, unk2, action) =>
         unk1 mustEqual 0
         unk2 mustEqual 0
-        str.isDefined mustEqual false
-        int1.isDefined mustEqual false
-        int2.isDefined mustEqual false
-        long1.isDefined mustEqual false
-        long2.isDefined mustEqual false
-        bool.isDefined mustEqual false
+        action mustEqual ListSquad()
       case _ =>
         ko
     }
@@ -66,17 +58,10 @@ class SquadDefinitionActionMessageTest extends Specification {
 
   "decode (10)" in {
     PacketCoding.DecodePacket(string_10).require match {
-      case SquadDefinitionActionMessage(action, unk1, unk2, str, int1, int2, long1, long2, bool) =>
-        action mustEqual 10
+      case SquadDefinitionActionMessage(unk1, unk2, action) =>
         unk1 mustEqual 0
         unk2 mustEqual 0
-        str.isDefined mustEqual false
-        int1.isDefined mustEqual true
-        int1.get mustEqual 1
-        int2.isDefined mustEqual false
-        long1.isDefined mustEqual false
-        long2.isDefined mustEqual false
-        bool.isDefined mustEqual false
+        action mustEqual SelectRoleForYourself(1)
       case _ =>
         ko
     }
@@ -84,17 +69,10 @@ class SquadDefinitionActionMessageTest extends Specification {
 
   "decode (19)" in {
     PacketCoding.DecodePacket(string_19).require match {
-      case SquadDefinitionActionMessage(action, unk1, unk2, str, int1, int2, long1, long2, bool) =>
-        action mustEqual 19
+      case SquadDefinitionActionMessage(unk1, unk2, action) =>
         unk1 mustEqual 0
         unk2 mustEqual 0
-        str.isDefined mustEqual true
-        str.get mustEqual "A-Team"
-        int1.isDefined mustEqual false
-        int2.isDefined mustEqual false
-        long1.isDefined mustEqual false
-        long2.isDefined mustEqual false
-        bool.isDefined mustEqual false
+        action mustEqual ChangeSquadPurpose("A-Team")
       case _ =>
         ko
     }
@@ -102,17 +80,10 @@ class SquadDefinitionActionMessageTest extends Specification {
 
   "decode (20)" in {
     PacketCoding.DecodePacket(string_20).require match {
-      case SquadDefinitionActionMessage(action, unk1, unk2, str, int1, int2, long1, long2, bool) =>
-        action mustEqual 20
+      case SquadDefinitionActionMessage(unk1, unk2, action) =>
         unk1 mustEqual 0
         unk2 mustEqual 0
-        str.isDefined mustEqual false
-        int1.isDefined mustEqual true
-        int1.get mustEqual 1
-        int2.isDefined mustEqual false
-        long1.isDefined mustEqual false
-        long2.isDefined mustEqual false
-        bool.isDefined mustEqual false
+        action mustEqual ChangeSquadZone(PlanetSideZoneID(1))
       case _ =>
         ko
     }
@@ -120,17 +91,10 @@ class SquadDefinitionActionMessageTest extends Specification {
 
   "decode (21)" in {
     PacketCoding.DecodePacket(string_21).require match {
-      case SquadDefinitionActionMessage(action, unk1, unk2, str, int1, int2, long1, long2, bool) =>
-        action mustEqual 21
+      case SquadDefinitionActionMessage(unk1, unk2, action) =>
         unk1 mustEqual 0
         unk2 mustEqual 0
-        str.isDefined mustEqual false
-        int1.isDefined mustEqual true
-        int1.get mustEqual 2
-        int2.isDefined mustEqual false
-        long1.isDefined mustEqual false
-        long2.isDefined mustEqual false
-        bool.isDefined mustEqual false
+        action mustEqual CloseSquadMemberPosition(2)
       case _ =>
         ko
     }
@@ -138,17 +102,10 @@ class SquadDefinitionActionMessageTest extends Specification {
 
   "decode (22)" in {
     PacketCoding.DecodePacket(string_22).require match {
-      case SquadDefinitionActionMessage(action, unk1, unk2, str, int1, int2, long1, long2, bool) =>
-        action mustEqual 22
+      case SquadDefinitionActionMessage(unk1, unk2, action) =>
         unk1 mustEqual 0
         unk2 mustEqual 0
-        str.isDefined mustEqual false
-        int1.isDefined mustEqual true
-        int1.get mustEqual 2
-        int2.isDefined mustEqual false
-        long1.isDefined mustEqual false
-        long2.isDefined mustEqual false
-        bool.isDefined mustEqual false
+        action mustEqual AddSquadMemberPosition(2)
       case _ =>
         ko
     }
@@ -156,18 +113,10 @@ class SquadDefinitionActionMessageTest extends Specification {
 
   "decode (23)" in {
     PacketCoding.DecodePacket(string_23).require match {
-      case SquadDefinitionActionMessage(action, unk1, unk2, str, int1, int2, long1, long2, bool) =>
-        action mustEqual 23
+      case SquadDefinitionActionMessage(unk1, unk2, action) =>
         unk1 mustEqual 0
         unk2 mustEqual 0
-        str.isDefined mustEqual true
-        str.get mustEqual "BLUFOR"
-        int1.isDefined mustEqual true
-        int1.get mustEqual 1
-        int2.isDefined mustEqual false
-        long1.isDefined mustEqual false
-        long2.isDefined mustEqual false
-        bool.isDefined mustEqual false
+        action mustEqual ChangeSquadMemberRequirementsRole(1, "BLUFOR")
       case _ =>
         ko
     }
@@ -175,18 +124,10 @@ class SquadDefinitionActionMessageTest extends Specification {
 
   "decode (24)" in {
     PacketCoding.DecodePacket(string_24).require match {
-      case SquadDefinitionActionMessage(action, unk1, unk2, str, int1, int2, long1, long2, bool) =>
-        action mustEqual 24
+      case SquadDefinitionActionMessage(unk1, unk2, action) =>
         unk1 mustEqual 0
         unk2 mustEqual 0
-        str.isDefined mustEqual true
-        str.get mustEqual "kill bad dudes"
-        int1.isDefined mustEqual true
-        int1.get mustEqual 1
-        int2.isDefined mustEqual false
-        long1.isDefined mustEqual false
-        long2.isDefined mustEqual false
-        bool.isDefined mustEqual false
+        action mustEqual ChangeSquadMemberRequirementsDetailedOrders(1, "kill bad dudes")
       case _ =>
         ko
     }
@@ -194,18 +135,10 @@ class SquadDefinitionActionMessageTest extends Specification {
 
   "decode (25)" in {
     PacketCoding.DecodePacket(string_25).require match {
-      case SquadDefinitionActionMessage(action, unk1, unk2, str, int1, int2, long1, long2, bool) =>
-        action mustEqual 25
+      case SquadDefinitionActionMessage(unk1, unk2, action) =>
         unk1 mustEqual 0
         unk2 mustEqual 0
-        str.isDefined mustEqual false
-        int1.isDefined mustEqual true
-        int1.get mustEqual 1
-        int2.isDefined mustEqual false
-        long1.isDefined mustEqual true
-        long1.get mustEqual 536870928L
-        long2.isDefined mustEqual false
-        bool.isDefined mustEqual false
+        action mustEqual ChangeSquadMemberRequirementsWeapons(1, 536870928L)
       case _ =>
         ko
     }
@@ -213,16 +146,10 @@ class SquadDefinitionActionMessageTest extends Specification {
 
   "decode (26)" in {
     PacketCoding.DecodePacket(string_26).require match {
-      case SquadDefinitionActionMessage(action, unk1, unk2, str, int1, int2, long1, long2, bool) =>
-        action mustEqual 26
+      case SquadDefinitionActionMessage(unk1, unk2, action) =>
         unk1 mustEqual 0
         unk2 mustEqual 0
-        str.isDefined mustEqual false
-        int1.isDefined mustEqual false
-        int2.isDefined mustEqual false
-        long1.isDefined mustEqual false
-        long2.isDefined mustEqual false
-        bool.isDefined mustEqual false
+        action mustEqual ResetAll()
       case _ =>
         ko
     }
@@ -230,17 +157,10 @@ class SquadDefinitionActionMessageTest extends Specification {
 
   "decode (28)" in {
     PacketCoding.DecodePacket(string_28).require match {
-      case SquadDefinitionActionMessage(action, unk1, unk2, str, int1, int2, long1, long2, bool) =>
-        action mustEqual 28
+      case SquadDefinitionActionMessage(unk1, unk2, action) =>
         unk1 mustEqual 0
         unk2 mustEqual 0
-        str.isDefined mustEqual false
-        int1.isDefined mustEqual false
-        int2.isDefined mustEqual false
-        long1.isDefined mustEqual false
-        long2.isDefined mustEqual false
-        bool.isDefined mustEqual true
-        bool.get mustEqual true
+        action mustEqual AutoApproveInvitationRequests(true)
       case _ =>
         ko
     }
@@ -248,17 +168,10 @@ class SquadDefinitionActionMessageTest extends Specification {
 
   "decode (31)" in {
     PacketCoding.DecodePacket(string_31).require match {
-      case SquadDefinitionActionMessage(action, unk1, unk2, str, int1, int2, long1, long2, bool) =>
-        action mustEqual 31
+      case SquadDefinitionActionMessage(unk1, unk2, action) =>
         unk1 mustEqual 0
         unk2 mustEqual 0
-        str.isDefined mustEqual false
-        int1.isDefined mustEqual false
-        int2.isDefined mustEqual false
-        long1.isDefined mustEqual false
-        long2.isDefined mustEqual false
-        bool.isDefined mustEqual true
-        bool.get mustEqual true
+        action mustEqual LocationFollowsSquadLead(true)
       case _ =>
         ko
     }
@@ -266,20 +179,10 @@ class SquadDefinitionActionMessageTest extends Specification {
 
   "decode (34a)" in {
     PacketCoding.DecodePacket(string_34a).require match {
-      case SquadDefinitionActionMessage(action, unk1, unk2, str, int1, int2, long1, long2, bool) =>
-        action mustEqual 34
+      case SquadDefinitionActionMessage(unk1, unk2, action) =>
         unk1 mustEqual 0
         unk2 mustEqual 0
-        str.isDefined mustEqual true
-        str.get mustEqual "Badass"
-        int1.isDefined mustEqual true
-        int1.get mustEqual 1
-        int2.isDefined mustEqual true
-        int2.get mustEqual 0
-        long1.isDefined mustEqual true
-        long1.get mustEqual 0
-        long2.isDefined mustEqual false
-        bool.isDefined mustEqual false
+        action mustEqual SearchForSquadsWithParticularRole("Badass", 0L, 1, 0)
       case _ =>
         ko
     }
@@ -287,20 +190,10 @@ class SquadDefinitionActionMessageTest extends Specification {
 
   "decode (34b)" in {
     PacketCoding.DecodePacket(string_34b).require match {
-      case SquadDefinitionActionMessage(action, unk1, unk2, str, int1, int2, long1, long2, bool) =>
-        action mustEqual 34
+      case SquadDefinitionActionMessage(unk1, unk2, action) =>
         unk1 mustEqual 0
         unk2 mustEqual 0
-        str.isDefined mustEqual true
-        str.get mustEqual "Badass"
-        int1.isDefined mustEqual true
-        int1.get mustEqual 2
-        int2.isDefined mustEqual true
-        int2.get mustEqual 0
-        long1.isDefined mustEqual true
-        long1.get mustEqual 0
-        long2.isDefined mustEqual false
-        bool.isDefined mustEqual false
+        action mustEqual SearchForSquadsWithParticularRole("Badass", 0L, 2, 0)
       case _ =>
         ko
     }
@@ -308,20 +201,10 @@ class SquadDefinitionActionMessageTest extends Specification {
 
   "decode (34c)" in {
     PacketCoding.DecodePacket(string_34c).require match {
-      case SquadDefinitionActionMessage(action, unk1, unk2, str, int1, int2, long1, long2, bool) =>
-        action mustEqual 34
+      case SquadDefinitionActionMessage(unk1, unk2, action) =>
         unk1 mustEqual 0
         unk2 mustEqual 0
-        str.isDefined mustEqual true
-        str.get mustEqual "Badass"
-        int1.isDefined mustEqual true
-        int1.get mustEqual 2
-        int2.isDefined mustEqual true
-        int2.get mustEqual 1
-        long1.isDefined mustEqual true
-        long1.get mustEqual 0
-        long2.isDefined mustEqual false
-        bool.isDefined mustEqual false
+        action mustEqual SearchForSquadsWithParticularRole("Badass", 0L, 2, 1)
       case _ =>
         ko
     }
@@ -329,20 +212,10 @@ class SquadDefinitionActionMessageTest extends Specification {
 
   "decode (34d)" in {
     PacketCoding.DecodePacket(string_34d).require match {
-      case SquadDefinitionActionMessage(action, unk1, unk2, str, int1, int2, long1, long2, bool) =>
-        action mustEqual 34
+      case SquadDefinitionActionMessage(unk1, unk2, action) =>
         unk1 mustEqual 0
         unk2 mustEqual 0
-        str.isDefined mustEqual true
-        str.get mustEqual "Badass"
-        int1.isDefined mustEqual true
-        int1.get mustEqual 2
-        int2.isDefined mustEqual true
-        int2.get mustEqual 2
-        long1.isDefined mustEqual true
-        long1.get mustEqual 536870928L
-        long2.isDefined mustEqual false
-        bool.isDefined mustEqual false
+        action mustEqual SearchForSquadsWithParticularRole("Badass", 536870928L, 2, 2)
       case _ =>
         ko
     }
@@ -350,20 +223,10 @@ class SquadDefinitionActionMessageTest extends Specification {
 
   "decode (34e)" in {
     PacketCoding.DecodePacket(string_34e).require match {
-      case SquadDefinitionActionMessage(action, unk1, unk2, str, int1, int2, long1, long2, bool) =>
-        action mustEqual 34
+      case SquadDefinitionActionMessage(unk1, unk2, action) =>
         unk1 mustEqual 0
         unk2 mustEqual 0
-        str.isDefined mustEqual true
-        str.get mustEqual "Badass"
-        int1.isDefined mustEqual true
-        int1.get mustEqual 2
-        int2.isDefined mustEqual true
-        int2.get mustEqual 3
-        long1.isDefined mustEqual true
-        long1.get mustEqual 536870928L
-        long2.isDefined mustEqual false
-        bool.isDefined mustEqual false
+        action mustEqual SearchForSquadsWithParticularRole("Badass", 536870928L, 2, 3)
       case _ =>
         ko
     }
@@ -371,16 +234,10 @@ class SquadDefinitionActionMessageTest extends Specification {
 
   "decode (35)" in {
     PacketCoding.DecodePacket(string_35).require match {
-      case SquadDefinitionActionMessage(action, unk1, unk2, str, int1, int2, long1, long2, bool) =>
-        action mustEqual 35
+      case SquadDefinitionActionMessage(unk1, unk2, action) =>
         unk1 mustEqual 0
         unk2 mustEqual 0
-        str.isDefined mustEqual false
-        int1.isDefined mustEqual false
-        int2.isDefined mustEqual false
-        long1.isDefined mustEqual false
-        long2.isDefined mustEqual false
-        bool.isDefined mustEqual false
+        action mustEqual CancelSquadSearch()
       case _ =>
         ko
     }
@@ -388,17 +245,10 @@ class SquadDefinitionActionMessageTest extends Specification {
 
   "decode (40)" in {
     PacketCoding.DecodePacket(string_40).require match {
-      case SquadDefinitionActionMessage(action, unk1, unk2, str, int1, int2, long1, long2, bool) =>
-        action mustEqual 40
+      case SquadDefinitionActionMessage(unk1, unk2, action) =>
         unk1 mustEqual 0
         unk2 mustEqual 0
-        str.isDefined mustEqual false
-        int1.isDefined mustEqual true
-        int1.get mustEqual 1
-        int2.isDefined mustEqual false
-        long1.isDefined mustEqual false
-        long2.isDefined mustEqual false
-        bool.isDefined mustEqual false
+        action mustEqual FindLfsSoldiersForRole(1)
       case _ =>
         ko
     }
@@ -406,165 +256,181 @@ class SquadDefinitionActionMessageTest extends Specification {
 
   "decode (41)" in {
     PacketCoding.DecodePacket(string_41).require match {
-      case SquadDefinitionActionMessage(action, unk1, unk2, str, int1, int2, long1, long2, bool) =>
-        action mustEqual 41
+      case SquadDefinitionActionMessage(unk1, unk2, action) =>
         unk1 mustEqual 0
         unk2 mustEqual 0
-        str.isDefined mustEqual false
-        int1.isDefined mustEqual false
-        int2.isDefined mustEqual false
-        long1.isDefined mustEqual false
-        long2.isDefined mustEqual false
-        bool.isDefined mustEqual false
+        action mustEqual CancelFind()
       case _ =>
         ko
     }
   }
 
+  "decode (43, unknown)" in {
+    PacketCoding.DecodePacket(string_43).require match {
+      case SquadDefinitionActionMessage(unk1, unk2, action) =>
+        unk1 mustEqual 0
+        unk2 mustEqual 0
+        action mustEqual Unknown(43, hex"00".toBitVector.take(6))
+      case _ =>
+        ko
+    }
+  }
+
+  "decode (failure)" in {
+    PacketCoding.DecodePacket(string_failure).isFailure mustEqual true
+  }
+
   "encode (03)" in {
-    val msg = SquadDefinitionActionMessage(3, 0, 3, None, None, None, None, None, None)
+    val msg = SquadDefinitionActionMessage(0, 3, SaveSquadDefinition())
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_03
   }
 
   "encode (08)" in {
-    val msg = SquadDefinitionActionMessage(8, 0, 0, None, None, None, None, None, None)
+    val msg = SquadDefinitionActionMessage(0, 0, ListSquad())
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_08
   }
 
   "encode (10)" in {
-    val msg = SquadDefinitionActionMessage(10, 0, 0, None, Some(1), None, None, None, None)
+    val msg = SquadDefinitionActionMessage(0, 0, SelectRoleForYourself(1))
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_10
   }
 
   "encode (19)" in {
-    val msg = SquadDefinitionActionMessage(19, 0, 0, Some("A-Team"), None, None, None, None, None)
+    val msg = SquadDefinitionActionMessage(0, 0, ChangeSquadPurpose("A-Team"))
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_19
   }
 
   "encode (20)" in {
-    val msg = SquadDefinitionActionMessage(20, 0, 0, None, Some(1), None, None, None, None)
+    val msg = SquadDefinitionActionMessage(0, 0, ChangeSquadZone(PlanetSideZoneID(1)))
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_20
   }
 
   "encode (21)" in {
-    val msg = SquadDefinitionActionMessage(21, 0, 0, None, Some(2), None, None, None, None)
+    val msg = SquadDefinitionActionMessage(0, 0, CloseSquadMemberPosition(2))
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_21
   }
 
   "encode (22)" in {
-    val msg = SquadDefinitionActionMessage(22, 0, 0, None, Some(2), None, None, None, None)
+    val msg = SquadDefinitionActionMessage(0, 0, AddSquadMemberPosition(2))
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_22
   }
 
   "encode (23)" in {
-    val msg = SquadDefinitionActionMessage(23, 0, 0, Some("BLUFOR"), Some(1), None, None, None, None)
+    val msg = SquadDefinitionActionMessage(0, 0, ChangeSquadMemberRequirementsRole(1, "BLUFOR"))
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_23
   }
 
   "encode (24)" in {
-    val msg = SquadDefinitionActionMessage(24, 0, 0, Some("kill bad dudes"), Some(1), None, None, None, None)
+    val msg = SquadDefinitionActionMessage(0, 0, ChangeSquadMemberRequirementsDetailedOrders(1, "kill bad dudes"))
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_24
   }
 
   "encode (25)" in {
-    val msg = SquadDefinitionActionMessage(25, 0, 0, None, Some(1), None, Some(536870928L), None, None)
+    val msg = SquadDefinitionActionMessage(0, 0, ChangeSquadMemberRequirementsWeapons(1, 536870928L))
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_25
   }
 
   "encode (26)" in {
-    val msg = SquadDefinitionActionMessage(26, 0, 0, None, None, None, None, None, None)
+    val msg = SquadDefinitionActionMessage(0, 0, ResetAll())
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_26
   }
 
   "encode (28)" in {
-    val msg = SquadDefinitionActionMessage(28, 0, 0, None, None, None, None, None, Some(true))
+    val msg = SquadDefinitionActionMessage(0, 0, AutoApproveInvitationRequests(true))
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_28
   }
 
   "encode (31)" in {
-    val msg = SquadDefinitionActionMessage(31, 0, 0, None, None, None, None, None, Some(true))
+    val msg = SquadDefinitionActionMessage(0, 0, LocationFollowsSquadLead(true))
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_31
   }
 
   "encode (34a)" in {
-    val msg = SquadDefinitionActionMessage(34, 0, 0, Some("Badass"), Some(1), Some(0), Some(0L), None, None)
+    val msg = SquadDefinitionActionMessage(0, 0, SearchForSquadsWithParticularRole("Badass", 0L, 1, 0))
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_34a
   }
 
   "encode (34b)" in {
-    val msg = SquadDefinitionActionMessage(34, 0, 0, Some("Badass"), Some(2), Some(0), Some(0L), None, None)
+    val msg = SquadDefinitionActionMessage(0, 0, SearchForSquadsWithParticularRole("Badass", 0L, 2, 0))
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_34b
   }
 
   "encode (34c)" in {
-    val msg = SquadDefinitionActionMessage(34, 0, 0, Some("Badass"), Some(2), Some(1), Some(0L), None, None)
+    val msg = SquadDefinitionActionMessage(0, 0, SearchForSquadsWithParticularRole("Badass", 0L, 2, 1))
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_34c
   }
 
   "encode (34d)" in {
-    val msg = SquadDefinitionActionMessage(34, 0, 0, Some("Badass"), Some(2), Some(2), Some(536870928L), None, None)
+    val msg = SquadDefinitionActionMessage(0, 0, SearchForSquadsWithParticularRole("Badass", 536870928L, 2, 2))
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_34d
   }
 
   "encode (34e)" in {
-    val msg = SquadDefinitionActionMessage(34, 0, 0, Some("Badass"), Some(2), Some(3), Some(536870928L), None, None)
+    val msg = SquadDefinitionActionMessage(0, 0, SearchForSquadsWithParticularRole("Badass", 536870928L, 2, 3))
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_34e
   }
 
   "encode (35)" in {
-    val msg = SquadDefinitionActionMessage(35, 0, 0, None, None, None, None, None, None)
+    val msg = SquadDefinitionActionMessage(0, 0, CancelSquadSearch())
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_35
   }
 
   "encode (40)" in {
-    val msg = SquadDefinitionActionMessage(40, 0, 0, None, Some(1), None, None, None, None)
+    val msg = SquadDefinitionActionMessage(0, 0, FindLfsSoldiersForRole(1))
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_40
   }
 
   "encode (41)" in {
-    val msg = SquadDefinitionActionMessage(41, 0, 0, None, None, None, None, None, None)
+    val msg = SquadDefinitionActionMessage(0, 0, CancelFind())
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_41
   }
+
+  "encode (43, unknown)" in {
+    val msg = SquadDefinitionActionMessage(0, 0, Unknown(43, BitVector.empty))
+    val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+    pkt mustEqual string_43
+  }
 }

From ceb145d94ffacfbc2ed67c81681e6706f6d1fb37 Mon Sep 17 00:00:00 2001
From: FateJH <Jason_DiDonato@yahoo.com>
Date: Tue, 28 May 2019 15:15:24 -0400
Subject: [PATCH 05/31] introduced service-level squad management operations,
 specifically to read SquadDefinitionAction packets from WorldSessionActor and
 send ReplicationStreamMessage packets back to relevant WSA when necessary

---
 .../psforever/objects/teamwork/Member.scala   |  25 +-
 .../psforever/objects/teamwork/Squad.scala    |  32 +-
 .../game/SquadDefinitionActionMessage.scala   |   2 +-
 .../psforever/types/PlanetSideEmpire.scala    |   9 +
 .../services/teamwork/SquadResponse.scala     |  12 +
 .../services/teamwork/SquadService.scala      | 276 ++++++++++++++++++
 .../teamwork/SquadServiceMessage.scala        |  13 +
 .../teamwork/SquadServiceResponse.scala       |   7 +
 pslogin/src/main/scala/PsLogin.scala          |   2 +
 .../src/main/scala/WorldSessionActor.scala    |  44 ++-
 10 files changed, 413 insertions(+), 9 deletions(-)
 create mode 100644 common/src/main/scala/services/teamwork/SquadResponse.scala
 create mode 100644 common/src/main/scala/services/teamwork/SquadService.scala
 create mode 100644 common/src/main/scala/services/teamwork/SquadServiceMessage.scala
 create mode 100644 common/src/main/scala/services/teamwork/SquadServiceResponse.scala

diff --git a/common/src/main/scala/net/psforever/objects/teamwork/Member.scala b/common/src/main/scala/net/psforever/objects/teamwork/Member.scala
index fc1988e3..46d9e15f 100644
--- a/common/src/main/scala/net/psforever/objects/teamwork/Member.scala
+++ b/common/src/main/scala/net/psforever/objects/teamwork/Member.scala
@@ -6,12 +6,13 @@ import net.psforever.types.{CertificationType, Vector3}
 class Member {
   //about the position to be filled
   private var role : String = ""
+  private var orders : String = ""
   private var restrictions : Set[CertificationType.Value] = Set()
   //about the individual filling the position
   private var name : String = ""
   private var health : Int = 0
   private var armor : Int = 0
-  private var zoneId : String = "Nowhere"
+  private var zoneId : Int = 0
   private var position : Vector3 = Vector3.Zero
 
   def Role : String = role
@@ -21,6 +22,13 @@ class Member {
     Role
   }
 
+  def Orders : String = orders
+
+  def Orders_=(text : String) : String = {
+    orders = text
+    Orders
+  }
+
   def Restrictions : Set[CertificationType.Value] = restrictions
 
   def Restrictions_=(requirements : Set[CertificationType.Value]) = {
@@ -49,9 +57,9 @@ class Member {
     Armor
   }
 
-  def ZoneId : String = zoneId
+  def ZoneId : Int = zoneId
 
-  def ZoneId_=(id : String) : String = {
+  def ZoneId_=(id : Int) : Int = {
     zoneId = id
     ZoneId
   }
@@ -62,4 +70,15 @@ class Member {
     position = pos
     Position
   }
+
+  def Close() : Unit = {
+    role = ""
+    restrictions = Set()
+    //about the individual filling the position
+    name = ""
+    health = 0
+    armor = 0
+    zoneId = 0
+    position = Vector3.Zero
+  }
 }
diff --git a/common/src/main/scala/net/psforever/objects/teamwork/Squad.scala b/common/src/main/scala/net/psforever/objects/teamwork/Squad.scala
index 23a96a7a..071d8a8e 100644
--- a/common/src/main/scala/net/psforever/objects/teamwork/Squad.scala
+++ b/common/src/main/scala/net/psforever/objects/teamwork/Squad.scala
@@ -8,25 +8,35 @@ import net.psforever.types.PlanetSideEmpire
 class Squad(squadId : PlanetSideGUID, alignment : PlanetSideEmpire.Value) extends IdentifiableEntity {
   super.GUID_=(squadId)
   private val faction : PlanetSideEmpire.Value = alignment //does not change
-  private val zoneId : Option[String] = None
+  private var zoneId : Option[Int] = None
   private var task : String = ""
   private var description : String = ""
   private val membership : Array[Member] = Array.fill[Member](10)(new Member)
   private val availability : Array[Boolean] = Array.fill[Boolean](10)(true)
+  private var listed : Boolean = false
 
   override def GUID_=(d : PlanetSideGUID) : PlanetSideGUID = GUID
 
   def Faction : PlanetSideEmpire.Value = faction
 
-  def ZoneId : String = zoneId.getOrElse({
+  def ZoneId : Int = zoneId.getOrElse({
     membership.headOption match {
       case Some(leader) =>
         leader.ZoneId
       case _ =>
-        "Nowhere"
+        0
     }
   })
 
+  def ZoneId_=(id : Int) : Int = {
+    ZoneId_=(Some(id))
+  }
+
+  def ZoneId_=(id : Option[Int]) : Int = {
+    zoneId = id
+    ZoneId
+  }
+
   def Task : String = task
 
   def Task_=(assignment : String) : String = {
@@ -41,10 +51,26 @@ class Squad(squadId : PlanetSideGUID, alignment : PlanetSideEmpire.Value) extend
     Description
   }
 
+  def Listed : Boolean = listed
+
+  def Listed_=(announce : Boolean) : Boolean = {
+    listed = announce
+    Listed
+  }
+
   def Membership : Array[Member] = membership
 
   def Availability : Array[Boolean] = availability
 
+  def Leader : String = {
+    membership.headOption match {
+      case Some(member) =>
+        member.Name
+      case None =>
+        ""
+    }
+  }
+
   def Size : Int = membership.count(member => !member.Name.equals(""))
 
   def Capacity : Int = availability.count(open => open)
diff --git a/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala b/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala
index 0baf8518..5b714fc8 100644
--- a/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala
+++ b/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala
@@ -12,7 +12,7 @@ import shapeless.{::, HNil}
   * All behaviors have a "code" that indicates how the rest of the data is parsed.
   * @param code the action behavior code
   */
-protected abstract class SquadAction(val code : Int)
+abstract class SquadAction(val code : Int)
 
 object SquadAction{
   final case class SaveSquadDefinition() extends SquadAction(3)
diff --git a/common/src/main/scala/net/psforever/types/PlanetSideEmpire.scala b/common/src/main/scala/net/psforever/types/PlanetSideEmpire.scala
index 522cd392..6494d07d 100644
--- a/common/src/main/scala/net/psforever/types/PlanetSideEmpire.scala
+++ b/common/src/main/scala/net/psforever/types/PlanetSideEmpire.scala
@@ -12,4 +12,13 @@ object PlanetSideEmpire extends Enumeration {
   val TR, NC, VS, NEUTRAL = Value
 
   implicit val codec = PacketHelpers.createEnumerationCodec(this, uint2L)
+
+  def apply(id : String) : PlanetSideEmpire.Value = {
+    values.find(_.toString.equals(id)) match {
+      case Some(faction) =>
+        faction
+      case None =>
+        throw new NoSuchElementException(s"can not find an empire associated with $id")
+    }
+  }
 }
diff --git a/common/src/main/scala/services/teamwork/SquadResponse.scala b/common/src/main/scala/services/teamwork/SquadResponse.scala
new file mode 100644
index 00000000..09f05fb3
--- /dev/null
+++ b/common/src/main/scala/services/teamwork/SquadResponse.scala
@@ -0,0 +1,12 @@
+// Copyright (c) 2019 PSForever
+package services.teamwork
+
+import net.psforever.packet.game.SquadInfo
+
+object SquadResponse {
+  trait Response
+
+  final case class Init(info : Vector[SquadInfo]) extends Response
+  final case class Update(infos : Iterable[(Int, SquadInfo)]) extends Response
+  final case class Remove(infos : Iterable[Int]) extends Response
+}
diff --git a/common/src/main/scala/services/teamwork/SquadService.scala b/common/src/main/scala/services/teamwork/SquadService.scala
new file mode 100644
index 00000000..4d58a1af
--- /dev/null
+++ b/common/src/main/scala/services/teamwork/SquadService.scala
@@ -0,0 +1,276 @@
+// Copyright (c) 2019 PSForever
+package services.teamwork
+
+import akka.actor.Actor
+import net.psforever.objects.Player
+import net.psforever.objects.teamwork.Squad
+import net.psforever.packet.game._
+import net.psforever.types.PlanetSideEmpire
+import services.{GenericEventBus, Service}
+
+import scala.collection.concurrent.TrieMap
+import scala.collection.mutable.ListBuffer
+
+//import scala.concurrent.duration._
+
+class SquadService extends Actor {
+  private var memberToSquad : TrieMap[String, Squad] = new TrieMap[String, Squad]()
+  private var idToSquad : TrieMap[PlanetSideGUID, Squad] = new TrieMap[PlanetSideGUID, Squad]()
+  private var i : Int = 1
+  private var publishedLists : TrieMap[PlanetSideEmpire.Value, ListBuffer[SquadInfo]] = TrieMap[PlanetSideEmpire.Value, ListBuffer[SquadInfo]](
+    PlanetSideEmpire.TR -> ListBuffer.empty,
+    PlanetSideEmpire.NC -> ListBuffer.empty,
+    PlanetSideEmpire.VS -> ListBuffer.empty
+  )
+
+  private [this] val log = org.log4s.getLogger
+
+  override def preStart = {
+    log.info("Starting...")
+  }
+
+  def GetNextSquadId() : PlanetSideGUID = {
+    val out = i
+    val j = i + 1
+    if(j == 65536) {
+      i = 1
+    }
+    else {
+      i = j
+    }
+    PlanetSideGUID(out)
+  }
+
+  def GetSquadFromPlayer(player : Player) : Squad = {
+    val name = player.Name
+    val faction = player.Faction
+    memberToSquad.get(name) match {
+      case Some(squad) =>
+        squad
+      case None =>
+        val id = GetNextSquadId()
+        val squad = new Squad(id, faction)
+        val leadPosition = squad.Membership(0)
+        leadPosition.Name = name
+        leadPosition.Health = player.Health
+        leadPosition.Armor = player.Armor
+        leadPosition.Position = player.Position
+        leadPosition.ZoneId = 1 //player.Continent //TODO how to resolve this?
+        log.info(s"$name-$faction has started a new squad")
+        memberToSquad += name -> squad
+        idToSquad += id -> squad
+        squad
+    }
+  }
+
+  val SquadEvents = new GenericEventBus[SquadServiceResponse]
+
+  def receive : Receive = {
+    //subscribe to a faction's channel - necessary to receive updates about listed squads
+    case Service.Join(faction) if "TRNCVS".indexOf(faction) > -1 =>
+      val path = s"$faction/Squad"
+      val who = sender()
+      log.info(s"$who has joined $path")
+      SquadEvents.subscribe(who, path)
+      //send initial squad catalog
+      sender ! SquadServiceResponse(s"$faction/Squad", SquadResponse.Init(publishedLists(PlanetSideEmpire(faction)).toVector))
+
+    //subscribe to the player's personal channel - necessary only to inform about any previous squad association
+    case Service.Join(name) =>
+      val path = s"$name/Squad"
+      val who = sender()
+      log.info(s"$who has joined $path")
+      SquadEvents.subscribe(who, path)
+      //check for renewable squad information
+      memberToSquad.get(name) match {
+        case None => ;
+        case Some(squad) =>
+          sender ! SquadServiceMessage.RecoverSquadMembership()
+      }
+
+    case Service.Leave(Some(name)) => ;
+      SquadEvents.unsubscribe(sender())
+      //TODO leave squad, if joined to one, and perform clean-up
+
+    case Service.Leave(None) | Service.LeaveAll() =>
+      SquadEvents.unsubscribe(sender())
+      //TODO might be better to invalidate these
+
+    case SquadServiceMessage.SquadDefinitionAction(tplayer, zone_ordinal_number, _, _, action) =>
+      import net.psforever.packet.game.SquadAction._
+      val squad = GetSquadFromPlayer(tplayer)
+      val member = squad.Membership.find(_.Name == tplayer.Name).get //should never fail
+      member.ZoneId = zone_ordinal_number //TODO improve this requirement
+      var listingChanged : List[Int] = Nil
+      action match {
+        case ChangeSquadPurpose(purpose) =>
+          log.info(s"${tplayer.Name}-${tplayer.Faction} has changed his squad's task to $purpose")
+          squad.Description = purpose
+          listingChanged = List(SquadInfo.Field.Task)
+
+        case ChangeSquadZone(zone) =>
+          log.info(s"${tplayer.Name}-${tplayer.Faction} has changed his squad's ops zone to $zone")
+          squad.ZoneId = zone.zoneId.toInt
+          listingChanged = List(SquadInfo.Field.ZoneId)
+
+        case CloseSquadMemberPosition(position) =>
+          if(position > 0) {
+            squad.Availability.lift(position) match {
+              case Some(true) =>
+                squad.Availability.update(position, false)
+                log.info(s"${tplayer.Name}-${tplayer.Faction} has closed the #$position position in his squad")
+                val memberPosition = squad.Membership(position)
+                listingChanged = if(memberPosition.Name.nonEmpty) {
+                  List(SquadInfo.Field.Size, SquadInfo.Field.Capacity)
+                }
+                else {
+                  List(SquadInfo.Field.Capacity)
+                }
+                memberPosition.Close()
+              case Some(false) => ;
+              case None => ;
+            }
+          }
+          else {
+            log.warn(s"can not close the lead position in squad-${squad.GUID.guid}")
+          }
+
+        case AddSquadMemberPosition(position) =>
+          squad.Availability.lift(position) match {
+            case Some(false) =>
+              log.info(s"${tplayer.Name}-${tplayer.Faction} has opened the #$position position in his squad")
+              squad.Availability.update(position, true)
+              listingChanged = List(SquadInfo.Field.Capacity)
+            case Some(true) => ;
+            case None => ;
+          }
+
+        case ChangeSquadMemberRequirementsRole(position, role) =>
+          squad.Availability.lift(position) match {
+            case Some(true) =>
+              squad.Membership(position).Role = role
+            case Some(false) => ;
+            case None => ;
+          }
+
+        case ChangeSquadMemberRequirementsDetailedOrders(position, orders) =>
+          squad.Availability.lift(position) match {
+            case Some(true) =>
+              squad.Membership(position).Orders = orders
+            case Some(false) => ;
+            case None => ;
+          }
+
+        case ListSquad() =>
+          if(!squad.Listed) {
+            log.info(s"${tplayer.Name}-${tplayer.Faction} has opened recruitment for his squad")
+            squad.Listed = true
+          }
+
+        case ResetAll() =>
+          squad.Description = ""
+          squad.ZoneId = None
+          squad.Availability.indices.foreach { i =>
+            squad.Availability.update(i, true)
+          }
+          //TODO squad members?
+
+        case _ => ;
+      }
+      //queue updates
+      if(squad.Listed) {
+        val entry = SquadService.Publish(squad)
+        val faction = squad.Faction
+        val factionListings = publishedLists(faction)
+        factionListings.find(info => {
+          info.squad_guid match {
+            case Some(guid) => guid == squad.GUID
+            case _ => false
+          }
+        }) match {
+          case Some(listedSquad) =>
+            val index = factionListings.indexOf(listedSquad)
+            if(squad.Listed) {
+              //squad information update
+              log.info(s"Squad will be updated")
+              factionListings(index) = entry
+              val changes = if(listingChanged.nonEmpty) {
+                SquadService.Differences(listingChanged, entry)
+              }
+              else {
+                SquadService.Differences(listedSquad, entry)
+              }
+              if(changes != SquadInfo.Blank) {
+                SquadEvents.publish(
+                  SquadServiceResponse(s"$faction/Squad", SquadResponse.Update(Seq((index, changes))))
+                )
+              }
+            }
+            else {
+              //remove squad from listing
+              log.info(s"Squad will be removed")
+              factionListings.remove(index)
+              SquadEvents.publish(
+                SquadServiceResponse(s"$faction/Squad", SquadResponse.Remove(Seq(index)))
+              )
+            }
+          case None if squad.Listed =>
+            log.info(s"Squad will be introduced")
+            //first time being published?
+            factionListings += SquadService.Publish(squad)
+            SquadEvents.publish(
+              SquadServiceResponse(s"$faction/Squad", SquadResponse.Init(factionListings.toVector))
+            )
+          case _ => ;
+        }
+      }
+
+    case msg =>
+      log.info(s"Unhandled message $msg from $sender")
+  }
+}
+
+object SquadService {
+  def Publish(squad : Squad) : SquadInfo = {
+    SquadInfo(
+      squad.Leader,
+      squad.Description,
+      PlanetSideZoneID(squad.ZoneId),
+      squad.Size,
+      squad.Capacity,
+      squad.GUID
+    )
+  }
+
+  def Differences(updates : List[Int], info : SquadInfo) : SquadInfo = {
+    if(updates.nonEmpty) {
+      var out = SquadInfo.Blank
+      ({
+        val list = Seq(
+          SquadInfo.Blank, //must be index-0
+          SquadInfo(info.leader, None, None, None, None),
+          SquadInfo(None, info.task, None, None, None),
+          SquadInfo(None, None, info.zone_id, None, None),
+          SquadInfo(None, None, None, info.size, None),
+          SquadInfo(None, None, None, None, info.capacity)
+        )
+        updates.map(i => list(i)).filterNot { _ == SquadInfo.Blank }
+      }) //ignore what code inspection tells you - the parenthesis is necessary
+        .foreach(sinfo => out = out And sinfo )
+      out
+    }
+    else {
+      SquadInfo.Blank
+    }
+  }
+
+  def Differences(before : SquadInfo, after : SquadInfo) : SquadInfo = {
+    SquadInfo(
+      if(!before.leader.equals(after.leader)) after.leader else None,
+      if(!before.task.equals(after.task)) after.task else None,
+      if(!before.zone_id.equals(after.zone_id)) after.zone_id else None,
+      if(!before.size.equals(after.size)) after.size else None,
+      if(!before.capacity.equals(after.capacity)) after.capacity else None
+    )
+  }
+}
diff --git a/common/src/main/scala/services/teamwork/SquadServiceMessage.scala b/common/src/main/scala/services/teamwork/SquadServiceMessage.scala
new file mode 100644
index 00000000..5f918787
--- /dev/null
+++ b/common/src/main/scala/services/teamwork/SquadServiceMessage.scala
@@ -0,0 +1,13 @@
+// Copyright (c) 2019 PSForever
+package services.teamwork
+
+import net.psforever.objects.Player
+import net.psforever.packet.game.SquadAction
+
+final case class SquadServiceMessage(forChannel : String, actionMessage : Any)
+
+object SquadServiceMessage {
+  final case class SquadDefinitionAction(player : Player, zone_ordinal_number : Int, u1 : Int, u2 : Int, action : SquadAction)
+
+  final case class RecoverSquadMembership()
+}
diff --git a/common/src/main/scala/services/teamwork/SquadServiceResponse.scala b/common/src/main/scala/services/teamwork/SquadServiceResponse.scala
new file mode 100644
index 00000000..5e6bc998
--- /dev/null
+++ b/common/src/main/scala/services/teamwork/SquadServiceResponse.scala
@@ -0,0 +1,7 @@
+// Copyright (c) 2019 PSForever
+package services.teamwork
+
+import net.psforever.packet.game.SquadInfo
+import services.GenericEventBusMsg
+
+final case class SquadServiceResponse(toChannel : String, response : SquadResponse.Response) extends GenericEventBusMsg
diff --git a/pslogin/src/main/scala/PsLogin.scala b/pslogin/src/main/scala/PsLogin.scala
index 58183746..e2a3b948 100644
--- a/pslogin/src/main/scala/PsLogin.scala
+++ b/pslogin/src/main/scala/PsLogin.scala
@@ -21,6 +21,7 @@ import services.ServiceManager
 import services.avatar._
 import services.galaxy.GalaxyService
 import services.local._
+import services.teamwork.SquadService
 import services.vehicle.VehicleService
 
 import scala.collection.JavaConverters._
@@ -216,6 +217,7 @@ object PsLogin {
     serviceManager ! ServiceManager.Register(Props[LocalService], "local")
     serviceManager ! ServiceManager.Register(Props[VehicleService], "vehicle")
     serviceManager ! ServiceManager.Register(Props[GalaxyService], "galaxy")
+    serviceManager ! ServiceManager.Register(Props[SquadService], "squad")
     serviceManager ! ServiceManager.Register(Props(classOf[InterstellarCluster], continentList), "cluster")
 
     //attach event bus entry point to each zone
diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala
index c33ba93a..170ede0e 100644
--- a/pslogin/src/main/scala/WorldSessionActor.scala
+++ b/pslogin/src/main/scala/WorldSessionActor.scala
@@ -52,6 +52,7 @@ import services.galaxy.{GalaxyResponse, GalaxyServiceResponse}
 import services.local.{LocalAction, LocalResponse, LocalServiceMessage, LocalServiceResponse}
 import services.vehicle.support.TurretUpgrader
 import services.vehicle.{VehicleAction, VehicleResponse, VehicleServiceMessage, VehicleServiceResponse}
+import services.teamwork._
 
 import scala.concurrent.duration._
 import scala.concurrent.ExecutionContext.Implicits.global
@@ -76,6 +77,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
   var localService : ActorRef = ActorRef.noSender
   var vehicleService : ActorRef = ActorRef.noSender
   var galaxyService : ActorRef = ActorRef.noSender
+  var squadService : ActorRef = ActorRef.noSender
   var taskResolver : ActorRef = Actor.noSender
   var cluster : ActorRef = Actor.noSender
   var continent : Zone = Zone.Nowhere
@@ -266,6 +268,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
       ServiceManager.serviceManager ! Lookup("taskResolver")
       ServiceManager.serviceManager ! Lookup("cluster")
       ServiceManager.serviceManager ! Lookup("galaxy")
+      ServiceManager.serviceManager ! Lookup("squad")
 
     case _ =>
       log.error("Unknown message")
@@ -291,6 +294,9 @@ class WorldSessionActor extends Actor with MDCContextAware {
     case ServiceManager.LookupResult("cluster", endpoint) =>
       cluster = endpoint
       log.info("ID: " + sessionId + " Got cluster service " + endpoint)
+    case ServiceManager.LookupResult("squad", endpoint) =>
+      squadService = endpoint
+      log.info("ID: " + sessionId + " Got squad service " + endpoint)
 
     case ControlPacket(_, ctrl) =>
       handleControlPkt(ctrl)
@@ -336,6 +342,37 @@ class WorldSessionActor extends Actor with MDCContextAware {
     case VehicleServiceResponse(toChannel, guid, reply) =>
       HandleVehicleServiceResponse(toChannel, guid, reply)
 
+    case SquadServiceResponse(toChannel, response) =>
+      response match {
+        case SquadResponse.Init(infos) if infos.nonEmpty =>
+          sendResponse(ReplicationStreamMessage(infos))
+
+        case SquadResponse.Update(infos) if infos.nonEmpty =>
+          val o = ReplicationStreamMessage(6, None,
+            infos.map { case (index, squadInfo) =>
+              SquadListing(index, squadInfo)
+            }.toVector
+          )
+          log.info(s"updating squad with msg $o")
+          sendResponse(
+            ReplicationStreamMessage(6, None,
+              infos.map { case (index, squadInfo) =>
+                SquadListing(index, squadInfo)
+              }.toVector
+            )
+          )
+
+        case SquadResponse.Remove(infos) if infos.nonEmpty =>
+          sendResponse(
+            ReplicationStreamMessage(1, None,
+              infos.map { index =>
+                SquadListing(index, None)
+              }.toVector
+            )
+          )
+        case _ => ;
+      }
+
     case Deployment.CanDeploy(obj, state) =>
       val vehicle_guid = obj.GUID
       //TODO remove this arbitrary allowance angle when no longer helpful
@@ -806,6 +843,8 @@ class WorldSessionActor extends Actor with MDCContextAware {
       vehicleService ! Service.Join(avatar.name) //channel will be player.Name
       galaxyService ! Service.Join("galaxy") //for galaxy-wide messages
       galaxyService ! Service.Join(s"${avatar.faction}") //for hotspots
+      squadService ! Service.Join(s"${avatar.faction}") //channel will be player.Faction
+      squadService ! Service.Join(avatar.name) //management of any lingering squad information connected to this player
       cluster ! InterstellarCluster.GetWorld("home3")
 
     case InterstellarCluster.GiveWorld(zoneId, zone) =>
@@ -4733,8 +4772,9 @@ class WorldSessionActor extends Actor with MDCContextAware {
     case msg @ AvatarGrenadeStateMessage(player_guid, state) =>
       log.info("AvatarGrenadeStateMessage: " + msg)
 
-    case msg @ SquadDefinitionActionMessage(a, b, c, d, e, f, g, h, i) =>
-      log.info("SquadDefinitionAction: " + msg)
+    case msg @ SquadDefinitionActionMessage(u1, u2, action) =>
+      log.info(s"SquadDefinitionAction: $msg")
+      squadService ! SquadServiceMessage.SquadDefinitionAction(player, continent.Number, u1, u2, action)
 
     case msg @ GenericCollisionMsg(u1, p, t, php, thp, pv, tv, ppos, tpos, u2, u3, u4) =>
       log.info("Ouch! " + msg)

From 5c433204cff57a615f306307281047f4d9dc6dfd Mon Sep 17 00:00:00 2001
From: FateJH <Jason_DiDonato@yahoo.com>
Date: Mon, 3 Jun 2019 23:45:10 -0400
Subject: [PATCH 06/31] initial work and dummy tests for
 SquadDetailDefinitionUpdateMessage packet; 46-bit certification encoding;
 indicating four previously unhandled SquadAction types; the squad leader is
 allowed to move around his squad

---
 .../psforever/objects/teamwork/Member.scala   |  12 +-
 .../psforever/objects/teamwork/Squad.scala    |  12 +-
 .../psforever/packet/GamePacketOpcode.scala   |   2 +-
 .../game/SquadDefinitionActionMessage.scala   |  65 ++++-
 .../SquadDetailDefinitionUpdateMessage.scala  | 195 +++++++++++++++
 .../psforever/types/CertificationType.scala   |  57 +++++
 .../services/teamwork/SquadService.scala      | 226 +++++++++---------
 .../SquadDefinitionActionMessageTest.scala    |  68 +++++-
 ...uadDetailDefinitionUpdateMessageTest.scala |  45 ++++
 .../src/main/scala/WorldSessionActor.scala    |  29 +++
 10 files changed, 582 insertions(+), 129 deletions(-)
 create mode 100644 common/src/main/scala/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala
 create mode 100644 common/src/test/scala/game/SquadDetailDefinitionUpdateMessageTest.scala

diff --git a/common/src/main/scala/net/psforever/objects/teamwork/Member.scala b/common/src/main/scala/net/psforever/objects/teamwork/Member.scala
index 46d9e15f..4d3b24e8 100644
--- a/common/src/main/scala/net/psforever/objects/teamwork/Member.scala
+++ b/common/src/main/scala/net/psforever/objects/teamwork/Member.scala
@@ -7,7 +7,7 @@ class Member {
   //about the position to be filled
   private var role : String = ""
   private var orders : String = ""
-  private var restrictions : Set[CertificationType.Value] = Set()
+  private var requirements : Set[CertificationType.Value] = Set()
   //about the individual filling the position
   private var name : String = ""
   private var health : Int = 0
@@ -29,11 +29,11 @@ class Member {
     Orders
   }
 
-  def Restrictions : Set[CertificationType.Value] = restrictions
+  def Requirements : Set[CertificationType.Value] = requirements
 
-  def Restrictions_=(requirements : Set[CertificationType.Value]) = {
-    restrictions = requirements
-    Restrictions
+  def Requirements_=(req : Set[CertificationType.Value]) : Set[CertificationType.Value] = {
+    requirements = req
+    Requirements
   }
 
   def Name : String = name
@@ -73,7 +73,7 @@ class Member {
 
   def Close() : Unit = {
     role = ""
-    restrictions = Set()
+    requirements = Set()
     //about the individual filling the position
     name = ""
     health = 0
diff --git a/common/src/main/scala/net/psforever/objects/teamwork/Squad.scala b/common/src/main/scala/net/psforever/objects/teamwork/Squad.scala
index 071d8a8e..4ac3cfc2 100644
--- a/common/src/main/scala/net/psforever/objects/teamwork/Squad.scala
+++ b/common/src/main/scala/net/psforever/objects/teamwork/Squad.scala
@@ -14,6 +14,7 @@ class Squad(squadId : PlanetSideGUID, alignment : PlanetSideEmpire.Value) extend
   private val membership : Array[Member] = Array.fill[Member](10)(new Member)
   private val availability : Array[Boolean] = Array.fill[Boolean](10)(true)
   private var listed : Boolean = false
+  private var leaderPositionIndex : Int = 0
 
   override def GUID_=(d : PlanetSideGUID) : PlanetSideGUID = GUID
 
@@ -62,8 +63,17 @@ class Squad(squadId : PlanetSideGUID, alignment : PlanetSideEmpire.Value) extend
 
   def Availability : Array[Boolean] = availability
 
+  def LeaderPositionIndex : Int = leaderPositionIndex
+
+  def LeaderPositionIndex_=(position : Int) : Int = {
+    if(availability.lift(position).contains(true)) {
+      leaderPositionIndex = position
+    }
+    LeaderPositionIndex
+  }
+
   def Leader : String = {
-    membership.headOption match {
+    membership.lift(leaderPositionIndex) match {
       case Some(member) =>
         member.Name
       case None =>
diff --git a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
index 3845c4a3..d0ab02cf 100644
--- a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
+++ b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
@@ -592,7 +592,7 @@ object GamePacketOpcode extends Enumeration {
     case 0xe6 => game.ReplicationStreamMessage.decode
     case 0xe7 => game.SquadDefinitionActionMessage.decode
     // 0xe8
-    case 0xe8 => noDecoder(SquadDetailDefinitionUpdateMessage)
+    case 0xe8 => game.SquadDetailDefinitionUpdateMessage.decode
     case 0xe9 => noDecoder(TacticsMessage)
     case 0xea => noDecoder(RabbitUpdateMessage)
     case 0xeb => noDecoder(SquadInvitationRequestMessage)
diff --git a/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala b/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala
index 5b714fc8..40193730 100644
--- a/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala
+++ b/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala
@@ -2,6 +2,7 @@
 package net.psforever.packet.game
 
 import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
+import net.psforever.types.CertificationType
 import scodec.bits.BitVector
 import scodec.{Attempt, Codec, Err}
 import scodec.codecs._
@@ -15,12 +16,20 @@ import shapeless.{::, HNil}
 abstract class SquadAction(val code : Int)
 
 object SquadAction{
+  final case class DisplaySquad() extends SquadAction(0)
+
   final case class SaveSquadDefinition() extends SquadAction(3)
 
+  final case class LoadSquadDefinition() extends SquadAction(4)
+
+  final case class ListSquadDefinition(name : String) extends SquadAction(7)
+
   final case class ListSquad() extends SquadAction(8)
 
   final case class SelectRoleForYourself(state : Int) extends SquadAction(10)
 
+  final case class CancelSelectRoleForYourself(value: Long = 0) extends SquadAction(15)
+
   final case class ChangeSquadPurpose(purpose : String) extends SquadAction(19)
 
   final case class ChangeSquadZone(zone : PlanetSideZoneID) extends SquadAction(20)
@@ -33,7 +42,7 @@ object SquadAction{
 
   final case class ChangeSquadMemberRequirementsDetailedOrders(u1 : Int, orders : String) extends SquadAction(24)
 
-  final case class ChangeSquadMemberRequirementsWeapons(u1 : Int, u2 : Long) extends SquadAction(25)
+  final case class ChangeSquadMemberRequirementsCertifications(u1 : Int, certs : Set[CertificationType.Value]) extends SquadAction(25)
 
   final case class ResetAll() extends SquadAction(26)
 
@@ -58,6 +67,13 @@ object SquadAction{
   object Codecs {
     private val everFailCondition = conditional(included = false, bool)
 
+    val displaySquadCodec = everFailCondition.xmap[DisplaySquad] (
+      _ => DisplaySquad(),
+      {
+        case DisplaySquad() => None
+      }
+    )
+
     val saveSquadDefinitionCodec = everFailCondition.xmap[SaveSquadDefinition] (
       _ => SaveSquadDefinition(),
       {
@@ -65,6 +81,20 @@ object SquadAction{
       }
     )
 
+    val loadSquadDefinitionCodec = everFailCondition.xmap[LoadSquadDefinition] (
+      _ => LoadSquadDefinition(),
+      {
+        case LoadSquadDefinition() => None
+      }
+    )
+
+    val listSquadDefinitionCodec = PacketHelpers.encodedWideStringAligned(6).xmap[ListSquadDefinition] (
+      text => ListSquadDefinition(text),
+      {
+        case ListSquadDefinition(text) => text
+      }
+    )
+
     val listSquadCodec = everFailCondition.xmap[ListSquad] (
       _ => ListSquad(),
       {
@@ -79,6 +109,13 @@ object SquadAction{
       }
     )
 
+    val cancelSelectRoleForYourselfCodec = uint32.xmap[CancelSelectRoleForYourself] (
+      value => CancelSelectRoleForYourself(value),
+      {
+        case CancelSelectRoleForYourself(value) => value
+      }
+    )
+
     val changeSquadPurposeCodec = PacketHelpers.encodedWideStringAligned(6).xmap[ChangeSquadPurpose] (
       purpose => ChangeSquadPurpose(purpose),
       {
@@ -125,12 +162,14 @@ object SquadAction{
       }
     )
 
-    val changeSquadMemberRequirementsWeaponsCodec = (uint4 :: ulongL(46)).xmap[ChangeSquadMemberRequirementsWeapons] (
+    val changeSquadMemberRequirementsCertificationsCodec = (uint4 :: ulongL(46)).xmap[ChangeSquadMemberRequirementsCertifications] (
       {
-        case u1 :: u2 :: HNil => ChangeSquadMemberRequirementsWeapons(u1, u2)
+        case u1 :: u2 :: HNil =>
+          ChangeSquadMemberRequirementsCertifications(u1, CertificationType.fromEncodedLong(u2))
       },
       {
-        case ChangeSquadMemberRequirementsWeapons(u1, u2) => u1 :: u2 :: HNil
+        case ChangeSquadMemberRequirementsCertifications(u1, u2) =>
+          u1 :: CertificationType.toEncodedLong(u2) :: HNil
       }
     )
 
@@ -219,11 +258,11 @@ object SquadAction{
   * The `action` code indicates the format of the remainder data in the packet.
   * The following formats are translated; their purposes are listed:<br>
   * &nbsp;&nbsp;`(None)`<br>
-  * &nbsp;&nbsp;&nbsp;&nbsp;`0 ` - UNKNOWN<br>
+  * &nbsp;&nbsp;&nbsp;&nbsp;`0 ` - Display Squad<br>
   * &nbsp;&nbsp;&nbsp;&nbsp;`1 ` - UNKNOWN<br>
   * &nbsp;&nbsp;&nbsp;&nbsp;`2 ` - UNKNOWN<br>
   * &nbsp;&nbsp;&nbsp;&nbsp;`3 ` - Save Squad Definition<br>
-  * &nbsp;&nbsp;&nbsp;&nbsp;`4 ` - UNKNOWN<br>
+  * &nbsp;&nbsp;&nbsp;&nbsp;`4 ` - Load Squad Definition<br>
   * &nbsp;&nbsp;&nbsp;&nbsp;`6 ` - UNKNOWN<br>
   * &nbsp;&nbsp;&nbsp;&nbsp;`8 ` - List Squad<br>
   * &nbsp;&nbsp;&nbsp;&nbsp;`9 ` - UNKNOWN<br>
@@ -251,10 +290,10 @@ object SquadAction{
   * &nbsp;&nbsp;`Long`<br>
   * &nbsp;&nbsp;&nbsp;&nbsp;`13` - UNKNOWN<br>
   * &nbsp;&nbsp;&nbsp;&nbsp;`14` - UNKNOWN<br>
-  * &nbsp;&nbsp;&nbsp;&nbsp;`15` - UNKNOWN<br>
+  * &nbsp;&nbsp;&nbsp;&nbsp;`15` - Select this Role for Yourself<br>
   * &nbsp;&nbsp;&nbsp;&nbsp;`37` - UNKNOWN<br>
   * &nbsp;&nbsp;`String`<br>
-  * &nbsp;&nbsp;&nbsp;&nbsp;`7 ` - UNKNOWN<br>
+  * &nbsp;&nbsp;&nbsp;&nbsp;`7 ` - List Squad Definition<br>
   * &nbsp;&nbsp;&nbsp;&nbsp;`19` - (Squad leader) Change Squad Purpose<br>
   * &nbsp;&nbsp;`Int :: Long`<br>
   * &nbsp;&nbsp;&nbsp;&nbsp;`12` - UNKNOWN<br>
@@ -293,16 +332,20 @@ object SquadDefinitionActionMessage extends Marshallable[SquadDefinitionActionMe
     import SquadAction.Codecs._
     import scala.annotation.switch
     ((code : @switch) match {
+      case 0 => displaySquadCodec
       case 3 => saveSquadDefinitionCodec
+      case 4 => loadSquadDefinitionCodec
+      case 7 => listSquadDefinitionCodec
       case 8 => listSquadCodec
       case 10 => selectRoleForYourselfCodec
+      case 15 => cancelSelectRoleForYourselfCodec
       case 19 => changeSquadPurposeCodec
       case 20 => changeSquadZoneCodec
       case 21 => closeSquadMemberPositionCodec
       case 22 => addSquadMemberPositionCodec
       case 23 => changeSquadMemberRequirementsRoleCodec
       case 24 => changeSquadMemberRequirementsDetailedOrdersCodec
-      case 25 => changeSquadMemberRequirementsWeaponsCodec
+      case 25 => changeSquadMemberRequirementsCertificationsCodec
       case 26 => resetAllCodec
       case 28 => autoApproveInvitationRequestsCodec
       case 31 => locationFollowsSquadLeadCodec
@@ -310,8 +353,8 @@ object SquadDefinitionActionMessage extends Marshallable[SquadDefinitionActionMe
       case 35 => cancelSquadSearchCodec
       case 40 => findLfsSoldiersForRoleCodec
       case 41 => cancelFindCodec
-      case 0 | 1 | 2 | 4 | 6 | 7 | 9 |
-           11 | 12 | 13 | 14 | 15 | 16 |
+      case 1 | 2 | 6 | 9 |
+           11 | 12 | 13 | 14 | 16 |
            17 | 18 | 29 | 30 | 33 | 36 |
            37 | 38 | 42 | 43 => unknownCodec(code)
       case _ => failureCodec(code)
diff --git a/common/src/main/scala/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala b/common/src/main/scala/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala
new file mode 100644
index 00000000..291ef219
--- /dev/null
+++ b/common/src/main/scala/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala
@@ -0,0 +1,195 @@
+// Copyright (c) 2019 PSForever
+package net.psforever.packet.game
+
+import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
+import net.psforever.types.CertificationType
+import scodec.bits.BitVector
+import scodec.{Attempt, Codec, Err}
+import scodec.codecs._
+import shapeless.HNil
+
+import scala.annotation.tailrec
+
+final case class SquadPositionDetail(is_closed : Boolean,
+                                     role : String,
+                                     detailed_orders : String,
+                                     requirements : Set[CertificationType.Value],
+                                     char_id : Long,
+                                     name : String)
+
+final case class SquadDetailDefinitionUpdateMessage(guid : PlanetSideGUID,
+                                                    unk : BitVector,
+                                                    leader_name : String,
+                                                    task : String,
+                                                    zone_id : PlanetSideZoneID,
+                                                    member_info : List[SquadPositionDetail])
+  extends PlanetSideGamePacket {
+  type Packet = SquadDetailDefinitionUpdateMessage
+  def opcode = GamePacketOpcode.SquadDetailDefinitionUpdateMessage
+  def encode = SquadDetailDefinitionUpdateMessage.encode(this)
+}
+
+object SquadPositionDetail {
+  final val Closed : SquadPositionDetail = SquadPositionDetail(is_closed = true, "", "", Set.empty, 0L, "")
+
+  private def reliableNameHash(name : String) : Long = {
+    val hash = name.hashCode.toLong
+    if(hash < 0) {
+      -1L * hash
+    }
+    else {
+      hash
+    }
+  }
+
+  def apply() : SquadPositionDetail = SquadPositionDetail(is_closed = false, "", "", Set.empty, 0L, "")
+
+  def apply(name : String) : SquadPositionDetail = SquadPositionDetail(is_closed = false, "", "", Set.empty, reliableNameHash(name), name)
+
+  def apply(role : String, detailed_orders : String) : SquadPositionDetail = SquadPositionDetail(is_closed = false, role, detailed_orders, Set.empty, 0L, "")
+
+  def apply(role : String, detailed_orders : String, requirements : Set[CertificationType.Value]) : SquadPositionDetail = SquadPositionDetail(is_closed = false, role, detailed_orders, requirements, 0L, "")
+
+  def apply(role : String, detailed_orders : String, name : String) : SquadPositionDetail = SquadPositionDetail(is_closed = false, role, detailed_orders, Set.empty, reliableNameHash(name), name)
+}
+
+object SquadDetailDefinitionUpdateMessage extends Marshallable[SquadDetailDefinitionUpdateMessage] {
+  final val defaultRequirements : Set[CertificationType.Value] = Set(
+    CertificationType.StandardAssault,
+    CertificationType.StandardExoSuit,
+    CertificationType.AgileExoSuit
+  )
+
+  def apply(guid : PlanetSideGUID, leader_name : String, task : String, zone_id : PlanetSideZoneID, member_info : List[SquadPositionDetail]) : SquadDetailDefinitionUpdateMessage = {
+    import scodec.bits._
+    SquadDetailDefinitionUpdateMessage(guid, hex"080000000000000000000".toBitVector, leader_name, task, zone_id, member_info)
+  }
+
+  private def memberCodec(pad : Int) : Codec[SquadPositionDetail] = {
+    import shapeless.::
+    (
+        uint8 :: //required value = 6
+        ("is_closed" | bool) :: //if all positions are closed, the squad detail menu display no positions at all
+        ("role" | PacketHelpers.encodedWideStringAligned(pad)) ::
+        ("detailed_orders" | PacketHelpers.encodedWideString) ::
+        ("char_id" | uint32L) ::
+        ("name" | PacketHelpers.encodedWideString) ::
+        ("requirements" | ulongL(46))
+      ).exmap[SquadPositionDetail] (
+      {
+        case 6 :: closed :: role :: orders :: char_id :: name :: requirements :: HNil =>
+          Attempt.Successful(
+            SquadPositionDetail(closed, role, orders, defaultRequirements ++ CertificationType.fromEncodedLong(requirements), char_id, name)
+          )
+        case data =>
+          Attempt.Failure(Err(s"can not decode a SquadDetailDefinitionUpdate member's data - $data"))
+      },
+      {
+        case SquadPositionDetail(closed, role, orders, requirements, char_id, name) =>
+          Attempt.Successful(6 :: closed :: role :: orders :: char_id :: name :: CertificationType.toEncodedLong(defaultRequirements ++ requirements) :: HNil)
+      }
+    )
+  }
+
+  private val first_member_codec : Codec[SquadPositionDetail] = memberCodec(pad = 7)
+
+  private val member_codec : Codec[SquadPositionDetail] = memberCodec(pad = 0)
+
+  private case class LinkedMemberList(member : SquadPositionDetail, next : Option[LinkedMemberList])
+
+  private def subsequent_member_codec : Codec[LinkedMemberList] = {
+    import shapeless.::
+    (
+      //disruptive coupling action (e.g., flatPrepend) necessary to allow for recursive Codec
+      ("member" | member_codec) >>:~ { _ =>
+        optional(bool, "next" | subsequent_member_codec).hlist
+      }
+      ).xmap[LinkedMemberList] (
+      {
+        case a :: b :: HNil =>
+          LinkedMemberList(a, b)
+      },
+      {
+        case LinkedMemberList(a, b) =>
+          a :: b :: HNil
+      }
+    )
+  }
+
+  private def initial_member_codec : Codec[LinkedMemberList] = {
+    import shapeless.::
+    (
+      ("member" | first_member_codec) ::
+        optional(bool, "next" | subsequent_member_codec)
+      ).xmap[LinkedMemberList] (
+      {
+        case a :: b :: HNil =>
+          LinkedMemberList(a, b)
+      },
+      {
+        case LinkedMemberList(a, b) =>
+          a :: b :: HNil
+      }
+    )
+  }
+
+  @tailrec
+  private def unlinkMemberList(list : LinkedMemberList, out : List[SquadPositionDetail] = Nil) : List[SquadPositionDetail] = {
+    list.next match {
+      case None =>
+        out :+ list.member
+      case Some(next) =>
+        unlinkMemberList(next, out :+ list.member)
+    }
+  }
+
+  private def linkMemberList(list : List[SquadPositionDetail]) : LinkedMemberList = {
+    list match {
+      case Nil =>
+        throw new Exception("")
+      case x :: Nil =>
+        LinkedMemberList(x, None)
+      case x :: xs =>
+        linkMemberList(xs, LinkedMemberList(x, None))
+    }
+  }
+
+  @tailrec
+  private def linkMemberList(list : List[SquadPositionDetail], out : LinkedMemberList) : LinkedMemberList = {
+    list match {
+      case Nil =>
+        out
+      case x :: Nil =>
+        LinkedMemberList(x, Some(out))
+      case x :: xs =>
+        linkMemberList(xs, LinkedMemberList(x, Some(out)))
+    }
+  }
+
+  implicit val codec : Codec[SquadDetailDefinitionUpdateMessage] = {
+    import shapeless.::
+    (
+      ("guid" | PlanetSideGUID.codec) ::
+        uint8 ::
+        uint4 ::
+        bits(83) :: //variable fields, but can be 0'd
+        uint(10) :: //constant = 0
+        ("leader" | PacketHelpers.encodedWideStringAligned(7)) ::
+        ("task" | PacketHelpers.encodedWideString) ::
+        ("zone_id" | PlanetSideZoneID.codec) ::
+        uint(23) :: //constant = 4983296
+        optional(bool, "member_info" | initial_member_codec)
+      ).exmap[SquadDetailDefinitionUpdateMessage] (
+      {
+        case guid :: _ :: _ :: _ :: _ :: leader :: task :: zone :: _ :: Some(member_list) :: HNil =>
+          Attempt.Successful(SquadDetailDefinitionUpdateMessage(guid, leader, task, zone, unlinkMemberList(member_list)))
+        case data =>
+          Attempt.failure(Err(s"can not get squad detail definition from data $data"))
+      },
+      {
+        case SquadDetailDefinitionUpdateMessage(guid, unk, leader, task, zone, member_list) =>
+          Attempt.Successful(guid :: 132 :: 8 :: unk.take(83) :: 0 :: leader :: task :: zone :: 4983296 :: Some(linkMemberList(member_list.reverse)) :: HNil)
+      }
+    )
+  }
+}
diff --git a/common/src/main/scala/net/psforever/types/CertificationType.scala b/common/src/main/scala/net/psforever/types/CertificationType.scala
index ea4ca3ab..99afa33c 100644
--- a/common/src/main/scala/net/psforever/types/CertificationType.scala
+++ b/common/src/main/scala/net/psforever/types/CertificationType.scala
@@ -3,6 +3,8 @@ package net.psforever.types
 
 import net.psforever.packet.PacketHelpers
 import scodec.codecs._
+
+import scala.annotation.tailrec
 /**
   * An `Enumeration` of the available certifications.<br>
   * <br>
@@ -76,4 +78,59 @@ object CertificationType extends Enumeration {
   = Value
 
   implicit val codec = PacketHelpers.createEnumerationCodec(this, uint8L)
+
+  /**
+    * Certifications are often stored, in object form, as a 46-member collection.
+    * Encode a subset of certification values for packet form.
+    * @see `ChangeSquadMemberRequirementsCertifications`
+    * @see `changeSquadMemberRequirementsCertificationsCodec`
+    * @param certs the certifications, as a sequence of values
+    * @return the certifications, as a single value
+    */
+  def toEncodedLong(certs : Set[CertificationType.Value]) : Long = {
+    certs
+      .map{ cert => math.pow(2, cert.id).toLong }
+      .foldLeft(0L)(_ + _)
+  }
+
+  /**
+    * Certifications are often stored, in packet form, as an encoded little-endian `46u` value.
+    * Decode a representative value into a subset of certification values.
+    * @see `ChangeSquadMemberRequirementsCertifications`
+    * @see `changeSquadMemberRequirementsCertificationsCodec`
+    * @see `fromEncodedLong(Long, Iterable[Long], Set[CertificationType.Value])`
+    * @param certs the certifications, as a single value
+    * @return the certifications, as a sequence of values
+    */
+  def fromEncodedLong(certs : Long) : Set[CertificationType.Value] = {
+    recursiveFromEncodedLong(
+      certs,
+      CertificationType.values.map{ cert => math.pow(2, cert.id).toLong }.toSeq.sorted
+    )
+  }
+
+  /**
+    * Certifications are often stored, in packet form, as an encoded little-endian `46u` value.
+    * Decode a representative value into a subset of certification values
+    * by repeatedly finding the partition point of values less than a specific one,
+    * providing for both the next lowest value (to subtract) and an index (of a certification).
+    * @see `ChangeSquadMemberRequirementsCertifications`
+    * @see `changeSquadMemberRequirementsCertificationsCodec`
+    * @see `fromEncodedLong(Long)`
+    * @param certs the certifications, as a single value
+    * @param splitList the available values to partition
+    * @param out the accumulating certification values;
+    *            defaults to an empty set
+    * @return the certifications, as a sequence of values
+    */
+  @tailrec
+  private def recursiveFromEncodedLong(certs : Long, splitList : Iterable[Long], out : Set[CertificationType.Value] = Set.empty) : Set[CertificationType.Value] = {
+    if(certs == 0 || splitList.isEmpty) {
+      out
+    }
+    else {
+      val (less, _) = splitList.partition(_ <= certs)
+      recursiveFromEncodedLong(certs - less.last, less, out ++ Set(CertificationType(less.size - 1)))
+    }
+  }
 }
diff --git a/common/src/main/scala/services/teamwork/SquadService.scala b/common/src/main/scala/services/teamwork/SquadService.scala
index 4d58a1af..3572bd96 100644
--- a/common/src/main/scala/services/teamwork/SquadService.scala
+++ b/common/src/main/scala/services/teamwork/SquadService.scala
@@ -50,7 +50,7 @@ class SquadService extends Actor {
       case None =>
         val id = GetNextSquadId()
         val squad = new Squad(id, faction)
-        val leadPosition = squad.Membership(0)
+        val leadPosition = squad.Membership(squad.LeaderPositionIndex)
         leadPosition.Name = name
         leadPosition.Health = player.Health
         leadPosition.Armor = player.Armor
@@ -80,7 +80,7 @@ class SquadService extends Actor {
       val path = s"$name/Squad"
       val who = sender()
       log.info(s"$who has joined $path")
-      SquadEvents.subscribe(who, path)
+      SquadEvents.subscribe(who, path) //TODO squad-specific switchboard
       //check for renewable squad information
       memberToSquad.get(name) match {
         case None => ;
@@ -101,99 +101,107 @@ class SquadService extends Actor {
       val squad = GetSquadFromPlayer(tplayer)
       val member = squad.Membership.find(_.Name == tplayer.Name).get //should never fail
       member.ZoneId = zone_ordinal_number //TODO improve this requirement
-      var listingChanged : List[Int] = Nil
-      action match {
-        case ChangeSquadPurpose(purpose) =>
-          log.info(s"${tplayer.Name}-${tplayer.Faction} has changed his squad's task to $purpose")
-          squad.Description = purpose
-          listingChanged = List(SquadInfo.Field.Task)
+      if(tplayer.Name.equals(squad.Leader)) {
+        var listingChanged : List[Int] = Nil
+        action match {
+          case ChangeSquadPurpose(purpose) =>
+            log.info(s"${tplayer.Name}-${tplayer.Faction} has changed his squad's task to $purpose")
+            squad.Description = purpose
+            listingChanged = List(SquadInfo.Field.Task)
 
-        case ChangeSquadZone(zone) =>
-          log.info(s"${tplayer.Name}-${tplayer.Faction} has changed his squad's ops zone to $zone")
-          squad.ZoneId = zone.zoneId.toInt
-          listingChanged = List(SquadInfo.Field.ZoneId)
+          case ChangeSquadZone(zone) =>
+            log.info(s"${tplayer.Name}-${tplayer.Faction} has changed his squad's ops zone to $zone")
+            squad.ZoneId = zone.zoneId.toInt
+            listingChanged = List(SquadInfo.Field.ZoneId)
 
-        case CloseSquadMemberPosition(position) =>
-          if(position > 0) {
+          case CloseSquadMemberPosition(position) =>
+            if(position != squad.LeaderPositionIndex) {
+              squad.Availability.lift(position) match {
+                case Some(true) =>
+                  squad.Availability.update(position, false)
+                  log.info(s"${tplayer.Name}-${tplayer.Faction} has closed the #$position position in his squad")
+                  val memberPosition = squad.Membership(position)
+                  listingChanged = if(memberPosition.Name.nonEmpty) {
+                    List(SquadInfo.Field.Size, SquadInfo.Field.Capacity)
+                  }
+                  else {
+                    List(SquadInfo.Field.Capacity)
+                  }
+                  memberPosition.Close()
+                case Some(false) => ;
+                case None => ;
+              }
+            }
+            else {
+              log.warn(s"can not close the leader position in squad-${squad.GUID.guid}")
+            }
+
+          case AddSquadMemberPosition(position) =>
+            squad.Availability.lift(position) match {
+              case Some(false) =>
+                log.info(s"${tplayer.Name}-${tplayer.Faction} has opened the #$position position in his squad")
+                squad.Availability.update(position, true)
+                listingChanged = List(SquadInfo.Field.Capacity)
+              case Some(true) => ;
+              case None => ;
+            }
+
+          case ChangeSquadMemberRequirementsRole(position, role) =>
             squad.Availability.lift(position) match {
               case Some(true) =>
-                squad.Availability.update(position, false)
-                log.info(s"${tplayer.Name}-${tplayer.Faction} has closed the #$position position in his squad")
-                val memberPosition = squad.Membership(position)
-                listingChanged = if(memberPosition.Name.nonEmpty) {
-                  List(SquadInfo.Field.Size, SquadInfo.Field.Capacity)
-                }
-                else {
-                  List(SquadInfo.Field.Capacity)
-                }
-                memberPosition.Close()
+                log.info(s"${tplayer.Name}-${tplayer.Faction} has changed the role of squad position #$position")
+                squad.Membership(position).Role = role
               case Some(false) => ;
               case None => ;
             }
-          }
-          else {
-            log.warn(s"can not close the lead position in squad-${squad.GUID.guid}")
-          }
 
-        case AddSquadMemberPosition(position) =>
-          squad.Availability.lift(position) match {
-            case Some(false) =>
-              log.info(s"${tplayer.Name}-${tplayer.Faction} has opened the #$position position in his squad")
-              squad.Availability.update(position, true)
-              listingChanged = List(SquadInfo.Field.Capacity)
-            case Some(true) => ;
-            case None => ;
-          }
+          case ChangeSquadMemberRequirementsDetailedOrders(position, orders) =>
+            squad.Availability.lift(position) match {
+              case Some(true) =>
+                log.info(s"${tplayer.Name}-${tplayer.Faction} has changed the orders for squad position #$position")
+                squad.Membership(position).Orders = orders
+              case Some(false) => ;
+              case None => ;
+            }
 
-        case ChangeSquadMemberRequirementsRole(position, role) =>
-          squad.Availability.lift(position) match {
-            case Some(true) =>
-              squad.Membership(position).Role = role
-            case Some(false) => ;
-            case None => ;
-          }
+          case ChangeSquadMemberRequirementsCertifications(position, certs) =>
+            squad.Availability.lift(position) match {
+              case Some(true) =>
+                log.info(s"${tplayer.Name}-${tplayer.Faction} has changed the requirements for squad position #$position")
+                squad.Membership(position).Requirements = certs
+              case Some(false) => ;
+              case None => ;
+            }
 
-        case ChangeSquadMemberRequirementsDetailedOrders(position, orders) =>
-          squad.Availability.lift(position) match {
-            case Some(true) =>
-              squad.Membership(position).Orders = orders
-            case Some(false) => ;
-            case None => ;
-          }
+          case ListSquad() =>
+            if(!squad.Listed) {
+              log.info(s"${tplayer.Name}-${tplayer.Faction} has opened recruitment for his squad")
+              squad.Listed = true
+            }
 
-        case ListSquad() =>
-          if(!squad.Listed) {
-            log.info(s"${tplayer.Name}-${tplayer.Faction} has opened recruitment for his squad")
-            squad.Listed = true
-          }
-
-        case ResetAll() =>
-          squad.Description = ""
-          squad.ZoneId = None
-          squad.Availability.indices.foreach { i =>
-            squad.Availability.update(i, true)
-          }
+          case ResetAll() =>
+            squad.Description = ""
+            squad.ZoneId = None
+            squad.Availability.indices.foreach { i =>
+              squad.Availability.update(i, true)
+            }
           //TODO squad members?
 
-        case _ => ;
-      }
-      //queue updates
-      if(squad.Listed) {
-        val entry = SquadService.Publish(squad)
-        val faction = squad.Faction
-        val factionListings = publishedLists(faction)
-        factionListings.find(info => {
-          info.squad_guid match {
-            case Some(guid) => guid == squad.GUID
-            case _ => false
-          }
-        }) match {
-          case Some(listedSquad) =>
-            val index = factionListings.indexOf(listedSquad)
-            if(squad.Listed) {
-              //squad information update
-              log.info(s"Squad will be updated")
-              factionListings(index) = entry
+          case _ => ;
+        }
+        //queue updates
+        if(squad.Listed) {
+          val entry = SquadService.Publish(squad)
+          val faction = squad.Faction
+          val factionListings = publishedLists(faction)
+          factionListings.find(info => {
+            info.squad_guid match {
+              case Some(guid) => guid == squad.GUID
+              case _ => false
+            }
+          }) match {
+            case Some(listedSquad) =>
+              val index = factionListings.indexOf(listedSquad)
               val changes = if(listingChanged.nonEmpty) {
                 SquadService.Differences(listingChanged, entry)
               }
@@ -201,27 +209,29 @@ class SquadService extends Actor {
                 SquadService.Differences(listedSquad, entry)
               }
               if(changes != SquadInfo.Blank) {
+                //squad information update
+                log.info(s"Squad will be updated")
+                factionListings(index) = entry
                 SquadEvents.publish(
                   SquadServiceResponse(s"$faction/Squad", SquadResponse.Update(Seq((index, changes))))
                 )
               }
-            }
-            else {
-              //remove squad from listing
-              log.info(s"Squad will be removed")
-              factionListings.remove(index)
+              else {
+                //remove squad from listing
+                log.info(s"Squad will be removed")
+                factionListings.remove(index)
+                SquadEvents.publish(
+                  SquadServiceResponse(s"$faction/Squad", SquadResponse.Remove(Seq(index)))
+                )
+              }
+            case None =>
+              //first time being published
+              log.info(s"Squad will be introduced")
+              factionListings += SquadService.Publish(squad)
               SquadEvents.publish(
-                SquadServiceResponse(s"$faction/Squad", SquadResponse.Remove(Seq(index)))
+                SquadServiceResponse(s"$faction/Squad", SquadResponse.Init(factionListings.toVector))
               )
-            }
-          case None if squad.Listed =>
-            log.info(s"Squad will be introduced")
-            //first time being published?
-            factionListings += SquadService.Publish(squad)
-            SquadEvents.publish(
-              SquadServiceResponse(s"$faction/Squad", SquadResponse.Init(factionListings.toVector))
-            )
-          case _ => ;
+          }
         }
       }
 
@@ -244,18 +254,18 @@ object SquadService {
 
   def Differences(updates : List[Int], info : SquadInfo) : SquadInfo = {
     if(updates.nonEmpty) {
+      val list = Seq(
+        SquadInfo.Blank, //must be index-0
+        SquadInfo(info.leader, None, None, None, None),
+        SquadInfo(None, info.task, None, None, None),
+        SquadInfo(None, None, info.zone_id, None, None),
+        SquadInfo(None, None, None, info.size, None),
+        SquadInfo(None, None, None, None, info.capacity)
+      )
       var out = SquadInfo.Blank
-      ({
-        val list = Seq(
-          SquadInfo.Blank, //must be index-0
-          SquadInfo(info.leader, None, None, None, None),
-          SquadInfo(None, info.task, None, None, None),
-          SquadInfo(None, None, info.zone_id, None, None),
-          SquadInfo(None, None, None, info.size, None),
-          SquadInfo(None, None, None, None, info.capacity)
-        )
-        updates.map(i => list(i)).filterNot { _ == SquadInfo.Blank }
-      }) //ignore what code inspection tells you - the parenthesis is necessary
+      updates
+        .map(i => list(i))
+        .filterNot { _ == SquadInfo.Blank }
         .foreach(sinfo => out = out And sinfo )
       out
     }
diff --git a/common/src/test/scala/game/SquadDefinitionActionMessageTest.scala b/common/src/test/scala/game/SquadDefinitionActionMessageTest.scala
index 934422a7..0acd6c8f 100644
--- a/common/src/test/scala/game/SquadDefinitionActionMessageTest.scala
+++ b/common/src/test/scala/game/SquadDefinitionActionMessageTest.scala
@@ -5,11 +5,15 @@ import org.specs2.mutable._
 import net.psforever.packet._
 import net.psforever.packet.game.SquadAction._
 import net.psforever.packet.game._
+import net.psforever.types.CertificationType
 import scodec.bits._
 
 class SquadDefinitionActionMessageTest extends Specification {
   //local test data; note that the second field - unk1 - is always blank for now, but that probably changes
+  val string_00 = hex"e7 00 0c0000" //guid: 3
   val string_03 = hex"E7 0c 0000c0" //index: 3
+  val string_04 = hex"E7 10 0000c0" //index: 3
+  val string_07 = hex"e7 1c 0000e68043006f0070007300200061006e00640020004d0069006c006900740061007200790020004f006600660069006300650072007300"
   val string_08 = hex"E7 20 000000"
   val string_10 = hex"E7 28 000004" //index: 1
   val string_19 = hex"E7 4c 0000218041002d005400650061006d00" //"A-Team"
@@ -34,6 +38,17 @@ class SquadDefinitionActionMessageTest extends Specification {
   val string_43 = hex"e7 ac 000000"
   val string_failure = hex"E7 ff"
 
+  "decode (00)" in {
+    PacketCoding.DecodePacket(string_00).require match {
+      case SquadDefinitionActionMessage(unk1, unk2, action) =>
+        unk1 mustEqual 3
+        unk2 mustEqual 0
+        action mustEqual DisplaySquad()
+      case _ =>
+        ko
+    }
+  }
+
   "decode (03)" in {
     PacketCoding.DecodePacket(string_03).require match {
       case SquadDefinitionActionMessage(unk1, unk2, action) =>
@@ -45,6 +60,28 @@ class SquadDefinitionActionMessageTest extends Specification {
     }
   }
 
+  "decode (03)" in {
+    PacketCoding.DecodePacket(string_04).require match {
+      case SquadDefinitionActionMessage(unk1, unk2, action) =>
+        unk1 mustEqual 0
+        unk2 mustEqual 3
+        action mustEqual LoadSquadDefinition()
+      case _ =>
+        ko
+    }
+  }
+
+  "decode (07)" in {
+    PacketCoding.DecodePacket(string_07).require match {
+      case SquadDefinitionActionMessage(unk1, unk2, action) =>
+        unk1 mustEqual 0
+        unk2 mustEqual 3
+        action mustEqual ListSquadDefinition("Cops and Military Officers")
+      case _ =>
+        ko
+    }
+  }
+
   "decode (08)" in {
     PacketCoding.DecodePacket(string_08).require match {
       case SquadDefinitionActionMessage(unk1, unk2, action) =>
@@ -138,7 +175,10 @@ class SquadDefinitionActionMessageTest extends Specification {
       case SquadDefinitionActionMessage(unk1, unk2, action) =>
         unk1 mustEqual 0
         unk2 mustEqual 0
-        action mustEqual ChangeSquadMemberRequirementsWeapons(1, 536870928L)
+        action mustEqual ChangeSquadMemberRequirementsCertifications(
+          1,
+          Set(CertificationType.AntiVehicular, CertificationType.InfiltrationSuit)
+        )
       case _ =>
         ko
     }
@@ -280,6 +320,13 @@ class SquadDefinitionActionMessageTest extends Specification {
     PacketCoding.DecodePacket(string_failure).isFailure mustEqual true
   }
 
+  "encode (00)" in {
+    val msg = SquadDefinitionActionMessage(3, 0, DisplaySquad())
+    val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+    pkt mustEqual string_00
+  }
+
   "encode (03)" in {
     val msg = SquadDefinitionActionMessage(0, 3, SaveSquadDefinition())
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
@@ -287,6 +334,20 @@ class SquadDefinitionActionMessageTest extends Specification {
     pkt mustEqual string_03
   }
 
+  "encode (03)" in {
+    val msg = SquadDefinitionActionMessage(0, 3, LoadSquadDefinition())
+    val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+    pkt mustEqual string_04
+  }
+
+  "encode (07)" in {
+    val msg = SquadDefinitionActionMessage(0, 3, ListSquadDefinition("Cops and Military Officers"))
+    val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+    pkt mustEqual string_07
+  }
+
   "encode (08)" in {
     val msg = SquadDefinitionActionMessage(0, 0, ListSquad())
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
@@ -344,7 +405,10 @@ class SquadDefinitionActionMessageTest extends Specification {
   }
 
   "encode (25)" in {
-    val msg = SquadDefinitionActionMessage(0, 0, ChangeSquadMemberRequirementsWeapons(1, 536870928L))
+    val msg = SquadDefinitionActionMessage(0, 0, ChangeSquadMemberRequirementsCertifications(
+      1,
+      Set(CertificationType.AntiVehicular, CertificationType.InfiltrationSuit)
+    ))
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_25
diff --git a/common/src/test/scala/game/SquadDetailDefinitionUpdateMessageTest.scala b/common/src/test/scala/game/SquadDetailDefinitionUpdateMessageTest.scala
new file mode 100644
index 00000000..5ad56bd0
--- /dev/null
+++ b/common/src/test/scala/game/SquadDetailDefinitionUpdateMessageTest.scala
@@ -0,0 +1,45 @@
+// Copyright (c) 2019 PSForever
+package game
+
+import net.psforever.packet._
+import net.psforever.packet.game._
+import org.specs2.mutable._
+import scodec.bits._
+
+class SquadDetailDefinitionUpdateMessageTest extends Specification {
+  val string = hex"e80300848180038021514601288a8400420048006f0066004400bf5c0023006600660064006300300030002a002a002a005c0023003900360034003000660066003d004b004f004b002b005300500043002b0046004c0059003d005c0023006600660064006300300030002a002a002a005c002300460046003400300034003000200041006c006c002000570065006c0063006f006d006500070000009814010650005c00230066006600300030003000300020007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c008000000000800100000c00020c8c5c00230066006600640063003000300020002000200043008000000000800100000c00020c8c5c002300660066006400630030003000200020002000480080eab58a02854f0070006f006c0045000100000c00020c8d5c002300660066006400630030003000200020002000200049008072d47a028b42006f006200610046003300740074003900300037000100000c00020c8c5c0023006600660064006300300030002000200020004e008000000000800100000c00020c8c5c00230066006600640063003000300020002000200041008000000000800100000c00020ca05c00230066006600300030003000300020007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c008000000000800100000c00020c8c5c0023003900360034003000660066002000200020004b008000000000800100000c00020c8c5c0023003900360034003000660066002000200020004f008042a28c028448006f00660044000100000c00020c8c5c0023003900360034003000660066002000200020004b008000000000800100000c0000"
+
+  "SquadDetailDefinitionUpdateMessage" should {
+    "decode" in {
+      PacketCoding.DecodePacket(string).require match {
+        case SquadDetailDefinitionUpdateMessage(guid, unk, leader, task, zone, member_info) =>
+          ok
+        case _ =>
+          ko
+      }
+    }
+
+    "encode" in {
+      val msg = SquadDetailDefinitionUpdateMessage(
+        PlanetSideGUID(3),
+        "HofD",
+        "\\#ffdc00***\\#9640ff=KOK+SPC+FLY=\\#ffdc00***\\#FF4040 All Welcome",
+        PlanetSideZoneID(7),
+        List(
+          SquadPositionDetail("\\#ff0000 |||||||||||||||||||||||", ""),
+          SquadPositionDetail("\\#ffdc00   C", ""),
+          SquadPositionDetail("\\#ffdc00   H", "", "OpoIE"),
+          SquadPositionDetail("\\#ffdc00    I", "", "BobaF3tt907"),
+          SquadPositionDetail("\\#ffdc00   N", ""),
+          SquadPositionDetail("\\#ffdc00   A", ""),
+          SquadPositionDetail("\\#ff0000 |||||||||||||||||||||||", ""),
+          SquadPositionDetail("\\#9640ff   K", ""),
+          SquadPositionDetail("\\#9640ff   O", "", "HofD"),
+          SquadPositionDetail("\\#9640ff   K", "")
+        )
+      )
+      val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+      ok
+    }
+  }
+}
diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala
index 170ede0e..9b83272a 100644
--- a/pslogin/src/main/scala/WorldSessionActor.scala
+++ b/pslogin/src/main/scala/WorldSessionActor.scala
@@ -2885,6 +2885,35 @@ class WorldSessionActor extends Actor with MDCContextAware {
         interstellarFerryTopLevelGUID = None
       case _ => ;
     }
+    sendResponse(ReplicationStreamMessage(
+      5,
+      Some(6),
+      Vector(
+        SquadListing(0, SquadInfo(Some("xNick"), Some("FLY,ALL WELCOME!"), Some(PlanetSideZoneID(7)), Some(8), Some(10), Some(PlanetSideGUID(1)))),
+        SquadListing(1, SquadInfo(Some("HofD"), Some("=KOK+SPC+FLY= All Welcome"), Some(PlanetSideZoneID(7)), Some(3), Some(10), Some(PlanetSideGUID(3))))
+      )
+    ))
+    //sendRawResponse(hex"e803008484000c800259e8809fda020043004a0069006d006d0079006e009b48006f006d006900630069006400690061006c00200053006d007500720066007300200041006e006f006e0079006d006f007500730004000000981401064580540061006e006b002000440072006900760065007200a05200650063006f006d006d0065006e00640065006400200074006f0020006800610076006500200065006e00670069006e0065006500720069006e0067002e0000000000800180000c00020c8c46007200650065006200690065002000730070006f007400cf44006f002000770068006100740065007600650072002c0020006200750074002000700072006500660065007200610062006c007900200073007400690063006b002000770069007400680020007400680065002000730071007500610064002e00200044006f006e002700740020006e00650065006400200061006e007900200073007000650063006900660069006300200063006500720074002e0096e27a0290540068006500460069006e0061006c005300740072007500670067006c0065000000000000020c8c46007200650065006200690065002000530070006f007400cf44006f002000770068006100740065007600650072002c0020006200750074002000700072006500660065007200610062006c007900200073007400690063006b002000770069007400680020007400680065002000730071007500610064002e00200044006f006e002700740020006e00650065006400200061006e007900200073007000650063006900660069006300200063006500720074002e0000000000800000000000020c8a41004d0053002000440072006900760065007200b34700690076006500200075007300200073007000610077006e00200070006f0069006e00740073002c0020006800610063006b0069006e006700200061006e006400200069006e00660069006c0020006100720065002000750073006500660075006c002e00fb02790287440030004f004d006700750079000100020c00020c8d410076006500720061006700650020004a0069006d006d007900a05200650069006e0066006f0072006300650064002000450078006f007300750069007400200077006f0075006c00640020006200650020006e0069006300650000000000800300000c00020c8b4100760065007200610067006500200042006f006200a05200650069006e0066006f0072006300650064002000450078006f007300750069007400200077006f0075006c00640020006200650020006e0069006300650000000000800300000c00020c8b410076006500720061006700650020004a006f006500a05200650069006e0066006f0072006300650064002000450078006f007300750069007400200077006f0075006c00640020006200650020006e0069006300650000000000800300000c00020c8753007500700070006f0072007400a2520065007300730075007200650063007400200073006f006c00640069006500720073002c0020006b00650065007000200075007300200061006c006900760065002e0000000000800100000c0c0a0c8845006e00670069006e00650065007200a043006f006d00620061007400200045006e00670069006e0065006500720069006e006700200077006f0075006c00640020006200650020006e0069006300650004b3d101864a0069006d006d0079006e000100000c000a0c854d0065006400690063009a4100640076002e0020004d00650064006900630061006c00200077006f0075006c00640020006200650020006e0069006300650000000000800100000c0400")
+    sendResponse(
+      SquadDetailDefinitionUpdateMessage(
+        PlanetSideGUID(3),
+        "HofD",
+        "\\#ffdc00***\\#9640ff=KOK+SPC+FLY=\\#ffdc00***\\#FF4040 All Welcome",
+        PlanetSideZoneID(7),
+        List(
+          SquadPositionDetail("\\#ff0000 |||||||||||||||||||||||", "Just a space filler"),
+          SquadPositionDetail("\\#ffdc00   C", ""),
+          SquadPositionDetail("\\#ffdc00   H", "", "OpoIE"),
+          SquadPositionDetail("\\#ffdc00    I", "", "BobaF3tt907"),
+          SquadPositionDetail("\\#ffdc00   N", ""),
+          SquadPositionDetail("\\#ffdc00   A", ""),
+          SquadPositionDetail("\\#ff0000 |||||||||||||||||||||||", "Another space filler"),
+          SquadPositionDetail("\\#9640ff   K", ""),
+          SquadPositionDetail("\\#9640ff   O", "", "HofD"),
+          SquadPositionDetail("\\#9640ff   K", "")
+        )
+      )
+    )
   }
 
   def handleControlPkt(pkt : PlanetSideControlPacket) = {

From 9cf1cd92ccab54ce7580a645ba2bd104e431dc36 Mon Sep 17 00:00:00 2001
From: FateJH <Jason_DiDonato@yahoo.com>
Date: Thu, 6 Jun 2019 14:54:53 -0400
Subject: [PATCH 07/31] communication of the details information for a squad
 and refactored messaging back to WSA; removed an unused description field
 from the squad and added two fields that were actually represented

---
 .../psforever/objects/teamwork/Squad.scala    |  41 +-
 .../game/SquadDefinitionActionMessage.scala   |  28 +-
 .../SquadDetailDefinitionUpdateMessage.scala  |   2 +
 .../packet/game/SquadMembershipRequest.scala  |   2 +-
 .../services/teamwork/SquadResponse.scala     |   4 +-
 .../services/teamwork/SquadService.scala      | 472 ++++++++++++------
 .../teamwork/SquadServiceMessage.scala        |   4 +-
 .../SquadDefinitionActionMessageTest.scala    | 100 ++--
 .../src/main/scala/WorldSessionActor.scala    |  76 +--
 9 files changed, 467 insertions(+), 262 deletions(-)

diff --git a/common/src/main/scala/net/psforever/objects/teamwork/Squad.scala b/common/src/main/scala/net/psforever/objects/teamwork/Squad.scala
index 4ac3cfc2..21a48cb3 100644
--- a/common/src/main/scala/net/psforever/objects/teamwork/Squad.scala
+++ b/common/src/main/scala/net/psforever/objects/teamwork/Squad.scala
@@ -10,18 +10,21 @@ class Squad(squadId : PlanetSideGUID, alignment : PlanetSideEmpire.Value) extend
   private val faction : PlanetSideEmpire.Value = alignment //does not change
   private var zoneId : Option[Int] = None
   private var task : String = ""
-  private var description : String = ""
   private val membership : Array[Member] = Array.fill[Member](10)(new Member)
   private val availability : Array[Boolean] = Array.fill[Boolean](10)(true)
   private var listed : Boolean = false
   private var leaderPositionIndex : Int = 0
+  private var autoApproveInvitationRequests : Boolean = false
+  private var locationFollowsSquadLead : Boolean = false
 
   override def GUID_=(d : PlanetSideGUID) : PlanetSideGUID = GUID
 
   def Faction : PlanetSideEmpire.Value = faction
 
+  def CustomZoneId : Boolean = zoneId.isDefined
+
   def ZoneId : Int = zoneId.getOrElse({
-    membership.headOption match {
+    membership.lift(leaderPositionIndex) match {
       case Some(leader) =>
         leader.ZoneId
       case _ =>
@@ -45,13 +48,6 @@ class Squad(squadId : PlanetSideGUID, alignment : PlanetSideEmpire.Value) extend
     Task
   }
 
-  def Description : String = description
-
-  def Description_=(desc : String) : String = {
-    description = desc
-    Description
-  }
-
   def Listed : Boolean = listed
 
   def Listed_=(announce : Boolean) : Boolean = {
@@ -59,6 +55,20 @@ class Squad(squadId : PlanetSideGUID, alignment : PlanetSideEmpire.Value) extend
     Listed
   }
 
+  def LocationFollowsSquadLead : Boolean = locationFollowsSquadLead
+
+  def LocationFollowsSquadLead_=(follow : Boolean) : Boolean = {
+    locationFollowsSquadLead = follow
+    LocationFollowsSquadLead
+  }
+
+  def AutoApproveInvitationRequests : Boolean = autoApproveInvitationRequests
+
+  def AutoApproveInvitationRequests_=(autoApprove : Boolean) : Boolean = {
+    autoApproveInvitationRequests = autoApprove
+    AutoApproveInvitationRequests
+  }
+
   def Membership : Array[Member] = membership
 
   def Availability : Array[Boolean] = availability
@@ -85,3 +95,16 @@ class Squad(squadId : PlanetSideGUID, alignment : PlanetSideEmpire.Value) extend
 
   def Capacity : Int = availability.count(open => open)
 }
+
+object Squad {
+  final val Blank = new Squad(PlanetSideGUID(0), PlanetSideEmpire.NEUTRAL) {
+    override def ZoneId : Int = 0
+    override def ZoneId_=(id : Int) : Int = 0
+    override def ZoneId_=(id : Option[Int]) : Int = 0
+    override def Task_=(assignment : String) : String =  ""
+    override def Listed_=(announce : Boolean) : Boolean = false
+    override def Membership : Array[Member] = Array.empty[Member]
+    override def Availability : Array[Boolean] = Array.fill[Boolean](10)(false)
+    override def LeaderPositionIndex_=(position : Int) : Int = 0
+  }
+}
diff --git a/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala b/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala
index 40193730..7709a0d6 100644
--- a/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala
+++ b/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala
@@ -306,13 +306,13 @@ object SquadAction{
   * &nbsp;&nbsp;&nbsp;&nbsp;`36` - UNKNOWN<br>
   * &nbsp;&nbsp;`String :: Long :: Int :: Int`<br>
   * &nbsp;&nbsp;&nbsp;&nbsp;`34` - Search for Squads with a Particular Role
-  * @param unk1 na
-  * @param unk2 na
+  * @param squad_guid the unique identifier of the squad, if non-zero
+  * @param line the original listing line number, if applicable
   * @param action the purpose of this packet;
   *               also decides the content of the parameter fields
   */
-final case class SquadDefinitionActionMessage(unk1 : Int,
-                                              unk2 : Int,
+final case class SquadDefinitionActionMessage(squad_guid : PlanetSideGUID,
+                                              line : Int,
                                               action : SquadAction)
   extends PlanetSideGamePacket {
   type Packet = SquadDefinitionActionMessage
@@ -353,28 +353,28 @@ object SquadDefinitionActionMessage extends Marshallable[SquadDefinitionActionMe
       case 35 => cancelSquadSearchCodec
       case 40 => findLfsSoldiersForRoleCodec
       case 41 => cancelFindCodec
-      case 1 | 2 | 6 | 9 |
-           11 | 12 | 13 | 14 | 16 |
-           17 | 18 | 29 | 30 | 33 | 36 |
-           37 | 38 | 42 | 43 => unknownCodec(code)
+      case 1 | 2 | 6 | 9 | 11 |
+           12 | 13 | 14 | 16 | 17 |
+           18 | 29 | 30 | 32 | 33 |
+           36 | 37 | 38 | 42 | 43 => unknownCodec(code)
       case _ => failureCodec(code)
     }).asInstanceOf[Codec[SquadAction]]
   }
 
   implicit val codec : Codec[SquadDefinitionActionMessage] = (
     uintL(6) >>:~ { code =>
-      ("unk1" | uint16L) ::
-        ("unk2" | uint4L) ::
+      ("squad_guid" | PlanetSideGUID.codec) ::
+        ("line" | uint4L) ::
         ("action" | selectFromActionCode(code))
     }
     ).xmap[SquadDefinitionActionMessage] (
     {
-      case _ :: u1 :: u2 :: action :: HNil =>
-        SquadDefinitionActionMessage(u1, u2, action)
+      case _ :: guid :: line :: action :: HNil =>
+        SquadDefinitionActionMessage(guid, line, action)
     },
     {
-      case SquadDefinitionActionMessage(u1, u2, action) =>
-        action.code :: u1 :: u2 :: action :: HNil
+      case SquadDefinitionActionMessage(guid, line, action) =>
+        action.code :: guid :: line :: action :: HNil
     }
   )
 }
diff --git a/common/src/main/scala/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala b/common/src/main/scala/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala
index 291ef219..418fbd2f 100644
--- a/common/src/main/scala/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala
+++ b/common/src/main/scala/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala
@@ -51,6 +51,8 @@ object SquadPositionDetail {
   def apply(role : String, detailed_orders : String, requirements : Set[CertificationType.Value]) : SquadPositionDetail = SquadPositionDetail(is_closed = false, role, detailed_orders, requirements, 0L, "")
 
   def apply(role : String, detailed_orders : String, name : String) : SquadPositionDetail = SquadPositionDetail(is_closed = false, role, detailed_orders, Set.empty, reliableNameHash(name), name)
+
+  def apply(role : String, detailed_orders : String, requirements : Set[CertificationType.Value], name : String) : SquadPositionDetail = SquadPositionDetail(is_closed = false, role, detailed_orders, requirements, reliableNameHash(name), name)
 }
 
 object SquadDetailDefinitionUpdateMessage extends Marshallable[SquadDetailDefinitionUpdateMessage] {
diff --git a/common/src/main/scala/net/psforever/packet/game/SquadMembershipRequest.scala b/common/src/main/scala/net/psforever/packet/game/SquadMembershipRequest.scala
index 55d73197..27cf2b79 100644
--- a/common/src/main/scala/net/psforever/packet/game/SquadMembershipRequest.scala
+++ b/common/src/main/scala/net/psforever/packet/game/SquadMembershipRequest.scala
@@ -28,7 +28,7 @@ final case class SquadMembershipRequest(request_type : SquadRequestType.Value,
       assert(unk3.nonEmpty, s"a $request_type request requires the unk3 field be defined")
   }
   if(request_type == SquadRequestType.Invite) {
-    assert(unk5.nonEmpty, "an Invite request requires the unk5 field be undefined")
+    assert(unk5.nonEmpty, "an Invite request requires the unk5 field be defined")
   }
 
   type Packet = SquadMembershipRequest
diff --git a/common/src/main/scala/services/teamwork/SquadResponse.scala b/common/src/main/scala/services/teamwork/SquadResponse.scala
index 09f05fb3..b898580b 100644
--- a/common/src/main/scala/services/teamwork/SquadResponse.scala
+++ b/common/src/main/scala/services/teamwork/SquadResponse.scala
@@ -1,7 +1,7 @@
 // Copyright (c) 2019 PSForever
 package services.teamwork
 
-import net.psforever.packet.game.SquadInfo
+import net.psforever.packet.game.{PlanetSideGUID, PlanetSideZoneID, SquadInfo, SquadPositionDetail}
 
 object SquadResponse {
   trait Response
@@ -9,4 +9,6 @@ object SquadResponse {
   final case class Init(info : Vector[SquadInfo]) extends Response
   final case class Update(infos : Iterable[(Int, SquadInfo)]) extends Response
   final case class Remove(infos : Iterable[Int]) extends Response
+
+  final case class Detail(guid : PlanetSideGUID, leader : String, task : String, zone : PlanetSideZoneID, member_info : List[SquadPositionDetail]) extends Response
 }
diff --git a/common/src/main/scala/services/teamwork/SquadService.scala b/common/src/main/scala/services/teamwork/SquadService.scala
index 3572bd96..f7b96aa0 100644
--- a/common/src/main/scala/services/teamwork/SquadService.scala
+++ b/common/src/main/scala/services/teamwork/SquadService.scala
@@ -3,9 +3,9 @@ package services.teamwork
 
 import akka.actor.Actor
 import net.psforever.objects.Player
-import net.psforever.objects.teamwork.Squad
+import net.psforever.objects.teamwork.{Member, Squad}
 import net.psforever.packet.game._
-import net.psforever.types.PlanetSideEmpire
+import net.psforever.types.{PlanetSideEmpire, Vector3}
 import services.{GenericEventBus, Service}
 
 import scala.collection.concurrent.TrieMap
@@ -17,7 +17,7 @@ class SquadService extends Actor {
   private var memberToSquad : TrieMap[String, Squad] = new TrieMap[String, Squad]()
   private var idToSquad : TrieMap[PlanetSideGUID, Squad] = new TrieMap[PlanetSideGUID, Squad]()
   private var i : Int = 1
-  private var publishedLists : TrieMap[PlanetSideEmpire.Value, ListBuffer[SquadInfo]] = TrieMap[PlanetSideEmpire.Value, ListBuffer[SquadInfo]](
+  private val publishedLists : TrieMap[PlanetSideEmpire.Value, ListBuffer[SquadInfo]] = TrieMap[PlanetSideEmpire.Value, ListBuffer[SquadInfo]](
     PlanetSideEmpire.TR -> ListBuffer.empty,
     PlanetSideEmpire.NC -> ListBuffer.empty,
     PlanetSideEmpire.VS -> ListBuffer.empty
@@ -41,28 +41,51 @@ class SquadService extends Actor {
     PlanetSideGUID(out)
   }
 
-  def GetSquadFromPlayer(player : Player) : Squad = {
-    val name = player.Name
-    val faction = player.Faction
-    memberToSquad.get(name) match {
-      case Some(squad) =>
-        squad
+  def GetParticipatingSquad(player : Player, zone : Int) : Option[Squad] = {
+    memberToSquad.get(player.Name) match {
+      case opt @ Some(squad) =>
+        squad.Membership.find(_.Name == player.Name).get.ZoneId = zone
+        opt
       case None =>
-        val id = GetNextSquadId()
-        val squad = new Squad(id, faction)
-        val leadPosition = squad.Membership(squad.LeaderPositionIndex)
-        leadPosition.Name = name
-        leadPosition.Health = player.Health
-        leadPosition.Armor = player.Armor
-        leadPosition.Position = player.Position
-        leadPosition.ZoneId = 1 //player.Continent //TODO how to resolve this?
-        log.info(s"$name-$faction has started a new squad")
-        memberToSquad += name -> squad
-        idToSquad += id -> squad
-        squad
+        None
     }
   }
 
+  def GetLeadingSquad(player : Player, zone : Int, opt : Option[Squad]) : Squad = {
+    val name = player.Name
+    val squadOut = opt match {
+      case Some(squad) =>
+        if(squad.Leader.equals(name)) {
+          squad
+        }
+        else {
+          GetLeadingSquad(player, zone, None)
+        }
+
+      case None =>
+        memberToSquad.get(name) match {
+          case Some(squad) if squad.Leader.equals(name) =>
+            squad
+          case _ =>
+            val faction = player.Faction
+            val id = GetNextSquadId()
+            val squad = new Squad(id, faction)
+            val leadPosition = squad.Membership(squad.LeaderPositionIndex)
+            leadPosition.Name = name
+            leadPosition.Health = player.Health
+            leadPosition.Armor = player.Armor
+            leadPosition.Position = player.Position
+            leadPosition.ZoneId = zone
+            log.info(s"$name-$faction has started a new squad")
+            memberToSquad += name -> squad
+            idToSquad += id -> squad
+            squad
+        }
+    }
+    squadOut.Membership(squadOut.LeaderPositionIndex).ZoneId = zone
+    squadOut
+  }
+
   val SquadEvents = new GenericEventBus[SquadServiceResponse]
 
   def receive : Receive = {
@@ -84,167 +107,306 @@ class SquadService extends Actor {
       //check for renewable squad information
       memberToSquad.get(name) match {
         case None => ;
-        case Some(squad) =>
-          sender ! SquadServiceMessage.RecoverSquadMembership()
+        case Some(_) =>
+          sender ! SquadServiceMessage.RecoverSquadMembership() //TODO?
       }
 
     case Service.Leave(Some(name)) => ;
       SquadEvents.unsubscribe(sender())
+      memberToSquad.get(name) match {
+        case Some(squad) =>
+          if(squad.Leader.equals(name)) {
+            //we were the leader
+            if(squad.Membership.count(p => p.Name.equals("")) > 1) {
+              //other players were in the squad; publicly disband it
+              squad.Membership.foreach(position => {
+                position.Name = ""
+                position.ZoneId = 0
+                position.Position = Vector3.Zero
+                position.Health = 0
+                position.Armor = 0
+              })
+            }
+            memberToSquad.remove(name)
+            idToSquad.remove(squad.GUID)
+            UpdateSquadList(squad, List())
+          }
+          else {
+            //we were just a grunt in the squad
+            val position = squad.Membership.find(_.Name == name).get
+            position.Name = ""
+            position.ZoneId = 0
+            position.Position = Vector3.Zero
+            position.Health = 0
+            position.Armor = 0
+            UpdateSquadDetail(squad)
+          }
+        case None => ;
+      }
       //TODO leave squad, if joined to one, and perform clean-up
 
-    case Service.Leave(None) | Service.LeaveAll() =>
-      SquadEvents.unsubscribe(sender())
-      //TODO might be better to invalidate these
+    case Service.Leave(None) | Service.LeaveAll() => ;
 
-    case SquadServiceMessage.SquadDefinitionAction(tplayer, zone_ordinal_number, _, _, action) =>
+    case SquadServiceMessage.SquadDefinitionAction(tplayer, zone_ordinal_number, guid, _, action) =>
       import net.psforever.packet.game.SquadAction._
-      val squad = GetSquadFromPlayer(tplayer)
-      val member = squad.Membership.find(_.Name == tplayer.Name).get //should never fail
-      member.ZoneId = zone_ordinal_number //TODO improve this requirement
-      if(tplayer.Name.equals(squad.Leader)) {
-        var listingChanged : List[Int] = Nil
-        action match {
-          case ChangeSquadPurpose(purpose) =>
-            log.info(s"${tplayer.Name}-${tplayer.Faction} has changed his squad's task to $purpose")
-            squad.Description = purpose
-            listingChanged = List(SquadInfo.Field.Task)
+      val squadOpt = GetParticipatingSquad(tplayer, zone_ordinal_number)
+      action match {
+        case ChangeSquadPurpose(purpose) =>
+          log.info(s"${tplayer.Name}-${tplayer.Faction} has changed his squad's task to $purpose")
+          val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt)
+          squad.Task = purpose
+          UpdateSquadList(squad, List(SquadInfo.Field.Task))
+          UpdateSquadDetail(squad)
 
-          case ChangeSquadZone(zone) =>
-            log.info(s"${tplayer.Name}-${tplayer.Faction} has changed his squad's ops zone to $zone")
-            squad.ZoneId = zone.zoneId.toInt
-            listingChanged = List(SquadInfo.Field.ZoneId)
+        case ChangeSquadZone(zone) =>
+          log.info(s"${tplayer.Name}-${tplayer.Faction} has changed squad's ops zone to $zone")
+          val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt)
+          squad.ZoneId = zone.zoneId.toInt
+          UpdateSquadList(squad, List(SquadInfo.Field.ZoneId))
+          UpdateSquadDetail(squad)
 
-          case CloseSquadMemberPosition(position) =>
-            if(position != squad.LeaderPositionIndex) {
-              squad.Availability.lift(position) match {
-                case Some(true) =>
-                  squad.Availability.update(position, false)
-                  log.info(s"${tplayer.Name}-${tplayer.Faction} has closed the #$position position in his squad")
-                  val memberPosition = squad.Membership(position)
-                  listingChanged = if(memberPosition.Name.nonEmpty) {
-                    List(SquadInfo.Field.Size, SquadInfo.Field.Capacity)
+        case CloseSquadMemberPosition(position) =>
+          val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt)
+          squad.Availability.lift(position) match {
+            case Some(true) =>
+              squad.Availability.update(position, false)
+              log.info(s"${tplayer.Name}-${tplayer.Faction} has closed the #$position position in squad")
+              val memberPosition = squad.Membership(position)
+              val listingChanged = if(memberPosition.Name.nonEmpty) {
+                List(SquadInfo.Field.Size, SquadInfo.Field.Capacity)
+              }
+              else {
+                List(SquadInfo.Field.Capacity)
+              }
+              memberPosition.Close()
+              UpdateSquadList(squad, listingChanged)
+              UpdateSquadDetail(squad)
+            case Some(false) | None => ;
+          }
+
+        case AddSquadMemberPosition(position) =>
+          val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt)
+          squad.Availability.lift(position) match {
+            case Some(false) =>
+              log.info(s"${tplayer.Name}-${tplayer.Faction} has opened the #$position position in squad")
+              squad.Availability.update(position, true)
+              UpdateSquadList(squad, List(SquadInfo.Field.Capacity))
+              UpdateSquadDetail(squad)
+            case Some(true) | None => ;
+          }
+
+        case ChangeSquadMemberRequirementsRole(position, role) =>
+          val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt)
+          squad.Availability.lift(position) match {
+            case Some(true) =>
+              log.info(s"${tplayer.Name}-${tplayer.Faction} has changed the role of squad position #$position")
+              squad.Membership(position).Role = role
+              UpdateSquadDetail(squad)
+            case Some(false) | None => ;
+          }
+
+        case ChangeSquadMemberRequirementsDetailedOrders(position, orders) =>
+          val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt)
+          squad.Availability.lift(position) match {
+            case Some(true) =>
+              log.info(s"${tplayer.Name}-${tplayer.Faction} has changed the orders for squad position #$position")
+              squad.Membership(position).Orders = orders
+              UpdateSquadDetail(squad)
+            case Some(false) | None => ;
+          }
+
+        case ChangeSquadMemberRequirementsCertifications(position, certs) =>
+          val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt)
+          squad.Availability.lift(position) match {
+            case Some(true) =>
+              log.info(s"${tplayer.Name}-${tplayer.Faction} has changed the requirements for squad position #$position")
+              squad.Membership(position).Requirements = certs
+              UpdateSquadDetail(squad)
+            case Some(false) | None => ;
+          }
+
+        case LocationFollowsSquadLead(state) =>
+          val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt)
+          if(state) {
+            log.info(s"${tplayer.Name}-${tplayer.Faction} has moves the rally to the leader's position")
+          }
+          else {
+            log.info(s"${tplayer.Name}-${tplayer.Faction} has let the rally move freely")
+          }
+          squad.LocationFollowsSquadLead = state
+
+        case AutoApproveInvitationRequests(state) =>
+          val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt)
+          if(state) {
+            log.info(s"${tplayer.Name}-${tplayer.Faction} is allowing all requests to join the squad")
+          }
+          else {
+            log.info(s"${tplayer.Name}-${tplayer.Faction} has started screening invitation requests")
+          }
+          squad.AutoApproveInvitationRequests = state
+
+        case SelectRoleForYourself(line) =>
+          //TODO need to ask permission from the squad leader, unless our character is the squad leader or already currently in the squad
+          val name = tplayer.Name
+          squadOpt match {
+            case Some(squad) =>
+              {
+                if(squad.Availability(line))
+                  squad.Membership.lift(line)
+                else
+                  None
+              } match {
+                case Some(desiredPosition : Member)
+                  if desiredPosition.Requirements.intersect(tplayer.Certifications) == desiredPosition.Requirements =>
+                  //our character is qualified for this new position
+                  if(squad.Leader.equals(tplayer.Name)) {
+                    squad.LeaderPositionIndex = line //update
                   }
-                  else {
-                    List(SquadInfo.Field.Capacity)
+                  val hadPreviousPosition = squad.Membership.find(_.Name == name) match {
+                    case Some(currentPosition)=>
+                      currentPosition.Name = ""
+                      currentPosition.ZoneId = 0
+                      currentPosition.Health = 0
+                      currentPosition.Armor = 0
+                      currentPosition.Position = Vector3.Zero
+                      true
+                    case None =>
+                      false
                   }
-                  memberPosition.Close()
-                case Some(false) => ;
+                  desiredPosition.Name = name
+                  desiredPosition.ZoneId = zone_ordinal_number
+                  desiredPosition.Health = tplayer.Health
+                  desiredPosition.Armor = tplayer.Armor
+                  desiredPosition.Position = tplayer.Position
+                  if(!hadPreviousPosition) {
+                    UpdateSquadList(squad, List(SquadInfo.Field.Size))
+                  }
+                  UpdateSquadDetail(squad)
                 case None => ;
               }
-            }
-            else {
-              log.warn(s"can not close the leader position in squad-${squad.GUID.guid}")
-            }
 
-          case AddSquadMemberPosition(position) =>
-            squad.Availability.lift(position) match {
-              case Some(false) =>
-                log.info(s"${tplayer.Name}-${tplayer.Faction} has opened the #$position position in his squad")
-                squad.Availability.update(position, true)
-                listingChanged = List(SquadInfo.Field.Capacity)
-              case Some(true) => ;
-              case None => ;
-            }
-
-          case ChangeSquadMemberRequirementsRole(position, role) =>
-            squad.Availability.lift(position) match {
-              case Some(true) =>
-                log.info(s"${tplayer.Name}-${tplayer.Faction} has changed the role of squad position #$position")
-                squad.Membership(position).Role = role
-              case Some(false) => ;
-              case None => ;
-            }
-
-          case ChangeSquadMemberRequirementsDetailedOrders(position, orders) =>
-            squad.Availability.lift(position) match {
-              case Some(true) =>
-                log.info(s"${tplayer.Name}-${tplayer.Faction} has changed the orders for squad position #$position")
-                squad.Membership(position).Orders = orders
-              case Some(false) => ;
-              case None => ;
-            }
-
-          case ChangeSquadMemberRequirementsCertifications(position, certs) =>
-            squad.Availability.lift(position) match {
-              case Some(true) =>
-                log.info(s"${tplayer.Name}-${tplayer.Faction} has changed the requirements for squad position #$position")
-                squad.Membership(position).Requirements = certs
-              case Some(false) => ;
-              case None => ;
-            }
-
-          case ListSquad() =>
-            if(!squad.Listed) {
-              log.info(s"${tplayer.Name}-${tplayer.Faction} has opened recruitment for his squad")
-              squad.Listed = true
-            }
-
-          case ResetAll() =>
-            squad.Description = ""
-            squad.ZoneId = None
-            squad.Availability.indices.foreach { i =>
-              squad.Availability.update(i, true)
-            }
-          //TODO squad members?
-
-          case _ => ;
-        }
-        //queue updates
-        if(squad.Listed) {
-          val entry = SquadService.Publish(squad)
-          val faction = squad.Faction
-          val factionListings = publishedLists(faction)
-          factionListings.find(info => {
-            info.squad_guid match {
-              case Some(guid) => guid == squad.GUID
-              case _ => false
-            }
-          }) match {
-            case Some(listedSquad) =>
-              val index = factionListings.indexOf(listedSquad)
-              val changes = if(listingChanged.nonEmpty) {
-                SquadService.Differences(listingChanged, entry)
-              }
-              else {
-                SquadService.Differences(listedSquad, entry)
-              }
-              if(changes != SquadInfo.Blank) {
-                //squad information update
-                log.info(s"Squad will be updated")
-                factionListings(index) = entry
-                SquadEvents.publish(
-                  SquadServiceResponse(s"$faction/Squad", SquadResponse.Update(Seq((index, changes))))
-                )
-              }
-              else {
-                //remove squad from listing
-                log.info(s"Squad will be removed")
-                factionListings.remove(index)
-                SquadEvents.publish(
-                  SquadServiceResponse(s"$faction/Squad", SquadResponse.Remove(Seq(index)))
-                )
-              }
-            case None =>
-              //first time being published
-              log.info(s"Squad will be introduced")
-              factionListings += SquadService.Publish(squad)
-              SquadEvents.publish(
-                SquadServiceResponse(s"$faction/Squad", SquadResponse.Init(factionListings.toVector))
-              )
+            case None => ;
           }
-        }
+
+        case ListSquad() =>
+          val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt)
+          if(!squad.Listed) {
+            log.info(s"${tplayer.Name}-${tplayer.Faction} has opened recruitment for this squad")
+            squad.Listed = true
+          }
+          UpdateSquadList(squad, List())
+
+        case ResetAll() =>
+          val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt)
+          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()
+          })
+          UpdateSquadList(squad, List(SquadInfo.Field.Task, SquadInfo.Field.ZoneId, SquadInfo.Field.Size, SquadInfo.Field.Capacity))
+          UpdateSquadDetail(squad)
+
+        case DisplaySquad() =>
+          idToSquad.get(guid) match {
+            case Some(squad) =>
+              sender ! SquadServiceResponse(s"${tplayer.Name}/Squad", GenSquadDetail(squad))
+            case None => ;
+          }
+
+        case _ => ;
       }
 
     case msg =>
       log.info(s"Unhandled message $msg from $sender")
   }
+
+  def UpdateSquadList(squad : Squad, listingChanged : List[Int]) : Unit = {
+    //queue updates
+    if(squad.Listed) {
+      val entry = SquadService.Publish(squad)
+      val faction = squad.Faction
+      val factionListings = publishedLists(faction)
+      factionListings.find(info => {
+        info.squad_guid match {
+          case Some(sguid) => sguid == squad.GUID
+          case _ => false
+        }
+      }) match {
+        case Some(listedSquad) =>
+          val index = factionListings.indexOf(listedSquad)
+          val changes = if(listingChanged.nonEmpty) {
+            SquadService.Differences(listingChanged, entry)
+          }
+          else {
+            SquadService.Differences(listedSquad, entry)
+          }
+          if(changes != SquadInfo.Blank) {
+            //squad information update
+            log.info(s"Squad will be updated")
+            factionListings(index) = entry
+            SquadEvents.publish(
+              SquadServiceResponse(s"$faction/Squad", SquadResponse.Update(Seq((index, changes))))
+            )
+          }
+          else {
+            //remove squad from listing
+            log.info(s"Squad will be removed")
+            factionListings.remove(index)
+            SquadEvents.publish(
+              SquadServiceResponse(s"$faction/Squad", SquadResponse.Remove(Seq(index)))
+            )
+          }
+        case None =>
+          //first time being published
+          log.info(s"Squad will be introduced")
+          factionListings += SquadService.Publish(squad)
+          SquadEvents.publish(
+            SquadServiceResponse(s"$faction/Squad", SquadResponse.Init(factionListings.toVector))
+          )
+      }
+    }
+  }
+
+  def GenSquadDetail(squad : Squad) : SquadResponse.Detail = {
+    SquadResponse.Detail(
+      squad.GUID,
+      squad.Leader,
+      squad.Task,
+      PlanetSideZoneID(squad.ZoneId),
+      squad.Membership.zipWithIndex.map({ case (p, index) =>
+        if(squad.Availability(index)) {
+          SquadPositionDetail(p.Role, p.Orders, p.Requirements, p.Name)
+        }
+        else {
+          SquadPositionDetail.Closed
+        }
+      }).toList
+    )
+  }
+
+  def UpdateSquadDetail(squad : Squad) : Unit = {
+    val detail = GenSquadDetail(squad)
+    squad.Membership.collect {
+      case member if !member.Name.equals("") =>
+        member.Name
+    }.foreach { name =>
+      SquadEvents.publish(
+        SquadServiceResponse(s"$name/Squad", detail)
+      )
+    }
+  }
 }
 
 object SquadService {
   def Publish(squad : Squad) : SquadInfo = {
     SquadInfo(
       squad.Leader,
-      squad.Description,
+      squad.Task,
       PlanetSideZoneID(squad.ZoneId),
       squad.Size,
       squad.Capacity,
diff --git a/common/src/main/scala/services/teamwork/SquadServiceMessage.scala b/common/src/main/scala/services/teamwork/SquadServiceMessage.scala
index 5f918787..2157d44c 100644
--- a/common/src/main/scala/services/teamwork/SquadServiceMessage.scala
+++ b/common/src/main/scala/services/teamwork/SquadServiceMessage.scala
@@ -2,12 +2,12 @@
 package services.teamwork
 
 import net.psforever.objects.Player
-import net.psforever.packet.game.SquadAction
+import net.psforever.packet.game.{PlanetSideGUID, SquadAction}
 
 final case class SquadServiceMessage(forChannel : String, actionMessage : Any)
 
 object SquadServiceMessage {
-  final case class SquadDefinitionAction(player : Player, zone_ordinal_number : Int, u1 : Int, u2 : Int, action : SquadAction)
+  final case class SquadDefinitionAction(player : Player, zone_ordinal_number : Int, guid : PlanetSideGUID, line : Int, action : SquadAction)
 
   final case class RecoverSquadMembership()
 }
diff --git a/common/src/test/scala/game/SquadDefinitionActionMessageTest.scala b/common/src/test/scala/game/SquadDefinitionActionMessageTest.scala
index 0acd6c8f..f36b61c5 100644
--- a/common/src/test/scala/game/SquadDefinitionActionMessageTest.scala
+++ b/common/src/test/scala/game/SquadDefinitionActionMessageTest.scala
@@ -41,7 +41,7 @@ class SquadDefinitionActionMessageTest extends Specification {
   "decode (00)" in {
     PacketCoding.DecodePacket(string_00).require match {
       case SquadDefinitionActionMessage(unk1, unk2, action) =>
-        unk1 mustEqual 3
+        unk1 mustEqual PlanetSideGUID(3)
         unk2 mustEqual 0
         action mustEqual DisplaySquad()
       case _ =>
@@ -52,7 +52,7 @@ class SquadDefinitionActionMessageTest extends Specification {
   "decode (03)" in {
     PacketCoding.DecodePacket(string_03).require match {
       case SquadDefinitionActionMessage(unk1, unk2, action) =>
-        unk1 mustEqual 0
+        unk1 mustEqual PlanetSideGUID(0)
         unk2 mustEqual 3
         action mustEqual SaveSquadDefinition()
       case _ =>
@@ -63,7 +63,7 @@ class SquadDefinitionActionMessageTest extends Specification {
   "decode (03)" in {
     PacketCoding.DecodePacket(string_04).require match {
       case SquadDefinitionActionMessage(unk1, unk2, action) =>
-        unk1 mustEqual 0
+        unk1 mustEqual PlanetSideGUID(0)
         unk2 mustEqual 3
         action mustEqual LoadSquadDefinition()
       case _ =>
@@ -74,7 +74,7 @@ class SquadDefinitionActionMessageTest extends Specification {
   "decode (07)" in {
     PacketCoding.DecodePacket(string_07).require match {
       case SquadDefinitionActionMessage(unk1, unk2, action) =>
-        unk1 mustEqual 0
+        unk1 mustEqual PlanetSideGUID(0)
         unk2 mustEqual 3
         action mustEqual ListSquadDefinition("Cops and Military Officers")
       case _ =>
@@ -85,7 +85,7 @@ class SquadDefinitionActionMessageTest extends Specification {
   "decode (08)" in {
     PacketCoding.DecodePacket(string_08).require match {
       case SquadDefinitionActionMessage(unk1, unk2, action) =>
-        unk1 mustEqual 0
+        unk1 mustEqual PlanetSideGUID(0)
         unk2 mustEqual 0
         action mustEqual ListSquad()
       case _ =>
@@ -96,7 +96,7 @@ class SquadDefinitionActionMessageTest extends Specification {
   "decode (10)" in {
     PacketCoding.DecodePacket(string_10).require match {
       case SquadDefinitionActionMessage(unk1, unk2, action) =>
-        unk1 mustEqual 0
+        unk1 mustEqual PlanetSideGUID(0)
         unk2 mustEqual 0
         action mustEqual SelectRoleForYourself(1)
       case _ =>
@@ -107,7 +107,7 @@ class SquadDefinitionActionMessageTest extends Specification {
   "decode (19)" in {
     PacketCoding.DecodePacket(string_19).require match {
       case SquadDefinitionActionMessage(unk1, unk2, action) =>
-        unk1 mustEqual 0
+        unk1 mustEqual PlanetSideGUID(0)
         unk2 mustEqual 0
         action mustEqual ChangeSquadPurpose("A-Team")
       case _ =>
@@ -118,7 +118,7 @@ class SquadDefinitionActionMessageTest extends Specification {
   "decode (20)" in {
     PacketCoding.DecodePacket(string_20).require match {
       case SquadDefinitionActionMessage(unk1, unk2, action) =>
-        unk1 mustEqual 0
+        unk1 mustEqual PlanetSideGUID(0)
         unk2 mustEqual 0
         action mustEqual ChangeSquadZone(PlanetSideZoneID(1))
       case _ =>
@@ -129,7 +129,7 @@ class SquadDefinitionActionMessageTest extends Specification {
   "decode (21)" in {
     PacketCoding.DecodePacket(string_21).require match {
       case SquadDefinitionActionMessage(unk1, unk2, action) =>
-        unk1 mustEqual 0
+        unk1 mustEqual PlanetSideGUID(0)
         unk2 mustEqual 0
         action mustEqual CloseSquadMemberPosition(2)
       case _ =>
@@ -140,7 +140,7 @@ class SquadDefinitionActionMessageTest extends Specification {
   "decode (22)" in {
     PacketCoding.DecodePacket(string_22).require match {
       case SquadDefinitionActionMessage(unk1, unk2, action) =>
-        unk1 mustEqual 0
+        unk1 mustEqual PlanetSideGUID(0)
         unk2 mustEqual 0
         action mustEqual AddSquadMemberPosition(2)
       case _ =>
@@ -151,7 +151,7 @@ class SquadDefinitionActionMessageTest extends Specification {
   "decode (23)" in {
     PacketCoding.DecodePacket(string_23).require match {
       case SquadDefinitionActionMessage(unk1, unk2, action) =>
-        unk1 mustEqual 0
+        unk1 mustEqual PlanetSideGUID(0)
         unk2 mustEqual 0
         action mustEqual ChangeSquadMemberRequirementsRole(1, "BLUFOR")
       case _ =>
@@ -162,7 +162,7 @@ class SquadDefinitionActionMessageTest extends Specification {
   "decode (24)" in {
     PacketCoding.DecodePacket(string_24).require match {
       case SquadDefinitionActionMessage(unk1, unk2, action) =>
-        unk1 mustEqual 0
+        unk1 mustEqual PlanetSideGUID(0)
         unk2 mustEqual 0
         action mustEqual ChangeSquadMemberRequirementsDetailedOrders(1, "kill bad dudes")
       case _ =>
@@ -173,7 +173,7 @@ class SquadDefinitionActionMessageTest extends Specification {
   "decode (25)" in {
     PacketCoding.DecodePacket(string_25).require match {
       case SquadDefinitionActionMessage(unk1, unk2, action) =>
-        unk1 mustEqual 0
+        unk1 mustEqual PlanetSideGUID(0)
         unk2 mustEqual 0
         action mustEqual ChangeSquadMemberRequirementsCertifications(
           1,
@@ -187,7 +187,7 @@ class SquadDefinitionActionMessageTest extends Specification {
   "decode (26)" in {
     PacketCoding.DecodePacket(string_26).require match {
       case SquadDefinitionActionMessage(unk1, unk2, action) =>
-        unk1 mustEqual 0
+        unk1 mustEqual PlanetSideGUID(0)
         unk2 mustEqual 0
         action mustEqual ResetAll()
       case _ =>
@@ -198,7 +198,7 @@ class SquadDefinitionActionMessageTest extends Specification {
   "decode (28)" in {
     PacketCoding.DecodePacket(string_28).require match {
       case SquadDefinitionActionMessage(unk1, unk2, action) =>
-        unk1 mustEqual 0
+        unk1 mustEqual PlanetSideGUID(0)
         unk2 mustEqual 0
         action mustEqual AutoApproveInvitationRequests(true)
       case _ =>
@@ -209,7 +209,7 @@ class SquadDefinitionActionMessageTest extends Specification {
   "decode (31)" in {
     PacketCoding.DecodePacket(string_31).require match {
       case SquadDefinitionActionMessage(unk1, unk2, action) =>
-        unk1 mustEqual 0
+        unk1 mustEqual PlanetSideGUID(0)
         unk2 mustEqual 0
         action mustEqual LocationFollowsSquadLead(true)
       case _ =>
@@ -220,7 +220,7 @@ class SquadDefinitionActionMessageTest extends Specification {
   "decode (34a)" in {
     PacketCoding.DecodePacket(string_34a).require match {
       case SquadDefinitionActionMessage(unk1, unk2, action) =>
-        unk1 mustEqual 0
+        unk1 mustEqual PlanetSideGUID(0)
         unk2 mustEqual 0
         action mustEqual SearchForSquadsWithParticularRole("Badass", 0L, 1, 0)
       case _ =>
@@ -231,7 +231,7 @@ class SquadDefinitionActionMessageTest extends Specification {
   "decode (34b)" in {
     PacketCoding.DecodePacket(string_34b).require match {
       case SquadDefinitionActionMessage(unk1, unk2, action) =>
-        unk1 mustEqual 0
+        unk1 mustEqual PlanetSideGUID(0)
         unk2 mustEqual 0
         action mustEqual SearchForSquadsWithParticularRole("Badass", 0L, 2, 0)
       case _ =>
@@ -242,7 +242,7 @@ class SquadDefinitionActionMessageTest extends Specification {
   "decode (34c)" in {
     PacketCoding.DecodePacket(string_34c).require match {
       case SquadDefinitionActionMessage(unk1, unk2, action) =>
-        unk1 mustEqual 0
+        unk1 mustEqual PlanetSideGUID(0)
         unk2 mustEqual 0
         action mustEqual SearchForSquadsWithParticularRole("Badass", 0L, 2, 1)
       case _ =>
@@ -253,7 +253,7 @@ class SquadDefinitionActionMessageTest extends Specification {
   "decode (34d)" in {
     PacketCoding.DecodePacket(string_34d).require match {
       case SquadDefinitionActionMessage(unk1, unk2, action) =>
-        unk1 mustEqual 0
+        unk1 mustEqual PlanetSideGUID(0)
         unk2 mustEqual 0
         action mustEqual SearchForSquadsWithParticularRole("Badass", 536870928L, 2, 2)
       case _ =>
@@ -264,7 +264,7 @@ class SquadDefinitionActionMessageTest extends Specification {
   "decode (34e)" in {
     PacketCoding.DecodePacket(string_34e).require match {
       case SquadDefinitionActionMessage(unk1, unk2, action) =>
-        unk1 mustEqual 0
+        unk1 mustEqual PlanetSideGUID(0)
         unk2 mustEqual 0
         action mustEqual SearchForSquadsWithParticularRole("Badass", 536870928L, 2, 3)
       case _ =>
@@ -275,7 +275,7 @@ class SquadDefinitionActionMessageTest extends Specification {
   "decode (35)" in {
     PacketCoding.DecodePacket(string_35).require match {
       case SquadDefinitionActionMessage(unk1, unk2, action) =>
-        unk1 mustEqual 0
+        unk1 mustEqual PlanetSideGUID(0)
         unk2 mustEqual 0
         action mustEqual CancelSquadSearch()
       case _ =>
@@ -286,7 +286,7 @@ class SquadDefinitionActionMessageTest extends Specification {
   "decode (40)" in {
     PacketCoding.DecodePacket(string_40).require match {
       case SquadDefinitionActionMessage(unk1, unk2, action) =>
-        unk1 mustEqual 0
+        unk1 mustEqual PlanetSideGUID(0)
         unk2 mustEqual 0
         action mustEqual FindLfsSoldiersForRole(1)
       case _ =>
@@ -297,7 +297,7 @@ class SquadDefinitionActionMessageTest extends Specification {
   "decode (41)" in {
     PacketCoding.DecodePacket(string_41).require match {
       case SquadDefinitionActionMessage(unk1, unk2, action) =>
-        unk1 mustEqual 0
+        unk1 mustEqual PlanetSideGUID(0)
         unk2 mustEqual 0
         action mustEqual CancelFind()
       case _ =>
@@ -308,7 +308,7 @@ class SquadDefinitionActionMessageTest extends Specification {
   "decode (43, unknown)" in {
     PacketCoding.DecodePacket(string_43).require match {
       case SquadDefinitionActionMessage(unk1, unk2, action) =>
-        unk1 mustEqual 0
+        unk1 mustEqual PlanetSideGUID(0)
         unk2 mustEqual 0
         action mustEqual Unknown(43, hex"00".toBitVector.take(6))
       case _ =>
@@ -321,91 +321,91 @@ class SquadDefinitionActionMessageTest extends Specification {
   }
 
   "encode (00)" in {
-    val msg = SquadDefinitionActionMessage(3, 0, DisplaySquad())
+    val msg = SquadDefinitionActionMessage(PlanetSideGUID(3), 0, DisplaySquad())
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_00
   }
 
   "encode (03)" in {
-    val msg = SquadDefinitionActionMessage(0, 3, SaveSquadDefinition())
+    val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 3, SaveSquadDefinition())
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_03
   }
 
   "encode (03)" in {
-    val msg = SquadDefinitionActionMessage(0, 3, LoadSquadDefinition())
+    val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 3, LoadSquadDefinition())
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_04
   }
 
   "encode (07)" in {
-    val msg = SquadDefinitionActionMessage(0, 3, ListSquadDefinition("Cops and Military Officers"))
+    val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 3, ListSquadDefinition("Cops and Military Officers"))
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_07
   }
 
   "encode (08)" in {
-    val msg = SquadDefinitionActionMessage(0, 0, ListSquad())
+    val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, ListSquad())
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_08
   }
 
   "encode (10)" in {
-    val msg = SquadDefinitionActionMessage(0, 0, SelectRoleForYourself(1))
+    val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SelectRoleForYourself(1))
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_10
   }
 
   "encode (19)" in {
-    val msg = SquadDefinitionActionMessage(0, 0, ChangeSquadPurpose("A-Team"))
+    val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, ChangeSquadPurpose("A-Team"))
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_19
   }
 
   "encode (20)" in {
-    val msg = SquadDefinitionActionMessage(0, 0, ChangeSquadZone(PlanetSideZoneID(1)))
+    val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, ChangeSquadZone(PlanetSideZoneID(1)))
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_20
   }
 
   "encode (21)" in {
-    val msg = SquadDefinitionActionMessage(0, 0, CloseSquadMemberPosition(2))
+    val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, CloseSquadMemberPosition(2))
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_21
   }
 
   "encode (22)" in {
-    val msg = SquadDefinitionActionMessage(0, 0, AddSquadMemberPosition(2))
+    val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, AddSquadMemberPosition(2))
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_22
   }
 
   "encode (23)" in {
-    val msg = SquadDefinitionActionMessage(0, 0, ChangeSquadMemberRequirementsRole(1, "BLUFOR"))
+    val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, ChangeSquadMemberRequirementsRole(1, "BLUFOR"))
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_23
   }
 
   "encode (24)" in {
-    val msg = SquadDefinitionActionMessage(0, 0, ChangeSquadMemberRequirementsDetailedOrders(1, "kill bad dudes"))
+    val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, ChangeSquadMemberRequirementsDetailedOrders(1, "kill bad dudes"))
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_24
   }
 
   "encode (25)" in {
-    val msg = SquadDefinitionActionMessage(0, 0, ChangeSquadMemberRequirementsCertifications(
+    val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, ChangeSquadMemberRequirementsCertifications(
       1,
       Set(CertificationType.AntiVehicular, CertificationType.InfiltrationSuit)
     ))
@@ -415,84 +415,84 @@ class SquadDefinitionActionMessageTest extends Specification {
   }
 
   "encode (26)" in {
-    val msg = SquadDefinitionActionMessage(0, 0, ResetAll())
+    val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, ResetAll())
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_26
   }
 
   "encode (28)" in {
-    val msg = SquadDefinitionActionMessage(0, 0, AutoApproveInvitationRequests(true))
+    val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, AutoApproveInvitationRequests(true))
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_28
   }
 
   "encode (31)" in {
-    val msg = SquadDefinitionActionMessage(0, 0, LocationFollowsSquadLead(true))
+    val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, LocationFollowsSquadLead(true))
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_31
   }
 
   "encode (34a)" in {
-    val msg = SquadDefinitionActionMessage(0, 0, SearchForSquadsWithParticularRole("Badass", 0L, 1, 0))
+    val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SearchForSquadsWithParticularRole("Badass", 0L, 1, 0))
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_34a
   }
 
   "encode (34b)" in {
-    val msg = SquadDefinitionActionMessage(0, 0, SearchForSquadsWithParticularRole("Badass", 0L, 2, 0))
+    val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SearchForSquadsWithParticularRole("Badass", 0L, 2, 0))
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_34b
   }
 
   "encode (34c)" in {
-    val msg = SquadDefinitionActionMessage(0, 0, SearchForSquadsWithParticularRole("Badass", 0L, 2, 1))
+    val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SearchForSquadsWithParticularRole("Badass", 0L, 2, 1))
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_34c
   }
 
   "encode (34d)" in {
-    val msg = SquadDefinitionActionMessage(0, 0, SearchForSquadsWithParticularRole("Badass", 536870928L, 2, 2))
+    val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SearchForSquadsWithParticularRole("Badass", 536870928L, 2, 2))
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_34d
   }
 
   "encode (34e)" in {
-    val msg = SquadDefinitionActionMessage(0, 0, SearchForSquadsWithParticularRole("Badass", 536870928L, 2, 3))
+    val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SearchForSquadsWithParticularRole("Badass", 536870928L, 2, 3))
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_34e
   }
 
   "encode (35)" in {
-    val msg = SquadDefinitionActionMessage(0, 0, CancelSquadSearch())
+    val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, CancelSquadSearch())
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_35
   }
 
   "encode (40)" in {
-    val msg = SquadDefinitionActionMessage(0, 0, FindLfsSoldiersForRole(1))
+    val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, FindLfsSoldiersForRole(1))
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_40
   }
 
   "encode (41)" in {
-    val msg = SquadDefinitionActionMessage(0, 0, CancelFind())
+    val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, CancelFind())
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_41
   }
 
   "encode (43, unknown)" in {
-    val msg = SquadDefinitionActionMessage(0, 0, Unknown(43, BitVector.empty))
+    val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, Unknown(43, BitVector.empty))
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_43
diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala
index 9b83272a..8f22f277 100644
--- a/pslogin/src/main/scala/WorldSessionActor.scala
+++ b/pslogin/src/main/scala/WorldSessionActor.scala
@@ -370,6 +370,17 @@ class WorldSessionActor extends Actor with MDCContextAware {
               }.toVector
             )
           )
+
+        case SquadResponse.Detail(guid, leader, task, zone, member_info) =>
+          sendResponse(
+            SquadDetailDefinitionUpdateMessage(
+              guid,
+              leader,
+              task,
+              zone,
+              member_info
+            )
+          )
         case _ => ;
       }
 
@@ -2850,7 +2861,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
     })
     //AvatarAwardMessage
     //DisplayAwardMessage
-    //SquadDefinitionActionMessage and SquadDetailDefinitionUpdateMessage
+    //SquadDefinitionActionMessage and SquadDetailDefinitionUpdateMessage; handled elsewhere
     //MapObjectStateBlockMessage and ObjectCreateMessage?
     //TacticsMessage?
     //change the owner on our deployables (re-draw the icons for our deployables too)
@@ -2885,35 +2896,35 @@ class WorldSessionActor extends Actor with MDCContextAware {
         interstellarFerryTopLevelGUID = None
       case _ => ;
     }
-    sendResponse(ReplicationStreamMessage(
-      5,
-      Some(6),
-      Vector(
-        SquadListing(0, SquadInfo(Some("xNick"), Some("FLY,ALL WELCOME!"), Some(PlanetSideZoneID(7)), Some(8), Some(10), Some(PlanetSideGUID(1)))),
-        SquadListing(1, SquadInfo(Some("HofD"), Some("=KOK+SPC+FLY= All Welcome"), Some(PlanetSideZoneID(7)), Some(3), Some(10), Some(PlanetSideGUID(3))))
-      )
-    ))
-    //sendRawResponse(hex"e803008484000c800259e8809fda020043004a0069006d006d0079006e009b48006f006d006900630069006400690061006c00200053006d007500720066007300200041006e006f006e0079006d006f007500730004000000981401064580540061006e006b002000440072006900760065007200a05200650063006f006d006d0065006e00640065006400200074006f0020006800610076006500200065006e00670069006e0065006500720069006e0067002e0000000000800180000c00020c8c46007200650065006200690065002000730070006f007400cf44006f002000770068006100740065007600650072002c0020006200750074002000700072006500660065007200610062006c007900200073007400690063006b002000770069007400680020007400680065002000730071007500610064002e00200044006f006e002700740020006e00650065006400200061006e007900200073007000650063006900660069006300200063006500720074002e0096e27a0290540068006500460069006e0061006c005300740072007500670067006c0065000000000000020c8c46007200650065006200690065002000530070006f007400cf44006f002000770068006100740065007600650072002c0020006200750074002000700072006500660065007200610062006c007900200073007400690063006b002000770069007400680020007400680065002000730071007500610064002e00200044006f006e002700740020006e00650065006400200061006e007900200073007000650063006900660069006300200063006500720074002e0000000000800000000000020c8a41004d0053002000440072006900760065007200b34700690076006500200075007300200073007000610077006e00200070006f0069006e00740073002c0020006800610063006b0069006e006700200061006e006400200069006e00660069006c0020006100720065002000750073006500660075006c002e00fb02790287440030004f004d006700750079000100020c00020c8d410076006500720061006700650020004a0069006d006d007900a05200650069006e0066006f0072006300650064002000450078006f007300750069007400200077006f0075006c00640020006200650020006e0069006300650000000000800300000c00020c8b4100760065007200610067006500200042006f006200a05200650069006e0066006f0072006300650064002000450078006f007300750069007400200077006f0075006c00640020006200650020006e0069006300650000000000800300000c00020c8b410076006500720061006700650020004a006f006500a05200650069006e0066006f0072006300650064002000450078006f007300750069007400200077006f0075006c00640020006200650020006e0069006300650000000000800300000c00020c8753007500700070006f0072007400a2520065007300730075007200650063007400200073006f006c00640069006500720073002c0020006b00650065007000200075007300200061006c006900760065002e0000000000800100000c0c0a0c8845006e00670069006e00650065007200a043006f006d00620061007400200045006e00670069006e0065006500720069006e006700200077006f0075006c00640020006200650020006e0069006300650004b3d101864a0069006d006d0079006e000100000c000a0c854d0065006400690063009a4100640076002e0020004d00650064006900630061006c00200077006f0075006c00640020006200650020006e0069006300650000000000800100000c0400")
-    sendResponse(
-      SquadDetailDefinitionUpdateMessage(
-        PlanetSideGUID(3),
-        "HofD",
-        "\\#ffdc00***\\#9640ff=KOK+SPC+FLY=\\#ffdc00***\\#FF4040 All Welcome",
-        PlanetSideZoneID(7),
-        List(
-          SquadPositionDetail("\\#ff0000 |||||||||||||||||||||||", "Just a space filler"),
-          SquadPositionDetail("\\#ffdc00   C", ""),
-          SquadPositionDetail("\\#ffdc00   H", "", "OpoIE"),
-          SquadPositionDetail("\\#ffdc00    I", "", "BobaF3tt907"),
-          SquadPositionDetail("\\#ffdc00   N", ""),
-          SquadPositionDetail("\\#ffdc00   A", ""),
-          SquadPositionDetail("\\#ff0000 |||||||||||||||||||||||", "Another space filler"),
-          SquadPositionDetail("\\#9640ff   K", ""),
-          SquadPositionDetail("\\#9640ff   O", "", "HofD"),
-          SquadPositionDetail("\\#9640ff   K", "")
-        )
-      )
-    )
+//    sendResponse(ReplicationStreamMessage(
+//      5,
+//      Some(6),
+//      Vector(
+//        SquadListing(0, SquadInfo(Some("xNick"), Some("FLY,ALL WELCOME!"), Some(PlanetSideZoneID(7)), Some(8), Some(10), Some(PlanetSideGUID(1)))),
+//        SquadListing(1, SquadInfo(Some("HofD"), Some("=KOK+SPC+FLY= All Welcome"), Some(PlanetSideZoneID(7)), Some(3), Some(10), Some(PlanetSideGUID(3))))
+//      )
+//    ))
+//    //sendRawResponse(hex"e803008484000c800259e8809fda020043004a0069006d006d0079006e009b48006f006d006900630069006400690061006c00200053006d007500720066007300200041006e006f006e0079006d006f007500730004000000981401064580540061006e006b002000440072006900760065007200a05200650063006f006d006d0065006e00640065006400200074006f0020006800610076006500200065006e00670069006e0065006500720069006e0067002e0000000000800180000c00020c8c46007200650065006200690065002000730070006f007400cf44006f002000770068006100740065007600650072002c0020006200750074002000700072006500660065007200610062006c007900200073007400690063006b002000770069007400680020007400680065002000730071007500610064002e00200044006f006e002700740020006e00650065006400200061006e007900200073007000650063006900660069006300200063006500720074002e0096e27a0290540068006500460069006e0061006c005300740072007500670067006c0065000000000000020c8c46007200650065006200690065002000530070006f007400cf44006f002000770068006100740065007600650072002c0020006200750074002000700072006500660065007200610062006c007900200073007400690063006b002000770069007400680020007400680065002000730071007500610064002e00200044006f006e002700740020006e00650065006400200061006e007900200073007000650063006900660069006300200063006500720074002e0000000000800000000000020c8a41004d0053002000440072006900760065007200b34700690076006500200075007300200073007000610077006e00200070006f0069006e00740073002c0020006800610063006b0069006e006700200061006e006400200069006e00660069006c0020006100720065002000750073006500660075006c002e00fb02790287440030004f004d006700750079000100020c00020c8d410076006500720061006700650020004a0069006d006d007900a05200650069006e0066006f0072006300650064002000450078006f007300750069007400200077006f0075006c00640020006200650020006e0069006300650000000000800300000c00020c8b4100760065007200610067006500200042006f006200a05200650069006e0066006f0072006300650064002000450078006f007300750069007400200077006f0075006c00640020006200650020006e0069006300650000000000800300000c00020c8b410076006500720061006700650020004a006f006500a05200650069006e0066006f0072006300650064002000450078006f007300750069007400200077006f0075006c00640020006200650020006e0069006300650000000000800300000c00020c8753007500700070006f0072007400a2520065007300730075007200650063007400200073006f006c00640069006500720073002c0020006b00650065007000200075007300200061006c006900760065002e0000000000800100000c0c0a0c8845006e00670069006e00650065007200a043006f006d00620061007400200045006e00670069006e0065006500720069006e006700200077006f0075006c00640020006200650020006e0069006300650004b3d101864a0069006d006d0079006e000100000c000a0c854d0065006400690063009a4100640076002e0020004d00650064006900630061006c00200077006f0075006c00640020006200650020006e0069006300650000000000800100000c0400")
+//    sendResponse(
+//      SquadDetailDefinitionUpdateMessage(
+//        PlanetSideGUID(3),
+//        "HofD",
+//        "\\#ffdc00***\\#9640ff=KOK+SPC+FLY=\\#ffdc00***\\#FF4040 All Welcome",
+//        PlanetSideZoneID(7),
+//        List(
+//          SquadPositionDetail("\\#ff0000 |||||||||||||||||||||||", "Just a space filler"),
+//          SquadPositionDetail("\\#ffdc00   C", ""),
+//          SquadPositionDetail("\\#ffdc00   H", "", "OpoIE"),
+//          SquadPositionDetail("\\#ffdc00    I", "", "BobaF3tt907"),
+//          SquadPositionDetail("\\#ffdc00   N", ""),
+//          SquadPositionDetail("\\#ffdc00   A", ""),
+//          SquadPositionDetail("\\#ff0000 |||||||||||||||||||||||", "Another space filler"),
+//          SquadPositionDetail("\\#9640ff   K", ""),
+//          SquadPositionDetail("\\#9640ff   O", "", "HofD"),
+//          SquadPositionDetail("\\#9640ff   K", "")
+//        )
+//      )
+//    )
   }
 
   def handleControlPkt(pkt : PlanetSideControlPacket) = {
@@ -3325,6 +3336,11 @@ class WorldSessionActor extends Actor with MDCContextAware {
 
     case msg @ PlayerStateMessageUpstream(avatar_guid, pos, vel, yaw, pitch, yaw_upper, seq_time, unk3, is_crouching, is_jumping, unk4, is_cloaking, unk5, unk6) =>
       if(deadState == DeadState.Alive) {
+//        if(!player.Crouching && is_crouching) {
+//          sendResponse(
+//            SquadMembershipRequest(SquadRequestType.Unk01, 1L, Some(1L), "FateJH", Some(None))
+//          )
+//        }
         player.Position = pos
         player.Velocity = vel
         player.Orientation = Vector3(player.Orientation.x, pitch, yaw)

From 14bdcb7a7e83f7f1aae2d1d91b296d009a339dd0 Mon Sep 17 00:00:00 2001
From: FateJH <Jason_DiDonato@yahoo.com>
Date: Mon, 10 Jun 2019 16:26:31 -0400
Subject: [PATCH 08/31] a lot of squad-relatd setup for little payoff; initial
 packet and test for SquadMemberEvent, originally by Aphedox; initial packet
 and tests for SquadState; new SquadDefinitionAction actions; a common initial
 SquadDetailDefinitionUpdate form

---
 .../psforever/packet/GamePacketOpcode.scala   |   4 +-
 .../game/SquadDefinitionActionMessage.scala   |  14 +-
 .../SquadDetailDefinitionUpdateMessage.scala  |  19 ++
 .../packet/game/SquadMemberEvent.scala        |  61 +++++
 .../psforever/packet/game/SquadState.scala    |  76 ++++++
 .../scala/game/SquadMemberEventTest.scala     |  32 +++
 .../src/test/scala/game/SquadStateTest.scala  | 244 ++++++++++++++++++
 .../src/main/scala/WorldSessionActor.scala    |  88 ++++---
 8 files changed, 499 insertions(+), 39 deletions(-)
 create mode 100644 common/src/main/scala/net/psforever/packet/game/SquadMemberEvent.scala
 create mode 100644 common/src/main/scala/net/psforever/packet/game/SquadState.scala
 create mode 100644 common/src/test/scala/game/SquadMemberEventTest.scala
 create mode 100644 common/src/test/scala/game/SquadStateTest.scala

diff --git a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
index d0ab02cf..03ed5e25 100644
--- a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
+++ b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
@@ -450,14 +450,14 @@ object GamePacketOpcode extends Enumeration {
     case 0x6f => game.SquadMembershipResponse.decode
 
     // OPCODES 0x70-7f
-    case 0x70 => noDecoder(SquadMemberEvent)
+    case 0x70 => game.SquadMemberEvent.decode
     case 0x71 => noDecoder(PlatoonEvent)
     case 0x72 => game.FriendsRequest.decode
     case 0x73 => game.FriendsResponse.decode
     case 0x74 => game.TriggerEnvironmentalDamageMessage.decode
     case 0x75 => game.TrainingZoneMessage.decode
     case 0x76 => game.DeployableObjectsInfoMessage.decode
-    case 0x77 => noDecoder(SquadState)
+    case 0x77 => game.SquadState.decode
     // 0x78
     case 0x78 => game.OxygenStateMessage.decode
     case 0x79 => noDecoder(TradeMessage)
diff --git a/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala b/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala
index 7709a0d6..43009a3b 100644
--- a/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala
+++ b/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala
@@ -18,6 +18,8 @@ abstract class SquadAction(val code : Int)
 object SquadAction{
   final case class DisplaySquad() extends SquadAction(0)
 
+  final case class AnswerSquadJoinRequest() extends SquadAction(1)
+
   final case class SaveSquadDefinition() extends SquadAction(3)
 
   final case class LoadSquadDefinition() extends SquadAction(4)
@@ -74,6 +76,13 @@ object SquadAction{
       }
     )
 
+    val answerSquadJoinRequestCodec = everFailCondition.xmap[AnswerSquadJoinRequest] (
+      _ => AnswerSquadJoinRequest(),
+      {
+        case AnswerSquadJoinRequest() => None
+      }
+    )
+
     val saveSquadDefinitionCodec = everFailCondition.xmap[SaveSquadDefinition] (
       _ => SaveSquadDefinition(),
       {
@@ -259,7 +268,7 @@ object SquadAction{
   * The following formats are translated; their purposes are listed:<br>
   * &nbsp;&nbsp;`(None)`<br>
   * &nbsp;&nbsp;&nbsp;&nbsp;`0 ` - Display Squad<br>
-  * &nbsp;&nbsp;&nbsp;&nbsp;`1 ` - UNKNOWN<br>
+  * &nbsp;&nbsp;&nbsp;&nbsp;`1 ` - Answer Squad Join Request<br>
   * &nbsp;&nbsp;&nbsp;&nbsp;`2 ` - UNKNOWN<br>
   * &nbsp;&nbsp;&nbsp;&nbsp;`3 ` - Save Squad Definition<br>
   * &nbsp;&nbsp;&nbsp;&nbsp;`4 ` - Load Squad Definition<br>
@@ -333,6 +342,7 @@ object SquadDefinitionActionMessage extends Marshallable[SquadDefinitionActionMe
     import scala.annotation.switch
     ((code : @switch) match {
       case 0 => displaySquadCodec
+      case 1 => answerSquadJoinRequestCodec
       case 3 => saveSquadDefinitionCodec
       case 4 => loadSquadDefinitionCodec
       case 7 => listSquadDefinitionCodec
@@ -353,7 +363,7 @@ object SquadDefinitionActionMessage extends Marshallable[SquadDefinitionActionMe
       case 35 => cancelSquadSearchCodec
       case 40 => findLfsSoldiersForRoleCodec
       case 41 => cancelFindCodec
-      case 1 | 2 | 6 | 9 | 11 |
+      case 2 | 6 | 9 | 11 |
            12 | 13 | 14 | 16 | 17 |
            18 | 29 | 30 | 32 | 33 |
            36 | 37 | 38 | 42 | 43 => unknownCodec(code)
diff --git a/common/src/main/scala/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala b/common/src/main/scala/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala
index 418fbd2f..76fae022 100644
--- a/common/src/main/scala/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala
+++ b/common/src/main/scala/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala
@@ -62,6 +62,25 @@ object SquadDetailDefinitionUpdateMessage extends Marshallable[SquadDetailDefini
     CertificationType.AgileExoSuit
   )
 
+  final val Init = SquadDetailDefinitionUpdateMessage(
+    PlanetSideGUID(0),
+    "",
+    "",
+    PlanetSideZoneID(0),
+    List(
+      SquadPositionDetail(),
+      SquadPositionDetail(),
+      SquadPositionDetail(),
+      SquadPositionDetail(),
+      SquadPositionDetail(),
+      SquadPositionDetail(),
+      SquadPositionDetail(),
+      SquadPositionDetail(),
+      SquadPositionDetail(),
+      SquadPositionDetail()
+    )
+  )
+
   def apply(guid : PlanetSideGUID, leader_name : String, task : String, zone_id : PlanetSideZoneID, member_info : List[SquadPositionDetail]) : SquadDetailDefinitionUpdateMessage = {
     import scodec.bits._
     SquadDetailDefinitionUpdateMessage(guid, hex"080000000000000000000".toBitVector, leader_name, task, zone_id, member_info)
diff --git a/common/src/main/scala/net/psforever/packet/game/SquadMemberEvent.scala b/common/src/main/scala/net/psforever/packet/game/SquadMemberEvent.scala
new file mode 100644
index 00000000..df8bd7b0
--- /dev/null
+++ b/common/src/main/scala/net/psforever/packet/game/SquadMemberEvent.scala
@@ -0,0 +1,61 @@
+// Copyright (c) 2019 PSForever
+package net.psforever.packet.game
+
+import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
+import scodec.{Attempt, Codec, Err}
+import scodec.codecs._
+import shapeless.{::, HNil}
+
+final case class SquadMemberEvent(unk1 : Int,
+                                  unk2 : Int,
+                                  unk3 : Long,
+                                  unk4 : Int,
+                                  unk5 : Option[String],
+                                  unk6 : Option[Int],
+                                  unk7 : Option[Long])
+  extends PlanetSideGamePacket {
+  type Packet = SquadMemberEvent
+  def opcode = GamePacketOpcode.SquadMemberEvent
+  def encode = SquadMemberEvent.encode(this)
+}
+
+object SquadMemberEvent extends Marshallable[SquadMemberEvent] {
+  def apply(unk1 : Int, unk2 : Int, unk3 : Long, unk4 : Int) : SquadMemberEvent =
+    SquadMemberEvent(unk1, unk2, unk3, unk4, None, None, None)
+
+  def apply(unk2 : Int, unk3 : Long, unk4 : Int, unk5 : String, unk6 : Int, unk7 : Long) : SquadMemberEvent =
+    SquadMemberEvent(0, unk2, unk3, unk4, Some(unk5), Some(unk6), Some(unk7))
+
+  def apply(unk2 : Int, unk3 : Long, unk4 : Int, unk6 : Int) : SquadMemberEvent =
+    SquadMemberEvent(3, unk2, unk3, unk4, None, Some(unk6), None)
+
+  def apply(unk2 : Int, unk3 : Long, unk4 : Int, unk7 : Long) : SquadMemberEvent =
+    SquadMemberEvent(4, unk2, unk3, unk4, None, None, Some(unk7))
+
+  implicit val codec : Codec[SquadMemberEvent] = (
+    ("unk1" | uintL(3)) >>:~ { unk1 =>
+      ("unk2" | uint16L) ::
+        ("unk3" | uint32L) ::
+        ("unk4" | uintL(4)) ::
+        conditional(unk1 == 0, "unk5" | PacketHelpers.encodedWideStringAligned(1)) ::
+        conditional(unk1 == 0 || unk1 == 3, "unk6" | uint16L) ::
+        conditional(unk1 == 0 || unk1 == 4, "unk7" | uint32L)
+    }).exmap[SquadMemberEvent] (
+    {
+      case unk1 :: unk2 :: unk3 :: unk4 :: unk5 :: unk6 :: unk7 :: HNil =>
+        Attempt.Successful(SquadMemberEvent(unk1, unk2, unk3, unk4, unk5, unk6, unk7))
+    },
+    {
+      case data @ SquadMemberEvent(0, unk2, unk3, unk4, Some(unk5), Some(unk6), Some(unk7)) =>
+        Attempt.Successful(0 :: unk2 :: unk3 :: unk4 :: Some(unk5) :: Some(unk6) :: Some(unk7) :: HNil)
+      case data @ SquadMemberEvent(3, unk2, unk3, unk4, None, Some(unk6), None) =>
+        Attempt.Successful(3 :: unk2 :: unk3 :: unk4 :: None :: Some(unk6) :: None :: HNil)
+      case data @ SquadMemberEvent(4, unk2, unk3, unk4, None, None, Some(unk7)) =>
+        Attempt.Successful(4 :: unk2 :: unk3 :: unk4 :: None :: None :: Some(unk7) :: HNil)
+      case data @ SquadMemberEvent(unk1, unk2, unk3, unk4, None, None, None) =>
+        Attempt.Successful(unk1 :: unk2 :: unk3 :: unk4 :: None :: None :: None :: HNil)
+      case data =>
+        Attempt.Failure(Err(s"SquadMemberEvent can not encode with this pattern - $data"))
+    }
+  )
+}
diff --git a/common/src/main/scala/net/psforever/packet/game/SquadState.scala b/common/src/main/scala/net/psforever/packet/game/SquadState.scala
new file mode 100644
index 00000000..ae745a74
--- /dev/null
+++ b/common/src/main/scala/net/psforever/packet/game/SquadState.scala
@@ -0,0 +1,76 @@
+// Copyright (c) 2019 PSForever
+package net.psforever.packet.game
+
+import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
+import net.psforever.types.Vector3
+import scodec.{Attempt, Codec, Err}
+import scodec.codecs._
+import shapeless.{::, HNil}
+
+final case class SquadStateInfo(char_id : Long,
+                                unk2 : Int,
+                                unk3 : Int,
+                                pos : Vector3,
+                                unk4 : Int,
+                                unk5 : Int,
+                                unk6 : Boolean,
+                                unk7 : Int,
+                                unk8 : Option[Int],
+                                unk9 : Option[Boolean])
+
+//7704001646c7a02810050a1a2bc97842280000
+// SquadState(PlanetSideGUID(4),List(SquadStateInfo(1684830722,64,64,Vector3(3152.1562,3045.0781,35.515625),2,2,false,0,None,None)))
+//770700342a28c028100420d60e9df8c2800000eab58a028100ce514b655ddc341400286d9130000021eb951539f4c4050800
+// SquadState(PlanetSideGUID(7),List(SquadStateInfo(1117948930,64,64,Vector3(2822.125,3919.0234,43.546875),0,0,false,0,None,None), SquadStateInfo(3937765890,64,64,Vector3(2856.3984,3882.3516,53.859375),0,0,false,416,None,None), SquadStateInfo(2262373120,0,0,Vector3(2909.0547,3740.539,67.296875),0,1,false,132,None,None)))
+//7704005dd9ccf01810132fdf9f9ef5c4a8000084de7a022c00e5898c8d5e4c429b004ed01d50181016395a4c364e08280001b901a070bd0140805308d59f90641f40000c001db11e280a00088d19e3a190f0ca6c0100
+// SquadState(PlanetSideGUID(4),List(SquadStateInfo(3718041345,64,64,Vector3(3966.5938,6095.8047,75.359375),2,2,false,0,None,None), SquadStateInfo(2229172738,22,0,Vector3(3268.4453,3690.3906,66.296875),2,2,false,728,None,None), SquadStateInfo(3976320257,64,64,Vector3(3530.6875,4635.1484,128.875),2,2,false,0,Some(441),Some(true)), SquadStateInfo(1088518658,64,64,Vector3(3336.3203,4601.3438,60.78125),2,2,false,0,Some(896),Some(true)), SquadStateInfo(1816627714,64,0,Vector3(5027.0625,4931.7734,48.234375),2,2,false,728,None,None)))
+final case class SquadState(guid : PlanetSideGUID,
+                            info_list : List[SquadStateInfo])
+  extends PlanetSideGamePacket {
+  type Packet = SquadState
+  def opcode = GamePacketOpcode.SquadState
+  def encode = SquadState.encode(this)
+}
+
+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(unk1 : Long, unk2 : Int, unk3 : Int, pos : Vector3, unk4 : Int, unk5 : Int, unk6 : Boolean, unk7 : Int, unk8 : Int, unk9 : Boolean) : SquadStateInfo =
+    SquadStateInfo(unk1, unk2, unk3, pos, unk4, unk5, unk6, unk7, Some(unk8), Some(unk9))
+}
+
+object SquadState extends Marshallable[SquadState] {
+  private val info_codec : Codec[SquadStateInfo] = (
+    ("char_id" | uint32L) ::
+      ("unk2" | uint(7)) ::
+      ("unk3" | uint(7)) ::
+      ("pos" | Vector3.codec_pos) ::
+      ("unk4" | uint2) ::
+      ("unk5" | uint2) ::
+      ("unk6" | bool) ::
+      ("unk7" | uint16L) ::
+      (bool >>:~ { out =>
+        conditional(out, "unk8" | uint16L) ::
+          conditional(out, "unk9" | bool)
+      })
+    ).exmap[SquadStateInfo] (
+    {
+      case char_id :: u2 :: u3 :: pos :: u4 :: u5 :: u6 :: u7 :: _ :: u8 :: u9 :: HNil =>
+        Attempt.Successful(SquadStateInfo(char_id, u2, u3, pos, u4, u5, u6, u7, u8, u9))
+    },
+    {
+      case SquadStateInfo(char_id, u2, u3, pos, u4, u5, u6, u7, Some(u8), Some(u9)) =>
+        Attempt.Successful(char_id :: u2 :: u3 :: pos :: u4 :: u5 :: u6 :: u7 :: true :: Some(u8) :: Some(u9) :: HNil)
+      case SquadStateInfo(char_id, u2, u3, pos, u4, u5, u6, u7, None, None) =>
+        Attempt.Successful(char_id :: u2 :: u3 :: pos :: u4 :: u5 :: u6 :: u7 :: false :: None :: None :: HNil)
+      case data @ (SquadStateInfo(_, _, _, _, _, _, _, _, Some(_), None) | SquadStateInfo(_, _, _, _, _, _, _, _, None, Some(_))) =>
+        Attempt.Failure(Err(s"SquadStateInfo requires both unk8 and unk9 to be either defined or undefined at the same time - $data"))
+    }
+  )
+
+  implicit val codec : Codec[SquadState] = (
+    ("guid" | PlanetSideGUID.codec) ::
+      ("info_list" | listOfN(uint4, info_codec))
+    ).as[SquadState]
+}
diff --git a/common/src/test/scala/game/SquadMemberEventTest.scala b/common/src/test/scala/game/SquadMemberEventTest.scala
new file mode 100644
index 00000000..5ecc1cef
--- /dev/null
+++ b/common/src/test/scala/game/SquadMemberEventTest.scala
@@ -0,0 +1,32 @@
+// Copyright (c) 2019 PSForever
+package game
+
+import net.psforever.packet._
+import net.psforever.packet.game._
+import org.specs2.mutable._
+import scodec.bits._
+
+class SquadMemberEventTest extends Specification {
+  val string = hex"7000e008545180410848006f0066004400070051150800"
+
+  "decode" in {
+    PacketCoding.DecodePacket(string).require match {
+      case SquadMemberEvent(u1, u2, u3, u4, u5, u6, u7) =>
+        u1 mustEqual 0
+        u2 mustEqual 7
+        u3 mustEqual 42771010L
+        u4 mustEqual 0
+        u5.contains("HofD") mustEqual true
+        u6.contains(7) mustEqual true
+        u7.contains(529745L) mustEqual true
+      case _ =>
+        ko
+    }
+  }
+
+  "encode" in {
+    val msg = SquadMemberEvent(0, 7, 42771010L, 0, Some("HofD"), Some(7), Some(529745L))
+    val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+    pkt mustEqual string
+  }
+}
diff --git a/common/src/test/scala/game/SquadStateTest.scala b/common/src/test/scala/game/SquadStateTest.scala
new file mode 100644
index 00000000..5cce7ae3
--- /dev/null
+++ b/common/src/test/scala/game/SquadStateTest.scala
@@ -0,0 +1,244 @@
+// Copyright (c) 2019 PSForever
+package game
+
+import net.psforever.packet._
+import net.psforever.packet.game._
+import net.psforever.types.Vector3
+import org.specs2.mutable._
+import scodec.bits._
+
+class SquadStateTest extends Specification {
+  val string1 = hex"770700186d9130081001b11b27c1c041680000"
+  val string2 = hex"770700242a28c020003e9237a90e3382695004eab58a0281017eb95613df4c42950040"
+  val stringx = hex"7704008dd9ccf010042a9837310e1b82a8c006646c7a028103984f34759c904a800014f01c26f3d014081ddd3896931bc25478037680ea80c081d699a147b01e154000031c0bc81407e08c1a3a890de1542c022070bd0140815958bf29efa6214300108023c01000ae491ac68d1a61342c023623c50140011d6ea0878f3026a00009e014"
+
+  "decode (1)" in {
+    PacketCoding.DecodePacket(string1).require match {
+      case SquadState(guid, list) =>
+        guid mustEqual PlanetSideGUID(7)
+        list.size mustEqual 1
+        //0
+        list.head match {
+          case SquadStateInfo(char_id, u2, u3, pos, u4, u5, u6, u7, u8, u9) =>
+            char_id mustEqual 1300870L
+            u2 mustEqual 64
+            u3 mustEqual 64
+            pos mustEqual Vector3(3464.0469f, 4065.5703f, 20.015625f)
+            u4 mustEqual 2
+            u5 mustEqual 2
+            u6 mustEqual false
+            u7 mustEqual 0
+            u8.isEmpty mustEqual true
+            u9.isEmpty mustEqual true
+          case _ =>
+            ko
+        }
+      case _ =>
+        ko
+    }
+  }
+
+  "decode (2)" in {
+    PacketCoding.DecodePacket(string2).require match {
+      case SquadState(guid, list) =>
+        guid mustEqual PlanetSideGUID(7)
+        list.size mustEqual 2
+        //0
+        list.head match {
+          case SquadStateInfo(char_id, u2, u3, pos, u4, u5, u6, u7, u8, u9) =>
+            char_id mustEqual 42771010L
+            u2 mustEqual 0
+            u3 mustEqual 0
+            pos mustEqual Vector3(6801.953f, 4231.828f, 39.21875f)
+            u4 mustEqual 2
+            u5 mustEqual 2
+            u6 mustEqual false
+            u7 mustEqual 680
+            u8.isEmpty mustEqual true
+            u9.isEmpty mustEqual true
+          case _ =>
+            ko
+        }
+        list(1) match {
+          case SquadStateInfo(char_id, u2, u3, pos, u4, u5, u6, u7, u8, u9) =>
+            char_id mustEqual 42644970L
+            u2 mustEqual 64
+            u3 mustEqual 64
+            pos mustEqual Vector3(2908.7422f, 3742.6875f, 67.296875f)
+            u4 mustEqual 2
+            u5 mustEqual 2
+            u6 mustEqual false
+            u7 mustEqual 680
+            u8.isEmpty mustEqual true
+            u9.isEmpty mustEqual true
+          case _ =>
+            ko
+        }
+      case _ =>
+        ko
+    }
+  }
+
+  "decode (8)" in {
+    PacketCoding.DecodePacket(stringx).require match {
+      case SquadState(guid, list) =>
+        guid mustEqual PlanetSideGUID(4)
+        list.size mustEqual 8
+        //0
+        list.head match {
+          case SquadStateInfo(char_id, u2, u3, pos, u4, u5, u6, u7, u8, u9) =>
+            char_id mustEqual 30383325L
+            u2 mustEqual 0
+            u3 mustEqual 16
+            pos mustEqual Vector3(6849.328f, 4231.5938f, 41.71875f)
+            u4 mustEqual 2
+            u5 mustEqual 2
+            u6 mustEqual false
+            u7 mustEqual 864
+            u8.isEmpty mustEqual true
+            u9.isEmpty mustEqual true
+          case _ =>
+            ko
+        }
+        list(1) match {
+          case SquadStateInfo(char_id, u2, u3, pos, u4, u5, u6, u7, u8, u9) =>
+            char_id mustEqual 41577572L
+            u2 mustEqual 64
+            u3 mustEqual 64
+            pos mustEqual Vector3(6183.797f, 4013.6328f, 72.5625f)
+            u4 mustEqual 2
+            u5 mustEqual 2
+            u6 mustEqual false
+            u7 mustEqual 0
+            u8.contains(335) mustEqual true
+            u9.contains(true) mustEqual true
+          case _ =>
+            ko
+        }
+        list(2) match {
+          case SquadStateInfo(char_id, u2, u3, pos, u4, u5, u6, u7, u8, u9) =>
+            char_id mustEqual 41606788L
+            u2 mustEqual 64
+            u3 mustEqual 64
+            pos mustEqual Vector3(6611.8594f, 4242.586f, 75.46875f)
+            u4 mustEqual 2
+            u5 mustEqual 2
+            u6 mustEqual false
+            u7 mustEqual 888
+            u8.isEmpty mustEqual true
+            u9.isEmpty mustEqual true
+          case _ =>
+            ko
+        }
+        list(3) match {
+          case SquadStateInfo(char_id, u2, u3, pos, u4, u5, u6, u7, u8, u9) =>
+            char_id mustEqual 30736877L
+            u2 mustEqual 64
+            u3 mustEqual 64
+            pos mustEqual Vector3(6809.836f, 4218.078f, 40.234375f)
+            u4 mustEqual 2
+            u5 mustEqual 2
+            u6 mustEqual false
+            u7 mustEqual 0
+            u8.isEmpty mustEqual true
+            u9.isEmpty mustEqual true
+          case _ =>
+            ko
+        }
+        list(4) match {
+          case SquadStateInfo(char_id, u2, u3, pos, u4, u5, u6, u7, u8, u9) =>
+            char_id mustEqual 41517411L
+            u2 mustEqual 64
+            u3 mustEqual 63
+            pos mustEqual Vector3(6848.0312f, 4232.2266f, 41.734375f)
+            u4 mustEqual 2
+            u5 mustEqual 2
+            u6 mustEqual false
+            u7 mustEqual 556
+            u8.isEmpty mustEqual true
+            u9.isEmpty mustEqual true
+          case _ =>
+            ko
+        }
+        list(5) match {
+          case SquadStateInfo(char_id, u2, u3, pos, u4, u5, u6, u7, u8, u9) =>
+            char_id mustEqual 41607488L
+            u2 mustEqual 64
+            u3 mustEqual 64
+            pos mustEqual Vector3(2905.3438f, 3743.9453f, 67.296875f)
+            u4 mustEqual 2
+            u5 mustEqual 2
+            u6 mustEqual false
+            u7 mustEqual 304
+            u8.isEmpty mustEqual true
+            u9.isEmpty mustEqual true
+          case _ =>
+            ko
+        }
+        list(6) match {
+          case SquadStateInfo(char_id, u2, u3, pos, u4, u5, u6, u7, u8, u9) =>
+            char_id mustEqual 41419792L
+            u2 mustEqual 0
+            u3 mustEqual 5
+            pos mustEqual Vector3(6800.8906f ,4236.7734f, 39.296875f)
+            u4 mustEqual 2
+            u5 mustEqual 2
+            u6 mustEqual false
+            u7 mustEqual 556
+            u8.isEmpty mustEqual true
+            u9.isEmpty mustEqual true
+          case _ =>
+            ko
+        }
+        list(7) match {
+          case SquadStateInfo(char_id, u2, u3, pos, u4, u5, u6, u7, u8, u9) =>
+            char_id mustEqual 42616684L
+            u2 mustEqual 64
+            u3 mustEqual 0
+            pos mustEqual Vector3(2927.1094f, 3704.0312f, 78.375f)
+            u4 mustEqual 1
+            u5 mustEqual 1
+            u6 mustEqual false
+            u7 mustEqual 0
+            u8.contains(572) mustEqual true
+            u9.contains(true) mustEqual true
+          case _ =>
+            ko
+        }
+      case _ =>
+        ko
+    }
+  }
+
+  "encode (1)" in {
+    val msg = SquadState(PlanetSideGUID(7), List(
+      SquadStateInfo(1300870L, 64, 64, Vector3(3464.0469f, 4065.5703f, 20.015625f), 2, 2, false, 0)
+    ))
+    val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+    pkt mustEqual string1
+  }
+
+  "encode (2)" in {
+    val msg = SquadState(PlanetSideGUID(7), List(
+      SquadStateInfo(42771010L, 0, 0, Vector3(6801.953f, 4231.828f, 39.21875f), 2, 2, false, 680),
+      SquadStateInfo(42644970L, 64, 64, Vector3(2908.7422f, 3742.6875f, 67.296875f), 2, 2, false, 680)
+    ))
+    val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+    pkt mustEqual string2
+  }
+
+  "encode (8)" in {
+    val msg = SquadState(PlanetSideGUID(4), List(
+      SquadStateInfo(30383325L, 0, 16, Vector3(6849.328f, 4231.5938f, 41.71875f), 2, 2, false, 864),
+      SquadStateInfo(41577572L, 64, 64, Vector3(6183.797f, 4013.6328f, 72.5625f), 2, 2, false, 0, 335, true),
+      SquadStateInfo(41606788L, 64, 64, Vector3(6611.8594f, 4242.586f, 75.46875f), 2, 2, false, 888),
+      SquadStateInfo(30736877L, 64, 64, Vector3(6809.836f, 4218.078f, 40.234375f), 2, 2, false, 0),
+      SquadStateInfo(41517411L, 64, 63, Vector3(6848.0312f, 4232.2266f, 41.734375f), 2, 2, false, 556),
+      SquadStateInfo(41607488L, 64, 64, Vector3(2905.3438f, 3743.9453f, 67.296875f), 2, 2, false, 304),
+      SquadStateInfo(41419792L, 0, 5, Vector3(6800.8906f, 4236.7734f, 39.296875f), 2, 2, false, 556),
+      SquadStateInfo(42616684L, 64, 0, Vector3(2927.1094f, 3704.0312f, 78.375f), 1, 1, false, 0, 572, true)
+    ))
+    val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+    pkt mustEqual stringx
+  }
+}
\ No newline at end of file
diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala
index 8f22f277..b20a2364 100644
--- a/pslogin/src/main/scala/WorldSessionActor.scala
+++ b/pslogin/src/main/scala/WorldSessionActor.scala
@@ -2853,6 +2853,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
     sendResponse(PlanetsideAttributeMessage(guid, 53, 1))
     sendResponse(AvatarSearchCriteriaMessage(guid, List(0, 0, 0, 0, 0, 0)))
     (1 to 73).foreach(i => {
+      // not all GUID's are set, and not all of the set ones will always be zero; what does this section do?
       sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(i), 67, 0))
     })
     (0 to 30).foreach(i => {
@@ -2861,7 +2862,15 @@ class WorldSessionActor extends Actor with MDCContextAware {
     })
     //AvatarAwardMessage
     //DisplayAwardMessage
-    //SquadDefinitionActionMessage and SquadDetailDefinitionUpdateMessage; handled elsewhere
+    sendResponse(PlanetsideStringAttributeMessage(guid, 0, "Outfit Name"))
+    sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(6, hex"".toBitVector)))
+    (0 to 9).foreach(line => {
+      sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), line, SquadAction.ListSquadDefinition("")))
+    })
+    sendResponse(SquadDetailDefinitionUpdateMessage.Init)
+    sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0,SquadAction.Unknown(16, hex"".toBitVector)))
+    sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0,SquadAction.Unknown(17, hex"".toBitVector)))
+    sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0,SquadAction.Unknown(18, hex"".toBitVector)))
     //MapObjectStateBlockMessage and ObjectCreateMessage?
     //TacticsMessage?
     //change the owner on our deployables (re-draw the icons for our deployables too)
@@ -2896,35 +2905,36 @@ class WorldSessionActor extends Actor with MDCContextAware {
         interstellarFerryTopLevelGUID = None
       case _ => ;
     }
-//    sendResponse(ReplicationStreamMessage(
-//      5,
-//      Some(6),
-//      Vector(
-//        SquadListing(0, SquadInfo(Some("xNick"), Some("FLY,ALL WELCOME!"), Some(PlanetSideZoneID(7)), Some(8), Some(10), Some(PlanetSideGUID(1)))),
-//        SquadListing(1, SquadInfo(Some("HofD"), Some("=KOK+SPC+FLY= All Welcome"), Some(PlanetSideZoneID(7)), Some(3), Some(10), Some(PlanetSideGUID(3))))
-//      )
-//    ))
-//    //sendRawResponse(hex"e803008484000c800259e8809fda020043004a0069006d006d0079006e009b48006f006d006900630069006400690061006c00200053006d007500720066007300200041006e006f006e0079006d006f007500730004000000981401064580540061006e006b002000440072006900760065007200a05200650063006f006d006d0065006e00640065006400200074006f0020006800610076006500200065006e00670069006e0065006500720069006e0067002e0000000000800180000c00020c8c46007200650065006200690065002000730070006f007400cf44006f002000770068006100740065007600650072002c0020006200750074002000700072006500660065007200610062006c007900200073007400690063006b002000770069007400680020007400680065002000730071007500610064002e00200044006f006e002700740020006e00650065006400200061006e007900200073007000650063006900660069006300200063006500720074002e0096e27a0290540068006500460069006e0061006c005300740072007500670067006c0065000000000000020c8c46007200650065006200690065002000530070006f007400cf44006f002000770068006100740065007600650072002c0020006200750074002000700072006500660065007200610062006c007900200073007400690063006b002000770069007400680020007400680065002000730071007500610064002e00200044006f006e002700740020006e00650065006400200061006e007900200073007000650063006900660069006300200063006500720074002e0000000000800000000000020c8a41004d0053002000440072006900760065007200b34700690076006500200075007300200073007000610077006e00200070006f0069006e00740073002c0020006800610063006b0069006e006700200061006e006400200069006e00660069006c0020006100720065002000750073006500660075006c002e00fb02790287440030004f004d006700750079000100020c00020c8d410076006500720061006700650020004a0069006d006d007900a05200650069006e0066006f0072006300650064002000450078006f007300750069007400200077006f0075006c00640020006200650020006e0069006300650000000000800300000c00020c8b4100760065007200610067006500200042006f006200a05200650069006e0066006f0072006300650064002000450078006f007300750069007400200077006f0075006c00640020006200650020006e0069006300650000000000800300000c00020c8b410076006500720061006700650020004a006f006500a05200650069006e0066006f0072006300650064002000450078006f007300750069007400200077006f0075006c00640020006200650020006e0069006300650000000000800300000c00020c8753007500700070006f0072007400a2520065007300730075007200650063007400200073006f006c00640069006500720073002c0020006b00650065007000200075007300200061006c006900760065002e0000000000800100000c0c0a0c8845006e00670069006e00650065007200a043006f006d00620061007400200045006e00670069006e0065006500720069006e006700200077006f0075006c00640020006200650020006e0069006300650004b3d101864a0069006d006d0079006e000100000c000a0c854d0065006400690063009a4100640076002e0020004d00650064006900630061006c00200077006f0075006c00640020006200650020006e0069006300650000000000800100000c0400")
-//    sendResponse(
-//      SquadDetailDefinitionUpdateMessage(
-//        PlanetSideGUID(3),
-//        "HofD",
-//        "\\#ffdc00***\\#9640ff=KOK+SPC+FLY=\\#ffdc00***\\#FF4040 All Welcome",
-//        PlanetSideZoneID(7),
-//        List(
-//          SquadPositionDetail("\\#ff0000 |||||||||||||||||||||||", "Just a space filler"),
-//          SquadPositionDetail("\\#ffdc00   C", ""),
-//          SquadPositionDetail("\\#ffdc00   H", "", "OpoIE"),
-//          SquadPositionDetail("\\#ffdc00    I", "", "BobaF3tt907"),
-//          SquadPositionDetail("\\#ffdc00   N", ""),
-//          SquadPositionDetail("\\#ffdc00   A", ""),
-//          SquadPositionDetail("\\#ff0000 |||||||||||||||||||||||", "Another space filler"),
-//          SquadPositionDetail("\\#9640ff   K", ""),
-//          SquadPositionDetail("\\#9640ff   O", "", "HofD"),
-//          SquadPositionDetail("\\#9640ff   K", "")
-//        )
-//      )
-//    )
+    //SQUAD TESTING CODE
+    sendResponse(ReplicationStreamMessage(
+      5,
+      Some(6),
+      Vector(
+        SquadListing(0, SquadInfo(Some("xNick"), Some("FLY,ALL WELCOME!"), Some(PlanetSideZoneID(7)), Some(8), Some(10), Some(PlanetSideGUID(1)))),
+        SquadListing(1, SquadInfo(Some("HofD"), Some("=KOK+SPC+FLY= All Welcome"), Some(PlanetSideZoneID(7)), Some(3), Some(10), Some(PlanetSideGUID(3))))
+      )
+    ))
+    //sendRawResponse(hex"e803008484000c800259e8809fda020043004a0069006d006d0079006e009b48006f006d006900630069006400690061006c00200053006d007500720066007300200041006e006f006e0079006d006f007500730004000000981401064580540061006e006b002000440072006900760065007200a05200650063006f006d006d0065006e00640065006400200074006f0020006800610076006500200065006e00670069006e0065006500720069006e0067002e0000000000800180000c00020c8c46007200650065006200690065002000730070006f007400cf44006f002000770068006100740065007600650072002c0020006200750074002000700072006500660065007200610062006c007900200073007400690063006b002000770069007400680020007400680065002000730071007500610064002e00200044006f006e002700740020006e00650065006400200061006e007900200073007000650063006900660069006300200063006500720074002e0096e27a0290540068006500460069006e0061006c005300740072007500670067006c0065000000000000020c8c46007200650065006200690065002000530070006f007400cf44006f002000770068006100740065007600650072002c0020006200750074002000700072006500660065007200610062006c007900200073007400690063006b002000770069007400680020007400680065002000730071007500610064002e00200044006f006e002700740020006e00650065006400200061006e007900200073007000650063006900660069006300200063006500720074002e0000000000800000000000020c8a41004d0053002000440072006900760065007200b34700690076006500200075007300200073007000610077006e00200070006f0069006e00740073002c0020006800610063006b0069006e006700200061006e006400200069006e00660069006c0020006100720065002000750073006500660075006c002e00fb02790287440030004f004d006700750079000100020c00020c8d410076006500720061006700650020004a0069006d006d007900a05200650069006e0066006f0072006300650064002000450078006f007300750069007400200077006f0075006c00640020006200650020006e0069006300650000000000800300000c00020c8b4100760065007200610067006500200042006f006200a05200650069006e0066006f0072006300650064002000450078006f007300750069007400200077006f0075006c00640020006200650020006e0069006300650000000000800300000c00020c8b410076006500720061006700650020004a006f006500a05200650069006e0066006f0072006300650064002000450078006f007300750069007400200077006f0075006c00640020006200650020006e0069006300650000000000800300000c00020c8753007500700070006f0072007400a2520065007300730075007200650063007400200073006f006c00640069006500720073002c0020006b00650065007000200075007300200061006c006900760065002e0000000000800100000c0c0a0c8845006e00670069006e00650065007200a043006f006d00620061007400200045006e00670069006e0065006500720069006e006700200077006f0075006c00640020006200650020006e0069006300650004b3d101864a0069006d006d0079006e000100000c000a0c854d0065006400690063009a4100640076002e0020004d00650064006900630061006c00200077006f0075006c00640020006200650020006e0069006300650000000000800100000c0400")
+    sendResponse(
+      SquadDetailDefinitionUpdateMessage(
+        PlanetSideGUID(3),
+        "HofD",
+        "\\#ffdc00***\\#9640ff=KOK+SPC+FLY=\\#ffdc00***\\#FF4040 All Welcome",
+        PlanetSideZoneID(7),
+        List(
+          SquadPositionDetail("\\#ff0000 |||||||||||||||||||||||", "Just a space filler"),
+          SquadPositionDetail("\\#ffdc00   C", ""),
+          SquadPositionDetail(false, "\\#ffdc00   H", "", Set(), 42644970L, "OpoIE"),
+          SquadPositionDetail(false, "\\#ffdc00    I", "", Set(), 41604210L, "BobaF3tt907"),
+          SquadPositionDetail("\\#ffdc00   N", ""),
+          SquadPositionDetail("\\#ffdc00   A", ""),
+          SquadPositionDetail("\\#ff0000 |||||||||||||||||||||||", "Another space filler"),
+          SquadPositionDetail("\\#9640ff   K", ""),
+          SquadPositionDetail(false, "\\#9640ff   O", "", Set(), 42771010L, "HofD"),
+          SquadPositionDetail("\\#9640ff   K", "")
+        )
+      )
+    )
   }
 
   def handleControlPkt(pkt : PlanetSideControlPacket) = {
@@ -3336,11 +3346,19 @@ class WorldSessionActor extends Actor with MDCContextAware {
 
     case msg @ PlayerStateMessageUpstream(avatar_guid, pos, vel, yaw, pitch, yaw_upper, seq_time, unk3, is_crouching, is_jumping, unk4, is_cloaking, unk5, unk6) =>
       if(deadState == DeadState.Alive) {
-//        if(!player.Crouching && is_crouching) {
-//          sendResponse(
-//            SquadMembershipRequest(SquadRequestType.Unk01, 1L, Some(1L), "FateJH", Some(None))
-//          )
-//        }
+        if(!player.Crouching && is_crouching) { //SQUAD TESTING CODE
+          sendResponse(SquadMembershipResponse(SquadRequestType.Invite,0,0,42771010,Some(41605313),"HofD",false,None))
+          sendResponse(SquadMembershipResponse(SquadRequestType.Accept,0,0,41605313,Some(42771010),"VirusGiver",true,Some(None)))
+          sendResponse(SquadMemberEvent(0,7,42771010,0,Some("HofD"),Some(7),Some(529745)))
+          sendResponse(SquadMemberEvent(0,7,42644970,1,Some("OpolE"),Some(7),Some(6418)))
+          sendResponse(SquadMemberEvent(0,7,41604210,8,Some("BobaF3tt907"),Some(12),Some(8097)))
+          sendResponse(PlanetsideAttributeMessage(player.GUID, 49, 7))
+          sendResponse(PlanetsideAttributeMessage(player.GUID, 50, 2))
+          sendResponse(PlanetsideAttributeMessage(player.GUID, 51, 8))
+          sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(3), 0, SquadAction.Unknown(18, hex"".toBitVector)))
+          sendResponse(PlanetsideAttributeMessage(player.GUID, 83, 0))
+          sendResponse(SquadState(PlanetSideGUID(7),List(SquadStateInfo(41605313L,64,64,Vector3(3464.0469f,4065.5703f,20.015625f),2,2,false,0,None,None))))
+        }
         player.Position = pos
         player.Velocity = vel
         player.Orientation = Vector3(player.Orientation.x, pitch, yaw)

From fd9a3a0216f3f509cd973e087944ae0ff7b46f13 Mon Sep 17 00:00:00 2001
From: FateJH <Jason_DiDonato@yahoo.com>
Date: Tue, 18 Jun 2019 14:43:26 -0400
Subject: [PATCH 09/31] commit before changes, and to switch gears momentarily;
 making charId somewhat native to Avatar and its converter, though not yet
 essential; protype for squad definition loadouts, even though no example of
 functionality exists; a lot of unimplemented work in preparation to convert
 SquadDetailDefinitionUpdateMessage from a static format to a variable-field
 format

---
 .../scala/net/psforever/objects/Avatar.scala  |  44 +-
 .../scala/net/psforever/objects/Player.scala  |   8 +-
 .../objects/avatar/LoadoutManager.scala       |  27 +
 .../converter/AvatarConverter.scala           |   2 +-
 .../objects/loadouts/EquipmentLoadout.scala   |  22 +
 .../objects/loadouts/InfantryLoadout.scala    |   2 +-
 .../psforever/objects/loadouts/Loadout.scala  |  49 +-
 .../objects/loadouts/SquadLoadout.scala       |  11 +
 .../objects/loadouts/VehicleLoadout.scala     |   2 +-
 .../terminals/OrderTerminalDefinition.scala   |   4 +-
 .../psforever/objects/teamwork/Member.scala   |   8 +
 .../game/SquadDefinitionActionMessage.scala   |   2 +-
 .../SquadDetailDefinitionUpdateMessage.scala  | 751 +++++++++++++++++-
 .../CharacterAppearanceData.scala             |   3 +-
 .../services/teamwork/SquadService.scala      |   7 +-
 ...uadDetailDefinitionUpdateMessageTest.scala |  15 +-
 .../game/objectcreate/CharacterDataTest.scala |   6 +-
 .../DetailedCharacterDataTest.scala           |  10 +-
 .../MountedVehiclesTest.scala                 |   2 +-
 .../src/test/scala/objects/AvatarTest.scala   |  32 +-
 .../objects/terminal/OrderTerminalTest.scala  |   4 +-
 .../src/main/scala/WorldSessionActor.scala    |  58 +-
 22 files changed, 938 insertions(+), 131 deletions(-)
 create mode 100644 common/src/main/scala/net/psforever/objects/avatar/LoadoutManager.scala
 create mode 100644 common/src/main/scala/net/psforever/objects/loadouts/EquipmentLoadout.scala
 create mode 100644 common/src/main/scala/net/psforever/objects/loadouts/SquadLoadout.scala

diff --git a/common/src/main/scala/net/psforever/objects/Avatar.scala b/common/src/main/scala/net/psforever/objects/Avatar.scala
index 1307ee58..01ce2d46 100644
--- a/common/src/main/scala/net/psforever/objects/Avatar.scala
+++ b/common/src/main/scala/net/psforever/objects/Avatar.scala
@@ -1,10 +1,9 @@
 // Copyright (c) 2017 PSForever
 package net.psforever.objects
 
-import net.psforever.objects.avatar.DeployableToolbox
+import net.psforever.objects.avatar.{DeployableToolbox, LoadoutManager}
 import net.psforever.objects.definition.{AvatarDefinition, ImplantDefinition}
 import net.psforever.objects.equipment.{EquipmentSize, EquipmentSlot}
-import net.psforever.objects.loadouts.Loadout
 import net.psforever.packet.game.objectcreate.Cosmetics
 import net.psforever.types._
 
@@ -12,6 +11,8 @@ import scala.annotation.tailrec
 import scala.collection.mutable
 
 class Avatar(val name : String, val faction : PlanetSideEmpire.Value, val sex : CharacterGender.Value, val head : Int, val voice : CharacterVoice.Value) {
+  /** Character ID; a unique identifier corresponding to a database table row index */
+  private var charId : Long = 0
   /** Battle Experience Points */
   private var bep : Long = 0
   /** Command Experience Points */
@@ -29,11 +30,15 @@ class Avatar(val name : String, val faction : PlanetSideEmpire.Value, val sex :
     * @see `DetailedCharacterData.implants`
     */
   private val implants : Array[ImplantSlot] = Array.fill[ImplantSlot](3)(new ImplantSlot)
-  /** Loadouts<br>
-    * 0-9 are Infantry loadouts
+  /** Equipment Loadouts<br>
+    * 0-9 are Infantry loadouts<br>
     * 10-14 are Vehicle loadouts
     */
-  private val loadouts : Array[Option[Loadout]] = Array.fill[Option[Loadout]](15)(None)
+  private val equipmentLoadouts : LoadoutManager = new LoadoutManager(15)
+  /**
+    * Squad Loadouts
+    */
+  private val squadLoadouts : LoadoutManager = new LoadoutManager(10)
   /** Locker */
   private val locker : LockerContainer = new LockerContainer() {
     override def toString : String = {
@@ -43,6 +48,15 @@ class Avatar(val name : String, val faction : PlanetSideEmpire.Value, val sex :
 
   private val deployables : DeployableToolbox = new DeployableToolbox
 
+  def CharId : Long = charId
+
+  def CharId_=(id : Long) : Long = {
+    if(charId == 0) { //does not need to be set but can only set once
+      charId = id
+    }
+    CharId
+  }
+
   def BEP : Long = bep
 
   def BEP_=(battleExperiencePoints : Long) : Long = {
@@ -164,25 +178,9 @@ class Avatar(val name : String, val faction : PlanetSideEmpire.Value, val sex :
     })
   }
 
-  def SaveLoadout(owner : Player, label : String, line : Int) : Unit = {
-    if(line > -1 && line < 10) {
-      loadouts(line) = Some(Loadout.Create(owner, label))
-    }
-  }
+  def EquipmentLoadouts : LoadoutManager = equipmentLoadouts
 
-  def SaveLoadout(owner : Vehicle, label : String, line : Int) : Unit = {
-    if(line > 9 && line < loadouts.length) {
-      loadouts(line) = Some(Loadout.Create(owner, label))
-    }
-  }
-
-  def LoadLoadout(line : Int) : Option[Loadout] = loadouts.lift(line).flatten
-
-  def DeleteLoadout(line : Int) : Unit = {
-    loadouts(line) = None
-  }
-
-  def Loadouts : Seq[(Int, Loadout)] = loadouts.zipWithIndex.collect { case(Some(loadout), index) => (index, loadout) } toSeq
+  def SquadLoadouts : LoadoutManager = squadLoadouts
 
   def Locker : LockerContainer = locker
 
diff --git a/common/src/main/scala/net/psforever/objects/Player.scala b/common/src/main/scala/net/psforever/objects/Player.scala
index e1e015f2..7577b545 100644
--- a/common/src/main/scala/net/psforever/objects/Player.scala
+++ b/common/src/main/scala/net/psforever/objects/Player.scala
@@ -1,10 +1,10 @@
 // Copyright (c) 2017 PSForever
 package net.psforever.objects
 
+import net.psforever.objects.avatar.LoadoutManager
 import net.psforever.objects.definition.{AvatarDefinition, ExoSuitDefinition, SpecialExoSuitDefinition}
 import net.psforever.objects.equipment.{Equipment, EquipmentSize, EquipmentSlot}
 import net.psforever.objects.inventory.{Container, GridInventory, InventoryItem}
-import net.psforever.objects.loadouts.Loadout
 import net.psforever.objects.serverobject.affinity.FactionAffinity
 import net.psforever.objects.vital.resistance.ResistanceProfile
 import net.psforever.objects.vital.{DamageResistanceModel, Vitality}
@@ -58,6 +58,8 @@ class Player(private val core : Avatar) extends PlanetSideGameObject
 
   Player.SuitSetup(this, exosuit)
 
+  def CharId : Long = core.CharId
+
   def Name : String = core.name
 
   def Faction : PlanetSideEmpire.Value = core.faction
@@ -294,7 +296,9 @@ class Player(private val core : Avatar) extends PlanetSideGameObject
 
   def RadiationShielding = exosuit.RadiationShielding
 
-  def LoadLoadout(line : Int) : Option[Loadout] = core.LoadLoadout(line)
+  def EquipmentLoadouts : LoadoutManager = core.EquipmentLoadouts
+
+  def SquadLoadouts : LoadoutManager = core.SquadLoadouts
 
   def BEP : Long = core.BEP
 
diff --git a/common/src/main/scala/net/psforever/objects/avatar/LoadoutManager.scala b/common/src/main/scala/net/psforever/objects/avatar/LoadoutManager.scala
new file mode 100644
index 00000000..c5978723
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/avatar/LoadoutManager.scala
@@ -0,0 +1,27 @@
+// Copyright (c) 2019 PSForever
+package net.psforever.objects.avatar
+
+import net.psforever.objects.loadouts.Loadout
+import net.psforever.types.LoadoutType
+
+import scala.util.Success
+
+class LoadoutManager(size : Int) {
+  private val entries : Array[Option[Loadout]] = Array.fill[Option[Loadout]](size)(None)
+
+  def SaveLoadout(owner : Any, label : String, line : Int) : Unit = {
+    Loadout.Create(owner, label) match {
+      case Success(loadout) =>
+        entries(line) = Some(loadout)
+      case _ => ;
+    }
+  }
+
+  def LoadLoadout(line : Int) : Option[Loadout] = entries.lift(line).flatten
+
+  def DeleteLoadout(line : Int) : Unit = {
+    entries(line) = None
+  }
+
+  def Loadouts : Seq[(Int, Loadout)] = entries.zipWithIndex.collect { case(Some(loadout), index) => (index, loadout) } toSeq
+}
diff --git a/common/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala
index a141529c..bbd9f1f5 100644
--- a/common/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala
+++ b/common/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala
@@ -81,7 +81,7 @@ object AvatarConverter {
       ),
       obj.ExoSuit,
       0,
-      0L,
+      obj.CharId,
       0,
       0,
       0,
diff --git a/common/src/main/scala/net/psforever/objects/loadouts/EquipmentLoadout.scala b/common/src/main/scala/net/psforever/objects/loadouts/EquipmentLoadout.scala
new file mode 100644
index 00000000..53925f90
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/loadouts/EquipmentLoadout.scala
@@ -0,0 +1,22 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.loadouts
+
+/**
+  * The base of all specific kinds of blueprint containers.
+  * This previous state can be restored on any appropriate template from which the loadout was copied
+  * by reconstructing any items (if warranted and permitted) or restoring any appropriate fields.
+  * The three fields are the name assigned to the loadout,
+  * the visible items that are created (which obey different rules depending on the source),
+  * and the concealed items that are created and added to the source's `Inventory`.<br>
+  * For example, the `visible_slots` on a `Player`-borne loadout will transform into the form `Array[EquipmentSlot]`;
+  * `Vehicle`-originating loadouts transform into the form `Map[Int, Equipment]`.
+  * <br>
+  * The lists of user-specific loadouts are initialized with `FavoritesMessage` packets.
+  * Specific entries are loaded or removed using `FavoritesRequest` packets.
+  * @param label the name by which this inventory will be known when displayed in a Favorites list
+  * @param visible_slots simplified representation of the `Equipment` that can see "seen" on the target
+  * @param inventory simplified representation of the `Equipment` in the target's inventory or trunk
+  */
+abstract class EquipmentLoadout(label : String,
+                                visible_slots : List[Loadout.SimplifiedEntry],
+                                inventory : List[Loadout.SimplifiedEntry]) extends Loadout(label)
diff --git a/common/src/main/scala/net/psforever/objects/loadouts/InfantryLoadout.scala b/common/src/main/scala/net/psforever/objects/loadouts/InfantryLoadout.scala
index 0f9986e2..18c2f57f 100644
--- a/common/src/main/scala/net/psforever/objects/loadouts/InfantryLoadout.scala
+++ b/common/src/main/scala/net/psforever/objects/loadouts/InfantryLoadout.scala
@@ -28,7 +28,7 @@ final case class InfantryLoadout(label : String,
                                  visible_slots : List[Loadout.SimplifiedEntry],
                                  inventory : List[Loadout.SimplifiedEntry],
                                  exosuit : ExoSuitType.Value,
-                                 subtype : Int) extends Loadout(label, visible_slots, inventory)
+                                 subtype : Int) extends EquipmentLoadout(label, visible_slots, inventory)
 
 object InfantryLoadout {
   import net.psforever.objects.Player
diff --git a/common/src/main/scala/net/psforever/objects/loadouts/Loadout.scala b/common/src/main/scala/net/psforever/objects/loadouts/Loadout.scala
index 12e0cf41..58490f60 100644
--- a/common/src/main/scala/net/psforever/objects/loadouts/Loadout.scala
+++ b/common/src/main/scala/net/psforever/objects/loadouts/Loadout.scala
@@ -5,30 +5,29 @@ import net.psforever.objects._
 import net.psforever.objects.definition._
 import net.psforever.objects.equipment.{Equipment, EquipmentSlot}
 import net.psforever.objects.inventory.InventoryItem
+import net.psforever.objects.teamwork.Squad
 
 import scala.annotation.tailrec
+import scala.util.{Failure, Success, Try}
 
 /**
   * The base of all specific kinds of blueprint containers.
   * This previous state can be restored on any appropriate template from which the loadout was copied
-  * by reconstructing the items (if permitted).
-  * The three fields are the name assigned to the loadout,
-  * the visible items that are created (which obey different rules depending on the source),
-  * and the concealed items that are created and added to the source's `Inventory`.<br>
-  * For example, the `visible_slots` on a `Player`-borne loadout will transform into the form `Array[EquipmentSlot]`;
-  * `Vehicle`-originating loadouts transform into the form `Map[Int, Equipment]`.
-  * <br>
-  * The lists of user-specific loadouts are initialized with `FavoritesMessage` packets.
-  * Specific entries are loaded or removed using `FavoritesRequest` packets.
+  * by reconstructing any items (if warranted and permitted) or restoring any appropriate fields.
   * @param label the name by which this inventory will be known when displayed in a Favorites list
-  * @param visible_slots simplified representation of the `Equipment` that can see "seen" on the target
-  * @param inventory simplified representation of the `Equipment` in the target's inventory or trunk
   */
-abstract class Loadout(label : String,
-                       visible_slots : List[Loadout.SimplifiedEntry],
-                       inventory : List[Loadout.SimplifiedEntry])
+abstract class Loadout(label : String)
 
 object Loadout {
+  def Create(owner : Any, label : String) : Try[Loadout] = {
+    owner match {
+      case p : Player => Success(Create(p, label))
+      case v : Vehicle => Success(Create(v, label))
+      case s : Squad => Success(Create(s, label))
+      case _ => Failure(new MatchError(s"can not create a loadout based on the (current status of) $owner"))
+    }
+  }
+
   /**
     * Produce the blueprint on a player.
     * @param player the player
@@ -54,12 +53,32 @@ object Loadout {
   def Create(vehicle : Vehicle, label : String) : Loadout = {
     VehicleLoadout(
       label,
-      packageSimplifications(vehicle.Weapons.map({ case ((index, weapon)) => InventoryItem(weapon.Equipment.get, index) }).toList),
+      packageSimplifications(vehicle.Weapons.map({ case (index, weapon) => InventoryItem(weapon.Equipment.get, index) }).toList),
       packageSimplifications(vehicle.Trunk.Items),
       vehicle.Definition
     )
   }
 
+  /**
+    *  na
+    */
+  def Create(squad : Squad, label : String) : Loadout = {
+    SquadLoadout(
+      label,
+      squad.Task,
+      if(squad.CustomZoneId) { Some(squad.ZoneId) } else { None },
+      squad.Membership
+        .zipWithIndex
+        .filter { case (_, index) =>
+          squad.Availability(index)
+        }
+        .map {case (member, index) =>
+          SquadPositionLoadout(index, member.Role, member.Orders, member.Requirements)
+        }
+        .toList
+    )
+  }
+
   /**
     * A basic `Trait` connecting all of the `Equipment` blueprints.
     */
diff --git a/common/src/main/scala/net/psforever/objects/loadouts/SquadLoadout.scala b/common/src/main/scala/net/psforever/objects/loadouts/SquadLoadout.scala
new file mode 100644
index 00000000..9039c5a0
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/loadouts/SquadLoadout.scala
@@ -0,0 +1,11 @@
+// Copyright (c) 2019 PSForever
+package net.psforever.objects.loadouts
+
+import net.psforever.types.CertificationType
+
+final case class SquadPositionLoadout(index : Int, role : String, orders : String, requirements : Set[CertificationType.Value])
+
+final case class SquadLoadout(label : String,
+                              task : String,
+                              zone_id : Option[Int],
+                              members : List[SquadPositionLoadout]) extends Loadout(label)
diff --git a/common/src/main/scala/net/psforever/objects/loadouts/VehicleLoadout.scala b/common/src/main/scala/net/psforever/objects/loadouts/VehicleLoadout.scala
index 5f7e0b63..c4199e0c 100644
--- a/common/src/main/scala/net/psforever/objects/loadouts/VehicleLoadout.scala
+++ b/common/src/main/scala/net/psforever/objects/loadouts/VehicleLoadout.scala
@@ -24,4 +24,4 @@ import net.psforever.objects.definition._
 final case class VehicleLoadout(label : String,
                                 visible_slots : List[Loadout.SimplifiedEntry],
                                 inventory : List[Loadout.SimplifiedEntry],
-                                vehicle_definition : VehicleDefinition) extends Loadout(label, visible_slots, inventory)
+                                vehicle_definition : VehicleDefinition) extends EquipmentLoadout(label, visible_slots, inventory)
diff --git a/common/src/main/scala/net/psforever/objects/serverobject/terminals/OrderTerminalDefinition.scala b/common/src/main/scala/net/psforever/objects/serverobject/terminals/OrderTerminalDefinition.scala
index 4784d8de..9c4e34d7 100644
--- a/common/src/main/scala/net/psforever/objects/serverobject/terminals/OrderTerminalDefinition.scala
+++ b/common/src/main/scala/net/psforever/objects/serverobject/terminals/OrderTerminalDefinition.scala
@@ -258,7 +258,7 @@ object OrderTerminalDefinition {
   //TODO block equipment by blocking ammunition type
   final case class InfantryLoadoutPage() extends LoadoutTab {
     override def Buy(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange = {
-      player.LoadLoadout(msg.unk1) match {
+      player.EquipmentLoadouts.LoadLoadout(msg.unk1) match {
         case Some(loadout : InfantryLoadout) if !Exclude.contains(loadout.exosuit) && !Exclude.contains((loadout.exosuit, loadout.subtype)) =>
           val holsters = loadout.visible_slots
             .map(entry => { InventoryItem(BuildSimplifiedPattern(entry.item), entry.index) })
@@ -287,7 +287,7 @@ object OrderTerminalDefinition {
     */
   final case class VehicleLoadoutPage() extends LoadoutTab {
     override def Buy(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange = {
-      player.LoadLoadout(msg.unk1 + 10) match {
+      player.EquipmentLoadouts.LoadLoadout(msg.unk1 + 10) match {
         case Some(loadout : VehicleLoadout) if !Exclude.contains(loadout.vehicle_definition) =>
           val weapons = loadout.visible_slots
             .map(entry => { InventoryItem(BuildSimplifiedPattern(entry.item), entry.index) })
diff --git a/common/src/main/scala/net/psforever/objects/teamwork/Member.scala b/common/src/main/scala/net/psforever/objects/teamwork/Member.scala
index 4d3b24e8..1ddfc46d 100644
--- a/common/src/main/scala/net/psforever/objects/teamwork/Member.scala
+++ b/common/src/main/scala/net/psforever/objects/teamwork/Member.scala
@@ -10,6 +10,7 @@ class Member {
   private var requirements : Set[CertificationType.Value] = Set()
   //about the individual filling the position
   private var name : String = ""
+  private var charId : Long = 0L
   private var health : Int = 0
   private var armor : Int = 0
   private var zoneId : Int = 0
@@ -43,6 +44,13 @@ class Member {
     Name
   }
 
+  def CharId : Long = charId
+
+  def CharId_=(id : Long) : Long = {
+    charId = id
+    CharId
+  }
+
   def Health : Int = health
 
   def Health_=(red : Int) : Int = {
diff --git a/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala b/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala
index 43009a3b..7b9b4647 100644
--- a/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala
+++ b/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala
@@ -366,7 +366,7 @@ object SquadDefinitionActionMessage extends Marshallable[SquadDefinitionActionMe
       case 2 | 6 | 9 | 11 |
            12 | 13 | 14 | 16 | 17 |
            18 | 29 | 30 | 32 | 33 |
-           36 | 37 | 38 | 42 | 43 => unknownCodec(code)
+           36 | 37 | 38 | 39 | 42 | 43 => unknownCodec(code)
       case _ => failureCodec(code)
     }).asInstanceOf[Codec[SquadAction]]
   }
diff --git a/common/src/main/scala/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala b/common/src/main/scala/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala
index 76fae022..98d6adef 100644
--- a/common/src/main/scala/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala
+++ b/common/src/main/scala/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala
@@ -1,6 +1,7 @@
 // Copyright (c) 2019 PSForever
 package net.psforever.packet.game
 
+import net.psforever.packet.game.SquadDetailDefinitionUpdateMessage.defaultRequirements
 import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
 import net.psforever.types.CertificationType
 import scodec.bits.BitVector
@@ -18,7 +19,9 @@ final case class SquadPositionDetail(is_closed : Boolean,
                                      name : String)
 
 final case class SquadDetailDefinitionUpdateMessage(guid : PlanetSideGUID,
-                                                    unk : BitVector,
+                                                    unk1 : Int,
+                                                    leader_char_id : Long,
+                                                    unk2 : BitVector,
                                                     leader_name : String,
                                                     task : String,
                                                     zone_id : PlanetSideZoneID,
@@ -32,27 +35,17 @@ final case class SquadDetailDefinitionUpdateMessage(guid : PlanetSideGUID,
 object SquadPositionDetail {
   final val Closed : SquadPositionDetail = SquadPositionDetail(is_closed = true, "", "", Set.empty, 0L, "")
 
-  private def reliableNameHash(name : String) : Long = {
-    val hash = name.hashCode.toLong
-    if(hash < 0) {
-      -1L * hash
-    }
-    else {
-      hash
-    }
-  }
-
   def apply() : SquadPositionDetail = SquadPositionDetail(is_closed = false, "", "", Set.empty, 0L, "")
 
-  def apply(name : String) : SquadPositionDetail = SquadPositionDetail(is_closed = false, "", "", Set.empty, reliableNameHash(name), name)
+  def apply(char_id : Long, name : String) : SquadPositionDetail = SquadPositionDetail(is_closed = false, "", "", Set.empty, char_id, name)
 
   def apply(role : String, detailed_orders : String) : SquadPositionDetail = SquadPositionDetail(is_closed = false, role, detailed_orders, Set.empty, 0L, "")
 
   def apply(role : String, detailed_orders : String, requirements : Set[CertificationType.Value]) : SquadPositionDetail = SquadPositionDetail(is_closed = false, role, detailed_orders, requirements, 0L, "")
 
-  def apply(role : String, detailed_orders : String, name : String) : SquadPositionDetail = SquadPositionDetail(is_closed = false, role, detailed_orders, Set.empty, reliableNameHash(name), name)
+  def apply(role : String, detailed_orders : String, char_id : Long, name : String) : SquadPositionDetail = SquadPositionDetail(is_closed = false, role, detailed_orders, Set.empty, char_id, name)
 
-  def apply(role : String, detailed_orders : String, requirements : Set[CertificationType.Value], name : String) : SquadPositionDetail = SquadPositionDetail(is_closed = false, role, detailed_orders, requirements, reliableNameHash(name), name)
+  def apply(role : String, detailed_orders : String, requirements : Set[CertificationType.Value], char_id : Long, name : String) : SquadPositionDetail = SquadPositionDetail(is_closed = false, role, detailed_orders, requirements, char_id, name)
 }
 
 object SquadDetailDefinitionUpdateMessage extends Marshallable[SquadDetailDefinitionUpdateMessage] {
@@ -64,6 +57,7 @@ object SquadDetailDefinitionUpdateMessage extends Marshallable[SquadDetailDefini
 
   final val Init = SquadDetailDefinitionUpdateMessage(
     PlanetSideGUID(0),
+    0L,
     "",
     "",
     PlanetSideZoneID(0),
@@ -81,9 +75,9 @@ object SquadDetailDefinitionUpdateMessage extends Marshallable[SquadDetailDefini
     )
   )
 
-  def apply(guid : PlanetSideGUID, leader_name : String, task : String, zone_id : PlanetSideZoneID, member_info : List[SquadPositionDetail]) : SquadDetailDefinitionUpdateMessage = {
+  def apply(guid : PlanetSideGUID, char_id : Long, leader_name : String, task : String, zone_id : PlanetSideZoneID, member_info : List[SquadPositionDetail]) : SquadDetailDefinitionUpdateMessage = {
     import scodec.bits._
-    SquadDetailDefinitionUpdateMessage(guid, hex"080000000000000000000".toBitVector, leader_name, task, zone_id, member_info)
+    SquadDetailDefinitionUpdateMessage(guid, 1, char_id, hex"000000".toBitVector, leader_name, task, zone_id, member_info)
   }
 
   private def memberCodec(pad : Int) : Codec[SquadPositionDetail] = {
@@ -191,9 +185,14 @@ object SquadDetailDefinitionUpdateMessage extends Marshallable[SquadDetailDefini
     import shapeless.::
     (
       ("guid" | PlanetSideGUID.codec) ::
-        uint8 ::
-        uint4 ::
-        bits(83) :: //variable fields, but can be 0'd
+        uint4 :: //constant = 8
+        uint4 :: //variable = 0-4
+        bool :: //true, when 4
+        uint4 :: //variable = 0-12?
+        ("unk1" | uint4) ::
+        uint24 :: //unknown, but can be 0'd
+        ("leader_char_id" | uint32L) ::
+        ("unk2" | bits(22)) :: //variable fields, but can be 0'd
         uint(10) :: //constant = 0
         ("leader" | PacketHelpers.encodedWideStringAligned(7)) ::
         ("task" | PacketHelpers.encodedWideString) ::
@@ -202,15 +201,721 @@ object SquadDetailDefinitionUpdateMessage extends Marshallable[SquadDetailDefini
         optional(bool, "member_info" | initial_member_codec)
       ).exmap[SquadDetailDefinitionUpdateMessage] (
       {
-        case guid :: _ :: _ :: _ :: _ :: leader :: task :: zone :: _ :: Some(member_list) :: HNil =>
-          Attempt.Successful(SquadDetailDefinitionUpdateMessage(guid, leader, task, zone, unlinkMemberList(member_list)))
+        case guid :: _ :: _ :: _ :: _ :: u1 :: _ :: char_id:: u2 :: _ :: leader :: task :: zone :: _ :: Some(member_list) :: HNil =>
+          Attempt.Successful(SquadDetailDefinitionUpdateMessage(guid, u1, char_id, u2, leader, task, zone, unlinkMemberList(member_list)))
         case data =>
           Attempt.failure(Err(s"can not get squad detail definition from data $data"))
       },
       {
-        case SquadDetailDefinitionUpdateMessage(guid, unk, leader, task, zone, member_list) =>
-          Attempt.Successful(guid :: 132 :: 8 :: unk.take(83) :: 0 :: leader :: task :: zone :: 4983296 :: Some(linkMemberList(member_list.reverse)) :: HNil)
+        case SquadDetailDefinitionUpdateMessage(guid, unk1, char_id, unk2, leader, task, zone, member_list) =>
+          Attempt.Successful(guid :: 8 :: 4 :: true :: 0 :: math.max(unk1, 1) :: 0 :: char_id :: unk2.take(22) :: 0 :: leader :: task :: zone :: 4983296 :: Some(linkMemberList(member_list.reverse)) :: HNil)
       }
     )
   }
 }
+
+//NEW FORM SquadDetailDefinitionUpdateMessage
+
+private class StreamLengthToken(init : Int = 0) {
+  private var bitLength : Int = init
+
+  def Length : Int = bitLength
+
+  def Length_=(toLength : Int) : StreamLengthToken = {
+    bitLength = toLength
+    this
+  }
+
+  def Add(more : Int) : StreamLengthToken = {
+    bitLength += more
+    this
+  }
+}
+
+final case class SquadPositionDetail2(is_closed : Option[Boolean],
+                                      role : Option[String],
+                                      detailed_orders : Option[String],
+                                      requirements : Option[Set[CertificationType.Value]],
+                                      char_id : Option[Long],
+                                      name : Option[String]) {
+  def And(info : SquadPositionDetail2) : SquadPositionDetail2 = {
+    SquadPositionDetail2(
+      is_closed match {
+        case Some(true) | None =>
+          info.is_closed.orElse(is_closed)
+        case _ =>
+          Some(false)
+      },
+      role.orElse(info.role),
+      detailed_orders.orElse(info.detailed_orders),
+      requirements.orElse(info.requirements),
+      char_id.orElse(info.char_id),
+      name.orElse(info.name)
+    )
+  }
+}
+
+final case class SquadPositionEntry(index : Int, info : Option[SquadPositionDetail2])
+
+final case class SquadDetail(unk1 : Option[Int],
+                             leader_char_id : Option[Long],
+                             unk2 : Option[BitVector],
+                             leader_name : Option[String],
+                             task : Option[String],
+                             zone_id : Option[PlanetSideZoneID],
+                             member_info : Option[List[SquadPositionEntry]]) {
+  def And(info : SquadDetail) : SquadDetail = {
+    SquadDetail(
+      unk1.orElse(info.unk1),
+      leader_char_id.orElse(info.leader_char_id),
+      unk2.orElse(info.unk2),
+      leader_name.orElse(info.leader_name),
+      task.orElse(info.task),
+      zone_id.orElse(info.zone_id),
+      (member_info, info.member_info) match {
+        case (Some(info1), Some(info2)) => Some(info1 ++ info2)
+        case (Some(info1), _) => Some(info1)
+        case (None, Some(info2)) => Some(info2)
+        case _ => None
+      }
+    )
+  }
+}
+
+final case class SquadDetailDefinitionUpdateMessage2(guid : PlanetSideGUID,
+                                                     detail : SquadDetail)
+
+object SquadPositionDetail2 {
+  final val Blank : SquadPositionDetail2 = SquadPositionDetail2()
+  final val Closed : SquadPositionDetail2 = SquadPositionDetail2(is_closed = Some(true), None, None, None, None, None)
+
+  def apply() : SquadPositionDetail2 = SquadPositionDetail2(is_closed = Some(false), None, None, None, None, None)
+
+  def apply(role : String, detailed_orders : Option[String]) : SquadPositionDetail2 = SquadPositionDetail2(is_closed = Some(false), Some(role), detailed_orders, None, None, None)
+
+  def apply(role : Option[String], detailed_orders : String) : SquadPositionDetail2 = SquadPositionDetail2(is_closed = Some(false), role, Some(detailed_orders), None, None, None)
+
+  def apply(char_id : Long) : SquadPositionDetail2 = SquadPositionDetail2(is_closed = Some(false), None, None, None, Some(char_id), None)
+
+  def apply(name : String) : SquadPositionDetail2 = SquadPositionDetail2(is_closed = Some(false), None, None, None, None, Some(name))
+
+  def apply(requirements : Set[CertificationType.Value]) : SquadPositionDetail2 = SquadPositionDetail2(is_closed = Some(false), None, None, Some(requirements), None, None)
+
+  def apply(role : String, detailed_orders : String, requirements : Set[CertificationType.Value], char_id : Long, name : String) : SquadPositionDetail2 = SquadPositionDetail2(is_closed = Some(false), Some(role), Some(detailed_orders), Some(requirements), Some(char_id), Some(name))
+}
+
+object SquadPositionEntry {
+  import SquadDetailDefinitionUpdateMessage2.paddedStringMetaCodec
+
+  private def roleCodec(bitsOverByte : StreamLengthToken) : Codec[SquadPositionDetail2] = paddedStringMetaCodec(bitsOverByte.Length).exmap[SquadPositionDetail2] (
+    role => Attempt.successful(SquadPositionDetail2(role)),
+    {
+      case SquadPositionDetail2(_, Some(role), _, _, _, _) =>
+        Attempt.successful(role)
+      case _ =>
+        Attempt.failure(Err("failed to encode squad position data for role"))
+    }
+  )
+
+  private def ordersCodec(bitsOverByte : StreamLengthToken) : Codec[SquadPositionDetail2] = paddedStringMetaCodec(bitsOverByte.Length).exmap[SquadPositionDetail2] (
+    orders => Attempt.successful(SquadPositionDetail2(None, orders)),
+    {
+      case SquadPositionDetail2(_, _, Some(orders), _, _, _) =>
+        Attempt.successful(orders)
+      case _ =>
+        Attempt.failure(Err("failed to encode squad position data for detailed orders"))
+    }
+  )
+
+  private val requirementsCodec : Codec[SquadPositionDetail2] = ulongL(46).exmap[SquadPositionDetail2] (
+    requirements => Attempt.successful(SquadPositionDetail2(CertificationType.fromEncodedLong(requirements))),
+    {
+      case SquadPositionDetail2(_, _, _, Some(requirements), _, _) =>
+        Attempt.successful(CertificationType.toEncodedLong(requirements))
+      case _ =>
+        Attempt.failure(Err("failed to encode squad position data for certification requirements"))
+    }
+  )
+
+  private val charIdCodec : Codec[SquadPositionDetail2] = uint32L.exmap[SquadPositionDetail2] (
+    char_id => Attempt.successful(SquadPositionDetail2(char_id)),
+    {
+      case SquadPositionDetail2(_, _, _, _, Some(char_id), _) =>
+        Attempt.successful(char_id)
+      case _ =>
+        Attempt.failure(Err("failed to encode squad data for member id"))
+    }
+  )
+
+  private def nameCodec(bitsOverByte : StreamLengthToken) : Codec[SquadPositionDetail2] = paddedStringMetaCodec(bitsOverByte.Length).exmap[SquadPositionDetail2] (
+    name => Attempt.successful(SquadPositionDetail2(name)),
+    {
+      case SquadPositionDetail2(_, _, _, _, _, Some(orders)) =>
+        Attempt.successful(orders)
+      case _ =>
+        Attempt.failure(Err("failed to encode squad position data for member name"))
+    }
+  )
+
+  /**
+    * `Codec` for failing to determine a valid `Codec` based on the entry data.
+    * This `Codec` is an invalid codec that does not read any bit data.
+    * The `conditional` will always return `None` because
+    * its determining conditional statement is explicitly `false`
+    * and all cases involving explicit failure.
+    */
+  private val failureCodec : Codec[SquadPositionDetail2] = conditional(included = false, bool).exmap[SquadPositionDetail2] (
+    _ => Attempt.failure(Err("decoding with unhandled codec")),
+    _ => Attempt.failure(Err("encoding with unhandled codec"))
+  )
+
+  private final case class LinkedSquadPositionInfo(code : Int, info : SquadPositionDetail2, next : Option[LinkedSquadPositionInfo])
+
+  private def unlinkSquadPositionInfo(info : LinkedSquadPositionInfo) : SquadPositionDetail2 = unlinkSquadPositionInfo(Some(info))
+
+  /**
+    * Concatenate a `SquadInfo` object chain into a single `SquadInfo` object.
+    * Recursively visits every link in a `SquadInfo` object chain.
+    * @param info the current link in the chain
+    * @param squadInfo the persistent `SquadInfo` concatenation object;
+    *                  defaults to `SquadInfo.Blank`
+    * @return the concatenated `SquadInfo` object
+    */
+  @tailrec
+  private def unlinkSquadPositionInfo(info : Option[LinkedSquadPositionInfo], squadInfo : SquadPositionDetail2 = SquadPositionDetail2.Blank) : SquadPositionDetail2 = {
+    info match {
+      case None =>
+        squadInfo
+      case Some(sqInfo) =>
+        unlinkSquadPositionInfo(sqInfo.next, squadInfo And sqInfo.info)
+    }
+  }
+
+  /**
+    * Decompose a single `SquadInfo` object into a `SquadInfo` object chain of the original's fields.
+    * The fields as a linked list are explicitly organized "leader", "task", "zone_id", "size", "capacity,"
+    * or as "(leader, (task, (zone_id, (size, (capacity, None)))))" when fully populated and composed.
+    * @param info a `SquadInfo` object that has all relevant fields populated
+    * @return a linked list of `SquadInfo` objects, each with a single field from the input `SquadInfo` object
+    */
+  private def linkSquadPositionInfo(info : SquadPositionDetail2) : LinkedSquadPositionInfo = {
+    //import scala.collection.immutable.::
+    Seq(
+      (5, SquadPositionDetail2(None, None, None, info.requirements, None, None)),
+      (4, SquadPositionDetail2(None, None, None, None, None, info.name)),
+      (3, SquadPositionDetail2(None, None, None, None, info.char_id, None)),
+      (2, SquadPositionDetail2(None, None, info.detailed_orders, None, None, None)),
+      (1, SquadPositionDetail2(None, info.role, None, None, None, None)),
+      (0, SquadPositionDetail2(info.is_closed, None, None, None, None, None))
+    ) //in reverse order so that the linked list is in the correct order
+      .filterNot { case (_, sqInfo) => sqInfo == SquadPositionDetail2.Blank}
+    match {
+      case Nil =>
+        throw new Exception("no linked list squad position fields encountered where at least one was expected") //bad end
+      case x :: Nil =>
+        val (code, squadInfo) = x
+        LinkedSquadPositionInfo(code, squadInfo, None)
+      case x :: xs =>
+        val (code, squadInfo) = x
+        linkSquadPositionInfo(xs, LinkedSquadPositionInfo(code, squadInfo, None))
+    }
+  }
+
+  /**
+    * Decompose a single `SquadInfo` object into a `SquadInfo` object chain of the original's fields.
+    * The fields as a linked list are explicitly organized "leader", "task", "zone_id", "size", "capacity,"
+    * or as "(leader, (task, (zone_id, (size, (capacity, None)))))" when fully populated and composed.
+    * @param infoList a series of paired field codes and `SquadInfo` objects with data in the indicated fields
+    * @return a linked list of `SquadInfo` objects, each with a single field from the input `SquadInfo` object
+    */
+  @tailrec
+  private def linkSquadPositionInfo(infoList : Seq[(Int, SquadPositionDetail2)], linkedInfo : LinkedSquadPositionInfo) : LinkedSquadPositionInfo = {
+    if(infoList.isEmpty) {
+      linkedInfo
+    }
+    else {
+      val (code, data) = infoList.head
+      linkSquadPositionInfo(infoList.tail, LinkedSquadPositionInfo(code, data, Some(linkedInfo)))
+    }
+  }
+
+  private def listing_codec(size : Int, bitsOverByte : StreamLengthToken) : Codec[LinkedSquadPositionInfo] = {
+    import shapeless.::
+    (
+      uint4 >>:~ { code =>
+        selectCodecAction(code, bitsOverByte.Add(4)) ::
+          conditional(size - 1 > 0, listing_codec(size - 1, modifyCodecPadValue(code, bitsOverByte)))
+      }
+      ).xmap[LinkedSquadPositionInfo] (
+      {
+        case code :: entry :: next :: HNil =>
+          LinkedSquadPositionInfo(code, entry, next)
+      },
+      {
+        case LinkedSquadPositionInfo(code, entry, next) =>
+          code :: entry :: next :: HNil
+      }
+    )
+  }
+
+  private def selectCodecAction(code : Int, bitsOverByte : StreamLengthToken) : Codec[SquadPositionDetail2] = {
+    code match {
+      case 1 => roleCodec(bitsOverByte)
+      case 2 => ordersCodec(bitsOverByte)
+      case 3 => charIdCodec
+      case 4 => nameCodec(bitsOverByte)
+      case 5 => requirementsCodec
+      case _ => failureCodec
+    }
+  }
+
+  private def modifyCodecPadValue(code : Int, bitsOverByte : StreamLengthToken) : StreamLengthToken = {
+    code match {
+      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 _ => bitsOverByte.Length = Int.MinValue //wildly incorrect
+    }
+  }
+
+  private def squad_member_details_codec(bitsOverByte : StreamLengthToken) : Codec[LinkedSquadPositionInfo] = {
+    import shapeless.::
+    (
+      uint8 >>:~ { size =>
+        listing_codec(size, bitsOverByte).hlist
+      }
+      ).xmap[LinkedSquadPositionInfo] (
+      {
+        case _ :: info :: HNil =>
+          info
+      },
+      info => {
+        var i = 1
+        var dinfo = info
+        while(info.next.nonEmpty) {
+          i += 1
+          dinfo = info.next.get
+        }
+        i :: info :: HNil
+      }
+    )
+  }
+
+  def codec(bitsOverByte : StreamLengthToken) : Codec[SquadPositionEntry] = {
+    import shapeless.::
+    (
+      ("index" | uint8) >>:~ { index =>
+        conditional(index < 255, bool) >>:~ { is_open =>
+          conditional(is_open.contains(true) && index < 255, "info" | squad_member_details_codec(bitsOverByte.Add(1))).hlist
+        }
+      }
+      ).xmap[SquadPositionEntry] (
+      {
+        case 255 :: _ :: _ :: HNil =>
+          SquadPositionEntry(255, None)
+        case ndx :: Some(false) :: None :: HNil =>
+          SquadPositionEntry(ndx, None)
+        case ndx :: Some(true) :: Some(info) :: HNil =>
+          SquadPositionEntry(ndx, Some(unlinkSquadPositionInfo(info)))
+      },
+      {
+        case SquadPositionEntry(255, _) =>
+          255 :: None :: None  :: HNil
+        case SquadPositionEntry(ndx, None) =>
+          ndx :: Some(false) :: None :: HNil
+        case SquadPositionEntry(ndx, Some(info)) =>
+          ndx :: Some(true) :: Some(linkSquadPositionInfo(info)) :: HNil
+      }
+    )
+  }
+}
+
+object SquadDetail {
+  final val Blank = SquadDetail(None, None, None, None, None, None, None)
+
+  def apply(leader_char_id : Long) : SquadDetail = SquadDetail(None, Some(leader_char_id), None, None, None, None, None)
+
+  def apply(leader_name : String, task : Option[String]) : SquadDetail = SquadDetail(None, None, None, Some(leader_name), task, None, None)
+
+  def apply(leader_name : Option[String], task : String) : SquadDetail = SquadDetail(None, None, None, leader_name, Some(task), None, None)
+
+  def apply(zone_id : PlanetSideZoneID) : SquadDetail = SquadDetail(None, None, None, None, None, Some(zone_id), None)
+
+  def apply(member_list : List[SquadPositionEntry]) : SquadDetail = SquadDetail(None, None, None, None, None, None, Some(member_list))
+
+  def apply(unk1 : Int, leader_char_id : Long, unk2 : BitVector, leader_name : String, task : String, zone_id : PlanetSideZoneID, member_info : List[SquadPositionEntry]) : SquadDetail = {
+    SquadDetail(Some(unk1), Some(leader_char_id), Some(unk2), Some(leader_name), Some(task), Some(zone_id), Some(member_info))
+  }
+}
+
+object SquadDetailDefinitionUpdateMessage2 {
+  /**
+    * Produces a `Codec` function for byte-aligned, padded Pascal strings encoded through common manipulations.
+    * @see `PacketHelpers.encodedWideStringAligned`
+    * @param bitsOverByte the number of bits past the previous byte-aligned index;
+    *             should be a 0-7 number that gets converted to a 1-7 string padding number
+    * @return the encoded string `Codec`
+    */
+  def paddedStringMetaCodec(bitsOverByte : Int) : Codec[String] = PacketHelpers.encodedWideStringAligned({
+    val mod8 = bitsOverByte % 8
+    if(mod8 == 0) {
+      0
+    }
+    else {
+      8 - mod8
+    }
+  })
+
+  private def memberCodec(pad : Int) : Codec[SquadPositionDetail2] = {
+    import shapeless.::
+    (
+      uint8 :: //required value = 6
+        ("is_closed" | bool) :: //if all positions are closed, the squad detail menu display no positions at all
+        ("role" | PacketHelpers.encodedWideStringAligned(pad)) ::
+        ("detailed_orders" | PacketHelpers.encodedWideString) ::
+        ("char_id" | uint32L) ::
+        ("name" | PacketHelpers.encodedWideString) ::
+        ("requirements" | ulongL(46))
+      ).exmap[SquadPositionDetail2] (
+      {
+        case 6 :: closed :: role :: orders :: char_id :: name :: requirements :: HNil =>
+          Attempt.Successful(
+            SquadPositionDetail2(Some(closed), Some(role), Some(orders), Some(defaultRequirements ++ CertificationType.fromEncodedLong(requirements)), Some(char_id), Some(name))
+          )
+        case data =>
+          Attempt.Failure(Err(s"can not decode a SquadDetailDefinitionUpdate member's data - $data"))
+      },
+      {
+        case SquadPositionDetail2(Some(closed), Some(role), Some(orders), Some(requirements), Some(char_id), Some(name)) =>
+          Attempt.Successful(6 :: closed :: role :: orders :: char_id :: name :: CertificationType.toEncodedLong(defaultRequirements ++ requirements) :: HNil)
+      }
+    )
+  }
+
+  private val first_member_codec : Codec[SquadPositionDetail2] = memberCodec(pad = 7)
+
+  private val member_codec : Codec[SquadPositionDetail2] = memberCodec(pad = 0)
+
+  private case class LinkedMemberList(member : SquadPositionDetail2, next : Option[LinkedMemberList])
+
+  private def subsequent_member_codec : Codec[LinkedMemberList] = {
+    import shapeless.::
+    (
+      //disruptive coupling action (e.g., flatPrepend) is necessary to allow for recursive Codec
+      ("member" | member_codec) >>:~ { _ =>
+        optional(bool, "next" | subsequent_member_codec).hlist
+      }
+      ).xmap[LinkedMemberList] (
+      {
+        case a :: b :: HNil =>
+          LinkedMemberList(a, b)
+      },
+      {
+        case LinkedMemberList(a, b) =>
+          a :: b :: HNil
+      }
+    )
+  }
+
+  private def initial_member_codec : Codec[LinkedMemberList] = {
+    import shapeless.::
+    (
+      ("member" | first_member_codec) ::
+        optional(bool, "next" | subsequent_member_codec)
+      ).xmap[LinkedMemberList] (
+      {
+        case a :: b :: HNil =>
+          LinkedMemberList(a, b)
+      },
+      {
+        case LinkedMemberList(a, b) =>
+          a :: b :: HNil
+      }
+    )
+  }
+
+  @tailrec
+  private def unlinkMemberList(list : LinkedMemberList, out : List[SquadPositionDetail2] = Nil) : List[SquadPositionDetail2] = {
+    list.next match {
+      case None =>
+        out :+ list.member
+      case Some(next) =>
+        unlinkMemberList(next, out :+ list.member)
+    }
+  }
+
+  private def linkMemberList(list : List[SquadPositionDetail2]) : LinkedMemberList = {
+    list match {
+      case Nil =>
+        throw new Exception("")
+      case x :: Nil =>
+        LinkedMemberList(x, None)
+      case x :: xs =>
+        linkMemberList(xs, LinkedMemberList(x, None))
+    }
+  }
+
+  @tailrec
+  private def linkMemberList(list : List[SquadPositionDetail2], out : LinkedMemberList) : LinkedMemberList = {
+    list match {
+      case Nil =>
+        out
+      case x :: Nil =>
+        LinkedMemberList(x, Some(out))
+      case x :: xs =>
+        linkMemberList(xs, LinkedMemberList(x, Some(out)))
+    }
+  }
+
+  val full_squad_detail_codec : Codec[SquadDetail] = {
+    import shapeless.::
+    (
+      ("unk1" | uint8) ::
+        uint24 :: //unknown, but can be 0'd
+        ("leader_char_id" | uint32L) ::
+        ("unk2" | bits(22)) :: //variable fields, but can be 0'd
+        uint(10) :: //constant = 0
+        ("leader" | PacketHelpers.encodedWideStringAligned(7)) ::
+        ("task" | PacketHelpers.encodedWideString) ::
+        ("zone_id" | PlanetSideZoneID.codec) ::
+        uint(23) :: //constant = 4983296
+        optional(bool, "member_info" | initial_member_codec)
+      ).exmap[SquadDetail] (
+      {
+        case u1 :: _ :: char_id :: u2 :: _ :: leader :: task :: zone :: _ :: Some(member_list) :: HNil =>
+          Attempt.Successful(
+            SquadDetail(Some(u1), Some(char_id), Some(u2), Some(leader), Some(task), Some(zone),
+              Some(unlinkMemberList(member_list).zipWithIndex.map { case (entry, index) => SquadPositionEntry(index, Some(entry)) })
+            )
+          )
+        case data =>
+          Attempt.failure(Err(s"can not get squad detail definition from data $data"))
+      },
+      {
+        case SquadDetail(Some(u1), Some(char_id), Some(u2), Some(leader), Some(task), Some(zone), Some(member_list)) =>
+          Attempt.Successful(
+            math.max(u1, 1) :: 0 :: char_id :: u2.take(22) :: 0 :: leader :: task :: zone :: 4983296 ::
+              Some(linkMemberList(member_list.collect { case SquadPositionEntry(_, Some(entry)) => entry }.reverse)) ::
+              HNil
+          )
+      }
+    )
+  }
+
+  private val leaderCharIdCodec : Codec[SquadDetail] = uint32L.exmap[SquadDetail] (
+    char_id => Attempt.successful(SquadDetail(char_id)),
+    {
+      case SquadDetail(_, Some(char_id), _,  _, _, _, _) =>
+        Attempt.successful(char_id)
+      case _ =>
+        Attempt.failure(Err("failed to encode squad data for leader id"))
+    }
+  )
+
+  private def leaderNameCodec(bitsOverByte : StreamLengthToken) : Codec[SquadDetail] = paddedStringMetaCodec(bitsOverByte.Length).exmap[SquadDetail] (
+    name => Attempt.successful(SquadDetail(name, None)),
+    {
+      case SquadDetail(_, _, _, Some(name), _, _, _) =>
+        Attempt.successful(name)
+      case _ =>
+        Attempt.failure(Err("failed to encode squad data for leader name"))
+    }
+  )
+
+  private def taskCodec(bitsOverByte : StreamLengthToken) : Codec[SquadDetail] = paddedStringMetaCodec(bitsOverByte.Length).exmap[SquadDetail] (
+    task => Attempt.successful(SquadDetail(None, task)),
+    {
+      case SquadDetail(_, _, _, _, Some(task), _, _) =>
+        Attempt.successful(task)
+      case _ =>
+        Attempt.failure(Err("failed to encode squad data for task"))
+    }
+  )
+
+  private val zoneCodec : Codec[SquadDetail] = PlanetSideZoneID.codec.exmap[SquadDetail] (
+    zone_id => Attempt.successful(SquadDetail(zone_id)),
+    {
+      case SquadDetail(_, _, _, _, _, Some(zone_id), _) =>
+        Attempt.successful(zone_id)
+      case _ =>
+        Attempt.failure(Err("failed to encode squad data for zone id"))
+    }
+  )
+
+  private def membersCodec(bitsOverByte : StreamLengthToken) : Codec[SquadDetail] = {
+    import shapeless.::
+    bitsOverByte.Add(19)
+    (
+      uint(19) :: //constant = 0x60040, or 393280 in 19u BE
+        vector(SquadPositionEntry.codec(bitsOverByte))
+      ).exmap[SquadDetail] (
+      {
+        case _ :: member_list :: HNil =>
+          Attempt.successful(SquadDetail(member_list.toList))
+      },
+      {
+        case SquadDetail(_, _, _, _, _, _, Some(member_list)) =>
+          Attempt.successful(393280 :: member_list.toVector :: HNil)
+        case _ =>
+          Attempt.failure(Err("failed to encode squad data for members"))
+      }
+    )
+  }
+
+  private val failureCodec : Codec[SquadDetail] = conditional(included = false, bool).exmap[SquadDetail] (
+    _ => Attempt.failure(Err("decoding with unhandled codec")),
+    _ => Attempt.failure(Err("encoding with unhandled codec"))
+  )
+
+  private def selectCodecAction(code : Int, bitsOverByte : StreamLengthToken) : Codec[SquadDetail] = {
+    code match {
+      case 2 => leaderCharIdCodec
+      case 4 => leaderNameCodec(bitsOverByte)
+      case 5 => taskCodec(bitsOverByte)
+      case 6 => zoneCodec
+      case 8 => membersCodec(bitsOverByte)
+      case _ => failureCodec
+    }
+  }
+
+  private def modifyCodecPadValue(code : Int, bitsOverByte : StreamLengthToken) : StreamLengthToken = {
+    code match {
+      case 2 => bitsOverByte //32u = no added padding
+      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 8 => bitsOverByte.Length = 0 //end of stream
+      case _ => bitsOverByte.Length = Int.MinValue //wildly incorrect
+    }
+  }
+
+  private final case class LinkedSquadInfo(code : Int, info : SquadDetail, next : Option[LinkedSquadInfo])
+
+  private def unlinkSquadInfo(info : LinkedSquadInfo) : SquadDetail = unlinkSquadInfo(Some(info))
+
+  @tailrec
+  private def unlinkSquadInfo(info : Option[LinkedSquadInfo], squadInfo : SquadDetail = SquadDetail.Blank) : SquadDetail = {
+    info match {
+      case None =>
+        squadInfo
+      case Some(sqInfo) =>
+        unlinkSquadInfo(sqInfo.next, squadInfo And sqInfo.info)
+    }
+  }
+
+  private def linkSquadInfo(info : SquadDetail) : LinkedSquadInfo = {
+    //import scala.collection.immutable.::
+    Seq(
+      (8, SquadDetail(None, None, None, None, None, None, info.member_info)),
+      (6, SquadDetail(None, None, None, None, None, info.zone_id, None)),
+      (5, SquadDetail(None, None, None, None, info.task, None, None)),
+      (4, SquadDetail(None, None, None, info.leader_name, None, None, None)),
+      (2, SquadDetail(None, info.leader_char_id, 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 =>
+        throw new Exception("no linked list squad fields encountered where at least one was expected") //bad end
+      case x :: Nil =>
+        val (code, squadInfo) = x
+        LinkedSquadInfo(code, squadInfo, None)
+      case x :: xs =>
+        val (code, squadInfo) = x
+        linkSquadInfo(xs, LinkedSquadInfo(code, squadInfo, None))
+    }
+  }
+
+  @tailrec
+  private def linkSquadInfo(infoList : Seq[(Int, SquadDetail)], linkedInfo : LinkedSquadInfo) : LinkedSquadInfo = {
+    if(infoList.isEmpty) {
+      linkedInfo
+    }
+    else {
+      val (code, data) = infoList.head
+      linkSquadInfo(infoList.tail, LinkedSquadInfo(code, data, Some(linkedInfo)))
+    }
+  }
+
+  private def linked_squad_detail_codec(size : Int, bitsOverByte : StreamLengthToken) : Codec[LinkedSquadInfo] = {
+    import shapeless.::
+    (
+      uint4 >>:~ { code =>
+        selectCodecAction(code, bitsOverByte.Add(4)) ::
+          conditional(size - 1 > 0, linked_squad_detail_codec(size - 1, modifyCodecPadValue(code, bitsOverByte)))
+      }
+      ).exmap[LinkedSquadInfo] (
+      {
+        case action :: detail :: next :: HNil =>
+          Attempt.Successful(LinkedSquadInfo(action, detail, next))
+      },
+      {
+        case LinkedSquadInfo(action, detail, next) =>
+          Attempt.Successful(action :: detail :: next :: HNil)
+      }
+    )
+  }
+
+  def squadDetailSelectCodec(size : Int) : Codec[SquadDetail] = {
+    if(size == 9) {
+      full_squad_detail_codec
+    }
+    else {
+      linked_squad_detail_codec(size, new StreamLengthToken(1)).xmap[SquadDetail] (
+        linkedDetail => unlinkSquadInfo(linkedDetail),
+        unlinkedDetail => linkSquadInfo(unlinkedDetail)
+      )
+    }
+  }
+
+  private def codec() : Codec[SquadDetailDefinitionUpdateMessage2] = {
+    import shapeless.::
+    (
+      ("guid" | PlanetSideGUID.codec) ::
+        bool ::
+        (uint8 >>:~ { size =>
+          squadDetailSelectCodec(size).hlist
+        })
+      ).exmap[SquadDetailDefinitionUpdateMessage2] (
+      {
+        case guid :: _ :: _ :: info :: HNil =>
+          Attempt.Successful(SquadDetailDefinitionUpdateMessage2(guid, info))
+      },
+      {
+        case SquadDetailDefinitionUpdateMessage2(guid, info) =>
+          val occupiedSquadFieldCount = List(info.unk1, info.leader_char_id, info.unk2, info.leader_name, info.task, info.zone_id, info.member_info)
+            .count(_.nonEmpty)
+          if(occupiedSquadFieldCount < 9) {
+            //itemized detail definition protocol
+            Attempt.Successful(guid :: true :: occupiedSquadFieldCount :: info :: HNil)
+          }
+          else {
+            info.member_info match {
+              case Some(list) =>
+                if(list.size == 10 &&
+                list
+                  .collect { case position if position.info.nonEmpty =>
+                    val info = position.info.get
+                    List(info.is_closed, info.role, info.detailed_orders, info.requirements, info.char_id, info.name)
+                  }
+                  .flatten
+                  .count(_.isEmpty) == 0) {
+                  //full squad detail definition protocol
+                  Attempt.Successful(guid :: true :: 9 :: info :: HNil)
+                }
+                else {
+                  //unhandled state
+                  Attempt.Failure(Err("can not split encoding patterns - all squad fields are defined but not all squad member fields are defined"))
+                }
+              case None =>
+                //impossible?
+                Attempt.Failure(Err("the members field can not be empty; the existence of this field was already proven"))
+            }
+          }
+      }
+    )
+  }
+
+  implicit val code : Codec[SquadDetailDefinitionUpdateMessage2] = codec()
+}
diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterAppearanceData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterAppearanceData.scala
index c2b064d3..c3f17921 100644
--- a/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterAppearanceData.scala
+++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterAppearanceData.scala
@@ -23,12 +23,13 @@ import shapeless.{::, HNil}
   *                  -player_guid - does nothing?
   * @param exosuit   the type of exo-suit the avatar will be depicted in;
   *                  for Black OPs, the agile exo-suit and the reinforced exo-suit are replaced with the Black OPs exo-suits
+  * @param char_id   a unique character reference identification number
   */
 final case class CharacterAppearanceA(app : BasicCharacterData,
                                       data : CommonFieldData,
                                       exosuit : ExoSuitType.Value,
                                       unk5 : Int,
-                                      unk6 : Long,
+                                      char_id : Long,
                                       unk7 : Int,
                                       unk8 : Int,
                                       unk9 : Int,
diff --git a/common/src/main/scala/services/teamwork/SquadService.scala b/common/src/main/scala/services/teamwork/SquadService.scala
index f7b96aa0..245c3a29 100644
--- a/common/src/main/scala/services/teamwork/SquadService.scala
+++ b/common/src/main/scala/services/teamwork/SquadService.scala
@@ -121,6 +121,7 @@ class SquadService extends Actor {
               //other players were in the squad; publicly disband it
               squad.Membership.foreach(position => {
                 position.Name = ""
+                position.CharId = 0L
                 position.ZoneId = 0
                 position.Position = Vector3.Zero
                 position.Health = 0
@@ -151,6 +152,8 @@ class SquadService extends Actor {
       import net.psforever.packet.game.SquadAction._
       val squadOpt = GetParticipatingSquad(tplayer, zone_ordinal_number)
       action match {
+        case SaveSquadDefinition() =>
+
         case ChangeSquadPurpose(purpose) =>
           log.info(s"${tplayer.Name}-${tplayer.Faction} has changed his squad's task to $purpose")
           val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt)
@@ -265,6 +268,7 @@ class SquadService extends Actor {
                   val hadPreviousPosition = squad.Membership.find(_.Name == name) match {
                     case Some(currentPosition)=>
                       currentPosition.Name = ""
+                      currentPosition.CharId = 0L
                       currentPosition.ZoneId = 0
                       currentPosition.Health = 0
                       currentPosition.Armor = 0
@@ -274,6 +278,7 @@ class SquadService extends Actor {
                       false
                   }
                   desiredPosition.Name = name
+                  desiredPosition.CharId = tplayer.CharId
                   desiredPosition.ZoneId = zone_ordinal_number
                   desiredPosition.Health = tplayer.Health
                   desiredPosition.Armor = tplayer.Armor
@@ -380,7 +385,7 @@ class SquadService extends Actor {
       PlanetSideZoneID(squad.ZoneId),
       squad.Membership.zipWithIndex.map({ case (p, index) =>
         if(squad.Availability(index)) {
-          SquadPositionDetail(p.Role, p.Orders, p.Requirements, p.Name)
+          SquadPositionDetail(p.Role, p.Orders, p.Requirements, p.CharId, p.Name)
         }
         else {
           SquadPositionDetail.Closed
diff --git a/common/src/test/scala/game/SquadDetailDefinitionUpdateMessageTest.scala b/common/src/test/scala/game/SquadDetailDefinitionUpdateMessageTest.scala
index 5ad56bd0..cf56fb86 100644
--- a/common/src/test/scala/game/SquadDetailDefinitionUpdateMessageTest.scala
+++ b/common/src/test/scala/game/SquadDetailDefinitionUpdateMessageTest.scala
@@ -12,7 +12,7 @@ class SquadDetailDefinitionUpdateMessageTest extends Specification {
   "SquadDetailDefinitionUpdateMessage" should {
     "decode" in {
       PacketCoding.DecodePacket(string).require match {
-        case SquadDetailDefinitionUpdateMessage(guid, unk, leader, task, zone, member_info) =>
+        case SquadDetailDefinitionUpdateMessage(guid, unk1, char_id, unk2, leader, task, zone, member_info) =>
           ok
         case _ =>
           ko
@@ -22,24 +22,25 @@ class SquadDetailDefinitionUpdateMessageTest extends Specification {
     "encode" in {
       val msg = SquadDetailDefinitionUpdateMessage(
         PlanetSideGUID(3),
+        42771010L,
         "HofD",
         "\\#ffdc00***\\#9640ff=KOK+SPC+FLY=\\#ffdc00***\\#FF4040 All Welcome",
         PlanetSideZoneID(7),
         List(
-          SquadPositionDetail("\\#ff0000 |||||||||||||||||||||||", ""),
+          SquadPositionDetail("\\#ff0000 |||||||||||||||||||||||", "Just a space filler"),
           SquadPositionDetail("\\#ffdc00   C", ""),
-          SquadPositionDetail("\\#ffdc00   H", "", "OpoIE"),
-          SquadPositionDetail("\\#ffdc00    I", "", "BobaF3tt907"),
+          SquadPositionDetail("\\#ffdc00   H", "", Set(), 42644970L, "OpoIE"),
+          SquadPositionDetail("\\#ffdc00    I", "", Set(), 41604210L, "BobaF3tt907"),
           SquadPositionDetail("\\#ffdc00   N", ""),
           SquadPositionDetail("\\#ffdc00   A", ""),
-          SquadPositionDetail("\\#ff0000 |||||||||||||||||||||||", ""),
+          SquadPositionDetail("\\#ff0000 |||||||||||||||||||||||", "Another space filler"),
           SquadPositionDetail("\\#9640ff   K", ""),
-          SquadPositionDetail("\\#9640ff   O", "", "HofD"),
+          SquadPositionDetail("\\#9640ff   O", "", Set(), 42771010L, "HofD"),
           SquadPositionDetail("\\#9640ff   K", "")
         )
       )
       val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
-      ok
+      pkt mustEqual string
     }
   }
 }
diff --git a/common/src/test/scala/game/objectcreate/CharacterDataTest.scala b/common/src/test/scala/game/objectcreate/CharacterDataTest.scala
index eece5121..22a1e7db 100644
--- a/common/src/test/scala/game/objectcreate/CharacterDataTest.scala
+++ b/common/src/test/scala/game/objectcreate/CharacterDataTest.scala
@@ -48,7 +48,7 @@ class CharacterDataTest extends Specification {
                   a.data.v5.isEmpty mustEqual true
                   a.exosuit mustEqual ExoSuitType.Reinforced
                   a.unk5 mustEqual 0
-                  a.unk6 mustEqual 30777081L
+                  a.char_id mustEqual 30777081L
                   a.unk7 mustEqual 1
                   a.unk8 mustEqual 4
                   a.unk9 mustEqual 0
@@ -167,7 +167,7 @@ class CharacterDataTest extends Specification {
                   a.data.v5.isEmpty mustEqual true
                   a.exosuit mustEqual ExoSuitType.Reinforced
                   a.unk5 mustEqual 0
-                  a.unk6 mustEqual 192L
+                  a.char_id mustEqual 192L
                   a.unk7 mustEqual 0
                   a.unk8 mustEqual 0
                   a.unk9 mustEqual 0
@@ -236,7 +236,7 @@ class CharacterDataTest extends Specification {
                   a.data.v5.isEmpty mustEqual true
                   a.exosuit mustEqual ExoSuitType.MAX
                   a.unk5 mustEqual 1
-                  a.unk6 mustEqual 0L
+                  a.char_id mustEqual 0L
                   a.unk7 mustEqual 0
                   a.unk8 mustEqual 0
                   a.unk9 mustEqual 0
diff --git a/common/src/test/scala/game/objectcreatedetailed/DetailedCharacterDataTest.scala b/common/src/test/scala/game/objectcreatedetailed/DetailedCharacterDataTest.scala
index e64dd8fc..34175c27 100644
--- a/common/src/test/scala/game/objectcreatedetailed/DetailedCharacterDataTest.scala
+++ b/common/src/test/scala/game/objectcreatedetailed/DetailedCharacterDataTest.scala
@@ -76,7 +76,7 @@ class DetailedCharacterDataTest extends Specification {
                   a.data.v5.isEmpty mustEqual true
                   a.exosuit mustEqual ExoSuitType.Standard
                   a.unk5 mustEqual 0
-                  a.unk6 mustEqual 41605313L
+                  a.char_id mustEqual 41605313L
                   a.unk7 mustEqual 0
                   a.unk8 mustEqual 0
                   a.unk9 mustEqual 0
@@ -264,7 +264,7 @@ class DetailedCharacterDataTest extends Specification {
                   a.data.v5.isEmpty mustEqual true
                   a.exosuit mustEqual ExoSuitType.Standard
                   a.unk5 mustEqual 0
-                  a.unk6 mustEqual 192L
+                  a.char_id mustEqual 192L
                   a.unk7 mustEqual 0
                   a.unk8 mustEqual 0
                   a.unk9 mustEqual 0
@@ -449,7 +449,7 @@ class DetailedCharacterDataTest extends Specification {
                   a.data.v5.isEmpty mustEqual true
                   a.exosuit mustEqual ExoSuitType.MAX
                   a.unk5 mustEqual 1
-                  a.unk6 mustEqual 41605870L
+                  a.char_id mustEqual 41605870L
                   a.unk7 mustEqual 0
                   a.unk8 mustEqual 0
                   a.unk9 mustEqual 0
@@ -657,7 +657,7 @@ class DetailedCharacterDataTest extends Specification {
                   a.data.v5.isEmpty mustEqual true
                   a.exosuit mustEqual ExoSuitType.Agile
                   a.unk5 mustEqual 0
-                  a.unk6 mustEqual 733931L
+                  a.char_id mustEqual 733931L
                   a.unk7 mustEqual 0
                   a.unk8 mustEqual 0
                   a.unk9 mustEqual 0
@@ -1165,7 +1165,7 @@ class DetailedCharacterDataTest extends Specification {
                   a.data.v5.isEmpty mustEqual true
                   a.exosuit mustEqual ExoSuitType.Standard
                   a.unk5 mustEqual 0
-                  a.unk6 mustEqual 1176612L
+                  a.char_id mustEqual 1176612L
                   a.unk7 mustEqual 15
                   a.unk8 mustEqual 5
                   a.unk9 mustEqual 10
diff --git a/common/src/test/scala/game/objectcreatevehicle/MountedVehiclesTest.scala b/common/src/test/scala/game/objectcreatevehicle/MountedVehiclesTest.scala
index 5b73667f..1c831f74 100644
--- a/common/src/test/scala/game/objectcreatevehicle/MountedVehiclesTest.scala
+++ b/common/src/test/scala/game/objectcreatevehicle/MountedVehiclesTest.scala
@@ -62,7 +62,7 @@ class MountedVehiclesTest extends Specification {
                         a.data.v5.isEmpty mustEqual true
                         a.exosuit mustEqual ExoSuitType.Agile
                         a.unk5 mustEqual 0
-                        a.unk6 mustEqual 30777081L
+                        a.char_id mustEqual 30777081L
                         a.unk7 mustEqual 1
                         a.unk8 mustEqual 4
                         a.unk9 mustEqual 0
diff --git a/common/src/test/scala/objects/AvatarTest.scala b/common/src/test/scala/objects/AvatarTest.scala
index c2c482c0..33b53ce7 100644
--- a/common/src/test/scala/objects/AvatarTest.scala
+++ b/common/src/test/scala/objects/AvatarTest.scala
@@ -281,7 +281,7 @@ class AvatarTest extends Specification {
 
   "does not have any loadout specifications by default" in {
     val (_, avatar) = CreatePlayer()
-    (0 to 9).foreach { avatar.LoadLoadout(_) mustEqual None }
+    (0 to 9).foreach { avatar.EquipmentLoadouts.LoadLoadout(_) mustEqual None }
     ok
   }
 
@@ -289,9 +289,9 @@ class AvatarTest extends Specification {
     val (obj, avatar) = CreatePlayer()
     obj.Slot(0).Equipment.get.asInstanceOf[Tool].Magazine = 1 //non-standard but legal
     obj.Slot(2).Equipment.get.asInstanceOf[Tool].AmmoSlot.Magazine = 100 //non-standard (and out of range, real=25)
-    avatar.SaveLoadout(obj, "test", 0)
+    avatar.EquipmentLoadouts.SaveLoadout(obj, "test", 0)
 
-    avatar.LoadLoadout(0) match {
+    avatar.EquipmentLoadouts.LoadLoadout(0) match {
       case Some(items : InfantryLoadout) =>
         items.label mustEqual "test"
         items.exosuit mustEqual obj.ExoSuit
@@ -329,25 +329,25 @@ class AvatarTest extends Specification {
 
   "save player's current inventory as a loadout, only found in the called-out slot number" in {
     val (obj, avatar) = CreatePlayer()
-    avatar.SaveLoadout(obj, "test", 0)
+    avatar.EquipmentLoadouts.SaveLoadout(obj, "test", 0)
 
-    avatar.LoadLoadout(1).isDefined mustEqual false
-    avatar.LoadLoadout(0).isDefined mustEqual true
+    avatar.EquipmentLoadouts.LoadLoadout(1).isDefined mustEqual false
+    avatar.EquipmentLoadouts.LoadLoadout(0).isDefined mustEqual true
   }
 
   "try to save player's current inventory as a loadout, but will not save to an invalid slot" in {
     val (obj, avatar) = CreatePlayer()
-    avatar.SaveLoadout(obj, "test", 10)
+    avatar.EquipmentLoadouts.SaveLoadout(obj, "test", 10)
 
-    avatar.LoadLoadout(10) mustEqual None
+    avatar.EquipmentLoadouts.LoadLoadout(10) mustEqual None
   }
 
   "save player's current inventory as a loadout, without inventory contents" in {
     val (obj, avatar) = CreatePlayer()
     obj.Inventory.Clear()
-    avatar.SaveLoadout(obj, "test", 0)
+    avatar.EquipmentLoadouts.SaveLoadout(obj, "test", 0)
 
-    avatar.LoadLoadout(0) match {
+    avatar.EquipmentLoadouts.LoadLoadout(0) match {
       case Some(items : InfantryLoadout) =>
         items.label mustEqual "test"
         items.exosuit mustEqual obj.ExoSuit
@@ -364,9 +364,9 @@ class AvatarTest extends Specification {
     obj.Slot(0).Equipment = None
     obj.Slot(2).Equipment = None
     obj.Slot(4).Equipment = None
-    avatar.SaveLoadout(obj, "test", 0)
+    avatar.EquipmentLoadouts.SaveLoadout(obj, "test", 0)
 
-    avatar.LoadLoadout(0) match {
+    avatar.EquipmentLoadouts.LoadLoadout(0) match {
       case Some(items : InfantryLoadout) =>
         items.label mustEqual "test"
         items.exosuit mustEqual obj.ExoSuit
@@ -380,11 +380,11 @@ class AvatarTest extends Specification {
 
   "save, load, delete; rapidly" in {
     val (obj, avatar) = CreatePlayer()
-    avatar.SaveLoadout(obj, "test", 0)
+    avatar.EquipmentLoadouts.SaveLoadout(obj, "test", 0)
 
-    avatar.LoadLoadout(0).isDefined mustEqual true
-    avatar.DeleteLoadout(0)
-    avatar.LoadLoadout(0) mustEqual None
+    avatar.EquipmentLoadouts.LoadLoadout(0).isDefined mustEqual true
+    avatar.EquipmentLoadouts.DeleteLoadout(0)
+    avatar.EquipmentLoadouts.LoadLoadout(0) mustEqual None
   }
 
   "the fifth slot is the locker wrapped in an EquipmentSlot" in {
diff --git a/common/src/test/scala/objects/terminal/OrderTerminalTest.scala b/common/src/test/scala/objects/terminal/OrderTerminalTest.scala
index 34b21670..c60e1c17 100644
--- a/common/src/test/scala/objects/terminal/OrderTerminalTest.scala
+++ b/common/src/test/scala/objects/terminal/OrderTerminalTest.scala
@@ -87,7 +87,7 @@ class OrderTerminalTest extends Specification {
       player.ExoSuit = ExoSuitType.Agile
       player.Slot(0).Equipment = Tool(GlobalDefinitions.beamer)
       player.Slot(6).Equipment = Tool(GlobalDefinitions.beamer)
-      avatar.SaveLoadout(player, "test", 0)
+      avatar.EquipmentLoadouts.SaveLoadout(player, "test", 0)
 
       val msg = infantryTerminal.Request(player, ItemTransactionMessage(PlanetSideGUID(10), TransactionType.Loadout, 4, "", 0, PlanetSideGUID(0)))
       msg.isInstanceOf[Terminal.InfantryLoadout] mustEqual true
@@ -137,7 +137,7 @@ class OrderTerminalTest extends Specification {
     "player can retrieve a vehicle loadout" in {
       val fury = Vehicle(GlobalDefinitions.fury)
       fury.Slot(30).Equipment = AmmoBox(GlobalDefinitions.hellfire_ammo)
-      avatar.SaveLoadout(fury, "test", 10)
+      avatar.EquipmentLoadouts.SaveLoadout(fury, "test", 10)
 
       val msg = ItemTransactionMessage(PlanetSideGUID(1), TransactionType.Loadout, 4, "test", 0, PlanetSideGUID(0))
       terminal.Request(player, msg) match {
diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala
index b20a2364..e59fe423 100644
--- a/pslogin/src/main/scala/WorldSessionActor.scala
+++ b/pslogin/src/main/scala/WorldSessionActor.scala
@@ -375,6 +375,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
           sendResponse(
             SquadDetailDefinitionUpdateMessage(
               guid,
+              member_info.find(_.name == leader).get.char_id,
               leader,
               task,
               zone,
@@ -532,7 +533,8 @@ class WorldSessionActor extends Actor with MDCContextAware {
         player.Stamina = stamina
         player.Armor = armor
       }
-      sendResponse(CharacterInfoMessage(15, PlanetSideZoneID(10000), 41605313, player.GUID, false, 6404428))
+      avatar.CharId = 41605313L
+      sendResponse(CharacterInfoMessage(15, PlanetSideZoneID(10000), avatar.CharId, player.GUID, false, 6404428))
       RemoveCharacterSelectScreenGUID(player)
       sendResponse(CharacterInfoMessage(0, PlanetSideZoneID(1), 0, PlanetSideGUID(0), true, 0))
       sendResponse(CharacterInfoMessage(0, PlanetSideZoneID(1), 0, PlanetSideGUID(0), true, 0))
@@ -2838,7 +2840,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
     sendResponse(CreateShortcutMessage(guid, 1, 0, true, Shortcut.MEDKIT))
     sendResponse(ChangeShortcutBankMessage(guid, 0))
     //Favorites lists
-    val (inf, veh) = avatar.Loadouts.partition { case (index, _) => index < 10 }
+    val (inf, veh) = avatar.EquipmentLoadouts.Loadouts.partition { case (index, _) => index < 10 }
     inf.foreach {
       case (index, loadout : InfantryLoadout) =>
         sendResponse(FavoritesMessage(LoadoutType.Infantry, guid, index, loadout.label, InfantryLoadout.DetermineSubtypeB(loadout.exosuit, loadout.subtype)))
@@ -2914,10 +2916,10 @@ class WorldSessionActor extends Actor with MDCContextAware {
         SquadListing(1, SquadInfo(Some("HofD"), Some("=KOK+SPC+FLY= All Welcome"), Some(PlanetSideZoneID(7)), Some(3), Some(10), Some(PlanetSideGUID(3))))
       )
     ))
-    //sendRawResponse(hex"e803008484000c800259e8809fda020043004a0069006d006d0079006e009b48006f006d006900630069006400690061006c00200053006d007500720066007300200041006e006f006e0079006d006f007500730004000000981401064580540061006e006b002000440072006900760065007200a05200650063006f006d006d0065006e00640065006400200074006f0020006800610076006500200065006e00670069006e0065006500720069006e0067002e0000000000800180000c00020c8c46007200650065006200690065002000730070006f007400cf44006f002000770068006100740065007600650072002c0020006200750074002000700072006500660065007200610062006c007900200073007400690063006b002000770069007400680020007400680065002000730071007500610064002e00200044006f006e002700740020006e00650065006400200061006e007900200073007000650063006900660069006300200063006500720074002e0096e27a0290540068006500460069006e0061006c005300740072007500670067006c0065000000000000020c8c46007200650065006200690065002000530070006f007400cf44006f002000770068006100740065007600650072002c0020006200750074002000700072006500660065007200610062006c007900200073007400690063006b002000770069007400680020007400680065002000730071007500610064002e00200044006f006e002700740020006e00650065006400200061006e007900200073007000650063006900660069006300200063006500720074002e0000000000800000000000020c8a41004d0053002000440072006900760065007200b34700690076006500200075007300200073007000610077006e00200070006f0069006e00740073002c0020006800610063006b0069006e006700200061006e006400200069006e00660069006c0020006100720065002000750073006500660075006c002e00fb02790287440030004f004d006700750079000100020c00020c8d410076006500720061006700650020004a0069006d006d007900a05200650069006e0066006f0072006300650064002000450078006f007300750069007400200077006f0075006c00640020006200650020006e0069006300650000000000800300000c00020c8b4100760065007200610067006500200042006f006200a05200650069006e0066006f0072006300650064002000450078006f007300750069007400200077006f0075006c00640020006200650020006e0069006300650000000000800300000c00020c8b410076006500720061006700650020004a006f006500a05200650069006e0066006f0072006300650064002000450078006f007300750069007400200077006f0075006c00640020006200650020006e0069006300650000000000800300000c00020c8753007500700070006f0072007400a2520065007300730075007200650063007400200073006f006c00640069006500720073002c0020006b00650065007000200075007300200061006c006900760065002e0000000000800100000c0c0a0c8845006e00670069006e00650065007200a043006f006d00620061007400200045006e00670069006e0065006500720069006e006700200077006f0075006c00640020006200650020006e0069006300650004b3d101864a0069006d006d0079006e000100000c000a0c854d0065006400690063009a4100640076002e0020004d00650064006900630061006c00200077006f0075006c00640020006200650020006e0069006300650000000000800100000c0400")
     sendResponse(
       SquadDetailDefinitionUpdateMessage(
         PlanetSideGUID(3),
+        42771010L,
         "HofD",
         "\\#ffdc00***\\#9640ff=KOK+SPC+FLY=\\#ffdc00***\\#FF4040 All Welcome",
         PlanetSideZoneID(7),
@@ -3347,17 +3349,19 @@ class WorldSessionActor extends Actor with MDCContextAware {
     case msg @ PlayerStateMessageUpstream(avatar_guid, pos, vel, yaw, pitch, yaw_upper, seq_time, unk3, is_crouching, is_jumping, unk4, is_cloaking, unk5, unk6) =>
       if(deadState == DeadState.Alive) {
         if(!player.Crouching && is_crouching) { //SQUAD TESTING CODE
-          sendResponse(SquadMembershipResponse(SquadRequestType.Invite,0,0,42771010,Some(41605313),"HofD",false,None))
-          sendResponse(SquadMembershipResponse(SquadRequestType.Accept,0,0,41605313,Some(42771010),"VirusGiver",true,Some(None)))
-          sendResponse(SquadMemberEvent(0,7,42771010,0,Some("HofD"),Some(7),Some(529745)))
-          sendResponse(SquadMemberEvent(0,7,42644970,1,Some("OpolE"),Some(7),Some(6418)))
-          sendResponse(SquadMemberEvent(0,7,41604210,8,Some("BobaF3tt907"),Some(12),Some(8097)))
-          sendResponse(PlanetsideAttributeMessage(player.GUID, 49, 7))
-          sendResponse(PlanetsideAttributeMessage(player.GUID, 50, 2))
-          sendResponse(PlanetsideAttributeMessage(player.GUID, 51, 8))
-          sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(3), 0, SquadAction.Unknown(18, hex"".toBitVector)))
-          sendResponse(PlanetsideAttributeMessage(player.GUID, 83, 0))
-          sendResponse(SquadState(PlanetSideGUID(7),List(SquadStateInfo(41605313L,64,64,Vector3(3464.0469f,4065.5703f,20.015625f),2,2,false,0,None,None))))
+          //sendRawResponse(hex"e8030080c603c043fe")
+          sendRawResponse(hex"e8 0300 80 c 600408c00000001200008811f6200400144004f007a007a0069004b0069006e006700ff")
+//          sendResponse(SquadMembershipResponse(SquadRequestType.Invite,0,0,42771010,Some(avatar.CharId),"HofD",false,None))
+//          sendResponse(SquadMembershipResponse(SquadRequestType.Accept,0,0,avatar.CharId,Some(42771010),"VirusGiver",true,Some(None)))
+//          sendResponse(SquadMemberEvent(0,7,42771010,0,Some("HofD"),Some(7),Some(529745)))
+//          sendResponse(SquadMemberEvent(0,7,42644970,1,Some("OpolE"),Some(7),Some(6418)))
+//          sendResponse(SquadMemberEvent(0,7,41604210,8,Some("BobaF3tt907"),Some(12),Some(8097)))
+//          sendResponse(PlanetsideAttributeMessage(player.GUID, 49, 7))
+//          sendResponse(PlanetsideAttributeMessage(player.GUID, 50, 2))
+//          sendResponse(PlanetsideAttributeMessage(player.GUID, 51, 8))
+//          sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(3), 0, SquadAction.Unknown(18, hex"".toBitVector)))
+//          sendResponse(PlanetsideAttributeMessage(player.GUID, 83, 0))
+//          sendResponse(SquadState(PlanetSideGUID(7),List(SquadStateInfo(avatar.CharId,64,64,Vector3(3464.0469f,4065.5703f,20.015625f),2,2,false,0,None,None))))
         }
         player.Position = pos
         player.Velocity = vel
@@ -4579,18 +4583,18 @@ class WorldSessionActor extends Actor with MDCContextAware {
               None
             }) match {
               case Some(owner : Player) => //InfantryLoadout
-                avatar.SaveLoadout(owner, name, lineno)
+                avatar.EquipmentLoadouts.SaveLoadout(owner, name, lineno)
                 import InfantryLoadout._
                 sendResponse(FavoritesMessage(list, player_guid, line, name, DetermineSubtypeB(player.ExoSuit, DetermineSubtype(player))))
               case Some(owner : Vehicle) => //VehicleLoadout
-                avatar.SaveLoadout(owner, name, lineno)
+                avatar.EquipmentLoadouts.SaveLoadout(owner, name, lineno)
                 sendResponse(FavoritesMessage(list, player_guid, line, name))
               case Some(_) | None =>
                 log.error("FavoritesRequest: unexpected owner for favorites")
             }
 
           case FavoritesAction.Delete =>
-            avatar.DeleteLoadout(lineno)
+            avatar.EquipmentLoadouts.DeleteLoadout(lineno)
             sendResponse(FavoritesMessage(list, player_guid, line, ""))
 
           case FavoritesAction.Unknown =>
@@ -7574,9 +7578,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
   /**
     * Properly format a `DestroyDisplayMessage` packet
     * given sufficient information about a target (victim) and an actor (killer).
-    * For the packet, the `*_charId` field is most important to determining distinction between players.
-    * The "char id" is not a currently supported field for different players so a name hash is used instead.
-    * The virtually negligent chance of a name hash collision is covered.
+    * For the packet, the `charId` field is important for determining distinction between players.
     * @param killer the killer's entry
     * @param victim the victim's entry
     * @param method the manner of death
@@ -7585,12 +7587,16 @@ class WorldSessionActor extends Actor with MDCContextAware {
     * @return a `DestroyDisplayMessage` packet that is properly formatted
     */
   def DestroyDisplayMessage(killer : SourceEntry, victim : SourceEntry, method : Int, unk : Int = 121) : DestroyDisplayMessage = {
-    //TODO charId should reflect the player more properly
-    val killerCharId = math.abs(killer.Name.hashCode)
-    var victimCharId = math.abs(victim.Name.hashCode)
-    if(killerCharId == victimCharId && !killer.Name.equals(victim.Name)) {
-      //odds of hash collision in a populated zone should be close to odds of being struck by lightning
-      victimCharId = Int.MaxValue - victimCharId + 1
+    val killerCharId : Long = killer.CharId
+    val victimCharId : Long = {
+      //TODO this block of code is only necessary for this particular dev build; use "victim.CharId" normallyLoado
+      val id = victim.CharId
+      if(killerCharId == id) {
+        Int.MaxValue - id + 1
+      }
+      else {
+        id
+      }
     }
     val killer_seated = killer match {
       case obj : PlayerSource => obj.Seated

From d9619e41853ce9cd09afcbfcbbc5dc71a1a92b2d Mon Sep 17 00:00:00 2001
From: FateJH <Jason_DiDonato@yahoo.com>
Date: Mon, 1 Jul 2019 18:43:57 -0400
Subject: [PATCH 10/31] conversion of variable field
 SquadDetailDefinitionUpdateMessage mostly complete, handling more than 99% of
 cases in our captures; modifications to existing test-important SSDUM packet
 in WSA

---
 .../SquadDetailDefinitionUpdateMessage.scala  | 448 ++++++-------
 ...uadDetailDefinitionUpdateMessageTest.scala | 597 +++++++++++++++++-
 .../src/main/scala/WorldSessionActor.scala    |  40 +-
 3 files changed, 797 insertions(+), 288 deletions(-)

diff --git a/common/src/main/scala/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala b/common/src/main/scala/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala
index 98d6adef..62a6702e 100644
--- a/common/src/main/scala/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala
+++ b/common/src/main/scala/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala
@@ -1,7 +1,6 @@
 // Copyright (c) 2019 PSForever
 package net.psforever.packet.game
 
-import net.psforever.packet.game.SquadDetailDefinitionUpdateMessage.defaultRequirements
 import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
 import net.psforever.types.CertificationType
 import scodec.bits.BitVector
@@ -18,7 +17,7 @@ final case class SquadPositionDetail(is_closed : Boolean,
                                      char_id : Long,
                                      name : String)
 
-final case class SquadDetailDefinitionUpdateMessage(guid : PlanetSideGUID,
+final case class OldSquadDetailDefinitionUpdateMessage(guid : PlanetSideGUID,
                                                     unk1 : Int,
                                                     leader_char_id : Long,
                                                     unk2 : BitVector,
@@ -26,11 +25,6 @@ final case class SquadDetailDefinitionUpdateMessage(guid : PlanetSideGUID,
                                                     task : String,
                                                     zone_id : PlanetSideZoneID,
                                                     member_info : List[SquadPositionDetail])
-  extends PlanetSideGamePacket {
-  type Packet = SquadDetailDefinitionUpdateMessage
-  def opcode = GamePacketOpcode.SquadDetailDefinitionUpdateMessage
-  def encode = SquadDetailDefinitionUpdateMessage.encode(this)
-}
 
 object SquadPositionDetail {
   final val Closed : SquadPositionDetail = SquadPositionDetail(is_closed = true, "", "", Set.empty, 0L, "")
@@ -48,169 +42,10 @@ object SquadPositionDetail {
   def apply(role : String, detailed_orders : String, requirements : Set[CertificationType.Value], char_id : Long, name : String) : SquadPositionDetail = SquadPositionDetail(is_closed = false, role, detailed_orders, requirements, char_id, name)
 }
 
-object SquadDetailDefinitionUpdateMessage extends Marshallable[SquadDetailDefinitionUpdateMessage] {
-  final val defaultRequirements : Set[CertificationType.Value] = Set(
-    CertificationType.StandardAssault,
-    CertificationType.StandardExoSuit,
-    CertificationType.AgileExoSuit
-  )
-
-  final val Init = SquadDetailDefinitionUpdateMessage(
-    PlanetSideGUID(0),
-    0L,
-    "",
-    "",
-    PlanetSideZoneID(0),
-    List(
-      SquadPositionDetail(),
-      SquadPositionDetail(),
-      SquadPositionDetail(),
-      SquadPositionDetail(),
-      SquadPositionDetail(),
-      SquadPositionDetail(),
-      SquadPositionDetail(),
-      SquadPositionDetail(),
-      SquadPositionDetail(),
-      SquadPositionDetail()
-    )
-  )
-
-  def apply(guid : PlanetSideGUID, char_id : Long, leader_name : String, task : String, zone_id : PlanetSideZoneID, member_info : List[SquadPositionDetail]) : SquadDetailDefinitionUpdateMessage = {
+object OldSquadDetailDefinitionUpdateMessage {
+  def apply(guid : PlanetSideGUID, char_id : Long, leader_name : String, task : String, zone_id : PlanetSideZoneID, member_info : List[SquadPositionDetail]) : OldSquadDetailDefinitionUpdateMessage = {
     import scodec.bits._
-    SquadDetailDefinitionUpdateMessage(guid, 1, char_id, hex"000000".toBitVector, leader_name, task, zone_id, member_info)
-  }
-
-  private def memberCodec(pad : Int) : Codec[SquadPositionDetail] = {
-    import shapeless.::
-    (
-        uint8 :: //required value = 6
-        ("is_closed" | bool) :: //if all positions are closed, the squad detail menu display no positions at all
-        ("role" | PacketHelpers.encodedWideStringAligned(pad)) ::
-        ("detailed_orders" | PacketHelpers.encodedWideString) ::
-        ("char_id" | uint32L) ::
-        ("name" | PacketHelpers.encodedWideString) ::
-        ("requirements" | ulongL(46))
-      ).exmap[SquadPositionDetail] (
-      {
-        case 6 :: closed :: role :: orders :: char_id :: name :: requirements :: HNil =>
-          Attempt.Successful(
-            SquadPositionDetail(closed, role, orders, defaultRequirements ++ CertificationType.fromEncodedLong(requirements), char_id, name)
-          )
-        case data =>
-          Attempt.Failure(Err(s"can not decode a SquadDetailDefinitionUpdate member's data - $data"))
-      },
-      {
-        case SquadPositionDetail(closed, role, orders, requirements, char_id, name) =>
-          Attempt.Successful(6 :: closed :: role :: orders :: char_id :: name :: CertificationType.toEncodedLong(defaultRequirements ++ requirements) :: HNil)
-      }
-    )
-  }
-
-  private val first_member_codec : Codec[SquadPositionDetail] = memberCodec(pad = 7)
-
-  private val member_codec : Codec[SquadPositionDetail] = memberCodec(pad = 0)
-
-  private case class LinkedMemberList(member : SquadPositionDetail, next : Option[LinkedMemberList])
-
-  private def subsequent_member_codec : Codec[LinkedMemberList] = {
-    import shapeless.::
-    (
-      //disruptive coupling action (e.g., flatPrepend) necessary to allow for recursive Codec
-      ("member" | member_codec) >>:~ { _ =>
-        optional(bool, "next" | subsequent_member_codec).hlist
-      }
-      ).xmap[LinkedMemberList] (
-      {
-        case a :: b :: HNil =>
-          LinkedMemberList(a, b)
-      },
-      {
-        case LinkedMemberList(a, b) =>
-          a :: b :: HNil
-      }
-    )
-  }
-
-  private def initial_member_codec : Codec[LinkedMemberList] = {
-    import shapeless.::
-    (
-      ("member" | first_member_codec) ::
-        optional(bool, "next" | subsequent_member_codec)
-      ).xmap[LinkedMemberList] (
-      {
-        case a :: b :: HNil =>
-          LinkedMemberList(a, b)
-      },
-      {
-        case LinkedMemberList(a, b) =>
-          a :: b :: HNil
-      }
-    )
-  }
-
-  @tailrec
-  private def unlinkMemberList(list : LinkedMemberList, out : List[SquadPositionDetail] = Nil) : List[SquadPositionDetail] = {
-    list.next match {
-      case None =>
-        out :+ list.member
-      case Some(next) =>
-        unlinkMemberList(next, out :+ list.member)
-    }
-  }
-
-  private def linkMemberList(list : List[SquadPositionDetail]) : LinkedMemberList = {
-    list match {
-      case Nil =>
-        throw new Exception("")
-      case x :: Nil =>
-        LinkedMemberList(x, None)
-      case x :: xs =>
-        linkMemberList(xs, LinkedMemberList(x, None))
-    }
-  }
-
-  @tailrec
-  private def linkMemberList(list : List[SquadPositionDetail], out : LinkedMemberList) : LinkedMemberList = {
-    list match {
-      case Nil =>
-        out
-      case x :: Nil =>
-        LinkedMemberList(x, Some(out))
-      case x :: xs =>
-        linkMemberList(xs, LinkedMemberList(x, Some(out)))
-    }
-  }
-
-  implicit val codec : Codec[SquadDetailDefinitionUpdateMessage] = {
-    import shapeless.::
-    (
-      ("guid" | PlanetSideGUID.codec) ::
-        uint4 :: //constant = 8
-        uint4 :: //variable = 0-4
-        bool :: //true, when 4
-        uint4 :: //variable = 0-12?
-        ("unk1" | uint4) ::
-        uint24 :: //unknown, but can be 0'd
-        ("leader_char_id" | uint32L) ::
-        ("unk2" | bits(22)) :: //variable fields, but can be 0'd
-        uint(10) :: //constant = 0
-        ("leader" | PacketHelpers.encodedWideStringAligned(7)) ::
-        ("task" | PacketHelpers.encodedWideString) ::
-        ("zone_id" | PlanetSideZoneID.codec) ::
-        uint(23) :: //constant = 4983296
-        optional(bool, "member_info" | initial_member_codec)
-      ).exmap[SquadDetailDefinitionUpdateMessage] (
-      {
-        case guid :: _ :: _ :: _ :: _ :: u1 :: _ :: char_id:: u2 :: _ :: leader :: task :: zone :: _ :: Some(member_list) :: HNil =>
-          Attempt.Successful(SquadDetailDefinitionUpdateMessage(guid, u1, char_id, u2, leader, task, zone, unlinkMemberList(member_list)))
-        case data =>
-          Attempt.failure(Err(s"can not get squad detail definition from data $data"))
-      },
-      {
-        case SquadDetailDefinitionUpdateMessage(guid, unk1, char_id, unk2, leader, task, zone, member_list) =>
-          Attempt.Successful(guid :: 8 :: 4 :: true :: 0 :: math.max(unk1, 1) :: 0 :: char_id :: unk2.take(22) :: 0 :: leader :: task :: zone :: 4983296 :: Some(linkMemberList(member_list.reverse)) :: HNil)
-      }
-    )
+    OldSquadDetailDefinitionUpdateMessage(guid, 1, char_id, hex"000000".toBitVector, leader_name, task, zone_id, member_info)
   }
 }
 
@@ -241,10 +76,10 @@ final case class SquadPositionDetail2(is_closed : Option[Boolean],
   def And(info : SquadPositionDetail2) : SquadPositionDetail2 = {
     SquadPositionDetail2(
       is_closed match {
-        case Some(true) | None =>
+        case Some(false) | None =>
           info.is_closed.orElse(is_closed)
         case _ =>
-          Some(false)
+          Some(true)
       },
       role.orElse(info.role),
       detailed_orders.orElse(info.detailed_orders),
@@ -258,20 +93,24 @@ final case class SquadPositionDetail2(is_closed : Option[Boolean],
 final case class SquadPositionEntry(index : Int, info : Option[SquadPositionDetail2])
 
 final case class SquadDetail(unk1 : Option[Int],
+                             unk2 : Option[Int],
                              leader_char_id : Option[Long],
-                             unk2 : Option[BitVector],
+                             unk3 : Option[Long],
                              leader_name : Option[String],
                              task : Option[String],
                              zone_id : Option[PlanetSideZoneID],
+                             unk7 : Option[Int],
                              member_info : Option[List[SquadPositionEntry]]) {
   def And(info : SquadDetail) : SquadDetail = {
     SquadDetail(
       unk1.orElse(info.unk1),
-      leader_char_id.orElse(info.leader_char_id),
       unk2.orElse(info.unk2),
+      leader_char_id.orElse(info.leader_char_id),
+      unk3.orElse(info.unk3),
       leader_name.orElse(info.leader_name),
       task.orElse(info.task),
       zone_id.orElse(info.zone_id),
+      unk7.orElse(info.unk7),
       (member_info, info.member_info) match {
         case (Some(info1), Some(info2)) => Some(info1 ++ info2)
         case (Some(info1), _) => Some(info1)
@@ -282,33 +121,57 @@ final case class SquadDetail(unk1 : Option[Int],
   }
 }
 
-final case class SquadDetailDefinitionUpdateMessage2(guid : PlanetSideGUID,
-                                                     detail : SquadDetail)
+final case class SquadDetailDefinitionUpdateMessage(guid : PlanetSideGUID,
+                                                    detail : SquadDetail)
+  extends PlanetSideGamePacket {
+  type Packet = SquadDetailDefinitionUpdateMessage
+  def opcode = GamePacketOpcode.SquadDetailDefinitionUpdateMessage
+  def encode = SquadDetailDefinitionUpdateMessage.encode(this)
+}
 
 object SquadPositionDetail2 {
   final val Blank : SquadPositionDetail2 = SquadPositionDetail2()
   final val Closed : SquadPositionDetail2 = SquadPositionDetail2(is_closed = Some(true), None, None, None, None, None)
+  final val Open : SquadPositionDetail2 = SquadPositionDetail2(is_closed = Some(false), None, None, None, None, None)
 
-  def apply() : SquadPositionDetail2 = SquadPositionDetail2(is_closed = Some(false), None, None, None, None, None)
+  def apply() : SquadPositionDetail2 = SquadPositionDetail2(None, None, None, None, None, None)
 
-  def apply(role : String, detailed_orders : Option[String]) : SquadPositionDetail2 = SquadPositionDetail2(is_closed = Some(false), Some(role), detailed_orders, None, None, None)
+  def apply(role : String, detailed_orders : Option[String]) : SquadPositionDetail2 = SquadPositionDetail2(None, Some(role), detailed_orders, None, None, None)
 
-  def apply(role : Option[String], detailed_orders : String) : SquadPositionDetail2 = SquadPositionDetail2(is_closed = Some(false), role, Some(detailed_orders), None, None, None)
+  def apply(role : Option[String], detailed_orders : String) : SquadPositionDetail2 = SquadPositionDetail2(None, role, Some(detailed_orders), None, None, None)
 
-  def apply(char_id : Long) : SquadPositionDetail2 = SquadPositionDetail2(is_closed = Some(false), None, None, None, Some(char_id), None)
+  def apply(role : String, detailed_orders : String) : SquadPositionDetail2 = SquadPositionDetail2(None, Some(role), Some(detailed_orders), None, None, None)
 
-  def apply(name : String) : SquadPositionDetail2 = SquadPositionDetail2(is_closed = Some(false), None, None, None, None, Some(name))
+  def apply(requirements : Set[CertificationType.Value]) : SquadPositionDetail2 = SquadPositionDetail2(None, None, None, Some(requirements), None, None)
 
-  def apply(requirements : Set[CertificationType.Value]) : SquadPositionDetail2 = SquadPositionDetail2(is_closed = Some(false), None, None, Some(requirements), None, None)
+  def apply(role : String, detailed_orders : String, requirements : Set[CertificationType.Value]) : SquadPositionDetail2 = SquadPositionDetail2(None, Some(role), Some(detailed_orders), Some(requirements), None, None)
+
+  def apply(char_id : Long) : SquadPositionDetail2 = SquadPositionDetail2(None, None, None, None, Some(char_id), None)
+
+  def apply(name : String) : SquadPositionDetail2 = SquadPositionDetail2(None, None, None, None, None, Some(name))
+
+  def apply(char_id : Long, name : String) : SquadPositionDetail2 = SquadPositionDetail2(None, None, None, None, Some(char_id), Some(name))
 
   def apply(role : String, detailed_orders : String, requirements : Set[CertificationType.Value], char_id : Long, name : String) : SquadPositionDetail2 = SquadPositionDetail2(is_closed = Some(false), Some(role), Some(detailed_orders), Some(requirements), Some(char_id), Some(name))
 }
 
 object SquadPositionEntry {
-  import SquadDetailDefinitionUpdateMessage2.paddedStringMetaCodec
+  import SquadDetailDefinitionUpdateMessage.paddedStringMetaCodec
+
+  def apply(index : Int, detail : SquadPositionDetail2) : SquadPositionEntry = SquadPositionEntry(index, Some(detail))
+
+  private val isClosedCodec : Codec[SquadPositionDetail2] = bool.exmap[SquadPositionDetail2] (
+    state => Attempt.successful(SquadPositionDetail2(Some(state), None, None, None, None, None)),
+    {
+      case SquadPositionDetail2(Some(state), _, _, _, _, _) =>
+        Attempt.successful(state)
+      case _ =>
+        Attempt.failure(Err("failed to encode squad position data for availability"))
+    }
+  )
 
   private def roleCodec(bitsOverByte : StreamLengthToken) : Codec[SquadPositionDetail2] = paddedStringMetaCodec(bitsOverByte.Length).exmap[SquadPositionDetail2] (
-    role => Attempt.successful(SquadPositionDetail2(role)),
+    role => Attempt.successful(SquadPositionDetail2(role, None)),
     {
       case SquadPositionDetail2(_, Some(role), _, _, _, _) =>
         Attempt.successful(role)
@@ -364,15 +227,13 @@ object SquadPositionEntry {
     * its determining conditional statement is explicitly `false`
     * and all cases involving explicit failure.
     */
-  private val failureCodec : Codec[SquadPositionDetail2] = conditional(included = false, bool).exmap[SquadPositionDetail2] (
-    _ => Attempt.failure(Err("decoding with unhandled codec")),
-    _ => Attempt.failure(Err("encoding with unhandled codec"))
+  private def failureCodec(code : Int) : Codec[SquadPositionDetail2] = conditional(included = false, bool).exmap[SquadPositionDetail2] (
+    _ => Attempt.failure(Err(s"decoding with unhandled codec - $code")),
+    _ => Attempt.failure(Err(s"encoding with unhandled codec - $code"))
   )
 
   private final case class LinkedSquadPositionInfo(code : Int, info : SquadPositionDetail2, next : Option[LinkedSquadPositionInfo])
 
-  private def unlinkSquadPositionInfo(info : LinkedSquadPositionInfo) : SquadPositionDetail2 = unlinkSquadPositionInfo(Some(info))
-
   /**
     * Concatenate a `SquadInfo` object chain into a single `SquadInfo` object.
     * Recursively visits every link in a `SquadInfo` object chain.
@@ -443,8 +304,10 @@ object SquadPositionEntry {
     import shapeless.::
     (
       uint4 >>:~ { code =>
-        selectCodecAction(code, bitsOverByte.Add(4)) ::
-          conditional(size - 1 > 0, listing_codec(size - 1, modifyCodecPadValue(code, bitsOverByte)))
+        selectCodecAction(code,  bitsOverByte.Add(4)) >>:~ { _ =>
+          modifyCodecPadValue(code, bitsOverByte)
+          conditional(size - 1 > 0, listing_codec(size - 1, bitsOverByte)).hlist
+        }
       }
       ).xmap[LinkedSquadPositionInfo] (
       {
@@ -460,17 +323,19 @@ object SquadPositionEntry {
 
   private def selectCodecAction(code : Int, bitsOverByte : StreamLengthToken) : Codec[SquadPositionDetail2] = {
     code match {
+      case 0 => isClosedCodec
       case 1 => roleCodec(bitsOverByte)
       case 2 => ordersCodec(bitsOverByte)
       case 3 => charIdCodec
       case 4 => nameCodec(bitsOverByte)
       case 5 => requirementsCodec
-      case _ => failureCodec
+      case _ => failureCodec(code)
     }
   }
 
   private def modifyCodecPadValue(code : Int, bitsOverByte : StreamLengthToken) : StreamLengthToken = {
     code match {
+      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
@@ -494,9 +359,9 @@ object SquadPositionEntry {
       info => {
         var i = 1
         var dinfo = info
-        while(info.next.nonEmpty) {
+        while(dinfo.next.nonEmpty) {
           i += 1
-          dinfo = info.next.get
+          dinfo = dinfo.next.get
         }
         i :: info :: HNil
       }
@@ -505,52 +370,109 @@ object SquadPositionEntry {
 
   def codec(bitsOverByte : StreamLengthToken) : Codec[SquadPositionEntry] = {
     import shapeless.::
+    /*
+     import net.psforever.newcodecs.newcodecs
+//        newcodecs.binary_choice[Option[LinkedSquadPositionInfo] :: HNil](
+//          index < 255,
+//          (bool :: squad_member_details_codec(bitsOverByte.Add(1))).exmap[Option[LinkedSquadPositionInfo] :: HNil] (
+//            {
+//              case _ :: info :: HNil => Attempt.Successful(Some(info) :: HNil)
+//            },
+//            {
+//              case Some(info) :: HNil => Attempt.Successful(true :: info :: HNil)
+//              case None :: HNil => Attempt.Failure(Err(s"this data for this position should not be None - $index < 255"))
+//            }
+//          ),
+//          bits.xmap[Option[LinkedSquadPositionInfo] :: HNil] (
+//            _ => None :: HNil,
+//            {
+//              case _ :: HNil => BitVector.empty
+//            }
+//          )
+//        )
+    */
     (
       ("index" | uint8) >>:~ { index =>
-        conditional(index < 255, bool) >>:~ { is_open =>
-          conditional(is_open.contains(true) && index < 255, "info" | squad_member_details_codec(bitsOverByte.Add(1))).hlist
-        }
+        conditional(index < 255, bool :: squad_member_details_codec(bitsOverByte.Add(1))) ::
+        conditional(index == 255, bits)
       }
       ).xmap[SquadPositionEntry] (
       {
         case 255 :: _ :: _ :: HNil =>
           SquadPositionEntry(255, None)
-        case ndx :: Some(false) :: None :: HNil =>
-          SquadPositionEntry(ndx, None)
-        case ndx :: Some(true) :: Some(info) :: HNil =>
-          SquadPositionEntry(ndx, Some(unlinkSquadPositionInfo(info)))
+        case ndx :: Some(_ :: info :: HNil) :: _ :: HNil =>
+          SquadPositionEntry(ndx, Some(unlinkSquadPositionInfo(Some(info))))
       },
       {
         case SquadPositionEntry(255, _) =>
-          255 :: None :: None  :: HNil
-        case SquadPositionEntry(ndx, None) =>
-          ndx :: Some(false) :: None :: HNil
+          255 :: None :: None :: HNil
         case SquadPositionEntry(ndx, Some(info)) =>
-          ndx :: Some(true) :: Some(linkSquadPositionInfo(info)) :: HNil
+          ndx :: Some(true :: linkSquadPositionInfo(info) :: HNil) :: None :: HNil
       }
     )
   }
 }
 
 object SquadDetail {
-  final val Blank = SquadDetail(None, None, None, None, None, None, None)
+  final val Blank = SquadDetail(None, None, None, None, None, None, None, None, None)
 
-  def apply(leader_char_id : Long) : SquadDetail = SquadDetail(None, Some(leader_char_id), None, None, None, None, None)
+  def apply(leader_char_id : Long) : SquadDetail = SquadDetail(None, None, Some(leader_char_id), None, None, None, None, None, None)
 
-  def apply(leader_name : String, task : Option[String]) : SquadDetail = SquadDetail(None, None, None, Some(leader_name), task, None, None)
+  def apply(leader_char_id : Long, leader_name : String) : SquadDetail = SquadDetail(None, None, Some(leader_char_id), None, Some(leader_name), None, None, None, None)
 
-  def apply(leader_name : Option[String], task : String) : SquadDetail = SquadDetail(None, None, None, leader_name, Some(task), None, None)
+  def apply(leader_name : String, task : Option[String]) : SquadDetail = SquadDetail(None, None, None, None, Some(leader_name), task, None, None, None)
 
-  def apply(zone_id : PlanetSideZoneID) : SquadDetail = SquadDetail(None, None, None, None, None, Some(zone_id), None)
+  def apply(leader_name : Option[String], task : String) : SquadDetail = SquadDetail(None, None, None, None, leader_name, Some(task), None, None, None)
 
-  def apply(member_list : List[SquadPositionEntry]) : SquadDetail = SquadDetail(None, None, None, None, None, None, Some(member_list))
+  def apply(zone_id : PlanetSideZoneID) : SquadDetail = SquadDetail(None, None, None, None, None, None, Some(zone_id), None, None)
 
-  def apply(unk1 : Int, leader_char_id : Long, unk2 : BitVector, leader_name : String, task : String, zone_id : PlanetSideZoneID, member_info : List[SquadPositionEntry]) : SquadDetail = {
-    SquadDetail(Some(unk1), Some(leader_char_id), Some(unk2), Some(leader_name), Some(task), Some(zone_id), Some(member_info))
+  def apply(member_list : List[SquadPositionEntry]) : SquadDetail = SquadDetail(None, None, None, None, None, None, None, None, Some(member_list))
+
+  def apply(unk1 : Int, unk2 : Int, leader_char_id : Long, unk3 : Long, leader_name : String, task : String, zone_id : PlanetSideZoneID, unk7 : Int, member_info : List[SquadPositionEntry]) : SquadDetail = {
+    SquadDetail(Some(unk1), Some(unk2), Some(leader_char_id), Some(unk3), Some(leader_name), Some(task), Some(zone_id), Some(unk7), Some(member_info))
   }
 }
 
-object SquadDetailDefinitionUpdateMessage2 {
+object SquadDetailDefinitionUpdateMessage extends Marshallable[SquadDetailDefinitionUpdateMessage] {
+  final val DefaultRequirements : Set[CertificationType.Value] = Set(
+    CertificationType.StandardAssault,
+    CertificationType.StandardExoSuit,
+    CertificationType.AgileExoSuit
+  )
+
+  final val Init = SquadDetailDefinitionUpdateMessage(
+    PlanetSideGUID(0),
+    SquadDetail(
+      0,
+      0,
+      0L,
+      0L,
+      "",
+      "",
+      PlanetSideZoneID(0),
+      0,
+      (0 to 9).map { i => SquadPositionEntry(i, SquadPositionDetail2.Blank)} toList
+    )
+  )
+
+
+  /*
+  DECOY CONSTRUCTORS
+   */
+  def apply(guid : PlanetSideGUID,
+             unk1 : Int,
+             leader_char_id : Long,
+             unk2 : BitVector,
+             leader_name : String,
+             task : String,
+             zone_id : PlanetSideZoneID,
+             member_info : List[SquadPositionDetail]) : SquadDetailDefinitionUpdateMessage = {
+    SquadDetailDefinitionUpdateMessage(PlanetSideGUID(0), SquadDetail.Blank)
+  }
+  def apply(guid : PlanetSideGUID, char_id : Long, leader_name : String, task : String, zone_id : PlanetSideZoneID, member_info : List[SquadPositionDetail]) : SquadDetailDefinitionUpdateMessage = {
+    SquadDetailDefinitionUpdateMessage(PlanetSideGUID(0), SquadDetail.Blank)
+  }
+
   /**
     * Produces a `Codec` function for byte-aligned, padded Pascal strings encoded through common manipulations.
     * @see `PacketHelpers.encodedWideStringAligned`
@@ -582,14 +504,14 @@ object SquadDetailDefinitionUpdateMessage2 {
       {
         case 6 :: closed :: role :: orders :: char_id :: name :: requirements :: HNil =>
           Attempt.Successful(
-            SquadPositionDetail2(Some(closed), Some(role), Some(orders), Some(defaultRequirements ++ CertificationType.fromEncodedLong(requirements)), Some(char_id), Some(name))
+            SquadPositionDetail2(Some(closed), Some(role), Some(orders), Some(DefaultRequirements ++ CertificationType.fromEncodedLong(requirements)), Some(char_id), Some(name))
           )
         case data =>
           Attempt.Failure(Err(s"can not decode a SquadDetailDefinitionUpdate member's data - $data"))
       },
       {
         case SquadPositionDetail2(Some(closed), Some(role), Some(orders), Some(requirements), Some(char_id), Some(name)) =>
-          Attempt.Successful(6 :: closed :: role :: orders :: char_id :: name :: CertificationType.toEncodedLong(defaultRequirements ++ requirements) :: HNil)
+          Attempt.Successful(6 :: closed :: role :: orders :: char_id :: name :: CertificationType.toEncodedLong(DefaultRequirements ++ requirements) :: HNil)
       }
     )
   }
@@ -673,20 +595,19 @@ object SquadDetailDefinitionUpdateMessage2 {
     import shapeless.::
     (
       ("unk1" | uint8) ::
-        uint24 :: //unknown, but can be 0'd
+        ("unk2" | uint24) :: //unknown, but can be 0'd
         ("leader_char_id" | uint32L) ::
-        ("unk2" | bits(22)) :: //variable fields, but can be 0'd
-        uint(10) :: //constant = 0
+        ("unk3" | uint32L) :: //variable fields, but can be 0'd
         ("leader" | PacketHelpers.encodedWideStringAligned(7)) ::
         ("task" | PacketHelpers.encodedWideString) ::
         ("zone_id" | PlanetSideZoneID.codec) ::
-        uint(23) :: //constant = 4983296
+        ("unk7" | uint(23)) :: //during full squad mode, constant = 4983296
         optional(bool, "member_info" | initial_member_codec)
       ).exmap[SquadDetail] (
       {
-        case u1 :: _ :: char_id :: u2 :: _ :: leader :: task :: zone :: _ :: Some(member_list) :: HNil =>
+        case u1 :: u2 :: char_id :: u3 :: leader :: task :: zone :: unk7 :: Some(member_list) :: HNil =>
           Attempt.Successful(
-            SquadDetail(Some(u1), Some(char_id), Some(u2), Some(leader), Some(task), Some(zone),
+            SquadDetail(Some(u1), Some(u2), Some(char_id), Some(u3), Some(leader), Some(task), Some(zone), Some(unk7),
               Some(unlinkMemberList(member_list).zipWithIndex.map { case (entry, index) => SquadPositionEntry(index, Some(entry)) })
             )
           )
@@ -694,9 +615,9 @@ object SquadDetailDefinitionUpdateMessage2 {
           Attempt.failure(Err(s"can not get squad detail definition from data $data"))
       },
       {
-        case SquadDetail(Some(u1), Some(char_id), Some(u2), Some(leader), Some(task), Some(zone), Some(member_list)) =>
+        case SquadDetail(Some(u1), Some(u2), Some(char_id), Some(u3), Some(leader), Some(task), Some(zone), Some(unk7), Some(member_list)) =>
           Attempt.Successful(
-            math.max(u1, 1) :: 0 :: char_id :: u2.take(22) :: 0 :: leader :: task :: zone :: 4983296 ::
+            math.max(u1, 1) :: u2 :: char_id :: u3 :: leader :: task :: zone :: unk7 ::
               Some(linkMemberList(member_list.collect { case SquadPositionEntry(_, Some(entry)) => entry }.reverse)) ::
               HNil
           )
@@ -704,20 +625,40 @@ object SquadDetailDefinitionUpdateMessage2 {
     )
   }
 
+  private val field1Codec : Codec[SquadDetail] = uint16L.exmap[SquadDetail] (
+    unk1 => Attempt.successful(SquadDetail(Some(unk1), None, None, None, None, None, None, None, None)),
+    {
+      case SquadDetail(Some(unk1), _, _, _,  _, _, _, _, _) =>
+        Attempt.successful(unk1)
+      case _ =>
+        Attempt.failure(Err("failed to encode squad data for unknown field #1"))
+    }
+  )
+
   private val leaderCharIdCodec : Codec[SquadDetail] = uint32L.exmap[SquadDetail] (
     char_id => Attempt.successful(SquadDetail(char_id)),
     {
-      case SquadDetail(_, Some(char_id), _,  _, _, _, _) =>
+      case SquadDetail(_, _, Some(char_id), _,  _, _, _, _, _) =>
         Attempt.successful(char_id)
       case _ =>
         Attempt.failure(Err("failed to encode squad data for leader id"))
     }
   )
 
+  private val field3Codec : Codec[SquadDetail] = uint32L.exmap[SquadDetail] (
+    unk3 => Attempt.successful(SquadDetail(None, None, None, Some(unk3), None, None, None, None, None)),
+    {
+      case SquadDetail(_, _, _, Some(unk3),  _, _, _, _, _) =>
+        Attempt.successful(unk3)
+      case _ =>
+        Attempt.failure(Err("failed to encode squad data for unknown field #3"))
+    }
+  )
+
   private def leaderNameCodec(bitsOverByte : StreamLengthToken) : Codec[SquadDetail] = paddedStringMetaCodec(bitsOverByte.Length).exmap[SquadDetail] (
     name => Attempt.successful(SquadDetail(name, None)),
     {
-      case SquadDetail(_, _, _, Some(name), _, _, _) =>
+      case SquadDetail(_, _, _, _, Some(name), _, _, _, _) =>
         Attempt.successful(name)
       case _ =>
         Attempt.failure(Err("failed to encode squad data for leader name"))
@@ -727,7 +668,7 @@ object SquadDetailDefinitionUpdateMessage2 {
   private def taskCodec(bitsOverByte : StreamLengthToken) : Codec[SquadDetail] = paddedStringMetaCodec(bitsOverByte.Length).exmap[SquadDetail] (
     task => Attempt.successful(SquadDetail(None, task)),
     {
-      case SquadDetail(_, _, _, _, Some(task), _, _) =>
+      case SquadDetail(_, _, _, _, _, Some(task), _, _, _) =>
         Attempt.successful(task)
       case _ =>
         Attempt.failure(Err("failed to encode squad data for task"))
@@ -737,27 +678,39 @@ object SquadDetailDefinitionUpdateMessage2 {
   private val zoneCodec : Codec[SquadDetail] = PlanetSideZoneID.codec.exmap[SquadDetail] (
     zone_id => Attempt.successful(SquadDetail(zone_id)),
     {
-      case SquadDetail(_, _, _, _, _, Some(zone_id), _) =>
+      case SquadDetail(_, _, _, _, _, _, Some(zone_id), _, _) =>
         Attempt.successful(zone_id)
       case _ =>
         Attempt.failure(Err("failed to encode squad data for zone id"))
     }
   )
 
+  private val field7Codec : Codec[SquadDetail] = {
+    uint4.exmap[SquadDetail] (
+      unk7 => Attempt.successful(SquadDetail(None, None, None, None, None, None, None, Some(unk7), None)),
+      {
+        case SquadDetail(_, _, _,  _, _, _, _, Some(unk7), _) =>
+          Attempt.successful(unk7)
+        case _ =>
+          Attempt.failure(Err("failed to encode squad data for unknown field #7"))
+      }
+    )
+  }
+
   private def membersCodec(bitsOverByte : StreamLengthToken) : Codec[SquadDetail] = {
     import shapeless.::
-    bitsOverByte.Add(19)
+    bitsOverByte.Add(4)
     (
-      uint(19) :: //constant = 0x60040, or 393280 in 19u BE
+      uint4 :: //constant = 12
         vector(SquadPositionEntry.codec(bitsOverByte))
       ).exmap[SquadDetail] (
       {
-        case _ :: member_list :: HNil =>
+        case 12 :: member_list :: HNil =>
           Attempt.successful(SquadDetail(member_list.toList))
       },
       {
-        case SquadDetail(_, _, _, _, _, _, Some(member_list)) =>
-          Attempt.successful(393280 :: member_list.toVector :: HNil)
+        case SquadDetail(_, _, _, _, _, _, _, _, Some(member_list)) =>
+          Attempt.successful(12 :: member_list.toVector :: HNil)
         case _ =>
           Attempt.failure(Err("failed to encode squad data for members"))
       }
@@ -771,10 +724,13 @@ object SquadDetailDefinitionUpdateMessage2 {
 
   private def selectCodecAction(code : Int, bitsOverByte : StreamLengthToken) : Codec[SquadDetail] = {
     code match {
+      case 1 => field1Codec
       case 2 => leaderCharIdCodec
+      case 3 => field3Codec
       case 4 => leaderNameCodec(bitsOverByte)
       case 5 => taskCodec(bitsOverByte)
       case 6 => zoneCodec
+      case 7 => field7Codec
       case 8 => membersCodec(bitsOverByte)
       case _ => failureCodec
     }
@@ -782,10 +738,13 @@ object SquadDetailDefinitionUpdateMessage2 {
 
   private def modifyCodecPadValue(code : Int, bitsOverByte : StreamLengthToken) : StreamLengthToken = {
     code match {
+      case 1 => bitsOverByte //16u = no added padding
       case 2 => bitsOverByte //32u = no added padding
+      case 3 => bitsOverByte //32u = no added padding
       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 8 => bitsOverByte.Length = 0 //end of stream
       case _ => bitsOverByte.Length = Int.MinValue //wildly incorrect
     }
@@ -808,11 +767,14 @@ object SquadDetailDefinitionUpdateMessage2 {
   private def linkSquadInfo(info : SquadDetail) : LinkedSquadInfo = {
     //import scala.collection.immutable.::
     Seq(
-      (8, SquadDetail(None, None, None, None, None, None, info.member_info)),
-      (6, SquadDetail(None, None, None, None, None, info.zone_id, None)),
-      (5, SquadDetail(None, None, None, None, info.task, None, None)),
-      (4, SquadDetail(None, None, None, info.leader_name, None, None, None)),
-      (2, SquadDetail(None, info.leader_char_id, None, None, None, None, None))
+      (8, SquadDetail(None, None, None, None, None, None, None, None, info.member_info)),
+      (7, SquadDetail(None, None, None, None, None, None, None, info.unk7, None)),
+      (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)),
+      (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))
     ) //in reverse order so that the linked list is in the correct order
       .filterNot { case (_, sqInfo) => sqInfo == SquadDetail.Blank}
     match {
@@ -869,7 +831,7 @@ object SquadDetailDefinitionUpdateMessage2 {
     }
   }
 
-  private def codec() : Codec[SquadDetailDefinitionUpdateMessage2] = {
+  implicit val codec : Codec[SquadDetailDefinitionUpdateMessage] = {
     import shapeless.::
     (
       ("guid" | PlanetSideGUID.codec) ::
@@ -877,14 +839,14 @@ object SquadDetailDefinitionUpdateMessage2 {
         (uint8 >>:~ { size =>
           squadDetailSelectCodec(size).hlist
         })
-      ).exmap[SquadDetailDefinitionUpdateMessage2] (
+      ).exmap[SquadDetailDefinitionUpdateMessage] (
       {
         case guid :: _ :: _ :: info :: HNil =>
-          Attempt.Successful(SquadDetailDefinitionUpdateMessage2(guid, info))
+          Attempt.Successful(SquadDetailDefinitionUpdateMessage(guid, info))
       },
       {
-        case SquadDetailDefinitionUpdateMessage2(guid, info) =>
-          val occupiedSquadFieldCount = List(info.unk1, info.leader_char_id, info.unk2, info.leader_name, info.task, info.zone_id, info.member_info)
+        case SquadDetailDefinitionUpdateMessage(guid, info) =>
+          val occupiedSquadFieldCount = List(info.unk1, info.unk2, info.leader_char_id, info.unk3, info.leader_name, info.task, info.zone_id, info.unk7, info.member_info)
             .count(_.nonEmpty)
           if(occupiedSquadFieldCount < 9) {
             //itemized detail definition protocol
@@ -916,6 +878,4 @@ object SquadDetailDefinitionUpdateMessage2 {
       }
     )
   }
-
-  implicit val code : Codec[SquadDetailDefinitionUpdateMessage2] = codec()
 }
diff --git a/common/src/test/scala/game/SquadDetailDefinitionUpdateMessageTest.scala b/common/src/test/scala/game/SquadDetailDefinitionUpdateMessageTest.scala
index cf56fb86..5118cfbb 100644
--- a/common/src/test/scala/game/SquadDetailDefinitionUpdateMessageTest.scala
+++ b/common/src/test/scala/game/SquadDetailDefinitionUpdateMessageTest.scala
@@ -3,44 +3,587 @@ package game
 
 import net.psforever.packet._
 import net.psforever.packet.game._
+import net.psforever.types.CertificationType
 import org.specs2.mutable._
 import scodec.bits._
 
 class SquadDetailDefinitionUpdateMessageTest extends Specification {
-  val string = hex"e80300848180038021514601288a8400420048006f0066004400bf5c0023006600660064006300300030002a002a002a005c0023003900360034003000660066003d004b004f004b002b005300500043002b0046004c0059003d005c0023006600660064006300300030002a002a002a005c002300460046003400300034003000200041006c006c002000570065006c0063006f006d006500070000009814010650005c00230066006600300030003000300020007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c008000000000800100000c00020c8c5c00230066006600640063003000300020002000200043008000000000800100000c00020c8c5c002300660066006400630030003000200020002000480080eab58a02854f0070006f006c0045000100000c00020c8d5c002300660066006400630030003000200020002000200049008072d47a028b42006f006200610046003300740074003900300037000100000c00020c8c5c0023006600660064006300300030002000200020004e008000000000800100000c00020c8c5c00230066006600640063003000300020002000200041008000000000800100000c00020ca05c00230066006600300030003000300020007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c008000000000800100000c00020c8c5c0023003900360034003000660066002000200020004b008000000000800100000c00020c8c5c0023003900360034003000660066002000200020004f008042a28c028448006f00660044000100000c00020c8c5c0023003900360034003000660066002000200020004b008000000000800100000c0000"
+  val string_unk = hex"e80300821104145011b9be840024284a00610061006b006f008c008118000000024000ff"
+  val string_unk1 = hex"e80300818800015c5189004603408c000000012000ff"
+  val string_leader_char_id = hex"e8050080904d56b808"
+  val string_unk3LeaderName = hex"e80300821104145011b9be840024284a00610061006b006f008c008118000000024000ff"
+  val string_task = hex"e8050080ac6041006c006c002000570065006c0063006f006d0065002000"
+  val string_zone = hex"e8030080b0a8000000"
+  val string_taskZone = hex"e80200812ce05c002300460046003000300030003000200054006800650020005c002300660066006600660066006600200042006c0061006400650073006040000000"
+  val string_unk7 = hex"e8030081ac8054006800650020004b0069006e00670027007300200053007100750061006400788c09808c4854006800650020004700750061007200640008808c5054006800650020004b006e00690067006800740007808c4054006800650020004500610072006c0006808c4054006800650020004c006f007200640005808c405400680065002000440075006b00650004808c4854006800650020004200610072006f006e0003808c6054006800650020005000720069006e00630065007300730002808c5054006800650020005000720069006e006300650001808c48540068006500200051007500650065006e0000808c4054006800650020004b0069006e006700ff"
+  val string_member_closed = hex"e8030080c602c043fe"
+  val string_member_role = hex"e8070080c60040462443006f006d006d0061006e00640065007200ff"
+  val string_member_roleRequirements = hex"e8010080c60340862841004400560020004800610063006b00650072005000000002003fc0"
+  val string_member_charIdName = hex"e8030080c602c08f2658480123004400750063006b006d006100730074006500720034003300ff"
+  val string_task_memberEtc = hex"e80100812ce05c002300460046003000300030003000200054006800650020005c002300660066006600660066006600200042006c0061006400650073008c09810c005000000000000220230007808c0006808c0005808c0004808c0003808c0002808c0001808c0000808c00ff"
+  val string_full = hex"e80300848180038021514601288a8400420048006f0066004400bf5c0023006600660064006300300030002a002a002a005c0023003900360034003000660066003d004b004f004b002b005300500043002b0046004c0059003d005c0023006600660064006300300030002a002a002a005c002300460046003400300034003000200041006c006c002000570065006c0063006f006d006500070000009814010650005c00230066006600300030003000300020007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c008000000000800100000c00020c8c5c00230066006600640063003000300020002000200043008000000000800100000c00020c8c5c002300660066006400630030003000200020002000480080eab58a02854f0070006f006c0045000100000c00020c8d5c002300660066006400630030003000200020002000200049008072d47a028b42006f006200610046003300740074003900300037000100000c00020c8c5c0023006600660064006300300030002000200020004e008000000000800100000c00020c8c5c00230066006600640063003000300020002000200041008000000000800100000c00020ca05c00230066006600300030003000300020007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c008000000000800100000c00020c8c5c0023003900360034003000660066002000200020004b008000000000800100000c00020c8c5c0023003900360034003000660066002000200020004f008042a28c028448006f00660044000100000c00020c8c5c0023003900360034003000660066002000200020004b008000000000800100000c0000"
 
   "SquadDetailDefinitionUpdateMessage" should {
-    "decode" in {
-      PacketCoding.DecodePacket(string).require match {
-        case SquadDetailDefinitionUpdateMessage(guid, unk1, char_id, unk2, leader, task, zone, member_info) =>
+    "decode (test)" in {
+      PacketCoding.DecodePacket(string_unk).require match {
+        case SquadDetailDefinitionUpdateMessage(guid, detail) =>
+          detail
           ok
         case _ =>
           ko
       }
+      ok
+    }
+
+    "decode (unk1 + members)" in {
+      PacketCoding.DecodePacket(string_unk1).require match {
+        case SquadDetailDefinitionUpdateMessage(guid, detail) =>
+          guid mustEqual PlanetSideGUID(3)
+          detail match {
+            case SquadDetail(Some(unk1), None, Some(char_id), None, None, None, None, None, Some(_)) =>
+              unk1 mustEqual 0
+              char_id mustEqual 1221560L
+              //members tests follow ...
+            case _ =>
+              ko
+          }
+        case _ =>
+          ko
+      }
     }
 
-    "encode" in {
-      val msg = SquadDetailDefinitionUpdateMessage(
-        PlanetSideGUID(3),
-        42771010L,
-        "HofD",
-        "\\#ffdc00***\\#9640ff=KOK+SPC+FLY=\\#ffdc00***\\#FF4040 All Welcome",
-        PlanetSideZoneID(7),
-        List(
-          SquadPositionDetail("\\#ff0000 |||||||||||||||||||||||", "Just a space filler"),
-          SquadPositionDetail("\\#ffdc00   C", ""),
-          SquadPositionDetail("\\#ffdc00   H", "", Set(), 42644970L, "OpoIE"),
-          SquadPositionDetail("\\#ffdc00    I", "", Set(), 41604210L, "BobaF3tt907"),
-          SquadPositionDetail("\\#ffdc00   N", ""),
-          SquadPositionDetail("\\#ffdc00   A", ""),
-          SquadPositionDetail("\\#ff0000 |||||||||||||||||||||||", "Another space filler"),
-          SquadPositionDetail("\\#9640ff   K", ""),
-          SquadPositionDetail("\\#9640ff   O", "", Set(), 42771010L, "HofD"),
-          SquadPositionDetail("\\#9640ff   K", "")
-        )
-      )
-      val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
-      pkt mustEqual string
-    }
+//    "decode (char id)" in {
+//      PacketCoding.DecodePacket(string_leader_char_id).require match {
+//        case SquadDetailDefinitionUpdateMessage(guid, detail) =>
+//          guid mustEqual PlanetSideGUID(5)
+//          detail match {
+//            case SquadDetail(None, None, Some(char_id), None, None, None, None, None, None) =>
+//              char_id mustEqual 30910985
+//            case _ =>
+//              ko
+//          }
+//          ok
+//        case _ =>
+//          ko
+//      }
+//    }
+//
+//    "decode (unk3 + leader name)" in {
+//      PacketCoding.DecodePacket(string_unk3LeaderName).require match {
+//        case SquadDetailDefinitionUpdateMessage(guid, detail) =>
+//          guid mustEqual PlanetSideGUID(3)
+//          detail match {
+//            case SquadDetail(None, None, Some(char_id), Some(unk3), Some(leader), None, None, None, Some(_)) =>
+//              char_id mustEqual 42631712L
+//              unk3 mustEqual 556403L
+//              leader mustEqual "Jaako"
+//              //members tests follow ...
+//            case _ =>
+//              ko
+//          }
+//        case _ =>
+//          ko
+//      }
+//    }
+//
+//    "decode (task)" in {
+//      PacketCoding.DecodePacket(string_task).require match {
+//        case SquadDetailDefinitionUpdateMessage(guid, detail) =>
+//          guid mustEqual PlanetSideGUID(5)
+//          detail match {
+//            case SquadDetail(None, None, None, None, None, Some(task), None, None, None) =>
+//              task mustEqual "All Welcome "
+//            case _ =>
+//              ko
+//          }
+//          ok
+//        case _ =>
+//          ko
+//      }
+//    }
+//
+//    "decode (zone)" in {
+//      PacketCoding.DecodePacket(string_zone).require match {
+//        case SquadDetailDefinitionUpdateMessage(guid, detail) =>
+//          guid mustEqual PlanetSideGUID(3)
+//          detail match {
+//            case SquadDetail(None, None, None, None, None, None, Some(zone), None, None) =>
+//              zone mustEqual PlanetSideZoneID(21)
+//            case _ =>
+//              ko
+//          }
+//          ok
+//        case _ =>
+//          ko
+//      }
+//    }
+//
+//    "decode (task + zone)" in {
+//      PacketCoding.DecodePacket(string_taskZone).require match {
+//        case SquadDetailDefinitionUpdateMessage(guid, detail) =>
+//          detail match {
+//            case SquadDetail(None, None, None, None, None, Some(task), Some(zone), None, None) =>
+//              task mustEqual "\\#FF0000 The \\#ffffff Blades"
+//              zone mustEqual PlanetSideZoneID(4)
+//            case _ =>
+//              ko
+//          }
+//        case _ =>
+//          ko
+//      }
+//      ok
+//    }
+//
+//      "decode (unk7 + members)" in {
+//        PacketCoding.DecodePacket(string_unk7).require match {
+//          case SquadDetailDefinitionUpdateMessage(guid, detail) =>
+//            guid mustEqual PlanetSideGUID(3)
+//            detail match {
+//              case SquadDetail(None, None, None, None, None, Some(task), None, Some(unk7), Some(_)) =>
+//                task mustEqual "The King's Squad"
+//                unk7 mustEqual 8
+//                //members tests follow ...
+//              case _ =>
+//                ko
+//            }
+//          case _ =>
+//            ko
+//        }
+//      }
+//
+//    "decode (member closed)" in {
+//      PacketCoding.DecodePacket(string_member_closed).require match {
+//        case SquadDetailDefinitionUpdateMessage(guid, detail) =>
+//          guid mustEqual PlanetSideGUID(3)
+//          detail match {
+//            case SquadDetail(None, None, None, None, None, None, None, None, Some(members)) =>
+//              members.size mustEqual 2
+//              members.head.index mustEqual 5
+//              members.head.info match {
+//                case Some(SquadPositionDetail2(Some(is_closed), None, None, None, None, None)) =>
+//                  is_closed mustEqual true
+//                case _ =>
+//                  ko
+//              }
+//            case _ =>
+//              ko
+//          }
+//          ok
+//        case _ =>
+//          ko
+//      }
+//    }
+//
+//    "decode (member role)" in {
+//      PacketCoding.DecodePacket(string_member_role).require match {
+//        case SquadDetailDefinitionUpdateMessage(guid, detail) =>
+//          guid mustEqual PlanetSideGUID(7)
+//          detail match {
+//            case SquadDetail(None, None, None, None, None, None, None, None, Some(members)) =>
+//              members.size mustEqual 2
+//              members.head.index mustEqual 0
+//              members.head.info match {
+//                case Some(SquadPositionDetail2(None, Some(role), None, None, None, None)) =>
+//                  role mustEqual "Commander"
+//                case _ =>
+//                  ko
+//              }
+//            case _ =>
+//              ko
+//          }
+//          ok
+//        case _ =>
+//          ko
+//      }
+//    }
+//
+//    "decode (member role + requirements)" in {
+//      PacketCoding.DecodePacket(string_member_roleRequirements).require match {
+//        case SquadDetailDefinitionUpdateMessage(guid, detail) =>
+//          guid mustEqual PlanetSideGUID(1)
+//          detail match {
+//            case SquadDetail(None, None, None, None, None, None, None, None, Some(members)) =>
+//              members.size mustEqual 2
+//              members.head.index mustEqual 6
+//              members.head.info match {
+//                case Some(SquadPositionDetail2(None, Some(role), None, Some(req), None, None)) =>
+//                  role mustEqual "ADV Hacker"
+//                  req.size mustEqual 1
+//                  req.contains(CertificationType.AdvancedHacking) mustEqual true
+//                case _ =>
+//                  ko
+//              }
+//            case _ =>
+//              ko
+//          }
+//          ok
+//        case _ =>
+//          ko
+//      }
+//    }
+//
+//    "decode (member char id + name)" in {
+//      PacketCoding.DecodePacket(string_member_charIdName).require match {
+//        case SquadDetailDefinitionUpdateMessage(guid, detail) =>
+//          guid mustEqual PlanetSideGUID(3)
+//          detail match {
+//            case SquadDetail(None, None, None, None, None, None, None, None, Some(members)) =>
+//              members.size mustEqual 2
+//              members.head.index mustEqual 5
+//              members.head.info match {
+//                case Some(SquadPositionDetail2(None, None, None, None, Some(char_id), Some(name))) =>
+//                  char_id mustEqual 1218249L
+//                  name mustEqual "Duckmaster43"
+//                case _ =>
+//                  ko
+//              }
+//            case _ =>
+//              ko
+//          }
+//          ok
+//        case _ =>
+//          ko
+//      }
+//    }
+//
+//    "decode (task + member etc)" in {
+//      PacketCoding.DecodePacket(string_task_memberEtc).require match {
+//        case SquadDetailDefinitionUpdateMessage(guid, detail) =>
+//          guid mustEqual PlanetSideGUID(1)
+//          detail match {
+//            case SquadDetail(None, None, None, None, None, Some(task), None, None, Some(members)) =>
+//              task mustEqual "\\#FF0000 The \\#ffffff Blades"
+//              members.size mustEqual 11
+//              //
+//              members.head.index mustEqual 9
+//              members.head.info match {
+//                case Some(SquadPositionDetail2(None, Some(role), None, Some(req), None, None)) =>
+//                  role mustEqual ""
+//                  req mustEqual Set.empty
+//                case _ =>
+//                  ko
+//              }
+//              //
+//              (1 to 9).foreach { index =>
+//                members(index).index mustEqual 9 - index
+//                members(index).info match {
+//                  case Some(SquadPositionDetail2(None, Some(role), None, None, None, None)) =>
+//                    role mustEqual ""
+//                  case _ =>
+//                    ko
+//                }
+//              }
+//            case _ =>
+//              ko
+//          }
+//        case _ =>
+//          ko
+//      }
+//      ok
+//    }
+//
+//    "decode (full squad)" in {
+//      PacketCoding.DecodePacket(string_full).require match {
+//        case SquadDetailDefinitionUpdateMessage(guid, detail) =>
+//          guid mustEqual PlanetSideGUID(3)
+//          detail match {
+//            case SquadDetail(Some(u1), Some(u2), Some(char_id), Some(u3), Some(leader), Some(task), Some(zone), Some(unk7), Some(member_list)) =>
+//              u1 mustEqual 3
+//              u2 mustEqual 1792
+//              char_id mustEqual 42771010L
+//              u3 mustEqual 529745L
+//              leader mustEqual "HofD"
+//              task mustEqual "\\#ffdc00***\\#9640ff=KOK+SPC+FLY=\\#ffdc00***\\#FF4040 All Welcome"
+//              zone mustEqual PlanetSideZoneID(7)
+//              unk7 mustEqual 4983296
+//              member_list.size mustEqual 10
+//              member_list.head mustEqual SquadPositionEntry(0,Some(
+//                SquadPositionDetail2(
+//                  Some(false),
+//                  Some("\\#ff0000 |||||||||||||||||||||||"),
+//                  Some(""),
+//                  Some(Set(CertificationType.StandardAssault, CertificationType.StandardExoSuit, CertificationType.AgileExoSuit)),
+//                  Some(0),
+//                  Some("")))
+//              )
+//              member_list(1) mustEqual SquadPositionEntry(1,Some(
+//                SquadPositionDetail2(
+//                  Some(false),
+//                  Some("\\#ffdc00   C"),
+//                  Some(""),
+//                  Some(Set(CertificationType.StandardAssault, CertificationType.StandardExoSuit, CertificationType.AgileExoSuit)),
+//                  Some(0),
+//                  Some("")))
+//              )
+//              member_list(2) mustEqual SquadPositionEntry(2,Some(
+//                SquadPositionDetail2(
+//                  Some(false),
+//                  Some("\\#ffdc00   H"),
+//                  Some(""),
+//                  Some(Set(CertificationType.StandardAssault, CertificationType.StandardExoSuit, CertificationType.AgileExoSuit)),
+//                  Some(42644970L),
+//                  Some("OpolE")
+//                )
+//              ))
+//              member_list(3) mustEqual SquadPositionEntry(3,Some(
+//                SquadPositionDetail2(
+//                  Some(false),
+//                  Some("\\#ffdc00    I"),
+//                  Some(""),
+//                  Some(Set(CertificationType.StandardAssault, CertificationType.StandardExoSuit, CertificationType.AgileExoSuit)),
+//                  Some(41604210L),
+//                  Some("BobaF3tt907")
+//                )
+//              ))
+//              member_list(4) mustEqual SquadPositionEntry(4,Some(
+//                SquadPositionDetail2(
+//                  Some(false),
+//                  Some("\\#ffdc00   N"),
+//                  Some(""),
+//                  Some(Set(CertificationType.StandardAssault, CertificationType.StandardExoSuit, CertificationType.AgileExoSuit)),
+//                  Some(0),
+//                  Some("")
+//                )
+//              ))
+//              member_list(5) mustEqual SquadPositionEntry(5,Some(
+//                SquadPositionDetail2(
+//                  Some(false),
+//                  Some("\\#ffdc00   A"),
+//                  Some(""),
+//                  Some(Set(CertificationType.StandardAssault, CertificationType.StandardExoSuit, CertificationType.AgileExoSuit)),
+//                  Some(0),
+//                  Some("")
+//                )
+//              ))
+//              member_list(6) mustEqual SquadPositionEntry(6,Some(
+//                SquadPositionDetail2(
+//                  Some(false),
+//                  Some("\\#ff0000 |||||||||||||||||||||||"),
+//                  Some(""),
+//                  Some(Set(CertificationType.StandardAssault, CertificationType.StandardExoSuit, CertificationType.AgileExoSuit)),
+//                  Some(0),
+//                  Some("")
+//                )
+//              ))
+//              member_list(7) mustEqual SquadPositionEntry(7,Some(
+//                SquadPositionDetail2(
+//                  Some(false),
+//                  Some("\\#9640ff   K"),
+//                  Some(""),
+//                  Some(Set(CertificationType.StandardAssault, CertificationType.StandardExoSuit, CertificationType.AgileExoSuit)),
+//                  Some(0),
+//                  Some("")
+//                )
+//              ))
+//              member_list(8) mustEqual SquadPositionEntry(8,Some(
+//                SquadPositionDetail2(
+//                  Some(false),
+//                  Some("\\#9640ff   O"),
+//                  Some(""),
+//                  Some(Set(CertificationType.StandardAssault, CertificationType.StandardExoSuit, CertificationType.AgileExoSuit)),
+//                  Some(42771010L),
+//                  Some("HofD")
+//                )
+//              ))
+//              member_list(9) mustEqual SquadPositionEntry(9,Some(
+//                SquadPositionDetail2(
+//                  Some(false),
+//                  Some("\\#9640ff   K"),
+//                  Some(""),
+//                  Some(Set(CertificationType.StandardAssault, CertificationType.StandardExoSuit, CertificationType.AgileExoSuit)),
+//                  Some(0),
+//                  Some("")
+//                )
+//              ))
+//            case _ =>
+//              ko
+//          }
+//        case _ =>
+//          ko
+//      }
+//    }
+//
+//    "encode (char id)" in {
+//      val msg = SquadDetailDefinitionUpdateMessage(
+//        PlanetSideGUID(5),
+//        SquadDetail(30910985L)
+//      )
+//      val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+//      pkt mustEqual string_leader_char_id
+//    }
+//
+//    "encode (unk3 + leader name)" in {
+//      val msg = SquadDetailDefinitionUpdateMessage(
+//        PlanetSideGUID(3),
+//        SquadDetail(None, None, Some(42631712L), Some(556403L), Some("Jaako"), None, None, None, Some(List(
+//          SquadPositionEntry(0, SquadPositionDetail2(0L, "")),
+//          SquadPositionEntry(255, None)
+//        )))
+//      )
+//      val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+//      pkt mustEqual string_unk3LeaderName
+//    }
+//
+//    "encode (task)" in {
+//      val msg = SquadDetailDefinitionUpdateMessage(
+//        PlanetSideGUID(5),
+//        SquadDetail(None, "All Welcome ")
+//      )
+//      val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+//      pkt mustEqual string_task
+//    }
+//
+//    "encode (zone)" in {
+//      val msg = SquadDetailDefinitionUpdateMessage(
+//        PlanetSideGUID(3),
+//        SquadDetail(PlanetSideZoneID(21))
+//      )
+//      val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+//      pkt mustEqual string_zone
+//    }
+//
+//    "encode (task + zone)" in {
+//      val msg = SquadDetailDefinitionUpdateMessage(
+//        PlanetSideGUID(2),
+//        SquadDetail(None, None, None, None, None, Some("\\#FF0000 The \\#ffffff Blades"), Some(PlanetSideZoneID(4)), None)
+//      )
+//      val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+//      pkt mustEqual string_taskZone
+//    }
+//
+//    "encode (unk7 + members)" in {
+//      val msg = SquadDetailDefinitionUpdateMessage(
+//        PlanetSideGUID(3),
+//        SquadDetail(
+//          None, None, None, None, None,
+//          Some("The King's Squad"),
+//          None, Some(8),
+//          Some(List(
+//            SquadPositionEntry(9, SquadPositionDetail2("The Guard", None)),
+//            SquadPositionEntry(8, SquadPositionDetail2("The Knight", None)),
+//            SquadPositionEntry(7, SquadPositionDetail2("The Earl", None)),
+//            SquadPositionEntry(6, SquadPositionDetail2("The Lord", None)),
+//            SquadPositionEntry(5, SquadPositionDetail2("The Duke", None)),
+//            SquadPositionEntry(4, SquadPositionDetail2("The Baron", None)),
+//            SquadPositionEntry(3, SquadPositionDetail2("The Princess", None)),
+//            SquadPositionEntry(2, SquadPositionDetail2("The Prince", None)),
+//            SquadPositionEntry(1, SquadPositionDetail2("The Queen", None)),
+//            SquadPositionEntry(0, SquadPositionDetail2("The King", None)),
+//            SquadPositionEntry(255, None)
+//          ))
+//        )
+//      )
+//      val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+//      pkt mustEqual string_unk7
+//    }
+//
+//    "encode (member closed)" in {
+//      val msg = SquadDetailDefinitionUpdateMessage(
+//        PlanetSideGUID(3),
+//        SquadDetail(List(
+//          SquadPositionEntry(5, SquadPositionDetail2.Closed),
+//          SquadPositionEntry(255, None)
+//        ))
+//      )
+//      val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+//      pkt mustEqual string_member_closed
+//    }
+//
+//
+//    "encode (member role)" in {
+//      val msg = SquadDetailDefinitionUpdateMessage(
+//        PlanetSideGUID(7),
+//        SquadDetail(List(
+//          SquadPositionEntry(0, SquadPositionDetail2("Commander", None)),
+//          SquadPositionEntry(255, None)
+//        ))
+//      )
+//      val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+//      pkt mustEqual string_member_role
+//    }
+//
+//    "encode (member role + requirements)" in {
+//      val msg = SquadDetailDefinitionUpdateMessage(
+//        PlanetSideGUID(1),
+//        SquadDetail(List(
+//          SquadPositionEntry(6, SquadPositionDetail2(None, Some("ADV Hacker"), None, Some(Set(CertificationType.AdvancedHacking)), None, None)),
+//          SquadPositionEntry(255, None)
+//        ))
+//      )
+//      val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+//      pkt mustEqual string_member_roleRequirements
+//    }
+//
+//    "encode (member char id + name)" in {
+//      val msg = SquadDetailDefinitionUpdateMessage(
+//        PlanetSideGUID(3),
+//        SquadDetail(List(
+//          SquadPositionEntry(5, SquadPositionDetail2(1218249L, "Duckmaster43")),
+//          SquadPositionEntry(255, None)
+//        ))
+//      )
+//      val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+//      pkt mustEqual string_member_charIdName
+//    }
+//
+//    "encode (task + member etc)" in {
+//      val msg = SquadDetailDefinitionUpdateMessage(
+//        PlanetSideGUID(1),
+//        SquadDetail(
+//          None, None, None, None, None,
+//          Some("\\#FF0000 The \\#ffffff Blades"), None, None,
+//          Some(List(
+//            SquadPositionEntry(9, Some(SquadPositionDetail2(None, Some(""), None, Some(Set()), None, None))),
+//            SquadPositionEntry(8, Some(SquadPositionDetail2(None, Some(""), None, None, None, None))),
+//            SquadPositionEntry(7, Some(SquadPositionDetail2(None, Some(""), None, None, None, None))),
+//            SquadPositionEntry(6, Some(SquadPositionDetail2(None, Some(""), None, None, None, None))),
+//            SquadPositionEntry(5, Some(SquadPositionDetail2(None, Some(""), None, None, None, None))),
+//            SquadPositionEntry(4, Some(SquadPositionDetail2(None, Some(""), None, None, None, None))),
+//            SquadPositionEntry(3, Some(SquadPositionDetail2(None, Some(""), None, None, None, None))),
+//            SquadPositionEntry(2, Some(SquadPositionDetail2(None, Some(""), None, None, None, None))),
+//            SquadPositionEntry(1, Some(SquadPositionDetail2(None, Some(""), None, None, None, None))),
+//            SquadPositionEntry(0, Some(SquadPositionDetail2(None, Some(""), None, None, None, None))),
+//            SquadPositionEntry(255, None)
+//          ))
+//        )
+//      )
+//      val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+//      pkt mustEqual string_task_memberEtc
+//    }
+//
+//    "encode (full squad)" in {
+//      val msg = SquadDetailDefinitionUpdateMessage(
+//        PlanetSideGUID(3),
+//        SquadDetail(
+//          Some(3),
+//          Some(1792),
+//          Some(42771010L),
+//          Some(529745L),
+//          Some("HofD"),
+//          Some("\\#ffdc00***\\#9640ff=KOK+SPC+FLY=\\#ffdc00***\\#FF4040 All Welcome"),
+//          Some(PlanetSideZoneID(7)),
+//          Some(4983296),
+//          Some(List(
+//            SquadPositionEntry(0, SquadPositionDetail2("\\#ff0000 |||||||||||||||||||||||", "", Set(), 0, "")),
+//            SquadPositionEntry(1, SquadPositionDetail2("\\#ffdc00   C", "", Set(), 0, "")),
+//            SquadPositionEntry(2, SquadPositionDetail2("\\#ffdc00   H", "", Set(), 42644970L, "OpolE")),
+//            SquadPositionEntry(3, SquadPositionDetail2("\\#ffdc00    I", "", Set(), 41604210L, "BobaF3tt907")),
+//            SquadPositionEntry(4, SquadPositionDetail2("\\#ffdc00   N", "", Set(), 0, "")),
+//            SquadPositionEntry(5, SquadPositionDetail2("\\#ffdc00   A", "", Set(), 0, "")),
+//            SquadPositionEntry(6, SquadPositionDetail2("\\#ff0000 |||||||||||||||||||||||", "", Set(), 0, "")),
+//            SquadPositionEntry(7, SquadPositionDetail2("\\#9640ff   K", "", Set(), 0, "")),
+//            SquadPositionEntry(8, SquadPositionDetail2("\\#9640ff   O", "", Set(), 42771010L ,"HofD")),
+//            SquadPositionEntry(9, SquadPositionDetail2("\\#9640ff   K", "", Set(), 0, ""))
+//          )
+//        ))
+//      )
+//      val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+//      val pktBits = pkt.toBitVector
+//      val strBits = string_full.toBitVector
+//      pktBits.grouped(100).zip(strBits.grouped(100)).foreach({ case (a, b) =>
+//        a mustEqual b
+//      })
+//      pkt mustEqual string_full
+//    }
   }
 }
diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala
index e59fe423..83d290f8 100644
--- a/pslogin/src/main/scala/WorldSessionActor.scala
+++ b/pslogin/src/main/scala/WorldSessionActor.scala
@@ -2919,21 +2919,28 @@ class WorldSessionActor extends Actor with MDCContextAware {
     sendResponse(
       SquadDetailDefinitionUpdateMessage(
         PlanetSideGUID(3),
-        42771010L,
-        "HofD",
-        "\\#ffdc00***\\#9640ff=KOK+SPC+FLY=\\#ffdc00***\\#FF4040 All Welcome",
-        PlanetSideZoneID(7),
-        List(
-          SquadPositionDetail("\\#ff0000 |||||||||||||||||||||||", "Just a space filler"),
-          SquadPositionDetail("\\#ffdc00   C", ""),
-          SquadPositionDetail(false, "\\#ffdc00   H", "", Set(), 42644970L, "OpoIE"),
-          SquadPositionDetail(false, "\\#ffdc00    I", "", Set(), 41604210L, "BobaF3tt907"),
-          SquadPositionDetail("\\#ffdc00   N", ""),
-          SquadPositionDetail("\\#ffdc00   A", ""),
-          SquadPositionDetail("\\#ff0000 |||||||||||||||||||||||", "Another space filler"),
-          SquadPositionDetail("\\#9640ff   K", ""),
-          SquadPositionDetail(false, "\\#9640ff   O", "", Set(), 42771010L, "HofD"),
-          SquadPositionDetail("\\#9640ff   K", "")
+        SquadDetail(
+          3,
+          1792,
+          42771010L,
+          529745L,
+          "HofD",
+          "\\#ffdc00***\\#9640ff=KOK+SPC+FLY=\\#ffdc00***\\#FF4040 All Welcome",
+          PlanetSideZoneID(7),
+          4983296,
+          List(
+            SquadPositionEntry(0, SquadPositionDetail2("\\#ff0000 |||||||||||||||||||||||", "", Set(), 0, "")),
+            SquadPositionEntry(1, SquadPositionDetail2("\\#ffdc00   C", "", Set(), 0, "")),
+            SquadPositionEntry(2, SquadPositionDetail2("\\#ffdc00   H", "", Set(), 42644970L, "OpolE")),
+            SquadPositionEntry(3, SquadPositionDetail2("\\#ffdc00    I", "", Set(), 41604210L, "BobaF3tt907")),
+            SquadPositionEntry(4, SquadPositionDetail2("\\#ffdc00   N", "", Set(), 0, "")),
+            SquadPositionEntry(5, SquadPositionDetail2("\\#ffdc00   A", "", Set(), 0, "")),
+//            SquadPositionEntry(6, SquadPositionDetail2("\\#ff0000 |||||||||||||||||||||||", "", Set(), 0, "")),
+            SquadPositionEntry(6, SquadPositionDetail2("\\#ff0000 |||||||||||||||||||||||", "", Set(), 1, "Test")),
+            SquadPositionEntry(7, SquadPositionDetail2("\\#9640ff   K", "", Set(), 0, "")),
+            SquadPositionEntry(8, SquadPositionDetail2("\\#9640ff   O", "", Set(), 42771010L ,"HofD")),
+            SquadPositionEntry(9, SquadPositionDetail2("\\#9640ff   K", "", Set(), 0, ""))
+          )
         )
       )
     )
@@ -3349,8 +3356,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
     case msg @ PlayerStateMessageUpstream(avatar_guid, pos, vel, yaw, pitch, yaw_upper, seq_time, unk3, is_crouching, is_jumping, unk4, is_cloaking, unk5, unk6) =>
       if(deadState == DeadState.Alive) {
         if(!player.Crouching && is_crouching) { //SQUAD TESTING CODE
-          //sendRawResponse(hex"e8030080c603c043fe")
-          sendRawResponse(hex"e8 0300 80 c 600408c00000001200008811f6200400144004f007a007a0069004b0069006e006700ff")
+          sendRawResponse(hex"e80300818800015c5189004603408c000000012000ff")
 //          sendResponse(SquadMembershipResponse(SquadRequestType.Invite,0,0,42771010,Some(avatar.CharId),"HofD",false,None))
 //          sendResponse(SquadMembershipResponse(SquadRequestType.Accept,0,0,avatar.CharId,Some(42771010),"VirusGiver",true,Some(None)))
 //          sendResponse(SquadMemberEvent(0,7,42771010,0,Some("HofD"),Some(7),Some(529745)))

From 2a1b05171074a815310414a379a2f7ddcc2b5cd7 Mon Sep 17 00:00:00 2001
From: FateJH <Jason_DiDonato@yahoo.com>
Date: Wed, 3 Jul 2019 01:55:42 -0400
Subject: [PATCH 11/31] completed form of SquadDetailDefinitionUpdate

---
 .../SquadDetailDefinitionUpdateMessage.scala  | 1927 ++++++++++-------
 .../services/teamwork/SquadResponse.scala     |    2 +-
 ...uadDetailDefinitionUpdateMessageTest.scala | 1225 ++++++-----
 .../src/main/scala/WorldSessionActor.scala    |   35 +-
 4 files changed, 1901 insertions(+), 1288 deletions(-)

diff --git a/common/src/main/scala/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala b/common/src/main/scala/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala
index 62a6702e..ad080bcf 100644
--- a/common/src/main/scala/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala
+++ b/common/src/main/scala/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala
@@ -3,55 +3,18 @@ package net.psforever.packet.game
 
 import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
 import net.psforever.types.CertificationType
-import scodec.bits.BitVector
 import scodec.{Attempt, Codec, Err}
 import scodec.codecs._
 import shapeless.HNil
 
 import scala.annotation.tailrec
 
-final case class SquadPositionDetail(is_closed : Boolean,
-                                     role : String,
-                                     detailed_orders : String,
-                                     requirements : Set[CertificationType.Value],
-                                     char_id : Long,
-                                     name : String)
-
-final case class OldSquadDetailDefinitionUpdateMessage(guid : PlanetSideGUID,
-                                                    unk1 : Int,
-                                                    leader_char_id : Long,
-                                                    unk2 : BitVector,
-                                                    leader_name : String,
-                                                    task : String,
-                                                    zone_id : PlanetSideZoneID,
-                                                    member_info : List[SquadPositionDetail])
-
-object SquadPositionDetail {
-  final val Closed : SquadPositionDetail = SquadPositionDetail(is_closed = true, "", "", Set.empty, 0L, "")
-
-  def apply() : SquadPositionDetail = SquadPositionDetail(is_closed = false, "", "", Set.empty, 0L, "")
-
-  def apply(char_id : Long, name : String) : SquadPositionDetail = SquadPositionDetail(is_closed = false, "", "", Set.empty, char_id, name)
-
-  def apply(role : String, detailed_orders : String) : SquadPositionDetail = SquadPositionDetail(is_closed = false, role, detailed_orders, Set.empty, 0L, "")
-
-  def apply(role : String, detailed_orders : String, requirements : Set[CertificationType.Value]) : SquadPositionDetail = SquadPositionDetail(is_closed = false, role, detailed_orders, requirements, 0L, "")
-
-  def apply(role : String, detailed_orders : String, char_id : Long, name : String) : SquadPositionDetail = SquadPositionDetail(is_closed = false, role, detailed_orders, Set.empty, char_id, name)
-
-  def apply(role : String, detailed_orders : String, requirements : Set[CertificationType.Value], char_id : Long, name : String) : SquadPositionDetail = SquadPositionDetail(is_closed = false, role, detailed_orders, requirements, char_id, name)
-}
-
-object OldSquadDetailDefinitionUpdateMessage {
-  def apply(guid : PlanetSideGUID, char_id : Long, leader_name : String, task : String, zone_id : PlanetSideZoneID, member_info : List[SquadPositionDetail]) : OldSquadDetailDefinitionUpdateMessage = {
-    import scodec.bits._
-    OldSquadDetailDefinitionUpdateMessage(guid, 1, char_id, hex"000000".toBitVector, leader_name, task, zone_id, member_info)
-  }
-}
-
-//NEW FORM SquadDetailDefinitionUpdateMessage
-
-private class StreamLengthToken(init : Int = 0) {
+/**
+  * A container that should be used to keep track of the current length of a stream of bits.
+  * @param init the starting pad value;
+  *             defaults to 0
+  */
+class StreamLengthToken(init : Int = 0) {
   private var bitLength : Int = init
 
   def Length : Int = bitLength
@@ -67,14 +30,36 @@ private class StreamLengthToken(init : Int = 0) {
   }
 }
 
-final case class SquadPositionDetail2(is_closed : Option[Boolean],
-                                      role : Option[String],
-                                      detailed_orders : Option[String],
-                                      requirements : Option[Set[CertificationType.Value]],
-                                      char_id : Option[Long],
-                                      name : Option[String]) {
-  def And(info : SquadPositionDetail2) : SquadPositionDetail2 = {
-    SquadPositionDetail2(
+/**
+  * Information regarding a squad's position as a series of common fields.
+  * When parsed in an itemized way, only the important fields are represented.
+  * When parsed in a continuous manner, all of the fields are populated.
+  * All fields are optional for that reason.
+  * @param is_closed availability, whether the position can be occupied by a player;
+  *                  an unavailable position is referenced as "Closed" and no other position detail is displayed;
+  *                  an available unoccupied position is "Available"
+  * @param role the title of the position
+  * @param detailed_orders the suggested responsibilities of the position
+  * @param requirements the actual responsibilities of the position
+  * @param char_id the unique character identification number for the player that is occupying this position
+  * @param name the name of the player who is occupying this position
+  */
+final case class SquadPositionDetail(is_closed : Option[Boolean],
+                                     role : Option[String],
+                                     detailed_orders : Option[String],
+                                     requirements : Option[Set[CertificationType.Value]],
+                                     char_id : Option[Long],
+                                     name : Option[String]) {
+  /**
+    * Combine two `SquadPositionDetail` objects, with priority given to `this` one.
+    * Most fields that are not empty are assigned.
+    * Even if the current object reports the squad position being open - `is_closed = Some(false)` -
+    * just one instance of the squad position being closed overwrites all future updates.
+    * @param info the object being combined
+    * @return the combined `SquadDetail` object
+    */
+  def And(info : SquadPositionDetail) : SquadPositionDetail = {
+    SquadPositionDetail(
       is_closed match {
         case Some(false) | None =>
           info.is_closed.orElse(is_closed)
@@ -88,10 +73,69 @@ final case class SquadPositionDetail2(is_closed : Option[Boolean],
       name.orElse(info.name)
     )
   }
+
+  //methods intended to combine the fields of itself and another object
+  def Open : SquadPositionDetail =
+    this And SquadPositionDetail(Some(false), None, None, None, None, None)
+  def Close : SquadPositionDetail =
+    this And SquadPositionDetail(Some(true), None, None, None, None, None)
+  def Role(role : String) : SquadPositionDetail =
+    this And SquadPositionDetail(None, Some(role), None, None, None, None)
+  def DetailedOrders(orders : String) : SquadPositionDetail =
+    this And SquadPositionDetail(None, None, Some(orders), None, None, None)
+  def Requirements(req : Set[CertificationType.Value]) : SquadPositionDetail =
+    this And SquadPositionDetail(None, None, None, Some(req), None, None)
+  def CharId(char_id : Long) : SquadPositionDetail =
+    this And SquadPositionDetail(None, None, None, None, Some(char_id), None)
+  def Name(name : String) : SquadPositionDetail =
+    this And SquadPositionDetail(None, None, None, None, None, Some(name))
+  def Player(char_id : Long, name : String) : SquadPositionDetail =
+    this And SquadPositionDetail(None, None, None, None, Some(char_id), Some(name))
+
+  /**
+    * Complete the object by providing placeholder values for all fields.
+    * @return a `SquadPositionDetail` object with all of its field populated
+    */
+  def Complete : SquadPositionDetail = SquadPositionDetail(
+    is_closed.orElse(Some(false)),
+    role.orElse(Some("")),
+    detailed_orders.orElse(Some("")),
+    requirements.orElse(Some(Set.empty)),
+    char_id.orElse(Some(0L)),
+    name.orElse(Some(""))
+  )
 }
 
-final case class SquadPositionEntry(index : Int, info : Option[SquadPositionDetail2])
+/**
+  * A container for squad position field data
+  * associating what would be the ordinal position of that field data in full squad data.
+  * @param index the index for this squad position;
+  *              expected to be a number 0-9 or 255;
+  *              when 255, this indicated the end of enumerated squad position data and the data for that position is absent
+  * @param info the squad position field data
+  */
+final case class SquadPositionEntry(index : Int, info : Option[SquadPositionDetail])
 
+/**
+  * Information regarding a squad's position as a series of common fields.
+  * When parsed in an itemized way, only the important fields are represented.
+  * When parsed in a continuous manner, all of the fields are populated.
+  * All fields are optional for that reason.<br>
+  * <br>
+  * The squad leader does not necessarily have to be a person from the `member_info` list.
+  * @param unk1 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 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;
+  *                can also indicate the zone of the squad leader
+  * @param unk7 na
+  * @param member_info a list of squad position data
+  */
 final case class SquadDetail(unk1 : Option[Int],
                              unk2 : Option[Int],
                              leader_char_id : Option[Long],
@@ -101,6 +145,12 @@ final case class SquadDetail(unk1 : Option[Int],
                              zone_id : Option[PlanetSideZoneID],
                              unk7 : Option[Int],
                              member_info : Option[List[SquadPositionEntry]]) {
+  /**
+    * Combine two `SquadDetail` objects, with priority given to `this` one.
+    * Most fields that are not empty are assigned.
+    * @param info the object being combined
+    * @return the combined `SquadDetail` object
+    */
   def And(info : SquadDetail) : SquadDetail = {
     SquadDetail(
       unk1.orElse(info.unk1),
@@ -111,16 +161,81 @@ final case class SquadDetail(unk1 : Option[Int],
       task.orElse(info.task),
       zone_id.orElse(info.zone_id),
       unk7.orElse(info.unk7),
-      (member_info, info.member_info) match {
-        case (Some(info1), Some(info2)) => Some(info1 ++ info2)
-        case (Some(info1), _) => Some(info1)
-        case (None, Some(info2)) => Some(info2)
-        case _ => None
-      }
+      member_info.orElse(info.member_info)
     )
   }
+
+  //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 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 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 =
+    this And SquadDetail(None, None, Some(char_id), None, Some(name), None, None, None, None)
+  def Task(task : String) : SquadDetail =
+    this And SquadDetail(None, None, None, None, None, Some(task), None, None, None)
+  def ZoneId(zone : PlanetSideZoneID) : SquadDetail =
+    this And SquadDetail(None, None, None, None, None, None, Some(zone), None, None)
+  def Field7(value : Int) : SquadDetail =
+    this And SquadDetail(None, None, None, None, None, None, None, Some(value), None)
+  def Members(list : List[SquadPositionEntry]) : SquadDetail =
+    this And SquadDetail(None, None, None, None, None, None, None, None, Some(list))
+
+  /**
+    * Complete the object by providing placeholder values for all fields.
+    * The `member_info` field requires additional allocation.
+    * @return a `SquadDetail` object with all of its field populated
+    */
+  def Complete : SquadDetail = SquadDetail(
+    unk1.orElse(Some(1)),
+    unk2.orElse(Some(0)),
+    leader_char_id.orElse(Some(0L)),
+    unk3.orElse(Some(0L)),
+    leader_name.orElse(Some("")),
+    task.orElse(Some("")),
+    zone_id.orElse(Some(PlanetSideZoneID(0))),
+    unk7.orElse(Some(0)),
+    {
+      val complete = SquadPositionDetail().Complete
+      Some(member_info match {
+        case Some(info) =>
+          //create one list that ensures all existing positions are "complete" then add a list of the missing indices
+          val fields = info.collect {
+            case SquadPositionEntry(a, Some(b)) => SquadPositionEntry(a, b.Complete)
+            case out @ SquadPositionEntry(_, None) => out
+          }
+          val indices = info.map { case SquadPositionEntry(a, _) => a }
+          ((0 to 9).toSet.diff(indices.toSet).map { SquadPositionEntry(_, complete) } ++ fields).toList.sortBy(_.index)
+        case None =>
+          //original list
+          (0 to 9).map { i => SquadPositionEntry(i, complete) }.toList
+      })
+    }
+  )
 }
 
+/**
+  * A compilation of the fields that communicate detailed information about squad structure and composition
+  * as a complement to the packet `ReplicationStreamMessage` and the packet `SquadDefinitionActionMessage`.
+  * The information communicated by the `SquadDefinitionActionMessage` packets allocates individual fields of the squad's structure
+  * and the `ReplicationStreamMessage` packet reports very surface-level information about the squad to other players.
+  * The `SquadDetailDefinitionUpdateMessage` packet serves as a realization of the field information reported by the former
+  * and a fully fleshed-out explanation of the information presented by the latter.<br>
+  * <br>
+  * Squads are generally referenced by their own non-zero globally unique identifier that is valid server-wide.
+  * A zero GUID squad is also accessible for information related to the local unpublished squad that exists on a specific client.
+  * Only one published squad can have its information displayed at a time.
+  * While imperfect squad information can be shown, two major formats for the data in this packet are common.
+  * The first format lists all of the squad's fields and data and is used as an initialization of the squad locally.
+  * This format is always used the first time information about the squad is communicated to the client.
+  * The second format lists specific portions of the squad's fields and data and is used primarily for simple updating purposes.
+  * @param guid the globally unique identifier of the squad
+  * @param detail information regarding the squad
+  */
 final case class SquadDetailDefinitionUpdateMessage(guid : PlanetSideGUID,
                                                     detail : SquadDetail)
   extends PlanetSideGamePacket {
@@ -129,358 +244,1042 @@ final case class SquadDetailDefinitionUpdateMessage(guid : PlanetSideGUID,
   def encode = SquadDetailDefinitionUpdateMessage.encode(this)
 }
 
-object SquadPositionDetail2 {
-  final val Blank : SquadPositionDetail2 = SquadPositionDetail2()
-  final val Closed : SquadPositionDetail2 = SquadPositionDetail2(is_closed = Some(true), None, None, None, None, None)
-  final val Open : SquadPositionDetail2 = SquadPositionDetail2(is_closed = Some(false), None, None, None, None, None)
+object SquadPositionDetail {
+  /**
+    * A featureless squad position.
+    * References the default overloaded constructor.
+    */
+  final val Blank : SquadPositionDetail = SquadPositionDetail()
+  /**
+    * An unavailable squad position.
+    */
+  final val Closed : SquadPositionDetail = SquadPositionDetail(is_closed = Some(true), None, None, None, None, None)
+  /**
+    * An available squad position.
+    */
+  final val Open : SquadPositionDetail = SquadPositionDetail(is_closed = Some(false), None, None, None, None, None)
 
-  def apply() : SquadPositionDetail2 = SquadPositionDetail2(None, None, None, None, None, None)
-
-  def apply(role : String, detailed_orders : Option[String]) : SquadPositionDetail2 = SquadPositionDetail2(None, Some(role), detailed_orders, None, None, None)
-
-  def apply(role : Option[String], detailed_orders : String) : SquadPositionDetail2 = SquadPositionDetail2(None, role, Some(detailed_orders), None, None, None)
-
-  def apply(role : String, detailed_orders : String) : SquadPositionDetail2 = SquadPositionDetail2(None, Some(role), Some(detailed_orders), None, None, None)
-
-  def apply(requirements : Set[CertificationType.Value]) : SquadPositionDetail2 = SquadPositionDetail2(None, None, None, Some(requirements), None, None)
-
-  def apply(role : String, detailed_orders : String, requirements : Set[CertificationType.Value]) : SquadPositionDetail2 = SquadPositionDetail2(None, Some(role), Some(detailed_orders), Some(requirements), None, None)
-
-  def apply(char_id : Long) : SquadPositionDetail2 = SquadPositionDetail2(None, None, None, None, Some(char_id), None)
-
-  def apply(name : String) : SquadPositionDetail2 = SquadPositionDetail2(None, None, None, None, None, Some(name))
-
-  def apply(char_id : Long, name : String) : SquadPositionDetail2 = SquadPositionDetail2(None, None, None, None, Some(char_id), Some(name))
-
-  def apply(role : String, detailed_orders : String, requirements : Set[CertificationType.Value], char_id : Long, name : String) : SquadPositionDetail2 = SquadPositionDetail2(is_closed = Some(false), Some(role), Some(detailed_orders), Some(requirements), Some(char_id), Some(name))
+  /**
+    * An overloaded constructor that produces a featureless squad position.
+    * @return a `SquadPositionDetail` object
+    */
+  def apply() : SquadPositionDetail = SquadPositionDetail(None, None, None, None, None, None)
+  /**
+    * An overloaded constructor that produces a full squad position with a role, detailed orders, and certification requirements.
+    * This basically defines an available squad position that is unoccupied.
+    * @return a `SquadPositionDetail` object
+    */
+  def apply(role : String, detailed_orders : String, requirements : Set[CertificationType.Value], char_id : Long, name : String) : SquadPositionDetail = SquadPositionDetail(Some(false), Some(role), Some(detailed_orders), Some(requirements), Some(char_id), Some(name))
 }
 
 object SquadPositionEntry {
-  import SquadDetailDefinitionUpdateMessage.paddedStringMetaCodec
-
-  def apply(index : Int, detail : SquadPositionDetail2) : SquadPositionEntry = SquadPositionEntry(index, Some(detail))
-
-  private val isClosedCodec : Codec[SquadPositionDetail2] = bool.exmap[SquadPositionDetail2] (
-    state => Attempt.successful(SquadPositionDetail2(Some(state), None, None, None, None, None)),
-    {
-      case SquadPositionDetail2(Some(state), _, _, _, _, _) =>
-        Attempt.successful(state)
-      case _ =>
-        Attempt.failure(Err("failed to encode squad position data for availability"))
-    }
-  )
-
-  private def roleCodec(bitsOverByte : StreamLengthToken) : Codec[SquadPositionDetail2] = paddedStringMetaCodec(bitsOverByte.Length).exmap[SquadPositionDetail2] (
-    role => Attempt.successful(SquadPositionDetail2(role, None)),
-    {
-      case SquadPositionDetail2(_, Some(role), _, _, _, _) =>
-        Attempt.successful(role)
-      case _ =>
-        Attempt.failure(Err("failed to encode squad position data for role"))
-    }
-  )
-
-  private def ordersCodec(bitsOverByte : StreamLengthToken) : Codec[SquadPositionDetail2] = paddedStringMetaCodec(bitsOverByte.Length).exmap[SquadPositionDetail2] (
-    orders => Attempt.successful(SquadPositionDetail2(None, orders)),
-    {
-      case SquadPositionDetail2(_, _, Some(orders), _, _, _) =>
-        Attempt.successful(orders)
-      case _ =>
-        Attempt.failure(Err("failed to encode squad position data for detailed orders"))
-    }
-  )
-
-  private val requirementsCodec : Codec[SquadPositionDetail2] = ulongL(46).exmap[SquadPositionDetail2] (
-    requirements => Attempt.successful(SquadPositionDetail2(CertificationType.fromEncodedLong(requirements))),
-    {
-      case SquadPositionDetail2(_, _, _, Some(requirements), _, _) =>
-        Attempt.successful(CertificationType.toEncodedLong(requirements))
-      case _ =>
-        Attempt.failure(Err("failed to encode squad position data for certification requirements"))
-    }
-  )
-
-  private val charIdCodec : Codec[SquadPositionDetail2] = uint32L.exmap[SquadPositionDetail2] (
-    char_id => Attempt.successful(SquadPositionDetail2(char_id)),
-    {
-      case SquadPositionDetail2(_, _, _, _, Some(char_id), _) =>
-        Attempt.successful(char_id)
-      case _ =>
-        Attempt.failure(Err("failed to encode squad data for member id"))
-    }
-  )
-
-  private def nameCodec(bitsOverByte : StreamLengthToken) : Codec[SquadPositionDetail2] = paddedStringMetaCodec(bitsOverByte.Length).exmap[SquadPositionDetail2] (
-    name => Attempt.successful(SquadPositionDetail2(name)),
-    {
-      case SquadPositionDetail2(_, _, _, _, _, Some(orders)) =>
-        Attempt.successful(orders)
-      case _ =>
-        Attempt.failure(Err("failed to encode squad position data for member name"))
-    }
-  )
-
   /**
-    * `Codec` for failing to determine a valid `Codec` based on the entry data.
-    * This `Codec` is an invalid codec that does not read any bit data.
-    * The `conditional` will always return `None` because
-    * its determining conditional statement is explicitly `false`
-    * and all cases involving explicit failure.
+    * An overloaded constructor.
+    * @return a `SquadPositionEntry` object
     */
-  private def failureCodec(code : Int) : Codec[SquadPositionDetail2] = conditional(included = false, bool).exmap[SquadPositionDetail2] (
-    _ => Attempt.failure(Err(s"decoding with unhandled codec - $code")),
-    _ => Attempt.failure(Err(s"encoding with unhandled codec - $code"))
-  )
-
-  private final case class LinkedSquadPositionInfo(code : Int, info : SquadPositionDetail2, next : Option[LinkedSquadPositionInfo])
-
-  /**
-    * Concatenate a `SquadInfo` object chain into a single `SquadInfo` object.
-    * Recursively visits every link in a `SquadInfo` object chain.
-    * @param info the current link in the chain
-    * @param squadInfo the persistent `SquadInfo` concatenation object;
-    *                  defaults to `SquadInfo.Blank`
-    * @return the concatenated `SquadInfo` object
-    */
-  @tailrec
-  private def unlinkSquadPositionInfo(info : Option[LinkedSquadPositionInfo], squadInfo : SquadPositionDetail2 = SquadPositionDetail2.Blank) : SquadPositionDetail2 = {
-    info match {
-      case None =>
-        squadInfo
-      case Some(sqInfo) =>
-        unlinkSquadPositionInfo(sqInfo.next, squadInfo And sqInfo.info)
-    }
-  }
-
-  /**
-    * Decompose a single `SquadInfo` object into a `SquadInfo` object chain of the original's fields.
-    * The fields as a linked list are explicitly organized "leader", "task", "zone_id", "size", "capacity,"
-    * or as "(leader, (task, (zone_id, (size, (capacity, None)))))" when fully populated and composed.
-    * @param info a `SquadInfo` object that has all relevant fields populated
-    * @return a linked list of `SquadInfo` objects, each with a single field from the input `SquadInfo` object
-    */
-  private def linkSquadPositionInfo(info : SquadPositionDetail2) : LinkedSquadPositionInfo = {
-    //import scala.collection.immutable.::
-    Seq(
-      (5, SquadPositionDetail2(None, None, None, info.requirements, None, None)),
-      (4, SquadPositionDetail2(None, None, None, None, None, info.name)),
-      (3, SquadPositionDetail2(None, None, None, None, info.char_id, None)),
-      (2, SquadPositionDetail2(None, None, info.detailed_orders, None, None, None)),
-      (1, SquadPositionDetail2(None, info.role, None, None, None, None)),
-      (0, SquadPositionDetail2(info.is_closed, None, None, None, None, None))
-    ) //in reverse order so that the linked list is in the correct order
-      .filterNot { case (_, sqInfo) => sqInfo == SquadPositionDetail2.Blank}
-    match {
-      case Nil =>
-        throw new Exception("no linked list squad position fields encountered where at least one was expected") //bad end
-      case x :: Nil =>
-        val (code, squadInfo) = x
-        LinkedSquadPositionInfo(code, squadInfo, None)
-      case x :: xs =>
-        val (code, squadInfo) = x
-        linkSquadPositionInfo(xs, LinkedSquadPositionInfo(code, squadInfo, None))
-    }
-  }
-
-  /**
-    * Decompose a single `SquadInfo` object into a `SquadInfo` object chain of the original's fields.
-    * The fields as a linked list are explicitly organized "leader", "task", "zone_id", "size", "capacity,"
-    * or as "(leader, (task, (zone_id, (size, (capacity, None)))))" when fully populated and composed.
-    * @param infoList a series of paired field codes and `SquadInfo` objects with data in the indicated fields
-    * @return a linked list of `SquadInfo` objects, each with a single field from the input `SquadInfo` object
-    */
-  @tailrec
-  private def linkSquadPositionInfo(infoList : Seq[(Int, SquadPositionDetail2)], linkedInfo : LinkedSquadPositionInfo) : LinkedSquadPositionInfo = {
-    if(infoList.isEmpty) {
-      linkedInfo
-    }
-    else {
-      val (code, data) = infoList.head
-      linkSquadPositionInfo(infoList.tail, LinkedSquadPositionInfo(code, data, Some(linkedInfo)))
-    }
-  }
-
-  private def listing_codec(size : Int, bitsOverByte : StreamLengthToken) : Codec[LinkedSquadPositionInfo] = {
-    import shapeless.::
-    (
-      uint4 >>:~ { code =>
-        selectCodecAction(code,  bitsOverByte.Add(4)) >>:~ { _ =>
-          modifyCodecPadValue(code, bitsOverByte)
-          conditional(size - 1 > 0, listing_codec(size - 1, bitsOverByte)).hlist
-        }
-      }
-      ).xmap[LinkedSquadPositionInfo] (
-      {
-        case code :: entry :: next :: HNil =>
-          LinkedSquadPositionInfo(code, entry, next)
-      },
-      {
-        case LinkedSquadPositionInfo(code, entry, next) =>
-          code :: entry :: next :: HNil
-      }
-    )
-  }
-
-  private def selectCodecAction(code : Int, bitsOverByte : StreamLengthToken) : Codec[SquadPositionDetail2] = {
-    code match {
-      case 0 => isClosedCodec
-      case 1 => roleCodec(bitsOverByte)
-      case 2 => ordersCodec(bitsOverByte)
-      case 3 => charIdCodec
-      case 4 => nameCodec(bitsOverByte)
-      case 5 => requirementsCodec
-      case _ => failureCodec(code)
-    }
-  }
-
-  private def modifyCodecPadValue(code : Int, bitsOverByte : StreamLengthToken) : StreamLengthToken = {
-    code match {
-      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 _ => bitsOverByte.Length = Int.MinValue //wildly incorrect
-    }
-  }
-
-  private def squad_member_details_codec(bitsOverByte : StreamLengthToken) : Codec[LinkedSquadPositionInfo] = {
-    import shapeless.::
-    (
-      uint8 >>:~ { size =>
-        listing_codec(size, bitsOverByte).hlist
-      }
-      ).xmap[LinkedSquadPositionInfo] (
-      {
-        case _ :: info :: HNil =>
-          info
-      },
-      info => {
-        var i = 1
-        var dinfo = info
-        while(dinfo.next.nonEmpty) {
-          i += 1
-          dinfo = dinfo.next.get
-        }
-        i :: info :: HNil
-      }
-    )
-  }
-
-  def codec(bitsOverByte : StreamLengthToken) : Codec[SquadPositionEntry] = {
-    import shapeless.::
-    /*
-     import net.psforever.newcodecs.newcodecs
-//        newcodecs.binary_choice[Option[LinkedSquadPositionInfo] :: HNil](
-//          index < 255,
-//          (bool :: squad_member_details_codec(bitsOverByte.Add(1))).exmap[Option[LinkedSquadPositionInfo] :: HNil] (
-//            {
-//              case _ :: info :: HNil => Attempt.Successful(Some(info) :: HNil)
-//            },
-//            {
-//              case Some(info) :: HNil => Attempt.Successful(true :: info :: HNil)
-//              case None :: HNil => Attempt.Failure(Err(s"this data for this position should not be None - $index < 255"))
-//            }
-//          ),
-//          bits.xmap[Option[LinkedSquadPositionInfo] :: HNil] (
-//            _ => None :: HNil,
-//            {
-//              case _ :: HNil => BitVector.empty
-//            }
-//          )
-//        )
-    */
-    (
-      ("index" | uint8) >>:~ { index =>
-        conditional(index < 255, bool :: squad_member_details_codec(bitsOverByte.Add(1))) ::
-        conditional(index == 255, bits)
-      }
-      ).xmap[SquadPositionEntry] (
-      {
-        case 255 :: _ :: _ :: HNil =>
-          SquadPositionEntry(255, None)
-        case ndx :: Some(_ :: info :: HNil) :: _ :: HNil =>
-          SquadPositionEntry(ndx, Some(unlinkSquadPositionInfo(Some(info))))
-      },
-      {
-        case SquadPositionEntry(255, _) =>
-          255 :: None :: None :: HNil
-        case SquadPositionEntry(ndx, Some(info)) =>
-          ndx :: Some(true :: linkSquadPositionInfo(info) :: HNil) :: None :: HNil
-      }
-    )
-  }
+  def apply(index : Int, detail : SquadPositionDetail) : SquadPositionEntry = SquadPositionEntry(index, Some(detail))
 }
 
 object SquadDetail {
-  final val Blank = SquadDetail(None, None, None, None, None, None, None, None, None)
-
-  def apply(leader_char_id : Long) : SquadDetail = SquadDetail(None, None, Some(leader_char_id), None, None, None, None, None, None)
-
-  def apply(leader_char_id : Long, leader_name : String) : SquadDetail = SquadDetail(None, None, Some(leader_char_id), None, Some(leader_name), None, None, None, None)
-
-  def apply(leader_name : String, task : Option[String]) : SquadDetail = SquadDetail(None, None, None, None, Some(leader_name), task, None, None, None)
-
-  def apply(leader_name : Option[String], task : String) : SquadDetail = SquadDetail(None, None, None, None, leader_name, Some(task), None, None, None)
-
-  def apply(zone_id : PlanetSideZoneID) : SquadDetail = SquadDetail(None, None, None, None, None, None, Some(zone_id), None, None)
-
-  def apply(member_list : List[SquadPositionEntry]) : SquadDetail = SquadDetail(None, None, None, None, None, None, None, None, Some(member_list))
+  /**
+    * A featureless squad.
+    * References the default overloaded constructor.
+    */
+  final val Blank = SquadDetail()
 
+  /**
+    * An overloaded constructor that produces a featureless squad.
+    * @return a `SquadDetail` object
+    */
+  def apply() : SquadDetail = SquadDetail(None, None, None, None, None, None, None, None, None)
+  /**
+    * An overloaded constructor that produces a complete squad with all fields populated.
+    * @return a `SquadDetail` object
+    */
   def apply(unk1 : Int, unk2 : Int, leader_char_id : Long, unk3 : Long, leader_name : String, task : String, zone_id : PlanetSideZoneID, unk7 : Int, member_info : List[SquadPositionEntry]) : SquadDetail = {
     SquadDetail(Some(unk1), Some(unk2), Some(leader_char_id), Some(unk3), Some(leader_name), Some(task), Some(zone_id), Some(unk7), Some(member_info))
   }
+
+  //individual field overloaded constructors
+  def Field1(unk1 : Int) : SquadDetail =
+    SquadDetail(Some(unk1), 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 LeaderName(name : String) : SquadDetail =
+    SquadDetail(None, None, None, None, Some(name), None, None, None, None)
+  def Leader(char_id : Long, name : String) : SquadDetail =
+    SquadDetail(None, None, Some(char_id), None, Some(name), None, None, None, None)
+  def Task(task : String) : SquadDetail =
+    SquadDetail(None, None, None, None, None, Some(task), None, None, None)
+  def ZoneId(zone : PlanetSideZoneID) : SquadDetail =
+    SquadDetail(None, None, None, None, None, None, Some(zone), None, None)
+  def Field7(unk7 : Int) : SquadDetail =
+    SquadDetail(None, None, None, None, None, None, None, Some(unk7), None)
+  def Members(list : List[SquadPositionEntry]) : SquadDetail =
+    SquadDetail(None, None, None, None, None, None, None, None, Some(list))
 }
 
 object SquadDetailDefinitionUpdateMessage extends Marshallable[SquadDetailDefinitionUpdateMessage] {
+  /**
+    * The patterns necessary to read uncoded squad data.
+    * All squad fields and all squad position fields are parsed.
+    */
+  object FullSquad {
+    /**
+      * The first squad position entry has its first string (`role`) field padded by a constant amount.
+      */
+    private val first_position_codec : Codec[SquadPositionDetail] = basePositionCodec(bitsOverByteLength = 1, DefaultRequirements)
+    /**
+      * All squad position entries asides from the first have unpadded strings.
+      * The very first entry aligns the remainder of the string fields along byte boundaries.
+      */
+    private val position_codec : Codec[SquadPositionDetail] = basePositionCodec(bitsOverByteLength = 0, DefaultRequirements)
+
+    /**
+      * Internal class for linked list operations.
+      * @param info details regarding the squad position
+      * @param next if there is a "next" squad position
+      */
+    private final case class LinkedFields(info: SquadPositionDetail, next: Option[LinkedFields])
+
+    /**
+      * Parse each squad position field in the bitstream after the first one.
+      * @return a pattern outlining sequential squad positions
+      */
+    private def subsequent_member_codec : Codec[LinkedFields] = {
+      import shapeless.::
+      (
+        //disruptive coupling action (e.g., flatPrepend) is necessary for recursive Codec
+        ("member" | position_codec) >>:~ { _ =>
+          optional(bool, "next" | subsequent_member_codec).hlist
+        }
+        ).xmap[LinkedFields] (
+        {
+          case a :: b :: HNil =>
+            LinkedFields(a, b)
+        },
+        {
+          case LinkedFields(a, b) =>
+            a :: b :: HNil
+        }
+      )
+    }
+
+    /**
+      * Parse the first squad position field in the bitstream.
+      * @return a pattern outlining sequential squad positions
+      */
+    private def initial_member_codec : Codec[LinkedFields] = {
+      import shapeless.::
+      (
+        ("member" | first_position_codec) ::
+          optional(bool, "next" | subsequent_member_codec)
+        ).xmap[LinkedFields] (
+        {
+          case a :: b :: HNil =>
+            LinkedFields(a, b)
+        },
+        {
+          case LinkedFields(a, b) =>
+            a :: b :: HNil
+        }
+      )
+    }
+
+    /**
+      * Transform a linked list of squad position data into a normal `List`.
+      * @param list the current section of the original linked list
+      * @param out the accumulative traditional `List`
+      * @return the final `List` output
+      */
+    @tailrec
+    private def unlinkFields(list : LinkedFields, out : List[SquadPositionDetail] = Nil) : List[SquadPositionDetail] = {
+      list.next match {
+        case None =>
+          out :+ list.info
+        case Some(next) =>
+          unlinkFields(next, out :+ list.info)
+      }
+    }
+
+    /**
+      * Transform a normal `List` of squad position data into a linked list.
+      * The original list becomes reversed in the process.
+      * @param list the original traditional `List`
+      * @return the final linked list output
+      */
+    private def linkFields(list : List[SquadPositionDetail]) : LinkedFields = {
+      list match {
+        case Nil =>
+          throw new Exception("")
+        case x :: Nil =>
+          LinkedFields(x, None)
+        case x :: xs =>
+          linkFields(xs, LinkedFields(x, None))
+      }
+    }
+
+    /**
+      * Transform a normal `List` of squad position data into a linked list.
+      * The original list becomes reversed in the process.
+      * @param list the current subsection of the original traditional `List`
+      * @param out the accumulative linked list
+      * @return the final linked list output
+      */
+    @tailrec
+    private def linkFields(list : List[SquadPositionDetail], out : LinkedFields) : LinkedFields = {
+      list match {
+        case Nil =>
+          out
+        case x :: Nil =>
+          LinkedFields(x, Some(out))
+        case x :: xs =>
+          linkFields(xs, LinkedFields(x, Some(out)))
+      }
+    }
+
+    /**
+      * Entry point.
+      */
+    val codec : Codec[SquadDetail] = {
+      import shapeless.::
+      (
+        ("unk1" | uint8) ::
+          ("unk2" | uint24) :: //unknown, but can be 0'd
+          ("leader_char_id" | uint32L) ::
+          ("unk3" | uint32L) :: //variable fields, but can be 0'd
+          ("leader" | PacketHelpers.encodedWideStringAligned(7)) ::
+          ("task" | PacketHelpers.encodedWideString) ::
+          ("zone_id" | PlanetSideZoneID.codec) ::
+          ("unk7" | uint(23)) :: //during full squad mode, constant = 4983296
+          optional(bool, "member_info" | initial_member_codec)
+        ).exmap[SquadDetail] (
+        {
+          case u1 :: u2 :: char_id :: u3 :: leader :: task :: zone :: unk7 :: Some(member_list) :: HNil =>
+            Attempt.Successful(
+              SquadDetail(Some(u1), Some(u2), Some(char_id), Some(u3), Some(leader), Some(task), Some(zone), Some(unk7),
+                Some(unlinkFields(member_list).zipWithIndex.map { case (entry, index) => SquadPositionEntry(index, Some(entry)) })
+              )
+            )
+          case data =>
+            Attempt.failure(Err(s"can not get squad detail definition from data $data"))
+        },
+        {
+          case SquadDetail(Some(u1), Some(u2), Some(char_id), Some(u3), Some(leader), Some(task), Some(zone), Some(unk7), Some(member_list)) =>
+            Attempt.Successful(
+              math.max(u1, 1) :: u2 :: char_id :: u3 :: leader :: task :: zone :: unk7 ::
+                Some(linkFields(member_list.collect { case SquadPositionEntry(_, Some(entry)) => entry }.reverse)) ::
+                HNil
+            )
+        }
+      )
+    }
+  }
+
+  /**
+    * The patterns necessary to read coded squad data fields.
+    * Any number of squad fields can be parsed,
+    * but the number is always counted and the fields are always preceded by a unique action code.
+    * Only important fields are listed as if to update them;
+    * unlisted fields indicate fields that do not get updated from their current values.
+    */
+  object ItemizedSquad {
+    /**
+      * A pattern for data related to "field1."
+      */
+    private val field1Codec : Codec[SquadDetail] = uint16L.exmap[SquadDetail] (
+      unk1 => Attempt.successful(SquadDetail(Some(unk1), None, None, None, None, None, None, None, None)),
+      {
+        case SquadDetail(Some(unk1), _, _, _,  _, _, _, _, _) =>
+          Attempt.successful(unk1)
+        case _ =>
+          Attempt.failure(Err("failed to encode squad data for unknown field #1"))
+      }
+    )
+    /**
+      * A pattern for data related to the squad leader's `char_id` field.
+      */
+    private val leaderCharIdCodec : Codec[SquadDetail] = uint32L.exmap[SquadDetail] (
+      char_id => Attempt.successful(SquadDetail(None, None, Some(char_id), None, None, None, None, None, None)),
+      {
+        case SquadDetail(_, _, Some(char_id), _,  _, _, _, _, _) =>
+          Attempt.successful(char_id)
+        case _ =>
+          Attempt.failure(Err("failed to encode squad data for leader id"))
+      }
+    )
+    /**
+      * A pattern for data related to "field3."
+      */
+    private val field3Codec : Codec[SquadDetail] = uint32L.exmap[SquadDetail] (
+      unk3 => Attempt.successful(SquadDetail(None, None, None, Some(unk3), None, None, None, None, None)),
+      {
+        case SquadDetail(_, _, _, Some(unk3),  _, _, _, _, _) =>
+          Attempt.successful(unk3)
+        case _ =>
+          Attempt.failure(Err("failed to encode squad data for unknown field #3"))
+      }
+    )
+    /**
+      * A pattern for data related to the squad leader's `name` field.
+      * @param bitsOverByte a token maintaining stream misalignment for purposes of calculating string padding
+      */
+    private def leaderNameCodec(bitsOverByte : StreamLengthToken) : Codec[SquadDetail] = paddedStringMetaCodec(bitsOverByte.Length).exmap[SquadDetail] (
+      name => Attempt.successful(SquadDetail(None, None, None, None, Some(name), None, None, None, None)),
+      {
+        case SquadDetail(_, _, _, _, Some(name), _, _, _, _) =>
+          Attempt.successful(name)
+        case _ =>
+          Attempt.failure(Err("failed to encode squad data for leader name"))
+      }
+    )
+    /**
+      * A pattern for data related to the squad's `task` field, also often described as the squad description.
+      * @param bitsOverByte a token maintaining stream misalignment for purposes of calculating string padding
+      */
+    private def taskCodec(bitsOverByte : StreamLengthToken) : Codec[SquadDetail] = paddedStringMetaCodec(bitsOverByte.Length).exmap[SquadDetail] (
+      task => Attempt.successful(SquadDetail(None, None, None, None, None, Some(task), None, None, None)),
+      {
+        case SquadDetail(_, _, _, _, _, Some(task), _, _, _) =>
+          Attempt.successful(task)
+        case _ =>
+          Attempt.failure(Err("failed to encode squad data for task"))
+      }
+    )
+    /**
+      * A pattern for data related to the squad leader's `zone_id` field.
+      * @see `PlanetSideZoneID.codec`
+      */
+    private val zoneCodec : Codec[SquadDetail] = PlanetSideZoneID.codec.exmap[SquadDetail] (
+      zone_id => Attempt.successful(SquadDetail(None, None, None, None, None, None, Some(zone_id), None, None)),
+      {
+        case SquadDetail(_, _, _, _, _, _, Some(zone_id), _, _) =>
+          Attempt.successful(zone_id)
+        case _ =>
+          Attempt.failure(Err("failed to encode squad data for zone id"))
+      }
+    )
+    /**
+      * A pattern for data related to "field7."
+      */
+    private val field7Codec : Codec[SquadDetail] = {
+      uint4.exmap[SquadDetail] (
+        unk7 => Attempt.successful(SquadDetail(None, None, None, None, None, None, None, Some(unk7), None)),
+        {
+          case SquadDetail(_, _, _,  _, _, _, _, Some(unk7), _) =>
+            Attempt.successful(unk7)
+          case _ =>
+            Attempt.failure(Err("failed to encode squad data for unknown field #7"))
+        }
+      )
+    }
+    /**
+      * A pattern for data related to the squad's position entry fields.
+      * The actual parsing of the data for the positions diverges
+      * into either an itemized parsing pattern
+      * or a fully populated parsing pattern.
+      * @param bitsOverByte a token maintaining stream misalignment for purposes of calculating string padding
+      */
+    private def membersCodec(bitsOverByte : StreamLengthToken) : Codec[SquadDetail] = {
+      import shapeless.::
+      (
+        //TODO you can replace this outer structure with an either Codec
+        bool >>:~ { flag =>
+          conditional(flag, {
+            bitsOverByte.Add(4)
+            uint(3) :: vector(ItemizedPositions.codec(bitsOverByte))
+          }) ::
+            conditional(!flag, {
+              bitsOverByte.Add(3)
+              uint(2) :: FullyPopulatedPositions.codec(bitsOverByte)
+            })
+        }
+        ).exmap[SquadDetail] (
+        {
+          case true :: Some(_ :: member_list :: HNil) :: _ :: HNil =>
+            Attempt.successful(SquadDetail(None, None, None, None, None, None, None, None, Some(member_list.toList)))
+          case false :: None :: Some(_ :: member_list :: HNil) :: HNil =>
+            Attempt.successful(SquadDetail(None, None, None, None, None, None, None, None, Some(member_list.toList)))
+        },
+        {
+          case SquadDetail(_, _, _, _, _, _, _, _, Some(member_list)) =>
+            if(member_list
+              .collect { case position if position.info.nonEmpty =>
+                val info = position.info.get
+                List(info.is_closed, info.role, info.detailed_orders, info.requirements, info.char_id, info.name)
+              }
+              .flatten
+              .count(_.isEmpty) == 0) {
+              Attempt.successful(false :: None :: Some(2 :: member_list.toVector :: HNil) :: HNil)
+            }
+            else {
+              Attempt.successful(true :: Some(4 :: member_list.toVector :: HNil) :: None :: HNil)
+            }
+          case _ =>
+            Attempt.failure(Err("failed to encode squad data for members"))
+        }
+      )
+    }
+    /**
+      * A failing pattern for when the coded value is not tied to a known field pattern.
+      * This pattern does not read or write any bit data.
+      * The `conditional` will always return `None` because
+      * its determining conditional statement is explicitly `false`
+      * and all cases involving explicit failure.
+      * @param code the unknown action code
+      */
+    private def failureCodec(code : Int) : Codec[SquadDetail] = conditional(included = false, bool).exmap[SquadDetail] (
+      _ => Attempt.failure(Err(s"decoding squad data with unhandled codec - $code")),
+      _ => Attempt.failure(Err(s"encoding squad data with unhandled codec - $code"))
+    )
+
+    /**
+      * Retrieve the field pattern by its associated action code.
+      * @param code the action code
+      * @param bitsOverByte a token maintaining stream misalignment for purposes of calculating string padding
+      * @return the field pattern
+      */
+    private def selectCodedAction(code : Int, bitsOverByte : StreamLengthToken) : Codec[SquadDetail] = {
+      code match {
+        case 1 => field1Codec
+        case 2 => leaderCharIdCodec
+        case 3 => field3Codec
+        case 4 => leaderNameCodec(bitsOverByte)
+        case 5 => taskCodec(bitsOverByte)
+        case 6 => zoneCodec
+        case 7 => field7Codec
+        case 8 => membersCodec(bitsOverByte)
+        case _ => failureCodec(code)
+      }
+    }
+
+    /**
+      * Advance information about the current stream length because 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
+      * @return a modified token maintaining stream misalignment
+      */
+    private def modifyCodedPadValue(code : Int, bitsOverByte : StreamLengthToken) : StreamLengthToken = {
+      code match {
+        case 1 => bitsOverByte //16u = no added padding
+        case 2 => bitsOverByte //32u = no added padding
+        case 3 => bitsOverByte //32u = no added padding
+        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 8 => bitsOverByte.Length = 0 //end of stream
+        case _ => bitsOverByte.Length = Int.MinValue //wildly incorrect
+      }
+    }
+
+    /**
+      * Internal class for linked list operations.
+      * @param code action code indicating the squad field
+      * @param info data for the squad field
+      * @param next if there is a "next" squad field
+      */
+    private final case class LinkedFields(code : Int, info : SquadDetail, next : Option[LinkedFields])
+
+    /**
+      * Transform a linked list of individual squad field data into a combined squad data object.
+      * @param list the current section of the original linked list
+      * @return the final squad object output
+      */
+    private def unlinkFields(list : LinkedFields) : SquadDetail = unlinkFields(Some(list))
+
+    /**
+      * Transform a linked list of individual squad field data into a combined squad data object.
+      * @param info the current section of the original linked list
+      * @param out the accumulative squad data object
+      * @return the final squad object output
+      */
+    @tailrec
+    private def unlinkFields(info : Option[LinkedFields], out : SquadDetail = SquadDetail.Blank) : SquadDetail = {
+      info match {
+        case None =>
+          out
+        case Some(sqInfo) =>
+          unlinkFields(sqInfo.next, out And sqInfo.info)
+      }
+    }
+
+    /**
+      * Transform a squad detail object whose field population may be sparse into a linked list of individual fields.
+      * Fields of the combined object are separated into a list of pairs
+      * of each of those fields's action codes and a squad detail object with only that given field populated.
+      * After the blank entries are eliminated, the remaining fields are transformed into a linked list.
+      * @param info the combined squad detail object
+      * @return the final linked list output
+      */
+    private def linkFields(info : SquadDetail) : LinkedFields = {
+      Seq(
+        (8, SquadDetail(None, None, None, None, None, None, None, None, info.member_info)),
+        (7, SquadDetail(None, None, None, None, None, None, None, info.unk7, None)),
+        (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)),
+        (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))
+      ) //in reverse order so that the linked list is in the correct order
+        .filterNot { case (_, sqInfo) => sqInfo == SquadDetail.Blank}
+      match {
+        case Nil =>
+          throw new Exception("no linked list squad fields encountered where at least one was expected") //bad end
+        case x :: Nil =>
+          val (code, squadInfo) = x
+          LinkedFields(code, squadInfo, None)
+        case x :: xs =>
+          val (code, squadInfo) = x
+          linkFields(xs, LinkedFields(code, squadInfo, None))
+      }
+    }
+
+    /**
+      * Transform a `List` of squad field data paired with its field action code into a linked list.
+      * @param list the current subsection of the original list of fields
+      * @param out the accumulative linked list
+      * @return the final linked list output
+      */
+    @tailrec
+    private def linkFields(list : Seq[(Int, SquadDetail)], out : LinkedFields) : LinkedFields = {
+      if(list.isEmpty) {
+        out
+      }
+      else {
+        val (code, data) = list.head
+        linkFields(list.tail, LinkedFields(code, data, Some(out)))
+      }
+    }
+
+    /**
+      * Parse each action code to determine the format of the following squad field.
+      * Keep parsing until all reported squad fields have been encountered.
+      * @param size the number of fields to be parsed
+      * @param bitsOverByte a token maintaining stream misalignment for purposes of calculating string padding
+      * @return a linked list composed of the squad fields
+      */
+    private def chain_linked_fields(size : Int, bitsOverByte : StreamLengthToken) : Codec[LinkedFields] = {
+      import shapeless.::
+      (
+        //disruptive coupling action (e.g., flatPrepend) is necessary for recursive Codec
+        uint4 >>:~ { code =>
+          selectCodedAction(code, bitsOverByte.Add(4)) ::
+            conditional(size - 1 > 0, chain_linked_fields(size - 1, modifyCodedPadValue(code, bitsOverByte)))
+        }
+        ).exmap[LinkedFields] (
+        {
+          case action :: detail :: next :: HNil =>
+            Attempt.Successful(LinkedFields(action, detail, next))
+        },
+        {
+          case LinkedFields(action, detail, next) =>
+            Attempt.Successful(action :: detail :: next :: HNil)
+        }
+      )
+    }
+
+    /**
+      * Entry point.
+      * The stream misalignment will always be by 1 bit over the previous boundary when this is invoked.
+      * @param size the number of squad fields to be parsed
+      * @return a pattern for parsing the coded squad field data between a coded linked list and a combined squad object
+      */
+    def codec(size : Int) : Codec[SquadDetail] = chain_linked_fields(size, new StreamLengthToken(1)).xmap[SquadDetail] (
+      linkedDetail => unlinkFields(linkedDetail),
+      unlinkedDetail => linkFields(unlinkedDetail)
+    )
+  }
+
+  /**
+    * The patterns necessary to read coded itemized squad position data fields.
+    * The main squad position data has been completed and now the squad's open positions are being parsed.
+    * Any number of squad position fields can be parsed,
+    * but the number is always counted and the fields are always preceded by a unique action code.
+    * Only important fields are listed as if to update them;
+    * unlisted fields indicate fields that do not get updated from their current values.
+    */
+  object ItemizedPositions {
+    /**
+      * A pattern for data related to the squad position's `is_closed` field.
+      */
+    private val isClosedCodec : Codec[SquadPositionDetail] = bool.exmap[SquadPositionDetail] (
+      state => Attempt.successful(SquadPositionDetail(Some(state), None, None, None, None, None)),
+      {
+        case SquadPositionDetail(Some(state), _, _, _, _, _) =>
+          Attempt.successful(state)
+        case _ =>
+          Attempt.failure(Err("failed to encode squad position data for availability"))
+      }
+    )
+    /**
+      * A pattern for data related to the squad position's `role` field.
+      * @see `SquadDetailDefinitionUpdateMessage.paddedStringMetaCodec`
+      * @param bitsOverByte a token maintaining stream misalignment for purposes of calculating string padding
+      */
+    private def roleCodec(bitsOverByte : StreamLengthToken) : Codec[SquadPositionDetail] = paddedStringMetaCodec(bitsOverByte.Length).exmap[SquadPositionDetail] (
+      role => Attempt.successful(SquadPositionDetail(None, Some(role), None, None, None, None)),
+      {
+        case SquadPositionDetail(_, Some(role), _, _, _, _) =>
+          Attempt.successful(role)
+        case _ =>
+          Attempt.failure(Err("failed to encode squad position data for role"))
+      }
+    )
+    /**
+      * A pattern for data related to the squad position's `detailed_orders` field.
+      * @see `SquadDetailDefinitionUpdateMessage.paddedStringMetaCodec`
+      * @param bitsOverByte a token maintaining stream misalignment for purposes of calculating string padding
+      */
+    private def ordersCodec(bitsOverByte : StreamLengthToken) : Codec[SquadPositionDetail] = paddedStringMetaCodec(bitsOverByte.Length).exmap[SquadPositionDetail] (
+      orders => Attempt.successful(SquadPositionDetail(None, None, Some(orders), None, None, None)),
+      {
+        case SquadPositionDetail(_, _, Some(orders), _, _, _) =>
+          Attempt.successful(orders)
+        case _ =>
+          Attempt.failure(Err("failed to encode squad position data for detailed orders"))
+      }
+    )
+    /**
+      * A pattern for data related to the squad position's `requirements` field.
+      * @see `CertificationType.fromEncodedLong`
+      * @see `CertificationType.toEncodedLong`
+      * @see `SquadDefinitionActionMessage.ChangeSquadMemberRequirementsCertifications`
+      */
+    private val requirementsCodec : Codec[SquadPositionDetail] = ulongL(46).exmap[SquadPositionDetail] (
+      requirements => Attempt.successful(SquadPositionDetail(None, None, None, Some(CertificationType.fromEncodedLong(requirements)), None, None)),
+      {
+        case SquadPositionDetail(_, _, _, Some(requirements), _, _) =>
+          Attempt.successful(CertificationType.toEncodedLong(requirements))
+        case _ =>
+          Attempt.failure(Err("failed to encode squad position data for certification requirements"))
+      }
+    )
+    /**
+      * A pattern for data related to the squad position's `char_id` field, when occupied.
+      */
+    private val charIdCodec : Codec[SquadPositionDetail] = uint32L.exmap[SquadPositionDetail] (
+      char_id => Attempt.successful(SquadPositionDetail(None, None, None, None, Some(char_id), None)),
+      {
+        case SquadPositionDetail(_, _, _, _, Some(char_id), _) =>
+          Attempt.successful(char_id)
+        case _ =>
+          Attempt.failure(Err("failed to encode squad data for member id"))
+      }
+    )
+    /**
+      * A pattern for data related to the squad position's `name` field, when occupied.
+      * @see `SquadDetailDefinitionUpdateMessage.paddedStringMetaCodec`
+      * @param bitsOverByte a token maintaining stream misalignment for purposes of calculating string padding
+      */
+    private def nameCodec(bitsOverByte : StreamLengthToken) : Codec[SquadPositionDetail] = paddedStringMetaCodec(bitsOverByte.Length).exmap[SquadPositionDetail] (
+      name => Attempt.successful(SquadPositionDetail(None, None, None, None, None, Some(name))),
+      {
+        case SquadPositionDetail(_, _, _, _, _, Some(orders)) =>
+          Attempt.successful(orders)
+        case _ =>
+          Attempt.failure(Err("failed to encode squad position data for member name"))
+      }
+    )
+    /**
+      * A failing pattern for when the coded value is not tied to a known field pattern.
+      * This pattern does not read or write any bit data.
+      * The `conditional` will always return `None` because
+      * its determining conditional statement is explicitly `false`
+      * and all cases involving explicit failure.
+      * @param code the unknown action code
+      */
+    private def failureCodec(code : Int) : Codec[SquadPositionDetail] = conditional(included = false, bool).exmap[SquadPositionDetail] (
+      _ => Attempt.failure(Err(s"decoding squad position data with unhandled codec - $code")),
+      _ => Attempt.failure(Err(s"encoding squad position data with unhandled codec - $code"))
+    )
+
+    /**
+      * Retrieve the field pattern by its associated action code.
+      * @param code the action code
+      * @param bitsOverByte a token maintaining stream misalignment for purposes of calculating string padding
+      * @return the field pattern
+      */
+    private def selectCodedAction(code : Int, bitsOverByte : StreamLengthToken) : Codec[SquadPositionDetail] = {
+      code match {
+        case 0 => isClosedCodec
+        case 1 => roleCodec(bitsOverByte)
+        case 2 => ordersCodec(bitsOverByte)
+        case 3 => charIdCodec
+        case 4 => nameCodec(bitsOverByte)
+        case 5 => requirementsCodec
+        case _ => failureCodec(code)
+      }
+    }
+
+    /**
+      * Advance information about the current stream length because 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
+      * @return a modified token maintaining stream misalignment
+      */
+    private def modifyCodedPadValue(code : Int, bitsOverByte : StreamLengthToken) : StreamLengthToken = {
+      code match {
+        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 _ => bitsOverByte.Length = Int.MinValue //wildly incorrect
+      }
+    }
+
+    /**
+      * Internal class for linked list operations.
+      * @param code action code indicating the squad position field
+      * @param info data for the squad position field
+      * @param next if there is a "next" squad position field
+      */
+    private final case class LinkedFields(code : Int, info : SquadPositionDetail, next : Option[LinkedFields])
+
+    /**
+      * Transform a linked list of individual squad position field data into a combined squad position object.
+      * @param info the current section of the original linked list
+      * @param out the accumulative squad position data object
+      * @return the final squad position object output
+      */
+    @tailrec
+    private def unlinkFields(info : Option[LinkedFields], out : SquadPositionDetail = SquadPositionDetail.Blank) : SquadPositionDetail = {
+      info match {
+        case None =>
+          out
+        case Some(sqInfo) =>
+          unlinkFields(sqInfo.next, out And sqInfo.info)
+      }
+    }
+
+    /**
+      * Transform a squad position object whose field population may be sparse into a linked list of individual fields.
+      * Fields of the combined object are separated into a list of pairs
+      * of each of those fields's action codes and a squad position object with only that given field populated.
+      * After the blank entries are eliminated, the remaining fields are transformed into a linked list.
+      * @param info the combined squad position object
+      * @return the final linked list output
+      */
+    private def linkFields(info : SquadPositionDetail) : LinkedFields = {
+      Seq(
+        (5, SquadPositionDetail(None, None, None, info.requirements, None, None)),
+        (4, SquadPositionDetail(None, None, None, None, None, info.name)),
+        (3, SquadPositionDetail(None, None, None, None, info.char_id, None)),
+        (2, SquadPositionDetail(None, None, info.detailed_orders, None, None, None)),
+        (1, SquadPositionDetail(None, info.role, None, None, None, None)),
+        (0, SquadPositionDetail(info.is_closed, None, None, None, None, None))
+      ) //in reverse order so that the linked list is in the correct order
+        .filterNot { case (_, sqInfo) => sqInfo == SquadPositionDetail.Blank}
+      match {
+        case Nil =>
+          throw new Exception("no linked list squad position fields encountered where at least one was expected") //bad end
+        case x :: Nil =>
+          val (code, squadInfo) = x
+          LinkedFields(code, squadInfo, None)
+        case x :: xs =>
+          val (code, squadInfo) = x
+          linkFields(xs, LinkedFields(code, squadInfo, None))
+      }
+    }
+
+    /**
+      * Transform a `List` of squad position field data paired with its field action code into a linked list.
+      * @param list the current subsection of the original list of fields
+      * @param out the accumulative linked list
+      * @return the final linked list output
+      */
+    @tailrec
+    private def linkFields(list : Seq[(Int, SquadPositionDetail)], out : LinkedFields) : LinkedFields = {
+      if(list.isEmpty) {
+        out
+      }
+      else {
+        val (code, data) = list.head
+        linkFields(list.tail, LinkedFields(code, data, Some(out)))
+      }
+    }
+
+    /**
+      * Parse each action code to determine the format of the following squad position field.
+      * Keep parsing until all reported squad position fields have been encountered.
+      * @param size the number of fields to be parsed
+      * @param bitsOverByte a token maintaining stream misalignment for purposes of calculating string padding
+      * @return a linked list composed of the squad position fields
+      */
+    private def chain_linked_fields(size : Int, bitsOverByte : StreamLengthToken) : Codec[LinkedFields] = {
+      import shapeless.::
+      (
+        uint4 >>:~ { code =>
+          selectCodedAction(code,  bitsOverByte.Add(4)) >>:~ { _ =>
+            modifyCodedPadValue(code, bitsOverByte)
+            conditional(size - 1 > 0, chain_linked_fields(size - 1, bitsOverByte)).hlist
+          }
+        }
+        ).xmap[LinkedFields] (
+        {
+          case code :: entry :: next :: HNil =>
+            LinkedFields(code, entry, next)
+        },
+        {
+          case LinkedFields(code, entry, next) =>
+            code :: entry :: next :: HNil
+        }
+      )
+    }
+
+    /**
+      * Parse the number of squad position fields anticipated and then start parsing those position fields.
+      * @param bitsOverByte a token maintaining stream misalignment for purposes of calculating string padding
+      */
+    private def squad_member_details_codec(bitsOverByte : StreamLengthToken) : Codec[LinkedFields] = {
+      import shapeless.::
+      (
+        uint8 >>:~ { size =>
+          chain_linked_fields(size, bitsOverByte).hlist
+        }
+        ).xmap[LinkedFields] (
+        {
+          case _ :: info :: HNil =>
+            info
+        },
+        info => {
+          //count the linked position fields by tracing the "next" field in the linked list
+          var i = 1
+          var dinfo = info
+          while(dinfo.next.nonEmpty) {
+            i += 1
+            dinfo = dinfo.next.get
+          }
+          i :: info :: HNil
+        }
+      )
+    }
+
+    /**
+      * Entry point.
+      * @param bitsOverByte a token maintaining stream misalignment for purposes of calculating string padding
+      */
+    def codec(bitsOverByte : StreamLengthToken) : Codec[SquadPositionEntry] = {
+      import shapeless.::
+      (
+        ("index" | uint8) >>:~ { index =>
+          conditional(index < 255, bool :: squad_member_details_codec(bitsOverByte.Add(1))) ::
+            conditional(index == 255, bits)
+        }
+        ).xmap[SquadPositionEntry] (
+        {
+          case 255 :: _ :: _ :: HNil =>
+            SquadPositionEntry(255, None)
+          case ndx :: Some(_ :: info :: HNil) :: _ :: HNil =>
+            SquadPositionEntry(ndx, Some(unlinkFields(Some(info))))
+        },
+        {
+          case SquadPositionEntry(255, _) =>
+            255 :: None :: None :: HNil
+          case SquadPositionEntry(ndx, Some(info)) =>
+            ndx :: Some(true :: linkFields(info) :: HNil) :: None :: HNil
+        }
+      )
+    }
+  }
+
+  /**
+    * The patterns necessary to read enumerated squad position data.
+    * The main squad position data has been completed and now the squad's open positions are being parsed.
+    * These patterns split the difference between `FullSquad` operations and `ItemizedSquad` operations.
+    * Normally the whole of the squad position data is parsed in a single pass in `FullSquad`
+    * and, during `ItemizedSquad`, only piecemeal squad position fields are parsed.
+    * Furthermore, `FullSquad` position data is un-indexed because it is always presented in correct order,
+    * and `ItemizedSquad` positional data is indexed because it can skip entries and may be encountered in any order.
+    * These patterns parse full squad position data that is also indexed.
+    */
+  object FullyPopulatedPositions {
+    /**
+      * The primary difference between the cores of `FullSquad` position data and `FullyPopulatedPositions` data,
+      * besides variable padding,
+      * involves the `requirements` field not having a basic set of values that are always masked.
+      * @param bitsOverByteLength a token maintaining stream misalignment for purposes of calculating string padding
+      */
+    private def position_codec(bitsOverByteLength : Int) : Codec[SquadPositionDetail] = basePositionCodec(bitsOverByteLength, Set.empty)
+
+    /**
+      * Internal class for linked list operations.
+      * @param index the current position's ordinal number
+      * @param info data for the squad position field
+      * @param next if there is a "next" squad position field
+      */
+    private final case class LinkedFields(index: Int, info: SquadPositionDetail, next: Option[LinkedFields])
+
+    /**
+      * Transform a linked list of squad position data into a normal `List`.
+      * @param list the current section of the original linked list
+      * @param out the accumulative traditional `List`
+      * @return the final `List` output
+      */
+    @tailrec
+    private def unlinkFields(list : LinkedFields, out : List[SquadPositionEntry] = Nil) : List[SquadPositionEntry] = {
+      list.next match {
+        case None =>
+          out :+ SquadPositionEntry(list.index, list.info)
+        case Some(next) =>
+          unlinkFields(next, out :+ SquadPositionEntry(list.index, list.info))
+      }
+    }
+
+    /**
+      * Transform a normal `List` of squad position data into a linked list.
+      * The original list becomes reversed in the process.
+      * @param list the original traditional `List`
+      * @return the final linked list output
+      */
+    private def linkFields(list : List[SquadPositionEntry]) : LinkedFields = {
+      list match {
+        case Nil =>
+          throw new Exception("")
+        case x :: xs if x.info.isEmpty =>
+          linkFields(xs, LinkedFields(x.index, SquadPositionDetail.Blank, None))
+        case x :: xs =>
+          linkFields(xs, LinkedFields(x.index, x.info.get, None))
+      }
+    }
+
+    /**
+      * Transform a normal `List` of squad position data into a linked list.
+      * The original list becomes reversed in the process.
+      * @param list the current subsection of the original traditional `List`
+      * @param out the accumulative linked list
+      * @return the final linked list output
+      */
+    @tailrec
+    private def linkFields(list : List[SquadPositionEntry], out : LinkedFields) : LinkedFields = {
+      list match {
+        case Nil =>
+          out
+        case x :: _ if x.info.isEmpty =>
+          LinkedFields(x.index, SquadPositionDetail.Blank, Some(out))
+        case x :: Nil =>
+          LinkedFields(x.index, x.info.get, Some(out))
+        case x :: xs =>
+          linkFields(xs, LinkedFields(x.index, x.info.get, Some(out)))
+      }
+    }
+
+    /**
+      * All squad position entries asides from the first have unpadded strings.
+      * The very first entry aligns the remainder of the string fields along byte boundaries.
+      */
+    private def subsequent_position_codec(size : Int) : Codec[LinkedFields] = {
+      import shapeless.::
+      (
+        uint8 >>:~ { index =>
+          conditional(index < 255, bool :: position_codec(bitsOverByteLength = 0)) ::
+            conditional(size - 1 > 0, subsequent_position_codec(size - 1)) ::
+            conditional(index == 255, bits)
+        }
+        ).xmap[LinkedFields](
+        {
+          case 255 :: _ :: _ :: _ :: HNil =>
+            LinkedFields(255, SquadPositionDetail.Blank, None)
+          case index :: Some(_ :: entry :: HNil) :: next :: _ :: HNil =>
+            LinkedFields(index, entry, next)
+        },
+        {
+          case LinkedFields(255, _, _) =>
+            255 :: None :: None :: None :: HNil
+          case LinkedFields(index, entry, next) =>
+            index :: Some(true :: entry :: HNil) :: next :: None :: HNil
+        }
+      )
+    }
+
+    /**
+      * The first squad position entry has its first string (`role`) field padded by an amount that can be determined.
+      * @param size the number of position entries to be parsed
+      * @param bitsOverByte a token maintaining stream misalignment for purposes of calculating string padding
+      */
+    private def initial_position_codec(size : Int, bitsOverByte : StreamLengthToken) : Codec[LinkedFields] = {
+      import shapeless.::
+      (
+        uint8 >>:~ { index =>
+          conditional(index < 255, {
+            bitsOverByte.Add(2) //1 (below) + 1 (position_codec)
+            bool :: position_codec(bitsOverByte.Length)
+          }) ::
+            conditional(index < 255 && size - 1 > 0, subsequent_position_codec(size - 1)) ::
+            conditional(index == 255, bits)
+        }
+        ).xmap[LinkedFields](
+        {
+          case 255 :: _ :: _ :: _ :: HNil =>
+            LinkedFields(255, SquadPositionDetail.Blank, None)
+          case index :: Some(_ :: entry :: HNil) :: next :: _ :: HNil =>
+            LinkedFields(index, entry, next)
+        },
+        {
+          case LinkedFields(255, _, _) =>
+            255 :: None :: None :: None :: HNil
+          case LinkedFields(index, entry, next) =>
+            index :: Some(true :: entry :: HNil) :: next :: None :: HNil
+        }
+      )
+    }
+
+    /**
+      * Entry point.
+      * @param bitsOverByte a token maintaining stream misalignment for purposes of calculating string padding
+      */
+    def codec(bitsOverByte : StreamLengthToken) : Codec[Vector[SquadPositionEntry]] = {
+      import shapeless.::
+      (
+        uint32L >>:~ { size =>
+          bitsOverByte.Add(4)
+          uint4 ::
+            initial_position_codec(size.toInt + 1, bitsOverByte)
+        }).xmap[Vector[SquadPositionEntry]] (
+        {
+          case _ :: _ :: linkedMembers :: HNil =>
+            unlinkFields(linkedMembers).toVector
+        },
+        //TODO "memberList.size - 1"? the only two examples are "10" anyway
+        memberList => memberList.size - 1 :: 12 :: linkFields(memberList.reverse.toList) :: HNil
+      )
+    }
+  }
+
+  /**
+    * Certification values that are front-loaded into the `FullSquad` operations for finding squad position requirements.
+    * In the game proper, these are three certification values that the user can not give up or interact with.
+    */
   final val DefaultRequirements : Set[CertificationType.Value] = Set(
     CertificationType.StandardAssault,
     CertificationType.StandardExoSuit,
     CertificationType.AgileExoSuit
   )
 
-  final val Init = SquadDetailDefinitionUpdateMessage(
-    PlanetSideGUID(0),
-    SquadDetail(
-      0,
-      0,
-      0L,
-      0L,
-      "",
-      "",
-      PlanetSideZoneID(0),
-      0,
-      (0 to 9).map { i => SquadPositionEntry(i, SquadPositionDetail2.Blank)} toList
-    )
-  )
-
-
-  /*
-  DECOY CONSTRUCTORS
-   */
-  def apply(guid : PlanetSideGUID,
-             unk1 : Int,
-             leader_char_id : Long,
-             unk2 : BitVector,
-             leader_name : String,
-             task : String,
-             zone_id : PlanetSideZoneID,
-             member_info : List[SquadPositionDetail]) : SquadDetailDefinitionUpdateMessage = {
-    SquadDetailDefinitionUpdateMessage(PlanetSideGUID(0), SquadDetail.Blank)
-  }
-  def apply(guid : PlanetSideGUID, char_id : Long, leader_name : String, task : String, zone_id : PlanetSideZoneID, member_info : List[SquadPositionDetail]) : SquadDetailDefinitionUpdateMessage = {
-    SquadDetailDefinitionUpdateMessage(PlanetSideGUID(0), SquadDetail.Blank)
-  }
+  /**
+    * Blank squad data set up for `FullSquad` parsing.
+    * The `guid` value is significant - it represents the client-local squad data.
+    */
+  final val Init = SquadDetailDefinitionUpdateMessage(PlanetSideGUID(0), SquadDetail().Complete)
 
   /**
-    * Produces a `Codec` function for byte-aligned, padded Pascal strings encoded through common manipulations.
+    * Produces a byte-aligned Pascal strings encoded through common manipulations.
+    * Rather than pass in the amount of the padding directly, however,
+    * the stream length or the misalignment to the stream's previous byte boundary is passed into the function
+    * and is converted into the proper padding value.
     * @see `PacketHelpers.encodedWideStringAligned`
     * @param bitsOverByte the number of bits past the previous byte-aligned index;
-    *             should be a 0-7 number that gets converted to a 1-7 string padding number
-    * @return the encoded string `Codec`
+    *             gets converted to a 0-7 string padding number based on how many bits remain befoire the next byte
+    * @return the padded string `Codec`
     */
-  def paddedStringMetaCodec(bitsOverByte : Int) : Codec[String] = PacketHelpers.encodedWideStringAligned({
+  private def paddedStringMetaCodec(bitsOverByte : Int) : Codec[String] = PacketHelpers.encodedWideStringAligned({
     val mod8 = bitsOverByte % 8
     if(mod8 == 0) {
       0
@@ -490,354 +1289,49 @@ object SquadDetailDefinitionUpdateMessage extends Marshallable[SquadDetailDefini
     }
   })
 
-  private def memberCodec(pad : Int) : Codec[SquadPositionDetail2] = {
+  /**
+    * Pattern for reading all of the fields for squad position data.
+    * @param bitsOverByteLength the number of bits past the previous byte-aligned index
+    * @param defaultRequirements `CertificationType` values that are automatically masked in the `requirements` field
+    */
+  private def basePositionCodec(bitsOverByteLength : Int, defaultRequirements : Set[CertificationType.Value]) : Codec[SquadPositionDetail] = {
     import shapeless.::
     (
       uint8 :: //required value = 6
         ("is_closed" | bool) :: //if all positions are closed, the squad detail menu display no positions at all
-        ("role" | PacketHelpers.encodedWideStringAligned(pad)) ::
+        ("role" | paddedStringMetaCodec(bitsOverByteLength)) ::
         ("detailed_orders" | PacketHelpers.encodedWideString) ::
         ("char_id" | uint32L) ::
         ("name" | PacketHelpers.encodedWideString) ::
         ("requirements" | ulongL(46))
-      ).exmap[SquadPositionDetail2] (
+      ).exmap[SquadPositionDetail] (
       {
         case 6 :: closed :: role :: orders :: char_id :: name :: requirements :: HNil =>
           Attempt.Successful(
-            SquadPositionDetail2(Some(closed), Some(role), Some(orders), Some(DefaultRequirements ++ CertificationType.fromEncodedLong(requirements)), Some(char_id), Some(name))
+            SquadPositionDetail(Some(closed), Some(role), Some(orders), Some(defaultRequirements ++ CertificationType.fromEncodedLong(requirements)), Some(char_id), Some(name))
           )
         case data =>
           Attempt.Failure(Err(s"can not decode a SquadDetailDefinitionUpdate member's data - $data"))
       },
       {
-        case SquadPositionDetail2(Some(closed), Some(role), Some(orders), Some(requirements), Some(char_id), Some(name)) =>
-          Attempt.Successful(6 :: closed :: role :: orders :: char_id :: name :: CertificationType.toEncodedLong(DefaultRequirements ++ requirements) :: HNil)
+        case SquadPositionDetail(Some(closed), Some(role), Some(orders), Some(requirements), Some(char_id), Some(name)) =>
+          Attempt.Successful(6 :: closed :: role :: orders :: char_id :: name :: CertificationType.toEncodedLong(defaultRequirements ++ requirements) :: HNil)
       }
     )
   }
 
-  private val first_member_codec : Codec[SquadPositionDetail2] = memberCodec(pad = 7)
-
-  private val member_codec : Codec[SquadPositionDetail2] = memberCodec(pad = 0)
-
-  private case class LinkedMemberList(member : SquadPositionDetail2, next : Option[LinkedMemberList])
-
-  private def subsequent_member_codec : Codec[LinkedMemberList] = {
-    import shapeless.::
-    (
-      //disruptive coupling action (e.g., flatPrepend) is necessary to allow for recursive Codec
-      ("member" | member_codec) >>:~ { _ =>
-        optional(bool, "next" | subsequent_member_codec).hlist
-      }
-      ).xmap[LinkedMemberList] (
-      {
-        case a :: b :: HNil =>
-          LinkedMemberList(a, b)
-      },
-      {
-        case LinkedMemberList(a, b) =>
-          a :: b :: HNil
-      }
-    )
-  }
-
-  private def initial_member_codec : Codec[LinkedMemberList] = {
-    import shapeless.::
-    (
-      ("member" | first_member_codec) ::
-        optional(bool, "next" | subsequent_member_codec)
-      ).xmap[LinkedMemberList] (
-      {
-        case a :: b :: HNil =>
-          LinkedMemberList(a, b)
-      },
-      {
-        case LinkedMemberList(a, b) =>
-          a :: b :: HNil
-      }
-    )
-  }
-
-  @tailrec
-  private def unlinkMemberList(list : LinkedMemberList, out : List[SquadPositionDetail2] = Nil) : List[SquadPositionDetail2] = {
-    list.next match {
-      case None =>
-        out :+ list.member
-      case Some(next) =>
-        unlinkMemberList(next, out :+ list.member)
-    }
-  }
-
-  private def linkMemberList(list : List[SquadPositionDetail2]) : LinkedMemberList = {
-    list match {
-      case Nil =>
-        throw new Exception("")
-      case x :: Nil =>
-        LinkedMemberList(x, None)
-      case x :: xs =>
-        linkMemberList(xs, LinkedMemberList(x, None))
-    }
-  }
-
-  @tailrec
-  private def linkMemberList(list : List[SquadPositionDetail2], out : LinkedMemberList) : LinkedMemberList = {
-    list match {
-      case Nil =>
-        out
-      case x :: Nil =>
-        LinkedMemberList(x, Some(out))
-      case x :: xs =>
-        linkMemberList(xs, LinkedMemberList(x, Some(out)))
-    }
-  }
-
-  val full_squad_detail_codec : Codec[SquadDetail] = {
-    import shapeless.::
-    (
-      ("unk1" | uint8) ::
-        ("unk2" | uint24) :: //unknown, but can be 0'd
-        ("leader_char_id" | uint32L) ::
-        ("unk3" | uint32L) :: //variable fields, but can be 0'd
-        ("leader" | PacketHelpers.encodedWideStringAligned(7)) ::
-        ("task" | PacketHelpers.encodedWideString) ::
-        ("zone_id" | PlanetSideZoneID.codec) ::
-        ("unk7" | uint(23)) :: //during full squad mode, constant = 4983296
-        optional(bool, "member_info" | initial_member_codec)
-      ).exmap[SquadDetail] (
-      {
-        case u1 :: u2 :: char_id :: u3 :: leader :: task :: zone :: unk7 :: Some(member_list) :: HNil =>
-          Attempt.Successful(
-            SquadDetail(Some(u1), Some(u2), Some(char_id), Some(u3), Some(leader), Some(task), Some(zone), Some(unk7),
-              Some(unlinkMemberList(member_list).zipWithIndex.map { case (entry, index) => SquadPositionEntry(index, Some(entry)) })
-            )
-          )
-        case data =>
-          Attempt.failure(Err(s"can not get squad detail definition from data $data"))
-      },
-      {
-        case SquadDetail(Some(u1), Some(u2), Some(char_id), Some(u3), Some(leader), Some(task), Some(zone), Some(unk7), Some(member_list)) =>
-          Attempt.Successful(
-            math.max(u1, 1) :: u2 :: char_id :: u3 :: leader :: task :: zone :: unk7 ::
-              Some(linkMemberList(member_list.collect { case SquadPositionEntry(_, Some(entry)) => entry }.reverse)) ::
-              HNil
-          )
-      }
-    )
-  }
-
-  private val field1Codec : Codec[SquadDetail] = uint16L.exmap[SquadDetail] (
-    unk1 => Attempt.successful(SquadDetail(Some(unk1), None, None, None, None, None, None, None, None)),
-    {
-      case SquadDetail(Some(unk1), _, _, _,  _, _, _, _, _) =>
-        Attempt.successful(unk1)
-      case _ =>
-        Attempt.failure(Err("failed to encode squad data for unknown field #1"))
-    }
-  )
-
-  private val leaderCharIdCodec : Codec[SquadDetail] = uint32L.exmap[SquadDetail] (
-    char_id => Attempt.successful(SquadDetail(char_id)),
-    {
-      case SquadDetail(_, _, Some(char_id), _,  _, _, _, _, _) =>
-        Attempt.successful(char_id)
-      case _ =>
-        Attempt.failure(Err("failed to encode squad data for leader id"))
-    }
-  )
-
-  private val field3Codec : Codec[SquadDetail] = uint32L.exmap[SquadDetail] (
-    unk3 => Attempt.successful(SquadDetail(None, None, None, Some(unk3), None, None, None, None, None)),
-    {
-      case SquadDetail(_, _, _, Some(unk3),  _, _, _, _, _) =>
-        Attempt.successful(unk3)
-      case _ =>
-        Attempt.failure(Err("failed to encode squad data for unknown field #3"))
-    }
-  )
-
-  private def leaderNameCodec(bitsOverByte : StreamLengthToken) : Codec[SquadDetail] = paddedStringMetaCodec(bitsOverByte.Length).exmap[SquadDetail] (
-    name => Attempt.successful(SquadDetail(name, None)),
-    {
-      case SquadDetail(_, _, _, _, Some(name), _, _, _, _) =>
-        Attempt.successful(name)
-      case _ =>
-        Attempt.failure(Err("failed to encode squad data for leader name"))
-    }
-  )
-
-  private def taskCodec(bitsOverByte : StreamLengthToken) : Codec[SquadDetail] = paddedStringMetaCodec(bitsOverByte.Length).exmap[SquadDetail] (
-    task => Attempt.successful(SquadDetail(None, task)),
-    {
-      case SquadDetail(_, _, _, _, _, Some(task), _, _, _) =>
-        Attempt.successful(task)
-      case _ =>
-        Attempt.failure(Err("failed to encode squad data for task"))
-    }
-  )
-
-  private val zoneCodec : Codec[SquadDetail] = PlanetSideZoneID.codec.exmap[SquadDetail] (
-    zone_id => Attempt.successful(SquadDetail(zone_id)),
-    {
-      case SquadDetail(_, _, _, _, _, _, Some(zone_id), _, _) =>
-        Attempt.successful(zone_id)
-      case _ =>
-        Attempt.failure(Err("failed to encode squad data for zone id"))
-    }
-  )
-
-  private val field7Codec : Codec[SquadDetail] = {
-    uint4.exmap[SquadDetail] (
-      unk7 => Attempt.successful(SquadDetail(None, None, None, None, None, None, None, Some(unk7), None)),
-      {
-        case SquadDetail(_, _, _,  _, _, _, _, Some(unk7), _) =>
-          Attempt.successful(unk7)
-        case _ =>
-          Attempt.failure(Err("failed to encode squad data for unknown field #7"))
-      }
-    )
-  }
-
-  private def membersCodec(bitsOverByte : StreamLengthToken) : Codec[SquadDetail] = {
-    import shapeless.::
-    bitsOverByte.Add(4)
-    (
-      uint4 :: //constant = 12
-        vector(SquadPositionEntry.codec(bitsOverByte))
-      ).exmap[SquadDetail] (
-      {
-        case 12 :: member_list :: HNil =>
-          Attempt.successful(SquadDetail(member_list.toList))
-      },
-      {
-        case SquadDetail(_, _, _, _, _, _, _, _, Some(member_list)) =>
-          Attempt.successful(12 :: member_list.toVector :: HNil)
-        case _ =>
-          Attempt.failure(Err("failed to encode squad data for members"))
-      }
-    )
-  }
-
-  private val failureCodec : Codec[SquadDetail] = conditional(included = false, bool).exmap[SquadDetail] (
-    _ => Attempt.failure(Err("decoding with unhandled codec")),
-    _ => Attempt.failure(Err("encoding with unhandled codec"))
-  )
-
-  private def selectCodecAction(code : Int, bitsOverByte : StreamLengthToken) : Codec[SquadDetail] = {
-    code match {
-      case 1 => field1Codec
-      case 2 => leaderCharIdCodec
-      case 3 => field3Codec
-      case 4 => leaderNameCodec(bitsOverByte)
-      case 5 => taskCodec(bitsOverByte)
-      case 6 => zoneCodec
-      case 7 => field7Codec
-      case 8 => membersCodec(bitsOverByte)
-      case _ => failureCodec
-    }
-  }
-
-  private def modifyCodecPadValue(code : Int, bitsOverByte : StreamLengthToken) : StreamLengthToken = {
-    code match {
-      case 1 => bitsOverByte //16u = no added padding
-      case 2 => bitsOverByte //32u = no added padding
-      case 3 => bitsOverByte //32u = no added padding
-      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 8 => bitsOverByte.Length = 0 //end of stream
-      case _ => bitsOverByte.Length = Int.MinValue //wildly incorrect
-    }
-  }
-
-  private final case class LinkedSquadInfo(code : Int, info : SquadDetail, next : Option[LinkedSquadInfo])
-
-  private def unlinkSquadInfo(info : LinkedSquadInfo) : SquadDetail = unlinkSquadInfo(Some(info))
-
-  @tailrec
-  private def unlinkSquadInfo(info : Option[LinkedSquadInfo], squadInfo : SquadDetail = SquadDetail.Blank) : SquadDetail = {
-    info match {
-      case None =>
-        squadInfo
-      case Some(sqInfo) =>
-        unlinkSquadInfo(sqInfo.next, squadInfo And sqInfo.info)
-    }
-  }
-
-  private def linkSquadInfo(info : SquadDetail) : LinkedSquadInfo = {
-    //import scala.collection.immutable.::
-    Seq(
-      (8, SquadDetail(None, None, None, None, None, None, None, None, info.member_info)),
-      (7, SquadDetail(None, None, None, None, None, None, None, info.unk7, None)),
-      (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)),
-      (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))
-    ) //in reverse order so that the linked list is in the correct order
-      .filterNot { case (_, sqInfo) => sqInfo == SquadDetail.Blank}
-    match {
-      case Nil =>
-        throw new Exception("no linked list squad fields encountered where at least one was expected") //bad end
-      case x :: Nil =>
-        val (code, squadInfo) = x
-        LinkedSquadInfo(code, squadInfo, None)
-      case x :: xs =>
-        val (code, squadInfo) = x
-        linkSquadInfo(xs, LinkedSquadInfo(code, squadInfo, None))
-    }
-  }
-
-  @tailrec
-  private def linkSquadInfo(infoList : Seq[(Int, SquadDetail)], linkedInfo : LinkedSquadInfo) : LinkedSquadInfo = {
-    if(infoList.isEmpty) {
-      linkedInfo
-    }
-    else {
-      val (code, data) = infoList.head
-      linkSquadInfo(infoList.tail, LinkedSquadInfo(code, data, Some(linkedInfo)))
-    }
-  }
-
-  private def linked_squad_detail_codec(size : Int, bitsOverByte : StreamLengthToken) : Codec[LinkedSquadInfo] = {
-    import shapeless.::
-    (
-      uint4 >>:~ { code =>
-        selectCodecAction(code, bitsOverByte.Add(4)) ::
-          conditional(size - 1 > 0, linked_squad_detail_codec(size - 1, modifyCodecPadValue(code, bitsOverByte)))
-      }
-      ).exmap[LinkedSquadInfo] (
-      {
-        case action :: detail :: next :: HNil =>
-          Attempt.Successful(LinkedSquadInfo(action, detail, next))
-      },
-      {
-        case LinkedSquadInfo(action, detail, next) =>
-          Attempt.Successful(action :: detail :: next :: HNil)
-      }
-    )
-  }
-
-  def squadDetailSelectCodec(size : Int) : Codec[SquadDetail] = {
-    if(size == 9) {
-      full_squad_detail_codec
-    }
-    else {
-      linked_squad_detail_codec(size, new StreamLengthToken(1)).xmap[SquadDetail] (
-        linkedDetail => unlinkSquadInfo(linkedDetail),
-        unlinkedDetail => linkSquadInfo(unlinkedDetail)
-      )
-    }
-  }
-
   implicit val codec : Codec[SquadDetailDefinitionUpdateMessage] = {
     import shapeless.::
+    import net.psforever.newcodecs.newcodecs
     (
       ("guid" | PlanetSideGUID.codec) ::
         bool ::
         (uint8 >>:~ { size =>
-          squadDetailSelectCodec(size).hlist
+          newcodecs.binary_choice(
+            size == 9,
+            FullSquad.codec,
+            ItemizedSquad.codec(size)
+          ).hlist
         })
       ).exmap[SquadDetailDefinitionUpdateMessage] (
       {
@@ -853,27 +1347,8 @@ object SquadDetailDefinitionUpdateMessage extends Marshallable[SquadDetailDefini
             Attempt.Successful(guid :: true :: occupiedSquadFieldCount :: info :: HNil)
           }
           else {
-            info.member_info match {
-              case Some(list) =>
-                if(list.size == 10 &&
-                list
-                  .collect { case position if position.info.nonEmpty =>
-                    val info = position.info.get
-                    List(info.is_closed, info.role, info.detailed_orders, info.requirements, info.char_id, info.name)
-                  }
-                  .flatten
-                  .count(_.isEmpty) == 0) {
-                  //full squad detail definition protocol
-                  Attempt.Successful(guid :: true :: 9 :: info :: HNil)
-                }
-                else {
-                  //unhandled state
-                  Attempt.Failure(Err("can not split encoding patterns - all squad fields are defined but not all squad member fields are defined"))
-                }
-              case None =>
-                //impossible?
-                Attempt.Failure(Err("the members field can not be empty; the existence of this field was already proven"))
-            }
+            //full squad detail definition protocol
+            Attempt.Successful(guid :: true :: 9 :: info :: HNil)
           }
       }
     )
diff --git a/common/src/main/scala/services/teamwork/SquadResponse.scala b/common/src/main/scala/services/teamwork/SquadResponse.scala
index b898580b..27ef6dcd 100644
--- a/common/src/main/scala/services/teamwork/SquadResponse.scala
+++ b/common/src/main/scala/services/teamwork/SquadResponse.scala
@@ -1,7 +1,7 @@
 // Copyright (c) 2019 PSForever
 package services.teamwork
 
-import net.psforever.packet.game.{PlanetSideGUID, PlanetSideZoneID, SquadInfo, SquadPositionDetail}
+import net.psforever.packet.game._
 
 object SquadResponse {
   trait Response
diff --git a/common/src/test/scala/game/SquadDetailDefinitionUpdateMessageTest.scala b/common/src/test/scala/game/SquadDetailDefinitionUpdateMessageTest.scala
index 5118cfbb..8f04cf53 100644
--- a/common/src/test/scala/game/SquadDetailDefinitionUpdateMessageTest.scala
+++ b/common/src/test/scala/game/SquadDetailDefinitionUpdateMessageTest.scala
@@ -8,7 +8,6 @@ import org.specs2.mutable._
 import scodec.bits._
 
 class SquadDetailDefinitionUpdateMessageTest extends Specification {
-  val string_unk = hex"e80300821104145011b9be840024284a00610061006b006f008c008118000000024000ff"
   val string_unk1 = hex"e80300818800015c5189004603408c000000012000ff"
   val string_leader_char_id = hex"e8050080904d56b808"
   val string_unk3LeaderName = hex"e80300821104145011b9be840024284a00610061006b006f008c008118000000024000ff"
@@ -22,19 +21,9 @@ class SquadDetailDefinitionUpdateMessageTest extends Specification {
   val string_member_charIdName = hex"e8030080c602c08f2658480123004400750063006b006d006100730074006500720034003300ff"
   val string_task_memberEtc = hex"e80100812ce05c002300460046003000300030003000200054006800650020005c002300660066006600660066006600200042006c0061006400650073008c09810c005000000000000220230007808c0006808c0005808c0004808c0003808c0002808c0001808c0000808c00ff"
   val string_full = hex"e80300848180038021514601288a8400420048006f0066004400bf5c0023006600660064006300300030002a002a002a005c0023003900360034003000660066003d004b004f004b002b005300500043002b0046004c0059003d005c0023006600660064006300300030002a002a002a005c002300460046003400300034003000200041006c006c002000570065006c0063006f006d006500070000009814010650005c00230066006600300030003000300020007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c008000000000800100000c00020c8c5c00230066006600640063003000300020002000200043008000000000800100000c00020c8c5c002300660066006400630030003000200020002000480080eab58a02854f0070006f006c0045000100000c00020c8d5c002300660066006400630030003000200020002000200049008072d47a028b42006f006200610046003300740074003900300037000100000c00020c8c5c0023006600660064006300300030002000200020004e008000000000800100000c00020c8c5c00230066006600640063003000300020002000200041008000000000800100000c00020ca05c00230066006600300030003000300020007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c007c008000000000800100000c00020c8c5c0023003900360034003000660066002000200020004b008000000000800100000c00020c8c5c0023003900360034003000660066002000200020004f008042a28c028448006f00660044000100000c00020c8c5c0023003900360034003000660066002000200020004b008000000000800100000c0000"
+  val string_mixed = hex"e80300812cd85000530046006f007200650076006500720020005000610063006b0065007400200043006f006c006c0065006300740069006f006e00841400000181306400800000000080000000000000220c808000000000800000000000001e0c808000000000800000000000001a0c80800000000080000000000000160c80800000000080000000000000120c808000000000800000000000000e0c808000000000800000000000000a0c80800000000080000000000000060c80800000000080000000000000020c80800000000080000000000003fc"
 
   "SquadDetailDefinitionUpdateMessage" should {
-    "decode (test)" in {
-      PacketCoding.DecodePacket(string_unk).require match {
-        case SquadDetailDefinitionUpdateMessage(guid, detail) =>
-          detail
-          ok
-        case _ =>
-          ko
-      }
-      ok
-    }
-
     "decode (unk1 + members)" in {
       PacketCoding.DecodePacket(string_unk1).require match {
         case SquadDetailDefinitionUpdateMessage(guid, detail) =>
@@ -52,538 +41,684 @@ class SquadDetailDefinitionUpdateMessageTest extends Specification {
       }
     }
 
-//    "decode (char id)" in {
-//      PacketCoding.DecodePacket(string_leader_char_id).require match {
-//        case SquadDetailDefinitionUpdateMessage(guid, detail) =>
-//          guid mustEqual PlanetSideGUID(5)
-//          detail match {
-//            case SquadDetail(None, None, Some(char_id), None, None, None, None, None, None) =>
-//              char_id mustEqual 30910985
-//            case _ =>
-//              ko
-//          }
-//          ok
-//        case _ =>
-//          ko
-//      }
-//    }
-//
-//    "decode (unk3 + leader name)" in {
-//      PacketCoding.DecodePacket(string_unk3LeaderName).require match {
-//        case SquadDetailDefinitionUpdateMessage(guid, detail) =>
-//          guid mustEqual PlanetSideGUID(3)
-//          detail match {
-//            case SquadDetail(None, None, Some(char_id), Some(unk3), Some(leader), None, None, None, Some(_)) =>
-//              char_id mustEqual 42631712L
-//              unk3 mustEqual 556403L
-//              leader mustEqual "Jaako"
-//              //members tests follow ...
-//            case _ =>
-//              ko
-//          }
-//        case _ =>
-//          ko
-//      }
-//    }
-//
-//    "decode (task)" in {
-//      PacketCoding.DecodePacket(string_task).require match {
-//        case SquadDetailDefinitionUpdateMessage(guid, detail) =>
-//          guid mustEqual PlanetSideGUID(5)
-//          detail match {
-//            case SquadDetail(None, None, None, None, None, Some(task), None, None, None) =>
-//              task mustEqual "All Welcome "
-//            case _ =>
-//              ko
-//          }
-//          ok
-//        case _ =>
-//          ko
-//      }
-//    }
-//
-//    "decode (zone)" in {
-//      PacketCoding.DecodePacket(string_zone).require match {
-//        case SquadDetailDefinitionUpdateMessage(guid, detail) =>
-//          guid mustEqual PlanetSideGUID(3)
-//          detail match {
-//            case SquadDetail(None, None, None, None, None, None, Some(zone), None, None) =>
-//              zone mustEqual PlanetSideZoneID(21)
-//            case _ =>
-//              ko
-//          }
-//          ok
-//        case _ =>
-//          ko
-//      }
-//    }
-//
-//    "decode (task + zone)" in {
-//      PacketCoding.DecodePacket(string_taskZone).require match {
-//        case SquadDetailDefinitionUpdateMessage(guid, detail) =>
-//          detail match {
-//            case SquadDetail(None, None, None, None, None, Some(task), Some(zone), None, None) =>
-//              task mustEqual "\\#FF0000 The \\#ffffff Blades"
-//              zone mustEqual PlanetSideZoneID(4)
-//            case _ =>
-//              ko
-//          }
-//        case _ =>
-//          ko
-//      }
-//      ok
-//    }
-//
-//      "decode (unk7 + members)" in {
-//        PacketCoding.DecodePacket(string_unk7).require match {
-//          case SquadDetailDefinitionUpdateMessage(guid, detail) =>
-//            guid mustEqual PlanetSideGUID(3)
-//            detail match {
-//              case SquadDetail(None, None, None, None, None, Some(task), None, Some(unk7), Some(_)) =>
-//                task mustEqual "The King's Squad"
-//                unk7 mustEqual 8
-//                //members tests follow ...
-//              case _ =>
-//                ko
-//            }
-//          case _ =>
-//            ko
-//        }
-//      }
-//
-//    "decode (member closed)" in {
-//      PacketCoding.DecodePacket(string_member_closed).require match {
-//        case SquadDetailDefinitionUpdateMessage(guid, detail) =>
-//          guid mustEqual PlanetSideGUID(3)
-//          detail match {
-//            case SquadDetail(None, None, None, None, None, None, None, None, Some(members)) =>
-//              members.size mustEqual 2
-//              members.head.index mustEqual 5
-//              members.head.info match {
-//                case Some(SquadPositionDetail2(Some(is_closed), None, None, None, None, None)) =>
-//                  is_closed mustEqual true
-//                case _ =>
-//                  ko
-//              }
-//            case _ =>
-//              ko
-//          }
-//          ok
-//        case _ =>
-//          ko
-//      }
-//    }
-//
-//    "decode (member role)" in {
-//      PacketCoding.DecodePacket(string_member_role).require match {
-//        case SquadDetailDefinitionUpdateMessage(guid, detail) =>
-//          guid mustEqual PlanetSideGUID(7)
-//          detail match {
-//            case SquadDetail(None, None, None, None, None, None, None, None, Some(members)) =>
-//              members.size mustEqual 2
-//              members.head.index mustEqual 0
-//              members.head.info match {
-//                case Some(SquadPositionDetail2(None, Some(role), None, None, None, None)) =>
-//                  role mustEqual "Commander"
-//                case _ =>
-//                  ko
-//              }
-//            case _ =>
-//              ko
-//          }
-//          ok
-//        case _ =>
-//          ko
-//      }
-//    }
-//
-//    "decode (member role + requirements)" in {
-//      PacketCoding.DecodePacket(string_member_roleRequirements).require match {
-//        case SquadDetailDefinitionUpdateMessage(guid, detail) =>
-//          guid mustEqual PlanetSideGUID(1)
-//          detail match {
-//            case SquadDetail(None, None, None, None, None, None, None, None, Some(members)) =>
-//              members.size mustEqual 2
-//              members.head.index mustEqual 6
-//              members.head.info match {
-//                case Some(SquadPositionDetail2(None, Some(role), None, Some(req), None, None)) =>
-//                  role mustEqual "ADV Hacker"
-//                  req.size mustEqual 1
-//                  req.contains(CertificationType.AdvancedHacking) mustEqual true
-//                case _ =>
-//                  ko
-//              }
-//            case _ =>
-//              ko
-//          }
-//          ok
-//        case _ =>
-//          ko
-//      }
-//    }
-//
-//    "decode (member char id + name)" in {
-//      PacketCoding.DecodePacket(string_member_charIdName).require match {
-//        case SquadDetailDefinitionUpdateMessage(guid, detail) =>
-//          guid mustEqual PlanetSideGUID(3)
-//          detail match {
-//            case SquadDetail(None, None, None, None, None, None, None, None, Some(members)) =>
-//              members.size mustEqual 2
-//              members.head.index mustEqual 5
-//              members.head.info match {
-//                case Some(SquadPositionDetail2(None, None, None, None, Some(char_id), Some(name))) =>
-//                  char_id mustEqual 1218249L
-//                  name mustEqual "Duckmaster43"
-//                case _ =>
-//                  ko
-//              }
-//            case _ =>
-//              ko
-//          }
-//          ok
-//        case _ =>
-//          ko
-//      }
-//    }
-//
-//    "decode (task + member etc)" in {
-//      PacketCoding.DecodePacket(string_task_memberEtc).require match {
-//        case SquadDetailDefinitionUpdateMessage(guid, detail) =>
-//          guid mustEqual PlanetSideGUID(1)
-//          detail match {
-//            case SquadDetail(None, None, None, None, None, Some(task), None, None, Some(members)) =>
-//              task mustEqual "\\#FF0000 The \\#ffffff Blades"
-//              members.size mustEqual 11
-//              //
-//              members.head.index mustEqual 9
-//              members.head.info match {
-//                case Some(SquadPositionDetail2(None, Some(role), None, Some(req), None, None)) =>
-//                  role mustEqual ""
-//                  req mustEqual Set.empty
-//                case _ =>
-//                  ko
-//              }
-//              //
-//              (1 to 9).foreach { index =>
-//                members(index).index mustEqual 9 - index
-//                members(index).info match {
-//                  case Some(SquadPositionDetail2(None, Some(role), None, None, None, None)) =>
-//                    role mustEqual ""
-//                  case _ =>
-//                    ko
-//                }
-//              }
-//            case _ =>
-//              ko
-//          }
-//        case _ =>
-//          ko
-//      }
-//      ok
-//    }
-//
-//    "decode (full squad)" in {
-//      PacketCoding.DecodePacket(string_full).require match {
-//        case SquadDetailDefinitionUpdateMessage(guid, detail) =>
-//          guid mustEqual PlanetSideGUID(3)
-//          detail match {
-//            case SquadDetail(Some(u1), Some(u2), Some(char_id), Some(u3), Some(leader), Some(task), Some(zone), Some(unk7), Some(member_list)) =>
-//              u1 mustEqual 3
-//              u2 mustEqual 1792
-//              char_id mustEqual 42771010L
-//              u3 mustEqual 529745L
-//              leader mustEqual "HofD"
-//              task mustEqual "\\#ffdc00***\\#9640ff=KOK+SPC+FLY=\\#ffdc00***\\#FF4040 All Welcome"
-//              zone mustEqual PlanetSideZoneID(7)
-//              unk7 mustEqual 4983296
-//              member_list.size mustEqual 10
-//              member_list.head mustEqual SquadPositionEntry(0,Some(
-//                SquadPositionDetail2(
-//                  Some(false),
-//                  Some("\\#ff0000 |||||||||||||||||||||||"),
-//                  Some(""),
-//                  Some(Set(CertificationType.StandardAssault, CertificationType.StandardExoSuit, CertificationType.AgileExoSuit)),
-//                  Some(0),
-//                  Some("")))
-//              )
-//              member_list(1) mustEqual SquadPositionEntry(1,Some(
-//                SquadPositionDetail2(
-//                  Some(false),
-//                  Some("\\#ffdc00   C"),
-//                  Some(""),
-//                  Some(Set(CertificationType.StandardAssault, CertificationType.StandardExoSuit, CertificationType.AgileExoSuit)),
-//                  Some(0),
-//                  Some("")))
-//              )
-//              member_list(2) mustEqual SquadPositionEntry(2,Some(
-//                SquadPositionDetail2(
-//                  Some(false),
-//                  Some("\\#ffdc00   H"),
-//                  Some(""),
-//                  Some(Set(CertificationType.StandardAssault, CertificationType.StandardExoSuit, CertificationType.AgileExoSuit)),
-//                  Some(42644970L),
-//                  Some("OpolE")
-//                )
-//              ))
-//              member_list(3) mustEqual SquadPositionEntry(3,Some(
-//                SquadPositionDetail2(
-//                  Some(false),
-//                  Some("\\#ffdc00    I"),
-//                  Some(""),
-//                  Some(Set(CertificationType.StandardAssault, CertificationType.StandardExoSuit, CertificationType.AgileExoSuit)),
-//                  Some(41604210L),
-//                  Some("BobaF3tt907")
-//                )
-//              ))
-//              member_list(4) mustEqual SquadPositionEntry(4,Some(
-//                SquadPositionDetail2(
-//                  Some(false),
-//                  Some("\\#ffdc00   N"),
-//                  Some(""),
-//                  Some(Set(CertificationType.StandardAssault, CertificationType.StandardExoSuit, CertificationType.AgileExoSuit)),
-//                  Some(0),
-//                  Some("")
-//                )
-//              ))
-//              member_list(5) mustEqual SquadPositionEntry(5,Some(
-//                SquadPositionDetail2(
-//                  Some(false),
-//                  Some("\\#ffdc00   A"),
-//                  Some(""),
-//                  Some(Set(CertificationType.StandardAssault, CertificationType.StandardExoSuit, CertificationType.AgileExoSuit)),
-//                  Some(0),
-//                  Some("")
-//                )
-//              ))
-//              member_list(6) mustEqual SquadPositionEntry(6,Some(
-//                SquadPositionDetail2(
-//                  Some(false),
-//                  Some("\\#ff0000 |||||||||||||||||||||||"),
-//                  Some(""),
-//                  Some(Set(CertificationType.StandardAssault, CertificationType.StandardExoSuit, CertificationType.AgileExoSuit)),
-//                  Some(0),
-//                  Some("")
-//                )
-//              ))
-//              member_list(7) mustEqual SquadPositionEntry(7,Some(
-//                SquadPositionDetail2(
-//                  Some(false),
-//                  Some("\\#9640ff   K"),
-//                  Some(""),
-//                  Some(Set(CertificationType.StandardAssault, CertificationType.StandardExoSuit, CertificationType.AgileExoSuit)),
-//                  Some(0),
-//                  Some("")
-//                )
-//              ))
-//              member_list(8) mustEqual SquadPositionEntry(8,Some(
-//                SquadPositionDetail2(
-//                  Some(false),
-//                  Some("\\#9640ff   O"),
-//                  Some(""),
-//                  Some(Set(CertificationType.StandardAssault, CertificationType.StandardExoSuit, CertificationType.AgileExoSuit)),
-//                  Some(42771010L),
-//                  Some("HofD")
-//                )
-//              ))
-//              member_list(9) mustEqual SquadPositionEntry(9,Some(
-//                SquadPositionDetail2(
-//                  Some(false),
-//                  Some("\\#9640ff   K"),
-//                  Some(""),
-//                  Some(Set(CertificationType.StandardAssault, CertificationType.StandardExoSuit, CertificationType.AgileExoSuit)),
-//                  Some(0),
-//                  Some("")
-//                )
-//              ))
-//            case _ =>
-//              ko
-//          }
-//        case _ =>
-//          ko
-//      }
-//    }
-//
-//    "encode (char id)" in {
-//      val msg = SquadDetailDefinitionUpdateMessage(
-//        PlanetSideGUID(5),
-//        SquadDetail(30910985L)
-//      )
-//      val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
-//      pkt mustEqual string_leader_char_id
-//    }
-//
-//    "encode (unk3 + leader name)" in {
-//      val msg = SquadDetailDefinitionUpdateMessage(
-//        PlanetSideGUID(3),
-//        SquadDetail(None, None, Some(42631712L), Some(556403L), Some("Jaako"), None, None, None, Some(List(
-//          SquadPositionEntry(0, SquadPositionDetail2(0L, "")),
-//          SquadPositionEntry(255, None)
-//        )))
-//      )
-//      val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
-//      pkt mustEqual string_unk3LeaderName
-//    }
-//
-//    "encode (task)" in {
-//      val msg = SquadDetailDefinitionUpdateMessage(
-//        PlanetSideGUID(5),
-//        SquadDetail(None, "All Welcome ")
-//      )
-//      val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
-//      pkt mustEqual string_task
-//    }
-//
-//    "encode (zone)" in {
-//      val msg = SquadDetailDefinitionUpdateMessage(
-//        PlanetSideGUID(3),
-//        SquadDetail(PlanetSideZoneID(21))
-//      )
-//      val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
-//      pkt mustEqual string_zone
-//    }
-//
-//    "encode (task + zone)" in {
-//      val msg = SquadDetailDefinitionUpdateMessage(
-//        PlanetSideGUID(2),
-//        SquadDetail(None, None, None, None, None, Some("\\#FF0000 The \\#ffffff Blades"), Some(PlanetSideZoneID(4)), None)
-//      )
-//      val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
-//      pkt mustEqual string_taskZone
-//    }
-//
-//    "encode (unk7 + members)" in {
-//      val msg = SquadDetailDefinitionUpdateMessage(
-//        PlanetSideGUID(3),
-//        SquadDetail(
-//          None, None, None, None, None,
-//          Some("The King's Squad"),
-//          None, Some(8),
-//          Some(List(
-//            SquadPositionEntry(9, SquadPositionDetail2("The Guard", None)),
-//            SquadPositionEntry(8, SquadPositionDetail2("The Knight", None)),
-//            SquadPositionEntry(7, SquadPositionDetail2("The Earl", None)),
-//            SquadPositionEntry(6, SquadPositionDetail2("The Lord", None)),
-//            SquadPositionEntry(5, SquadPositionDetail2("The Duke", None)),
-//            SquadPositionEntry(4, SquadPositionDetail2("The Baron", None)),
-//            SquadPositionEntry(3, SquadPositionDetail2("The Princess", None)),
-//            SquadPositionEntry(2, SquadPositionDetail2("The Prince", None)),
-//            SquadPositionEntry(1, SquadPositionDetail2("The Queen", None)),
-//            SquadPositionEntry(0, SquadPositionDetail2("The King", None)),
-//            SquadPositionEntry(255, None)
-//          ))
-//        )
-//      )
-//      val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
-//      pkt mustEqual string_unk7
-//    }
-//
-//    "encode (member closed)" in {
-//      val msg = SquadDetailDefinitionUpdateMessage(
-//        PlanetSideGUID(3),
-//        SquadDetail(List(
-//          SquadPositionEntry(5, SquadPositionDetail2.Closed),
-//          SquadPositionEntry(255, None)
-//        ))
-//      )
-//      val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
-//      pkt mustEqual string_member_closed
-//    }
-//
-//
-//    "encode (member role)" in {
-//      val msg = SquadDetailDefinitionUpdateMessage(
-//        PlanetSideGUID(7),
-//        SquadDetail(List(
-//          SquadPositionEntry(0, SquadPositionDetail2("Commander", None)),
-//          SquadPositionEntry(255, None)
-//        ))
-//      )
-//      val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
-//      pkt mustEqual string_member_role
-//    }
-//
-//    "encode (member role + requirements)" in {
-//      val msg = SquadDetailDefinitionUpdateMessage(
-//        PlanetSideGUID(1),
-//        SquadDetail(List(
-//          SquadPositionEntry(6, SquadPositionDetail2(None, Some("ADV Hacker"), None, Some(Set(CertificationType.AdvancedHacking)), None, None)),
-//          SquadPositionEntry(255, None)
-//        ))
-//      )
-//      val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
-//      pkt mustEqual string_member_roleRequirements
-//    }
-//
-//    "encode (member char id + name)" in {
-//      val msg = SquadDetailDefinitionUpdateMessage(
-//        PlanetSideGUID(3),
-//        SquadDetail(List(
-//          SquadPositionEntry(5, SquadPositionDetail2(1218249L, "Duckmaster43")),
-//          SquadPositionEntry(255, None)
-//        ))
-//      )
-//      val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
-//      pkt mustEqual string_member_charIdName
-//    }
-//
-//    "encode (task + member etc)" in {
-//      val msg = SquadDetailDefinitionUpdateMessage(
-//        PlanetSideGUID(1),
-//        SquadDetail(
-//          None, None, None, None, None,
-//          Some("\\#FF0000 The \\#ffffff Blades"), None, None,
-//          Some(List(
-//            SquadPositionEntry(9, Some(SquadPositionDetail2(None, Some(""), None, Some(Set()), None, None))),
-//            SquadPositionEntry(8, Some(SquadPositionDetail2(None, Some(""), None, None, None, None))),
-//            SquadPositionEntry(7, Some(SquadPositionDetail2(None, Some(""), None, None, None, None))),
-//            SquadPositionEntry(6, Some(SquadPositionDetail2(None, Some(""), None, None, None, None))),
-//            SquadPositionEntry(5, Some(SquadPositionDetail2(None, Some(""), None, None, None, None))),
-//            SquadPositionEntry(4, Some(SquadPositionDetail2(None, Some(""), None, None, None, None))),
-//            SquadPositionEntry(3, Some(SquadPositionDetail2(None, Some(""), None, None, None, None))),
-//            SquadPositionEntry(2, Some(SquadPositionDetail2(None, Some(""), None, None, None, None))),
-//            SquadPositionEntry(1, Some(SquadPositionDetail2(None, Some(""), None, None, None, None))),
-//            SquadPositionEntry(0, Some(SquadPositionDetail2(None, Some(""), None, None, None, None))),
-//            SquadPositionEntry(255, None)
-//          ))
-//        )
-//      )
-//      val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
-//      pkt mustEqual string_task_memberEtc
-//    }
-//
-//    "encode (full squad)" in {
-//      val msg = SquadDetailDefinitionUpdateMessage(
-//        PlanetSideGUID(3),
-//        SquadDetail(
-//          Some(3),
-//          Some(1792),
-//          Some(42771010L),
-//          Some(529745L),
-//          Some("HofD"),
-//          Some("\\#ffdc00***\\#9640ff=KOK+SPC+FLY=\\#ffdc00***\\#FF4040 All Welcome"),
-//          Some(PlanetSideZoneID(7)),
-//          Some(4983296),
-//          Some(List(
-//            SquadPositionEntry(0, SquadPositionDetail2("\\#ff0000 |||||||||||||||||||||||", "", Set(), 0, "")),
-//            SquadPositionEntry(1, SquadPositionDetail2("\\#ffdc00   C", "", Set(), 0, "")),
-//            SquadPositionEntry(2, SquadPositionDetail2("\\#ffdc00   H", "", Set(), 42644970L, "OpolE")),
-//            SquadPositionEntry(3, SquadPositionDetail2("\\#ffdc00    I", "", Set(), 41604210L, "BobaF3tt907")),
-//            SquadPositionEntry(4, SquadPositionDetail2("\\#ffdc00   N", "", Set(), 0, "")),
-//            SquadPositionEntry(5, SquadPositionDetail2("\\#ffdc00   A", "", Set(), 0, "")),
-//            SquadPositionEntry(6, SquadPositionDetail2("\\#ff0000 |||||||||||||||||||||||", "", Set(), 0, "")),
-//            SquadPositionEntry(7, SquadPositionDetail2("\\#9640ff   K", "", Set(), 0, "")),
-//            SquadPositionEntry(8, SquadPositionDetail2("\\#9640ff   O", "", Set(), 42771010L ,"HofD")),
-//            SquadPositionEntry(9, SquadPositionDetail2("\\#9640ff   K", "", Set(), 0, ""))
-//          )
-//        ))
-//      )
-//      val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
-//      val pktBits = pkt.toBitVector
-//      val strBits = string_full.toBitVector
-//      pktBits.grouped(100).zip(strBits.grouped(100)).foreach({ case (a, b) =>
-//        a mustEqual b
-//      })
-//      pkt mustEqual string_full
-//    }
+    "decode (char id)" in {
+      PacketCoding.DecodePacket(string_leader_char_id).require match {
+        case SquadDetailDefinitionUpdateMessage(guid, detail) =>
+          guid mustEqual PlanetSideGUID(5)
+          detail match {
+            case SquadDetail(None, None, Some(char_id), None, None, None, None, None, None) =>
+              char_id mustEqual 30910985
+            case _ =>
+              ko
+          }
+          ok
+        case _ =>
+          ko
+      }
+    }
+
+    "decode (unk3 + leader name)" in {
+      PacketCoding.DecodePacket(string_unk3LeaderName).require match {
+        case SquadDetailDefinitionUpdateMessage(guid, detail) =>
+          guid mustEqual PlanetSideGUID(3)
+          detail match {
+            case SquadDetail(None, None, Some(char_id), Some(unk3), Some(leader), None, None, None, Some(_)) =>
+              char_id mustEqual 42631712L
+              unk3 mustEqual 556403L
+              leader mustEqual "Jaako"
+              //members tests follow ...
+            case _ =>
+              ko
+          }
+        case _ =>
+          ko
+      }
+    }
+
+    "decode (task)" in {
+      PacketCoding.DecodePacket(string_task).require match {
+        case SquadDetailDefinitionUpdateMessage(guid, detail) =>
+          guid mustEqual PlanetSideGUID(5)
+          detail match {
+            case SquadDetail(None, None, None, None, None, Some(task), None, None, None) =>
+              task mustEqual "All Welcome "
+            case _ =>
+              ko
+          }
+          ok
+        case _ =>
+          ko
+      }
+    }
+
+    "decode (zone)" in {
+      PacketCoding.DecodePacket(string_zone).require match {
+        case SquadDetailDefinitionUpdateMessage(guid, detail) =>
+          guid mustEqual PlanetSideGUID(3)
+          detail match {
+            case SquadDetail(None, None, None, None, None, None, Some(zone), None, None) =>
+              zone mustEqual PlanetSideZoneID(21)
+            case _ =>
+              ko
+          }
+          ok
+        case _ =>
+          ko
+      }
+    }
+
+    "decode (task + zone)" in {
+      PacketCoding.DecodePacket(string_taskZone).require match {
+        case SquadDetailDefinitionUpdateMessage(_, detail) =>
+          detail match {
+            case SquadDetail(None, None, None, None, None, Some(task), Some(zone), None, None) =>
+              task mustEqual "\\#FF0000 The \\#ffffff Blades"
+              zone mustEqual PlanetSideZoneID(4)
+            case _ =>
+              ko
+          }
+        case _ =>
+          ko
+      }
+      ok
+    }
+
+      "decode (unk7 + members)" in {
+        PacketCoding.DecodePacket(string_unk7).require match {
+          case SquadDetailDefinitionUpdateMessage(guid, detail) =>
+            guid mustEqual PlanetSideGUID(3)
+            detail match {
+              case SquadDetail(None, None, None, None, None, Some(task), None, Some(unk7), Some(_)) =>
+                task mustEqual "The King's Squad"
+                unk7 mustEqual 8
+                //members tests follow ...
+              case _ =>
+                ko
+            }
+          case _ =>
+            ko
+        }
+      }
+
+    "decode (member closed)" in {
+      PacketCoding.DecodePacket(string_member_closed).require match {
+        case SquadDetailDefinitionUpdateMessage(guid, detail) =>
+          guid mustEqual PlanetSideGUID(3)
+          detail match {
+            case SquadDetail(None, None, None, None, None, None, None, None, Some(members)) =>
+              members.size mustEqual 2
+              members.head.index mustEqual 5
+              members.head.info match {
+                case Some(SquadPositionDetail(Some(is_closed), None, None, None, None, None)) =>
+                  is_closed mustEqual true
+                case _ =>
+                  ko
+              }
+            case _ =>
+              ko
+          }
+          ok
+        case _ =>
+          ko
+      }
+    }
+
+    "decode (member role)" in {
+      PacketCoding.DecodePacket(string_member_role).require match {
+        case SquadDetailDefinitionUpdateMessage(guid, detail) =>
+          guid mustEqual PlanetSideGUID(7)
+          detail match {
+            case SquadDetail(None, None, None, None, None, None, None, None, Some(members)) =>
+              members.size mustEqual 2
+              members.head.index mustEqual 0
+              members.head.info match {
+                case Some(SquadPositionDetail(None, Some(role), None, None, None, None)) =>
+                  role mustEqual "Commander"
+                case _ =>
+                  ko
+              }
+            case _ =>
+              ko
+          }
+          ok
+        case _ =>
+          ko
+      }
+    }
+
+    "decode (member role + requirements)" in {
+      PacketCoding.DecodePacket(string_member_roleRequirements).require match {
+        case SquadDetailDefinitionUpdateMessage(guid, detail) =>
+          guid mustEqual PlanetSideGUID(1)
+          detail match {
+            case SquadDetail(None, None, None, None, None, None, None, None, Some(members)) =>
+              members.size mustEqual 2
+              members.head.index mustEqual 6
+              members.head.info match {
+                case Some(SquadPositionDetail(None, Some(role), None, Some(req), None, None)) =>
+                  role mustEqual "ADV Hacker"
+                  req.size mustEqual 1
+                  req.contains(CertificationType.AdvancedHacking) mustEqual true
+                case _ =>
+                  ko
+              }
+            case _ =>
+              ko
+          }
+          ok
+        case _ =>
+          ko
+      }
+    }
+
+    "decode (member char id + name)" in {
+      PacketCoding.DecodePacket(string_member_charIdName).require match {
+        case SquadDetailDefinitionUpdateMessage(guid, detail) =>
+          guid mustEqual PlanetSideGUID(3)
+          detail match {
+            case SquadDetail(None, None, None, None, None, None, None, None, Some(members)) =>
+              members.size mustEqual 2
+              members.head.index mustEqual 5
+              members.head.info match {
+                case Some(SquadPositionDetail(None, None, None, None, Some(char_id), Some(name))) =>
+                  char_id mustEqual 1218249L
+                  name mustEqual "Duckmaster43"
+                case _ =>
+                  ko
+              }
+            case _ =>
+              ko
+          }
+          ok
+        case _ =>
+          ko
+      }
+    }
+
+    "decode (task + member etc)" in {
+      PacketCoding.DecodePacket(string_task_memberEtc).require match {
+        case SquadDetailDefinitionUpdateMessage(guid, detail) =>
+          guid mustEqual PlanetSideGUID(1)
+          detail match {
+            case SquadDetail(None, None, None, None, None, Some(task), None, None, Some(members)) =>
+              task mustEqual "\\#FF0000 The \\#ffffff Blades"
+              members.size mustEqual 11
+              //
+              members.head.index mustEqual 9
+              members.head.info match {
+                case Some(SquadPositionDetail(None, Some(role), None, Some(req), None, None)) =>
+                  role mustEqual ""
+                  req mustEqual Set.empty
+                case _ =>
+                  ko
+              }
+              //
+              (1 to 9).foreach { index =>
+                members(index).index mustEqual 9 - index
+                members(index).info match {
+                  case Some(SquadPositionDetail(None, Some(role), None, None, None, None)) =>
+                    role mustEqual ""
+                  case _ =>
+                    ko
+                }
+              }
+            case _ =>
+              ko
+          }
+        case _ =>
+          ko
+      }
+      ok
+    }
+
+    "decode (full squad)" in {
+      PacketCoding.DecodePacket(string_full).require match {
+        case SquadDetailDefinitionUpdateMessage(guid, detail) =>
+          guid mustEqual PlanetSideGUID(3)
+          detail match {
+            case SquadDetail(Some(u1), Some(u2), Some(char_id), Some(u3), Some(leader), Some(task), Some(zone), Some(unk7), Some(member_list)) =>
+              u1 mustEqual 3
+              u2 mustEqual 1792
+              char_id mustEqual 42771010L
+              u3 mustEqual 529745L
+              leader mustEqual "HofD"
+              task mustEqual "\\#ffdc00***\\#9640ff=KOK+SPC+FLY=\\#ffdc00***\\#FF4040 All Welcome"
+              zone mustEqual PlanetSideZoneID(7)
+              unk7 mustEqual 4983296
+              member_list.size mustEqual 10
+              member_list.head mustEqual SquadPositionEntry(0,Some(
+                SquadPositionDetail(
+                  Some(false),
+                  Some("\\#ff0000 |||||||||||||||||||||||"),
+                  Some(""),
+                  Some(Set(CertificationType.StandardAssault, CertificationType.StandardExoSuit, CertificationType.AgileExoSuit)),
+                  Some(0),
+                  Some("")))
+              )
+              member_list(1) mustEqual SquadPositionEntry(1,Some(
+                SquadPositionDetail(
+                  Some(false),
+                  Some("\\#ffdc00   C"),
+                  Some(""),
+                  Some(Set(CertificationType.StandardAssault, CertificationType.StandardExoSuit, CertificationType.AgileExoSuit)),
+                  Some(0),
+                  Some("")))
+              )
+              member_list(2) mustEqual SquadPositionEntry(2,Some(
+                SquadPositionDetail(
+                  Some(false),
+                  Some("\\#ffdc00   H"),
+                  Some(""),
+                  Some(Set(CertificationType.StandardAssault, CertificationType.StandardExoSuit, CertificationType.AgileExoSuit)),
+                  Some(42644970L),
+                  Some("OpolE")
+                )
+              ))
+              member_list(3) mustEqual SquadPositionEntry(3,Some(
+                SquadPositionDetail(
+                  Some(false),
+                  Some("\\#ffdc00    I"),
+                  Some(""),
+                  Some(Set(CertificationType.StandardAssault, CertificationType.StandardExoSuit, CertificationType.AgileExoSuit)),
+                  Some(41604210L),
+                  Some("BobaF3tt907")
+                )
+              ))
+              member_list(4) mustEqual SquadPositionEntry(4,Some(
+                SquadPositionDetail(
+                  Some(false),
+                  Some("\\#ffdc00   N"),
+                  Some(""),
+                  Some(Set(CertificationType.StandardAssault, CertificationType.StandardExoSuit, CertificationType.AgileExoSuit)),
+                  Some(0),
+                  Some("")
+                )
+              ))
+              member_list(5) mustEqual SquadPositionEntry(5,Some(
+                SquadPositionDetail(
+                  Some(false),
+                  Some("\\#ffdc00   A"),
+                  Some(""),
+                  Some(Set(CertificationType.StandardAssault, CertificationType.StandardExoSuit, CertificationType.AgileExoSuit)),
+                  Some(0),
+                  Some("")
+                )
+              ))
+              member_list(6) mustEqual SquadPositionEntry(6,Some(
+                SquadPositionDetail(
+                  Some(false),
+                  Some("\\#ff0000 |||||||||||||||||||||||"),
+                  Some(""),
+                  Some(Set(CertificationType.StandardAssault, CertificationType.StandardExoSuit, CertificationType.AgileExoSuit)),
+                  Some(0),
+                  Some("")
+                )
+              ))
+              member_list(7) mustEqual SquadPositionEntry(7,Some(
+                SquadPositionDetail(
+                  Some(false),
+                  Some("\\#9640ff   K"),
+                  Some(""),
+                  Some(Set(CertificationType.StandardAssault, CertificationType.StandardExoSuit, CertificationType.AgileExoSuit)),
+                  Some(0),
+                  Some("")
+                )
+              ))
+              member_list(8) mustEqual SquadPositionEntry(8,Some(
+                SquadPositionDetail(
+                  Some(false),
+                  Some("\\#9640ff   O"),
+                  Some(""),
+                  Some(Set(CertificationType.StandardAssault, CertificationType.StandardExoSuit, CertificationType.AgileExoSuit)),
+                  Some(42771010L),
+                  Some("HofD")
+                )
+              ))
+              member_list(9) mustEqual SquadPositionEntry(9,Some(
+                SquadPositionDetail(
+                  Some(false),
+                  Some("\\#9640ff   K"),
+                  Some(""),
+                  Some(Set(CertificationType.StandardAssault, CertificationType.StandardExoSuit, CertificationType.AgileExoSuit)),
+                  Some(0),
+                  Some("")
+                )
+              ))
+            case _ =>
+              ko
+          }
+        case _ =>
+          ko
+      }
+    }
+
+    "decode (mixed)" in {
+      PacketCoding.DecodePacket(string_mixed).require match {
+        case SquadDetailDefinitionUpdateMessage(guid, detail) =>
+          guid mustEqual PlanetSideGUID(3)
+          detail match {
+            case SquadDetail(None, None, None, None, None, Some(task), None, None, Some(member_list)) =>
+              task mustEqual "PSForever Packet Collection"
+              member_list.size mustEqual 11
+              member_list.head mustEqual SquadPositionEntry(9,Some(
+                SquadPositionDetail(
+                  Some(false),
+                  Some(""),
+                  Some(""),
+                  Some(Set.empty),
+                  Some(0),
+                  Some("")
+                ))
+              )
+              member_list(1) mustEqual SquadPositionEntry(8,Some(
+                SquadPositionDetail(
+                  Some(false),
+                  Some(""),
+                  Some(""),
+                  Some(Set.empty),
+                  Some(0),
+                  Some("")
+                ))
+              )
+              member_list(2) mustEqual SquadPositionEntry(7,Some(
+                SquadPositionDetail(
+                  Some(false),
+                  Some(""),
+                  Some(""),
+                  Some(Set.empty),
+                  Some(0),
+                  Some("")
+                )
+              ))
+              member_list(3) mustEqual SquadPositionEntry(6,Some(
+                SquadPositionDetail(
+                  Some(false),
+                  Some(""),
+                  Some(""),
+                  Some(Set.empty),
+                  Some(0),
+                  Some("")
+                )
+              ))
+              member_list(4) mustEqual SquadPositionEntry(5,Some(
+                SquadPositionDetail(
+                  Some(false),
+                  Some(""),
+                  Some(""),
+                  Some(Set.empty),
+                  Some(0),
+                  Some("")
+                )
+              ))
+              member_list(5) mustEqual SquadPositionEntry(4,Some(
+                SquadPositionDetail(
+                  Some(false),
+                  Some(""),
+                  Some(""),
+                  Some(Set.empty),
+                  Some(0),
+                  Some("")
+                )
+              ))
+              member_list(6) mustEqual SquadPositionEntry(3,Some(
+                SquadPositionDetail(
+                  Some(false),
+                  Some(""),
+                  Some(""),
+                  Some(Set.empty),
+                  Some(0),
+                  Some("")
+                )
+              ))
+              member_list(7) mustEqual SquadPositionEntry(2,Some(
+                SquadPositionDetail(
+                  Some(false),
+                  Some(""),
+                  Some(""),
+                  Some(Set.empty),
+                  Some(0),
+                  Some("")
+                )
+              ))
+              member_list(8) mustEqual SquadPositionEntry(1,Some(
+                SquadPositionDetail(
+                  Some(false),
+                  Some(""),
+                  Some(""),
+                  Some(Set.empty),
+                  Some(0),
+                  Some("")
+                )
+              ))
+              member_list(9) mustEqual SquadPositionEntry(0,Some(
+                SquadPositionDetail(
+                  Some(false),
+                  Some(""),
+                  Some(""),
+                  Some(Set.empty),
+                  Some(0),
+                  Some("")
+                )
+              ))
+            case _ =>
+              ko
+          }
+        case _ =>
+          ko
+      }
+      ok
+    }
+
+    "encode (unk1 + members)" in {
+      val msg = SquadDetailDefinitionUpdateMessage(
+        PlanetSideGUID(3),
+        SquadDetail()
+          .Field1(0)
+          .LeaderCharId(1221560L)
+          .Members(List(
+            SquadPositionEntry(6, SquadPositionDetail().Player(0L, "")),
+            SquadPositionEntry(255, None)
+          ))
+      )
+      val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+      pkt mustEqual string_unk1
+    }
+
+    "encode (char id)" in {
+      val msg = SquadDetailDefinitionUpdateMessage(
+        PlanetSideGUID(5),
+        SquadDetail().LeaderCharId(30910985L)
+      )
+      val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+      pkt mustEqual string_leader_char_id
+    }
+
+    "encode (unk3 + leader name)" in {
+      val msg = SquadDetailDefinitionUpdateMessage(
+        PlanetSideGUID(3),
+        SquadDetail()
+          .Leader(42631712L, "Jaako")
+          .Field3(556403L)
+          .Members(List(
+            SquadPositionEntry(0, SquadPositionDetail().Player(0L, "")),
+            SquadPositionEntry(255, None)
+          ))
+      )
+      val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+      pkt mustEqual string_unk3LeaderName
+    }
+
+    "encode (task)" in {
+      val msg = SquadDetailDefinitionUpdateMessage(
+        PlanetSideGUID(5),
+        SquadDetail().Task("All Welcome ")
+      )
+      val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+      pkt mustEqual string_task
+    }
+
+    "encode (zone)" in {
+      val msg = SquadDetailDefinitionUpdateMessage(
+        PlanetSideGUID(3),
+        SquadDetail().ZoneId(PlanetSideZoneID(21))
+      )
+      val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+      pkt mustEqual string_zone
+    }
+
+    "encode (task + zone)" in {
+      val msg = SquadDetailDefinitionUpdateMessage(
+        PlanetSideGUID(2),
+        SquadDetail()
+          .Task("\\#FF0000 The \\#ffffff Blades")
+          .ZoneId(PlanetSideZoneID(4))
+      )
+      val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+      pkt mustEqual string_taskZone
+    }
+
+    "encode (unk7 + members)" in {
+      val msg = SquadDetailDefinitionUpdateMessage(
+        PlanetSideGUID(3),
+        SquadDetail()
+          .Task("The King's Squad")
+          .Field7(8)
+          .Members(List(
+            SquadPositionEntry(9, SquadPositionDetail().Role("The Guard")),
+            SquadPositionEntry(8, SquadPositionDetail().Role("The Knight")),
+            SquadPositionEntry(7, SquadPositionDetail().Role("The Earl")),
+            SquadPositionEntry(6, SquadPositionDetail().Role("The Lord")),
+            SquadPositionEntry(5, SquadPositionDetail().Role("The Duke")),
+            SquadPositionEntry(4, SquadPositionDetail().Role("The Baron")),
+            SquadPositionEntry(3, SquadPositionDetail().Role("The Princess")),
+            SquadPositionEntry(2, SquadPositionDetail().Role("The Prince")),
+            SquadPositionEntry(1, SquadPositionDetail().Role("The Queen")),
+            SquadPositionEntry(0, SquadPositionDetail().Role("The King")),
+            SquadPositionEntry(255, None)
+          ))
+      )
+      val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+      pkt mustEqual string_unk7
+    }
+
+    "encode (member closed)" in {
+      val msg = SquadDetailDefinitionUpdateMessage(
+        PlanetSideGUID(3),
+        SquadDetail()
+          .Members(List(
+            SquadPositionEntry(5, SquadPositionDetail.Closed),
+            SquadPositionEntry(255, None)
+          ))
+      )
+      val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+      pkt mustEqual string_member_closed
+    }
+
+
+    "encode (member role)" in {
+      val msg = SquadDetailDefinitionUpdateMessage(
+        PlanetSideGUID(7),
+        SquadDetail()
+          .Members(List(
+            SquadPositionEntry(0, SquadPositionDetail().Role("Commander")),
+            SquadPositionEntry(255, None)
+          ))
+      )
+      val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+      pkt mustEqual string_member_role
+    }
+
+    "encode (member role + requirements)" in {
+      val msg = SquadDetailDefinitionUpdateMessage(
+        PlanetSideGUID(1),
+        SquadDetail()
+          .Members(List(
+            SquadPositionEntry(6, SquadPositionDetail()
+              .Role("ADV Hacker")
+              .Requirements(Set(CertificationType.AdvancedHacking))),
+            SquadPositionEntry(255, None)
+          ))
+      )
+      val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+      pkt mustEqual string_member_roleRequirements
+    }
+
+    "encode (member char id + name)" in {
+      val msg = SquadDetailDefinitionUpdateMessage(
+        PlanetSideGUID(3),
+        SquadDetail()
+          .Members(List(
+          SquadPositionEntry(5, SquadPositionDetail().Player(1218249L, "Duckmaster43")),
+          SquadPositionEntry(255, None)
+        ))
+      )
+      val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+      pkt mustEqual string_member_charIdName
+    }
+
+    "encode (task + member etc)" in {
+      val msg = SquadDetailDefinitionUpdateMessage(
+        PlanetSideGUID(1),
+        SquadDetail()
+          .Task("\\#FF0000 The \\#ffffff Blades")
+          .Members(List(
+            SquadPositionEntry(9, SquadPositionDetail().Role("").Requirements(Set())),
+            SquadPositionEntry(8, SquadPositionDetail().Role("")),
+            SquadPositionEntry(7, SquadPositionDetail().Role("")),
+            SquadPositionEntry(6, SquadPositionDetail().Role("")),
+            SquadPositionEntry(5, SquadPositionDetail().Role("")),
+            SquadPositionEntry(4, SquadPositionDetail().Role("")),
+            SquadPositionEntry(3, SquadPositionDetail().Role("")),
+            SquadPositionEntry(2, SquadPositionDetail().Role("")),
+            SquadPositionEntry(1, SquadPositionDetail().Role("")),
+            SquadPositionEntry(0, SquadPositionDetail().Role("")),
+            SquadPositionEntry(255, None)
+          ))
+      )
+      val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+      pkt mustEqual string_task_memberEtc
+    }
+
+    "encode (full squad)" in {
+      val msg = SquadDetailDefinitionUpdateMessage(
+        PlanetSideGUID(3),
+        SquadDetail(
+          3,
+          1792,
+          42771010L,
+          529745L,
+          "HofD",
+          "\\#ffdc00***\\#9640ff=KOK+SPC+FLY=\\#ffdc00***\\#FF4040 All Welcome",
+          PlanetSideZoneID(7),
+          4983296,
+          List(
+            SquadPositionEntry(0, SquadPositionDetail("\\#ff0000 |||||||||||||||||||||||", "", Set(), 0, "")),
+            SquadPositionEntry(1, SquadPositionDetail("\\#ffdc00   C", "", Set(), 0, "")),
+            SquadPositionEntry(2, SquadPositionDetail("\\#ffdc00   H", "", Set(), 42644970L, "OpolE")),
+            SquadPositionEntry(3, SquadPositionDetail("\\#ffdc00    I", "", Set(), 41604210L, "BobaF3tt907")),
+            SquadPositionEntry(4, SquadPositionDetail("\\#ffdc00   N", "", Set(), 0, "")),
+            SquadPositionEntry(5, SquadPositionDetail("\\#ffdc00   A", "", Set(), 0, "")),
+            SquadPositionEntry(6, SquadPositionDetail("\\#ff0000 |||||||||||||||||||||||", "", Set(), 0, "")),
+            SquadPositionEntry(7, SquadPositionDetail("\\#9640ff   K", "", Set(), 0, "")),
+            SquadPositionEntry(8, SquadPositionDetail("\\#9640ff   O", "", Set(), 42771010L ,"HofD")),
+            SquadPositionEntry(9, SquadPositionDetail("\\#9640ff   K", "", Set(), 0, ""))
+          )
+        )
+      )
+      val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+      pkt mustEqual string_full
+    }
+
+    "encode (mixed)" in {
+      val position = Some(SquadPositionDetail("", "", Set(), 0, ""))
+      val msg = SquadDetailDefinitionUpdateMessage(
+        PlanetSideGUID(3),
+        SquadDetail
+          .Task("PSForever Packet Collection")
+          .Members((0 to 9).map { index => SquadPositionEntry(index, position) }.reverse.toList :+ SquadPositionEntry(255, None))
+      )
+      val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+      pkt mustEqual string_mixed
+    }
   }
 }
diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala
index 83d290f8..7aa34958 100644
--- a/pslogin/src/main/scala/WorldSessionActor.scala
+++ b/pslogin/src/main/scala/WorldSessionActor.scala
@@ -375,11 +375,15 @@ class WorldSessionActor extends Actor with MDCContextAware {
           sendResponse(
             SquadDetailDefinitionUpdateMessage(
               guid,
-              member_info.find(_.name == leader).get.char_id,
-              leader,
-              task,
-              zone,
-              member_info
+              SquadDetail()
+                .LeaderCharId(member_info.find(_.name == leader).get.char_id.get)
+                .LeaderName(leader)
+                .Task(task)
+                .ZoneId(zone)
+                .Members(
+                  member_info.zipWithIndex.map { case (a, b) => SquadPositionEntry(b, a) }
+                )
+                .Complete
             )
           )
         case _ => ;
@@ -2929,17 +2933,16 @@ class WorldSessionActor extends Actor with MDCContextAware {
           PlanetSideZoneID(7),
           4983296,
           List(
-            SquadPositionEntry(0, SquadPositionDetail2("\\#ff0000 |||||||||||||||||||||||", "", Set(), 0, "")),
-            SquadPositionEntry(1, SquadPositionDetail2("\\#ffdc00   C", "", Set(), 0, "")),
-            SquadPositionEntry(2, SquadPositionDetail2("\\#ffdc00   H", "", Set(), 42644970L, "OpolE")),
-            SquadPositionEntry(3, SquadPositionDetail2("\\#ffdc00    I", "", Set(), 41604210L, "BobaF3tt907")),
-            SquadPositionEntry(4, SquadPositionDetail2("\\#ffdc00   N", "", Set(), 0, "")),
-            SquadPositionEntry(5, SquadPositionDetail2("\\#ffdc00   A", "", Set(), 0, "")),
-//            SquadPositionEntry(6, SquadPositionDetail2("\\#ff0000 |||||||||||||||||||||||", "", Set(), 0, "")),
-            SquadPositionEntry(6, SquadPositionDetail2("\\#ff0000 |||||||||||||||||||||||", "", Set(), 1, "Test")),
-            SquadPositionEntry(7, SquadPositionDetail2("\\#9640ff   K", "", Set(), 0, "")),
-            SquadPositionEntry(8, SquadPositionDetail2("\\#9640ff   O", "", Set(), 42771010L ,"HofD")),
-            SquadPositionEntry(9, SquadPositionDetail2("\\#9640ff   K", "", Set(), 0, ""))
+            SquadPositionEntry(0, SquadPositionDetail("\\#ff0000 |||||||||||||||||||||||", "", Set(), 0, "")),
+            SquadPositionEntry(1, SquadPositionDetail("\\#ffdc00   C", "", Set(), 0, "")),
+            SquadPositionEntry(2, SquadPositionDetail("\\#ffdc00   H", "", Set(), 42644970L, "OpolE")),
+            SquadPositionEntry(3, SquadPositionDetail("\\#ffdc00    I", "", Set(), 41604210L, "BobaF3tt907")),
+            SquadPositionEntry(4, SquadPositionDetail("\\#ffdc00   N", "", Set(), 0, "")),
+            SquadPositionEntry(5, SquadPositionDetail("\\#ffdc00   A", "", Set(), 0, "")),
+            SquadPositionEntry(6, SquadPositionDetail("\\#ff0000 |||||||||||||||||||||||", "", Set(), 0, "")),
+            SquadPositionEntry(7, SquadPositionDetail("\\#9640ff   K", "", Set(), 0, "")),
+            SquadPositionEntry(8, SquadPositionDetail("\\#9640ff   O", "", Set(), 42771010L ,"HofD")),
+            SquadPositionEntry(9, SquadPositionDetail("\\#9640ff   K", "", Set(), 0, ""))
           )
         )
       )

From 60a11b8c52d47ff6d57db75de3dda35f7dc422be Mon Sep 17 00:00:00 2001
From: FateJH <Jason_DiDonato@yahoo.com>
Date: Mon, 8 Jul 2019 23:04:57 -0400
Subject: [PATCH 12/31] sufficient functionality that allows the UI for squads
 to display properly on squad joining and on squad leaving; clarification for
 SquadMemberEvent fields; QoL support functionality intergrated into SDDUM
 streams

---
 .../psforever/objects/teamwork/Squad.scala    |  12 +-
 .../SquadDetailDefinitionUpdateMessage.scala  |  91 +++-
 .../packet/game/SquadMemberEvent.scala        |  54 +--
 .../scala/services/teamwork/SquadAction.scala |  13 +
 .../services/teamwork/SquadResponse.scala     |   6 +
 .../services/teamwork/SquadService.scala      | 413 ++++++++++--------
 .../teamwork/SquadServiceMessage.scala        |   5 +-
 ...uadDetailDefinitionUpdateMessageTest.scala |  38 +-
 .../src/main/scala/WorldSessionActor.scala    | 133 +++---
 9 files changed, 456 insertions(+), 309 deletions(-)
 create mode 100644 common/src/main/scala/services/teamwork/SquadAction.scala

diff --git a/common/src/main/scala/net/psforever/objects/teamwork/Squad.scala b/common/src/main/scala/net/psforever/objects/teamwork/Squad.scala
index 21a48cb3..b5e5d81d 100644
--- a/common/src/main/scala/net/psforever/objects/teamwork/Squad.scala
+++ b/common/src/main/scala/net/psforever/objects/teamwork/Squad.scala
@@ -82,12 +82,12 @@ class Squad(squadId : PlanetSideGUID, alignment : PlanetSideEmpire.Value) extend
     LeaderPositionIndex
   }
 
-  def Leader : String = {
-    membership.lift(leaderPositionIndex) match {
-      case Some(member) =>
-        member.Name
-      case None =>
-        ""
+  def Leader : Member = {
+    membership(leaderPositionIndex) match {
+      case member if !member.Name.equals("") =>
+        member
+      case _ =>
+        throw new Exception("can not find squad leader!")
     }
   }
 
diff --git a/common/src/main/scala/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala b/common/src/main/scala/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala
index ad080bcf..9bda7527 100644
--- a/common/src/main/scala/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala
+++ b/common/src/main/scala/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala
@@ -198,17 +198,16 @@ final case class SquadDetail(unk1 : Option[Int],
     leader_name.orElse(Some("")),
     task.orElse(Some("")),
     zone_id.orElse(Some(PlanetSideZoneID(0))),
-    unk7.orElse(Some(0)),
+    unk7.orElse(Some(4983296)), //FullSquad value
     {
       val complete = SquadPositionDetail().Complete
       Some(member_info match {
         case Some(info) =>
           //create one list that ensures all existing positions are "complete" then add a list of the missing indices
-          val fields = info.collect {
-            case SquadPositionEntry(a, Some(b)) => SquadPositionEntry(a, b.Complete)
-            case out @ SquadPositionEntry(_, None) => out
-          }
-          val indices = info.map { case SquadPositionEntry(a, _) => a }
+          val (indices, fields) = info.collect {
+            case SquadPositionEntry(a, Some(b)) => (a, SquadPositionEntry(a, b.Complete))
+            case out @ SquadPositionEntry(a, None) => (a, out)
+          }.unzip
           ((0 to 9).toSet.diff(indices.toSet).map { SquadPositionEntry(_, complete) } ++ fields).toList.sortBy(_.index)
         case None =>
           //original list
@@ -585,7 +584,6 @@ object SquadDetailDefinitionUpdateMessage extends Marshallable[SquadDetailDefini
     private def membersCodec(bitsOverByte : StreamLengthToken) : Codec[SquadDetail] = {
       import shapeless.::
       (
-        //TODO you can replace this outer structure with an either Codec
         bool >>:~ { flag =>
           conditional(flag, {
             bitsOverByte.Add(4)
@@ -593,15 +591,15 @@ object SquadDetailDefinitionUpdateMessage extends Marshallable[SquadDetailDefini
           }) ::
             conditional(!flag, {
               bitsOverByte.Add(3)
-              uint(2) :: FullyPopulatedPositions.codec(bitsOverByte)
+              uint2 :: FullyPopulatedPositions.codec(bitsOverByte)
             })
         }
         ).exmap[SquadDetail] (
         {
           case true :: Some(_ :: member_list :: HNil) :: _ :: HNil =>
-            Attempt.successful(SquadDetail(None, None, None, None, None, None, None, None, Some(member_list.toList)))
+            Attempt.successful(SquadDetail(None, None, None, None, None, None, None, None, Some(ignoreTerminatingEntry(member_list.toList))))
           case false :: None :: Some(_ :: member_list :: HNil) :: HNil =>
-            Attempt.successful(SquadDetail(None, None, None, None, None, None, None, None, Some(member_list.toList)))
+            Attempt.successful(SquadDetail(None, None, None, None, None, None, None, None, Some(ignoreTerminatingEntry(member_list.toList))))
         },
         {
           case SquadDetail(_, _, _, _, _, _, _, _, Some(member_list)) =>
@@ -612,16 +610,54 @@ object SquadDetailDefinitionUpdateMessage extends Marshallable[SquadDetailDefini
               }
               .flatten
               .count(_.isEmpty) == 0) {
-              Attempt.successful(false :: None :: Some(2 :: member_list.toVector :: HNil) :: HNil)
+              Attempt.successful(false :: None :: Some(2 :: ensureTerminatingEntry(member_list).toVector :: HNil) :: HNil)
             }
             else {
-              Attempt.successful(true :: Some(4 :: member_list.toVector :: HNil) :: None :: HNil)
+              Attempt.successful(true :: Some(4 :: ensureTerminatingEntry(member_list).toVector :: HNil) :: None :: HNil)
             }
           case _ =>
             Attempt.failure(Err("failed to encode squad data for members"))
         }
       )
     }
+    //TODO while this pattern looks elegant, bitsOverByte does not accumulate properly with the either(bool, L, R); why?
+//    private def membersCodec(bitsOverByte : StreamLengthToken) : Codec[SquadDetail] = {
+//      import shapeless.::
+//      either(bool,
+//        { //false
+//          bitsOverByte.Add(3)
+//          uint2 :: FullyPopulatedPositions.codec(bitsOverByte)
+//        },
+//        { //true
+//          bitsOverByte.Add(4)
+//          uint(3) :: vector(ItemizedPositions.codec(bitsOverByte))
+//        }
+//      ).exmap[SquadDetail] (
+//        {
+//          case Left(_ :: member_list :: HNil) =>
+//            Attempt.successful(SquadDetail(None, None, None, None, None, None, None, None, Some(ignoreTerminatingEntry(member_list.toList))))
+//          case Right(_ :: member_list :: HNil) =>
+//            Attempt.successful(SquadDetail(None, None, None, None, None, None, None, None, Some(ignoreTerminatingEntry(member_list.toList))))
+//        },
+//        {
+//          case SquadDetail(_, _, _, _, _, _, _, _, Some(member_list)) =>
+//            if(member_list
+//              .collect { case position if position.info.nonEmpty =>
+//                val info = position.info.get
+//                List(info.is_closed, info.role, info.detailed_orders, info.requirements, info.char_id, info.name)
+//              }
+//              .flatten
+//              .count(_.isEmpty) == 0) {
+//              Attempt.successful(Left(2 :: ensureTerminatingEntry(member_list).toVector :: HNil))
+//            }
+//            else {
+//              Attempt.successful(Right(4 :: ensureTerminatingEntry(member_list).toVector :: HNil))
+//            }
+//          case _ =>
+//            Attempt.failure(Err("failed to encode squad data for members"))
+//        }
+//      )
+//    }
     /**
       * A failing pattern for when the coded value is not tied to a known field pattern.
       * This pattern does not read or write any bit data.
@@ -1245,10 +1281,9 @@ object SquadDetailDefinitionUpdateMessage extends Marshallable[SquadDetailDefini
         }).xmap[Vector[SquadPositionEntry]] (
         {
           case _ :: _ :: linkedMembers :: HNil =>
-            unlinkFields(linkedMembers).toVector
+            ignoreTerminatingEntry(unlinkFields(linkedMembers)).toVector
         },
-        //TODO "memberList.size - 1"? the only two examples are "10" anyway
-        memberList => memberList.size - 1 :: 12 :: linkFields(memberList.reverse.toList) :: HNil
+        memberList => 10 :: 12 :: linkFields(ensureTerminatingEntry(memberList.toList).reverse) :: HNil
       )
     }
   }
@@ -1320,6 +1355,32 @@ object SquadDetailDefinitionUpdateMessage extends Marshallable[SquadDetailDefini
     )
   }
 
+  /**
+    * The last entry in the sequence of squad information listings should be a dummied listing with an index of 255.
+    * Ensure that this terminal entry is located at the end.
+    * @param list the listing of squad information
+    * @return the listing of squad information, with a specific final entry
+    */
+  private def ensureTerminatingEntry(list : List[SquadPositionEntry]) : List[SquadPositionEntry] = {
+    list.lastOption match {
+      case Some(SquadPositionEntry(255, _)) => list
+      case Some(_) | None => list :+ SquadPositionEntry(255, None)
+    }
+  }
+
+  /**
+    * The last entry in the sequence of squad information listings should be a dummied listing with an index of 255.
+    * Remove this terminal entry from the end of the list so as not to hassle with it.
+    * @param list the listing of squad information
+    * @return the listing of squad information, with a specific final entry truncated
+    */
+  private def ignoreTerminatingEntry(list : List[SquadPositionEntry]) : List[SquadPositionEntry] = {
+    list.lastOption match {
+      case Some(SquadPositionEntry(255, _)) => list.init
+      case Some(_) | None => list
+    }
+  }
+
   implicit val codec : Codec[SquadDetailDefinitionUpdateMessage] = {
     import shapeless.::
     import net.psforever.newcodecs.newcodecs
diff --git a/common/src/main/scala/net/psforever/packet/game/SquadMemberEvent.scala b/common/src/main/scala/net/psforever/packet/game/SquadMemberEvent.scala
index df8bd7b0..50dec0f6 100644
--- a/common/src/main/scala/net/psforever/packet/game/SquadMemberEvent.scala
+++ b/common/src/main/scala/net/psforever/packet/game/SquadMemberEvent.scala
@@ -8,10 +8,10 @@ import shapeless.{::, HNil}
 
 final case class SquadMemberEvent(unk1 : Int,
                                   unk2 : Int,
-                                  unk3 : Long,
-                                  unk4 : Int,
-                                  unk5 : Option[String],
-                                  unk6 : Option[Int],
+                                  char_id : Long,
+                                  member_position : Int,
+                                  player_name : Option[String],
+                                  zone_number : Option[Int],
                                   unk7 : Option[Long])
   extends PlanetSideGamePacket {
   type Packet = SquadMemberEvent
@@ -20,40 +20,40 @@ final case class SquadMemberEvent(unk1 : Int,
 }
 
 object SquadMemberEvent extends Marshallable[SquadMemberEvent] {
-  def apply(unk1 : Int, unk2 : Int, unk3 : Long, unk4 : Int) : SquadMemberEvent =
-    SquadMemberEvent(unk1, unk2, unk3, unk4, None, None, None)
+  def apply(unk1 : Int, unk2 : Int, char_id : Long, member_position : Int) : SquadMemberEvent =
+    SquadMemberEvent(unk1, unk2, char_id, member_position, None, None, None)
 
-  def apply(unk2 : Int, unk3 : Long, unk4 : Int, unk5 : String, unk6 : Int, unk7 : Long) : SquadMemberEvent =
-    SquadMemberEvent(0, unk2, unk3, unk4, Some(unk5), Some(unk6), Some(unk7))
+  def apply(unk2 : Int, char_id : Long, member_position : Int, player_name : String, zone_number : Int, unk7 : Long) : SquadMemberEvent =
+    SquadMemberEvent(0, unk2, char_id, member_position, Some(player_name), Some(zone_number), Some(unk7))
 
-  def apply(unk2 : Int, unk3 : Long, unk4 : Int, unk6 : Int) : SquadMemberEvent =
-    SquadMemberEvent(3, unk2, unk3, unk4, None, Some(unk6), None)
+  def apply(unk2 : Int, char_id : Long, member_position : Int, zone_number : Int) : SquadMemberEvent =
+    SquadMemberEvent(3, unk2, char_id, member_position, None, Some(zone_number), None)
 
-  def apply(unk2 : Int, unk3 : Long, unk4 : Int, unk7 : Long) : SquadMemberEvent =
-    SquadMemberEvent(4, unk2, unk3, unk4, None, None, Some(unk7))
+  def apply(unk2 : Int, char_id : Long, member_position : Int, unk7 : Long) : SquadMemberEvent =
+    SquadMemberEvent(4, unk2, char_id, member_position, None, None, Some(unk7))
 
   implicit val codec : Codec[SquadMemberEvent] = (
-    ("unk1" | uintL(3)) >>:~ { unk1 =>
+    ("unk1" | uint(3)) >>:~ { unk1 =>
       ("unk2" | uint16L) ::
-        ("unk3" | uint32L) ::
-        ("unk4" | uintL(4)) ::
-        conditional(unk1 == 0, "unk5" | PacketHelpers.encodedWideStringAligned(1)) ::
-        conditional(unk1 == 0 || unk1 == 3, "unk6" | uint16L) ::
+        ("char_id" | uint32L) ::
+        ("member_position" | uint4) ::
+        conditional(unk1 == 0, "player_name" | PacketHelpers.encodedWideStringAligned(1)) ::
+        conditional(unk1 == 0 || unk1 == 3, "zone_number" | uint16L) ::
         conditional(unk1 == 0 || unk1 == 4, "unk7" | uint32L)
     }).exmap[SquadMemberEvent] (
     {
-      case unk1 :: unk2 :: unk3 :: unk4 :: unk5 :: unk6 :: unk7 :: HNil =>
-        Attempt.Successful(SquadMemberEvent(unk1, unk2, unk3, unk4, unk5, unk6, unk7))
+      case unk1 :: unk2 :: char_id :: member_position :: player_name :: zone_number :: unk7 :: HNil =>
+        Attempt.Successful(SquadMemberEvent(unk1, unk2, char_id, member_position, player_name, zone_number, unk7))
     },
     {
-      case data @ SquadMemberEvent(0, unk2, unk3, unk4, Some(unk5), Some(unk6), Some(unk7)) =>
-        Attempt.Successful(0 :: unk2 :: unk3 :: unk4 :: Some(unk5) :: Some(unk6) :: Some(unk7) :: HNil)
-      case data @ SquadMemberEvent(3, unk2, unk3, unk4, None, Some(unk6), None) =>
-        Attempt.Successful(3 :: unk2 :: unk3 :: unk4 :: None :: Some(unk6) :: None :: HNil)
-      case data @ SquadMemberEvent(4, unk2, unk3, unk4, None, None, Some(unk7)) =>
-        Attempt.Successful(4 :: unk2 :: unk3 :: unk4 :: None :: None :: Some(unk7) :: HNil)
-      case data @ SquadMemberEvent(unk1, unk2, unk3, unk4, None, None, None) =>
-        Attempt.Successful(unk1 :: unk2 :: unk3 :: unk4 :: None :: None :: None :: HNil)
+      case data @ SquadMemberEvent(0, unk2, char_id, member_position, Some(player_name), Some(zone_number), Some(unk7)) =>
+        Attempt.Successful(0 :: unk2 :: char_id :: member_position :: Some(player_name) :: Some(zone_number) :: Some(unk7) :: HNil)
+      case data @ SquadMemberEvent(3, unk2, char_id, member_position, None, Some(zone_number), None) =>
+        Attempt.Successful(3 :: unk2 :: char_id :: member_position :: None :: Some(zone_number) :: None :: HNil)
+      case data @ SquadMemberEvent(4, unk2, char_id, member_position, None, None, Some(unk7)) =>
+        Attempt.Successful(4 :: unk2 :: char_id :: member_position :: None :: None :: Some(unk7) :: HNil)
+      case data @ SquadMemberEvent(unk1, unk2, char_id, member_position, None, None, None) =>
+        Attempt.Successful(unk1 :: unk2 :: char_id :: member_position :: None :: None :: None :: HNil)
       case data =>
         Attempt.Failure(Err(s"SquadMemberEvent can not encode with this pattern - $data"))
     }
diff --git a/common/src/main/scala/services/teamwork/SquadAction.scala b/common/src/main/scala/services/teamwork/SquadAction.scala
new file mode 100644
index 00000000..744a4940
--- /dev/null
+++ b/common/src/main/scala/services/teamwork/SquadAction.scala
@@ -0,0 +1,13 @@
+// Copyright (c) 2019 PSForever
+package services.teamwork
+
+import net.psforever.objects.Player
+import net.psforever.packet.game._
+import net.psforever.types.SquadRequestType
+
+object SquadAction {
+  trait Action
+
+  final case class Membership(request_type : SquadRequestType.Value, unk2 : Long, unk3 : Option[Long], player_name : String, unk5 : Option[Option[String]]) extends Action
+  final case class Definition(player : Player, zone_ordinal_number : Int, guid : PlanetSideGUID, line : Int, action : SquadAction)
+}
diff --git a/common/src/main/scala/services/teamwork/SquadResponse.scala b/common/src/main/scala/services/teamwork/SquadResponse.scala
index 27ef6dcd..773b4067 100644
--- a/common/src/main/scala/services/teamwork/SquadResponse.scala
+++ b/common/src/main/scala/services/teamwork/SquadResponse.scala
@@ -1,7 +1,9 @@
 // Copyright (c) 2019 PSForever
 package services.teamwork
 
+import net.psforever.objects.teamwork.{Member, Squad}
 import net.psforever.packet.game._
+import net.psforever.types.SquadRequestType
 
 object SquadResponse {
   trait Response
@@ -10,5 +12,9 @@ object SquadResponse {
   final case class Update(infos : Iterable[(Int, SquadInfo)]) extends Response
   final case class Remove(infos : Iterable[Int]) extends Response
 
+  final case class Membership(request_type : SquadRequestType.Value, unk1 : Int, unk2 : Int, unk3 : Long, unk4 : Option[Long], player_name : String, unk5 : Boolean, unk6 : Option[Option[String]]) extends Response //see SquadMembershipResponse
+  final case class Join(squad : Squad, positionsToUpdate : List[Int]) extends Response
+  final case class Leave(squad : Squad, positionsToUpdate : List[(Long, Int)]) extends Response
+
   final case class Detail(guid : PlanetSideGUID, leader : String, task : String, zone : PlanetSideZoneID, member_info : List[SquadPositionDetail]) extends Response
 }
diff --git a/common/src/main/scala/services/teamwork/SquadService.scala b/common/src/main/scala/services/teamwork/SquadService.scala
index 245c3a29..070cdacc 100644
--- a/common/src/main/scala/services/teamwork/SquadService.scala
+++ b/common/src/main/scala/services/teamwork/SquadService.scala
@@ -3,12 +3,13 @@ package services.teamwork
 
 import akka.actor.Actor
 import net.psforever.objects.Player
-import net.psforever.objects.teamwork.{Member, Squad}
+import net.psforever.objects.teamwork.Squad
 import net.psforever.packet.game._
-import net.psforever.types.{PlanetSideEmpire, Vector3}
+import net.psforever.types.{PlanetSideEmpire, SquadRequestType, Vector3}
 import services.{GenericEventBus, Service}
 
 import scala.collection.concurrent.TrieMap
+import scala.collection.mutable
 import scala.collection.mutable.ListBuffer
 
 //import scala.concurrent.duration._
@@ -22,11 +23,35 @@ class SquadService extends Actor {
     PlanetSideEmpire.NC -> ListBuffer.empty,
     PlanetSideEmpire.VS -> ListBuffer.empty
   )
+  private val bids : mutable.LongMap[(PlanetSideGUID, Int)] = mutable.LongMap[(PlanetSideGUID, Int)]()
 
   private [this] val log = org.log4s.getLogger
 
   override def preStart = {
     log.info("Starting...")
+
+    val testSquad = new Squad(PlanetSideGUID(3), PlanetSideEmpire.VS)
+    testSquad.Task = "\\#66CCFF Sentinels of Auraxis\\#FFFFFF ... \\#40FF40 Squad Up!!"
+    testSquad.ZoneId = 5
+    testSquad.Membership(0).Name = "Wizkid45"
+    testSquad.Membership(0).CharId = 30910985L
+    testSquad.Membership(0).ZoneId = 5
+    testSquad.Membership(0).Position = Vector3(5526.5234f, 3818.7344f, 54.59375f)
+    testSquad.Membership(1).Name = "xoBLADEox"
+    testSquad.Membership(1).CharId = 42781919L
+    testSquad.Membership(1).ZoneId = 5
+    testSquad.Membership(1).Position = Vector3(4673.5312f, 2604.8047f, 40.015625f)
+    testSquad.Membership(3).Name = "cabal0428"
+    testSquad.Membership(3).CharId = 353380L
+    testSquad.Membership(3).ZoneId = 5
+    testSquad.Membership(3).Position = Vector3(4727.492f, 2613.5312f, 51.390625f)
+    testSquad.Membership(4).Name = "xSkiku"
+    testSquad.Membership(4).CharId = 41588340L
+    testSquad.Membership(4).ZoneId = 5
+    testSquad.Membership(4).Position = Vector3(3675.0f, 4789.8047f, 63.21875f)
+    idToSquad(PlanetSideGUID(3)) = testSquad
+    testSquad.Listed = true
+    UpdateSquadList(testSquad, List())
   }
 
   def GetNextSquadId() : PlanetSideGUID = {
@@ -55,7 +80,7 @@ class SquadService extends Actor {
     val name = player.Name
     val squadOut = opt match {
       case Some(squad) =>
-        if(squad.Leader.equals(name)) {
+        if(squad.Leader.Name.equals(name)) {
           squad
         }
         else {
@@ -64,7 +89,7 @@ class SquadService extends Actor {
 
       case None =>
         memberToSquad.get(name) match {
-          case Some(squad) if squad.Leader.equals(name) =>
+          case Some(squad) if squad.Leader.Name.equals(name) =>
             squad
           case _ =>
             val faction = player.Faction
@@ -115,7 +140,7 @@ class SquadService extends Actor {
       SquadEvents.unsubscribe(sender())
       memberToSquad.get(name) match {
         case Some(squad) =>
-          if(squad.Leader.equals(name)) {
+          if(squad.Leader.Name.equals(name)) {
             //we were the leader
             if(squad.Membership.count(p => p.Name.equals("")) > 1) {
               //other players were in the squad; publicly disband it
@@ -144,190 +169,218 @@ class SquadService extends Actor {
           }
         case None => ;
       }
-      //TODO leave squad, if joined to one, and perform clean-up
+    //TODO leave squad, if joined to one, and perform clean-up
 
     case Service.Leave(None) | Service.LeaveAll() => ;
 
-    case SquadServiceMessage.SquadDefinitionAction(tplayer, zone_ordinal_number, guid, _, action) =>
-      import net.psforever.packet.game.SquadAction._
-      val squadOpt = GetParticipatingSquad(tplayer, zone_ordinal_number)
-      action match {
-        case SaveSquadDefinition() =>
-
-        case ChangeSquadPurpose(purpose) =>
-          log.info(s"${tplayer.Name}-${tplayer.Faction} has changed his squad's task to $purpose")
-          val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt)
-          squad.Task = purpose
-          UpdateSquadList(squad, List(SquadInfo.Field.Task))
-          UpdateSquadDetail(squad)
-
-        case ChangeSquadZone(zone) =>
-          log.info(s"${tplayer.Name}-${tplayer.Faction} has changed squad's ops zone to $zone")
-          val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt)
-          squad.ZoneId = zone.zoneId.toInt
-          UpdateSquadList(squad, List(SquadInfo.Field.ZoneId))
-          UpdateSquadDetail(squad)
-
-        case CloseSquadMemberPosition(position) =>
-          val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt)
-          squad.Availability.lift(position) match {
-            case Some(true) =>
-              squad.Availability.update(position, false)
-              log.info(s"${tplayer.Name}-${tplayer.Faction} has closed the #$position position in squad")
-              val memberPosition = squad.Membership(position)
-              val listingChanged = if(memberPosition.Name.nonEmpty) {
-                List(SquadInfo.Field.Size, SquadInfo.Field.Capacity)
+    case SquadServiceMessage(tplayer, squad_action) => squad_action match {
+      case SquadAction.Membership(request_type, char_id, optional_char_id, name, unk5) => request_type match {
+        case SquadRequestType.Accept =>
+          bids.get(char_id) match {
+            case Some((squadGUID, line)) if idToSquad.get(squadGUID).nonEmpty =>
+              //join squad
+              val squad = idToSquad(squadGUID)
+              val position = squad.Membership(line)
+              if(squad.Availability(line) && position.CharId == 0 &&
+                tplayer.Certifications.intersect(position.Requirements) == position.Requirements) {
+                position.Name = tplayer.Name
+                position.CharId = char_id
+                position.Health = tplayer.Health
+                position.Armor = tplayer.Armor
+                position.Position = tplayer.Position
+                position.ZoneId = 13
+                memberToSquad(tplayer.Name) = squad
+                sender ! SquadServiceResponse("", SquadResponse.Join(
+                  squad,
+                  squad.Membership.zipWithIndex.collect({ case (member, index) if member.CharId != 0 => index }).toList
+                ))
               }
-              else {
-                List(SquadInfo.Field.Capacity)
-              }
-              memberPosition.Close()
-              UpdateSquadList(squad, listingChanged)
-              UpdateSquadDetail(squad)
-            case Some(false) | None => ;
+              bids.remove(char_id)
+            case _ => ;
           }
 
-        case AddSquadMemberPosition(position) =>
-          val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt)
-          squad.Availability.lift(position) match {
-            case Some(false) =>
-              log.info(s"${tplayer.Name}-${tplayer.Faction} has opened the #$position position in squad")
-              squad.Availability.update(position, true)
-              UpdateSquadList(squad, List(SquadInfo.Field.Capacity))
-              UpdateSquadDetail(squad)
-            case Some(true) | None => ;
-          }
-
-        case ChangeSquadMemberRequirementsRole(position, role) =>
-          val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt)
-          squad.Availability.lift(position) match {
-            case Some(true) =>
-              log.info(s"${tplayer.Name}-${tplayer.Faction} has changed the role of squad position #$position")
-              squad.Membership(position).Role = role
-              UpdateSquadDetail(squad)
-            case Some(false) | None => ;
-          }
-
-        case ChangeSquadMemberRequirementsDetailedOrders(position, orders) =>
-          val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt)
-          squad.Availability.lift(position) match {
-            case Some(true) =>
-              log.info(s"${tplayer.Name}-${tplayer.Faction} has changed the orders for squad position #$position")
-              squad.Membership(position).Orders = orders
-              UpdateSquadDetail(squad)
-            case Some(false) | None => ;
-          }
-
-        case ChangeSquadMemberRequirementsCertifications(position, certs) =>
-          val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt)
-          squad.Availability.lift(position) match {
-            case Some(true) =>
-              log.info(s"${tplayer.Name}-${tplayer.Faction} has changed the requirements for squad position #$position")
-              squad.Membership(position).Requirements = certs
-              UpdateSquadDetail(squad)
-            case Some(false) | None => ;
-          }
-
-        case LocationFollowsSquadLead(state) =>
-          val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt)
-          if(state) {
-            log.info(s"${tplayer.Name}-${tplayer.Faction} has moves the rally to the leader's position")
-          }
-          else {
-            log.info(s"${tplayer.Name}-${tplayer.Faction} has let the rally move freely")
-          }
-          squad.LocationFollowsSquadLead = state
-
-        case AutoApproveInvitationRequests(state) =>
-          val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt)
-          if(state) {
-            log.info(s"${tplayer.Name}-${tplayer.Faction} is allowing all requests to join the squad")
-          }
-          else {
-            log.info(s"${tplayer.Name}-${tplayer.Faction} has started screening invitation requests")
-          }
-          squad.AutoApproveInvitationRequests = state
-
-        case SelectRoleForYourself(line) =>
-          //TODO need to ask permission from the squad leader, unless our character is the squad leader or already currently in the squad
-          val name = tplayer.Name
-          squadOpt match {
-            case Some(squad) =>
-              {
-                if(squad.Availability(line))
-                  squad.Membership.lift(line)
-                else
-                  None
-              } match {
-                case Some(desiredPosition : Member)
-                  if desiredPosition.Requirements.intersect(tplayer.Certifications) == desiredPosition.Requirements =>
-                  //our character is qualified for this new position
-                  if(squad.Leader.equals(tplayer.Name)) {
-                    squad.LeaderPositionIndex = line //update
-                  }
-                  val hadPreviousPosition = squad.Membership.find(_.Name == name) match {
-                    case Some(currentPosition)=>
-                      currentPosition.Name = ""
-                      currentPosition.CharId = 0L
-                      currentPosition.ZoneId = 0
-                      currentPosition.Health = 0
-                      currentPosition.Armor = 0
-                      currentPosition.Position = Vector3.Zero
-                      true
-                    case None =>
-                      false
-                  }
-                  desiredPosition.Name = name
-                  desiredPosition.CharId = tplayer.CharId
-                  desiredPosition.ZoneId = zone_ordinal_number
-                  desiredPosition.Health = tplayer.Health
-                  desiredPosition.Armor = tplayer.Armor
-                  desiredPosition.Position = tplayer.Position
-                  if(!hadPreviousPosition) {
-                    UpdateSquadList(squad, List(SquadInfo.Field.Size))
-                  }
-                  UpdateSquadDetail(squad)
-                case None => ;
-              }
-
-            case None => ;
-          }
-
-        case ListSquad() =>
-          val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt)
-          if(!squad.Listed) {
-            log.info(s"${tplayer.Name}-${tplayer.Faction} has opened recruitment for this squad")
-            squad.Listed = true
-          }
-          UpdateSquadList(squad, List())
-
-        case ResetAll() =>
-          val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt)
-          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()
-          })
-          UpdateSquadList(squad, List(SquadInfo.Field.Task, SquadInfo.Field.ZoneId, SquadInfo.Field.Size, SquadInfo.Field.Capacity))
-          UpdateSquadDetail(squad)
-
-        case DisplaySquad() =>
-          idToSquad.get(guid) match {
-            case Some(squad) =>
-              sender ! SquadServiceResponse(s"${tplayer.Name}/Squad", GenSquadDetail(squad))
-            case None => ;
+        case SquadRequestType.Leave =>
+          if(optional_char_id.contains(char_id)) {
+            //we're leaving the squad on our own
+            val name = tplayer.Name
+            val squad = memberToSquad(name)
+            val membership = squad.Membership.zipWithIndex
+            val (member, index) = membership
+              .find { case (_member, _) => _member.Name == name }
+              .get
+            val updateList = membership.collect({ case (_member, _) if _member.CharId > 0 => (_member.CharId, index) }).toList
+            memberToSquad.remove(name)
+            member.Name = ""
+            member.CharId = 0
+            sender ! SquadServiceResponse("", SquadResponse.Leave(squad, updateList))
           }
 
         case _ => ;
       }
 
-    case msg =>
-      log.info(s"Unhandled message $msg from $sender")
+      case SquadAction.Definition(tplayer : Player, zone_ordinal_number : Int, guid : PlanetSideGUID, line : Int, action : SquadAction) =>
+        import net.psforever.packet.game.SquadAction._
+        val squadOpt = GetParticipatingSquad(tplayer, zone_ordinal_number)
+        action match {
+          case SaveSquadDefinition() =>
+
+          case ChangeSquadPurpose(purpose) =>
+            log.info(s"${tplayer.Name}-${tplayer.Faction} has changed his squad's task to $purpose")
+            val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt)
+            squad.Task = purpose
+            UpdateSquadList(squad, List(SquadInfo.Field.Task))
+            UpdateSquadDetail(squad)
+
+          case ChangeSquadZone(zone) =>
+            log.info(s"${tplayer.Name}-${tplayer.Faction} has changed squad's ops zone to $zone")
+            val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt)
+            squad.ZoneId = zone.zoneId.toInt
+            UpdateSquadList(squad, List(SquadInfo.Field.ZoneId))
+            UpdateSquadDetail(squad)
+
+          case CloseSquadMemberPosition(position) =>
+            val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt)
+            squad.Availability.lift(position) match {
+              case Some(true) =>
+                squad.Availability.update(position, false)
+                log.info(s"${tplayer.Name}-${tplayer.Faction} has closed the #$position position in squad")
+                val memberPosition = squad.Membership(position)
+                val listingChanged = if(memberPosition.Name.nonEmpty) {
+                  List(SquadInfo.Field.Size, SquadInfo.Field.Capacity)
+                }
+                else {
+                  List(SquadInfo.Field.Capacity)
+                }
+                memberPosition.Close()
+                UpdateSquadList(squad, listingChanged)
+                UpdateSquadDetail(squad)
+              case Some(false) | None => ;
+            }
+
+          case AddSquadMemberPosition(position) =>
+            val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt)
+            squad.Availability.lift(position) match {
+              case Some(false) =>
+                log.info(s"${tplayer.Name}-${tplayer.Faction} has opened the #$position position in squad")
+                squad.Availability.update(position, true)
+                UpdateSquadList(squad, List(SquadInfo.Field.Capacity))
+                UpdateSquadDetail(squad)
+              case Some(true) | None => ;
+            }
+
+          case ChangeSquadMemberRequirementsRole(position, role) =>
+            val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt)
+            squad.Availability.lift(position) match {
+              case Some(true) =>
+                log.info(s"${tplayer.Name}-${tplayer.Faction} has changed the role of squad position #$position")
+                squad.Membership(position).Role = role
+                UpdateSquadDetail(squad)
+              case Some(false) | None => ;
+            }
+
+          case ChangeSquadMemberRequirementsDetailedOrders(position, orders) =>
+            val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt)
+            squad.Availability.lift(position) match {
+              case Some(true) =>
+                log.info(s"${tplayer.Name}-${tplayer.Faction} has changed the orders for squad position #$position")
+                squad.Membership(position).Orders = orders
+                UpdateSquadDetail(squad)
+              case Some(false) | None => ;
+            }
+
+          case ChangeSquadMemberRequirementsCertifications(position, certs) =>
+            val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt)
+            squad.Availability.lift(position) match {
+              case Some(true) =>
+                log.info(s"${tplayer.Name}-${tplayer.Faction} has changed the requirements for squad position #$position")
+                squad.Membership(position).Requirements = certs
+                UpdateSquadDetail(squad)
+              case Some(false) | None => ;
+            }
+
+          case LocationFollowsSquadLead(state) =>
+            val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt)
+            if(state) {
+              log.info(s"${tplayer.Name}-${tplayer.Faction} has moves the rally to the leader's position")
+            }
+            else {
+              log.info(s"${tplayer.Name}-${tplayer.Faction} has let the rally move freely")
+            }
+            squad.LocationFollowsSquadLead = state
+
+          case AutoApproveInvitationRequests(state) =>
+            val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt)
+            if(state) {
+              log.info(s"${tplayer.Name}-${tplayer.Faction} is allowing all requests to join the squad")
+            }
+            else {
+              log.info(s"${tplayer.Name}-${tplayer.Faction} has started screening invitation requests")
+            }
+            squad.AutoApproveInvitationRequests = state
+
+          case SelectRoleForYourself(position) =>
+            //TODO need to ask permission from the squad leader, unless our character is the squad leader or already currently in the squad
+            val name = tplayer.Name
+            squadOpt match {
+              case Some(squad) if squad.GUID == guid =>
+              //already a member of this squad; swap positions freely
+              case Some(squad) =>
+              //not a member of the requesting squad; do nothing
+              case None =>
+                //not a member of any squad; consider request of joining the target squad
+                idToSquad.get(guid) match {
+                  case Some(squad) =>
+                    val member = squad.Membership(position)
+                    if(squad.Availability(position) && member.CharId == 0 &&
+                      tplayer.Certifications.intersect(member.Requirements) == member.Requirements) {
+                      bids(tplayer.CharId) = (guid, position)
+                      val leader = squad.Leader
+                      //TODO need to ask permission from the squad leader, unless auto-approve is in effect
+                      sender ! SquadServiceResponse("", SquadResponse.Membership(SquadRequestType.Invite, 0, 0, leader.CharId, Some(tplayer.CharId), leader.Name, false, None))
+                    }
+
+                  case None =>
+                  //squad does not exist!? assume old data
+                  //reload squad list data and blank the squad definition the user is looking at
+                }
+            }
+
+          case ListSquad() =>
+            val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt)
+            if(!squad.Listed) {
+              log.info(s"${tplayer.Name}-${tplayer.Faction} has opened recruitment for this squad")
+              squad.Listed = true
+            }
+            UpdateSquadList(squad, List())
+
+          case ResetAll() =>
+            val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt)
+            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()
+            })
+            UpdateSquadList(squad, List(SquadInfo.Field.Task, SquadInfo.Field.ZoneId, SquadInfo.Field.Size, SquadInfo.Field.Capacity))
+            UpdateSquadDetail(squad)
+
+          case DisplaySquad() =>
+            idToSquad.get(guid) match {
+              case Some(squad) =>
+                sender ! SquadServiceResponse(s"${tplayer.Name}/Squad", GenSquadDetail(squad))
+              case None => ;
+            }
+
+          case _ => ;
+        }
+
+      case msg =>
+        log.info(s"Unhandled message $msg from $sender")
+    }
   }
 
   def UpdateSquadList(squad : Squad, listingChanged : List[Int]) : Unit = {
@@ -380,7 +433,7 @@ class SquadService extends Actor {
   def GenSquadDetail(squad : Squad) : SquadResponse.Detail = {
     SquadResponse.Detail(
       squad.GUID,
-      squad.Leader,
+      squad.Leader.Name,
       squad.Task,
       PlanetSideZoneID(squad.ZoneId),
       squad.Membership.zipWithIndex.map({ case (p, index) =>
@@ -410,7 +463,7 @@ class SquadService extends Actor {
 object SquadService {
   def Publish(squad : Squad) : SquadInfo = {
     SquadInfo(
-      squad.Leader,
+      squad.Leader.Name,
       squad.Task,
       PlanetSideZoneID(squad.ZoneId),
       squad.Size,
diff --git a/common/src/main/scala/services/teamwork/SquadServiceMessage.scala b/common/src/main/scala/services/teamwork/SquadServiceMessage.scala
index 2157d44c..1e6c5559 100644
--- a/common/src/main/scala/services/teamwork/SquadServiceMessage.scala
+++ b/common/src/main/scala/services/teamwork/SquadServiceMessage.scala
@@ -2,12 +2,9 @@
 package services.teamwork
 
 import net.psforever.objects.Player
-import net.psforever.packet.game.{PlanetSideGUID, SquadAction}
 
-final case class SquadServiceMessage(forChannel : String, actionMessage : Any)
+final case class SquadServiceMessage(tplayer : Player, actionMessage : Any)
 
 object SquadServiceMessage {
-  final case class SquadDefinitionAction(player : Player, zone_ordinal_number : Int, guid : PlanetSideGUID, line : Int, action : SquadAction)
-
   final case class RecoverSquadMembership()
 }
diff --git a/common/src/test/scala/game/SquadDetailDefinitionUpdateMessageTest.scala b/common/src/test/scala/game/SquadDetailDefinitionUpdateMessageTest.scala
index 8f04cf53..9e3fd2c1 100644
--- a/common/src/test/scala/game/SquadDetailDefinitionUpdateMessageTest.scala
+++ b/common/src/test/scala/game/SquadDetailDefinitionUpdateMessageTest.scala
@@ -146,7 +146,7 @@ class SquadDetailDefinitionUpdateMessageTest extends Specification {
           guid mustEqual PlanetSideGUID(3)
           detail match {
             case SquadDetail(None, None, None, None, None, None, None, None, Some(members)) =>
-              members.size mustEqual 2
+              members.size mustEqual 1
               members.head.index mustEqual 5
               members.head.info match {
                 case Some(SquadPositionDetail(Some(is_closed), None, None, None, None, None)) =>
@@ -169,7 +169,7 @@ class SquadDetailDefinitionUpdateMessageTest extends Specification {
           guid mustEqual PlanetSideGUID(7)
           detail match {
             case SquadDetail(None, None, None, None, None, None, None, None, Some(members)) =>
-              members.size mustEqual 2
+              members.size mustEqual 1
               members.head.index mustEqual 0
               members.head.info match {
                 case Some(SquadPositionDetail(None, Some(role), None, None, None, None)) =>
@@ -192,7 +192,7 @@ class SquadDetailDefinitionUpdateMessageTest extends Specification {
           guid mustEqual PlanetSideGUID(1)
           detail match {
             case SquadDetail(None, None, None, None, None, None, None, None, Some(members)) =>
-              members.size mustEqual 2
+              members.size mustEqual 1
               members.head.index mustEqual 6
               members.head.info match {
                 case Some(SquadPositionDetail(None, Some(role), None, Some(req), None, None)) =>
@@ -217,7 +217,7 @@ class SquadDetailDefinitionUpdateMessageTest extends Specification {
           guid mustEqual PlanetSideGUID(3)
           detail match {
             case SquadDetail(None, None, None, None, None, None, None, None, Some(members)) =>
-              members.size mustEqual 2
+              members.size mustEqual 1
               members.head.index mustEqual 5
               members.head.info match {
                 case Some(SquadPositionDetail(None, None, None, None, Some(char_id), Some(name))) =>
@@ -242,7 +242,7 @@ class SquadDetailDefinitionUpdateMessageTest extends Specification {
           detail match {
             case SquadDetail(None, None, None, None, None, Some(task), None, None, Some(members)) =>
               task mustEqual "\\#FF0000 The \\#ffffff Blades"
-              members.size mustEqual 11
+              members.size mustEqual 10
               //
               members.head.index mustEqual 9
               members.head.info match {
@@ -399,7 +399,7 @@ class SquadDetailDefinitionUpdateMessageTest extends Specification {
           detail match {
             case SquadDetail(None, None, None, None, None, Some(task), None, None, Some(member_list)) =>
               task mustEqual "PSForever Packet Collection"
-              member_list.size mustEqual 11
+              member_list.size mustEqual 10
               member_list.head mustEqual SquadPositionEntry(9,Some(
                 SquadPositionDetail(
                   Some(false),
@@ -516,8 +516,7 @@ class SquadDetailDefinitionUpdateMessageTest extends Specification {
           .Field1(0)
           .LeaderCharId(1221560L)
           .Members(List(
-            SquadPositionEntry(6, SquadPositionDetail().Player(0L, "")),
-            SquadPositionEntry(255, None)
+            SquadPositionEntry(6, SquadPositionDetail().Player(0L, ""))
           ))
       )
       val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
@@ -540,8 +539,7 @@ class SquadDetailDefinitionUpdateMessageTest extends Specification {
           .Leader(42631712L, "Jaako")
           .Field3(556403L)
           .Members(List(
-            SquadPositionEntry(0, SquadPositionDetail().Player(0L, "")),
-            SquadPositionEntry(255, None)
+            SquadPositionEntry(0, SquadPositionDetail().Player(0L, ""))
           ))
       )
       val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
@@ -593,8 +591,7 @@ class SquadDetailDefinitionUpdateMessageTest extends Specification {
             SquadPositionEntry(3, SquadPositionDetail().Role("The Princess")),
             SquadPositionEntry(2, SquadPositionDetail().Role("The Prince")),
             SquadPositionEntry(1, SquadPositionDetail().Role("The Queen")),
-            SquadPositionEntry(0, SquadPositionDetail().Role("The King")),
-            SquadPositionEntry(255, None)
+            SquadPositionEntry(0, SquadPositionDetail().Role("The King"))
           ))
       )
       val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
@@ -606,8 +603,7 @@ class SquadDetailDefinitionUpdateMessageTest extends Specification {
         PlanetSideGUID(3),
         SquadDetail()
           .Members(List(
-            SquadPositionEntry(5, SquadPositionDetail.Closed),
-            SquadPositionEntry(255, None)
+            SquadPositionEntry(5, SquadPositionDetail.Closed)
           ))
       )
       val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
@@ -620,8 +616,7 @@ class SquadDetailDefinitionUpdateMessageTest extends Specification {
         PlanetSideGUID(7),
         SquadDetail()
           .Members(List(
-            SquadPositionEntry(0, SquadPositionDetail().Role("Commander")),
-            SquadPositionEntry(255, None)
+            SquadPositionEntry(0, SquadPositionDetail().Role("Commander"))
           ))
       )
       val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
@@ -635,8 +630,7 @@ class SquadDetailDefinitionUpdateMessageTest extends Specification {
           .Members(List(
             SquadPositionEntry(6, SquadPositionDetail()
               .Role("ADV Hacker")
-              .Requirements(Set(CertificationType.AdvancedHacking))),
-            SquadPositionEntry(255, None)
+              .Requirements(Set(CertificationType.AdvancedHacking)))
           ))
       )
       val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
@@ -648,8 +642,7 @@ class SquadDetailDefinitionUpdateMessageTest extends Specification {
         PlanetSideGUID(3),
         SquadDetail()
           .Members(List(
-          SquadPositionEntry(5, SquadPositionDetail().Player(1218249L, "Duckmaster43")),
-          SquadPositionEntry(255, None)
+          SquadPositionEntry(5, SquadPositionDetail().Player(1218249L, "Duckmaster43"))
         ))
       )
       val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
@@ -671,8 +664,7 @@ class SquadDetailDefinitionUpdateMessageTest extends Specification {
             SquadPositionEntry(3, SquadPositionDetail().Role("")),
             SquadPositionEntry(2, SquadPositionDetail().Role("")),
             SquadPositionEntry(1, SquadPositionDetail().Role("")),
-            SquadPositionEntry(0, SquadPositionDetail().Role("")),
-            SquadPositionEntry(255, None)
+            SquadPositionEntry(0, SquadPositionDetail().Role(""))
           ))
       )
       val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
@@ -715,7 +707,7 @@ class SquadDetailDefinitionUpdateMessageTest extends Specification {
         PlanetSideGUID(3),
         SquadDetail
           .Task("PSForever Packet Collection")
-          .Members((0 to 9).map { index => SquadPositionEntry(index, position) }.reverse.toList :+ SquadPositionEntry(255, None))
+          .Members((0 to 9).map { index => SquadPositionEntry(index, position) }.reverse.toList)
       )
       val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
       pkt mustEqual string_mixed
diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala
index 7aa34958..0499a0aa 100644
--- a/pslogin/src/main/scala/WorldSessionActor.scala
+++ b/pslogin/src/main/scala/WorldSessionActor.scala
@@ -52,7 +52,7 @@ import services.galaxy.{GalaxyResponse, GalaxyServiceResponse}
 import services.local.{LocalAction, LocalResponse, LocalServiceMessage, LocalServiceResponse}
 import services.vehicle.support.TurretUpgrader
 import services.vehicle.{VehicleAction, VehicleResponse, VehicleServiceMessage, VehicleServiceResponse}
-import services.teamwork._
+import services.teamwork.{SquadAction => SquadServiceAction, SquadServiceMessage, SquadServiceResponse, SquadResponse, SquadService}
 
 import scala.concurrent.duration._
 import scala.concurrent.ExecutionContext.Implicits.global
@@ -376,16 +376,65 @@ class WorldSessionActor extends Actor with MDCContextAware {
             SquadDetailDefinitionUpdateMessage(
               guid,
               SquadDetail()
-                .LeaderCharId(member_info.find(_.name == leader).get.char_id.get)
+                .LeaderCharId(member_info.find(_.name.contains(leader)).get.char_id.get)
                 .LeaderName(leader)
                 .Task(task)
                 .ZoneId(zone)
-                .Members(
-                  member_info.zipWithIndex.map { case (a, b) => SquadPositionEntry(b, a) }
-                )
+                .Members(member_info.zipWithIndex.map { case (a, b) => SquadPositionEntry(b, a) })
                 .Complete
             )
           )
+
+        case SquadResponse.Membership(request_type, unk1, unk2, unk3, unk4, player_name, unk5, unk6) =>
+          sendResponse(SquadMembershipResponse(request_type, unk1, unk2, unk3, unk4, player_name, unk5, unk6))
+
+        case SquadResponse.Join(squad, positionsToUpdate) =>
+          val leader = squad.Leader
+          val id = 11
+          val membershipPositions = squad.Membership
+            .zipWithIndex
+            .filter { case (_, index ) => positionsToUpdate.contains(index) }
+          sendResponse(SquadMembershipResponse(SquadRequestType.Accept, 0, 0, player.CharId, Some(leader.CharId), player.Name, true, Some(None)))
+          //load each member's entry
+          membershipPositions.foreach { case(member, index) =>
+            sendResponse(SquadMemberEvent(0, id, member.CharId, index, Some(member.Name), Some(member.ZoneId), Some(0)))
+          }
+          //repeat own entry and initialize connection to squad
+          membershipPositions.find({ case(member, _) => member.CharId == avatar.CharId }) match {
+            case Some((member, index)) =>
+              sendResponse(SquadMemberEvent(0, id, member.CharId, index, Some(member.Name), Some(member.ZoneId), Some(0)))
+              sendResponse(PlanetsideAttributeMessage(player.GUID, 31, id)) //associate with squad?
+              sendResponse(PlanetsideAttributeMessage(player.GUID, 32, index)) //associate with member position in squad?
+            case _ => ;
+          }
+          //send first update for map icons
+          sendResponse(SquadState(PlanetSideGUID(id),
+            membershipPositions
+              .filterNot { case (member, _) => member.CharId == avatar.CharId }
+              .map{ case (member, _) => SquadStateInfo(member.CharId, 64,64, member.Position, 2,2, false, 429, None,None) }
+              .toList
+          ))
+          //a finalization? what does this do?
+          sendResponse(SquadDefinitionActionMessage(squad.GUID, 0, SquadAction.Unknown(18, hex"00".toBitVector.take(6))))
+
+        case SquadResponse.Leave(_, positionsToUpdate) =>
+          val id = 11
+          sendResponse(SquadMembershipResponse(SquadRequestType.Leave, 0,1, avatar.CharId, Some(avatar.CharId), avatar.name, true, Some(None)))
+          //remove each member's entry (our own too)
+          positionsToUpdate.foreach { case(member, index) =>
+            sendResponse(SquadMemberEvent(1, id, member, index, None, None, None))
+          }
+          //repeat own entry and rescind connection to squad
+          positionsToUpdate.find({ case(member, _) => member == avatar.CharId }) match {
+            case Some((member, index)) =>
+              sendResponse(SquadMemberEvent(1, id, member, index, None, None, None))
+              sendResponse(PlanetsideAttributeMessage(player.GUID, 31, 0)) //disassociate with squad?
+              sendResponse(PlanetsideAttributeMessage(player.GUID, 32, 0)) //disassociate with member position in squad?
+              sendResponse(PlanetsideAttributeMessage(player.GUID, 34, 4294967295L)) //unknown
+            case _ => ;
+          }
+          //a finalization? what does this do?
+          sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(18, hex"00".toBitVector.take(6))))
         case _ => ;
       }
 
@@ -2911,42 +2960,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
         interstellarFerryTopLevelGUID = None
       case _ => ;
     }
-    //SQUAD TESTING CODE
-    sendResponse(ReplicationStreamMessage(
-      5,
-      Some(6),
-      Vector(
-        SquadListing(0, SquadInfo(Some("xNick"), Some("FLY,ALL WELCOME!"), Some(PlanetSideZoneID(7)), Some(8), Some(10), Some(PlanetSideGUID(1)))),
-        SquadListing(1, SquadInfo(Some("HofD"), Some("=KOK+SPC+FLY= All Welcome"), Some(PlanetSideZoneID(7)), Some(3), Some(10), Some(PlanetSideGUID(3))))
-      )
-    ))
-    sendResponse(
-      SquadDetailDefinitionUpdateMessage(
-        PlanetSideGUID(3),
-        SquadDetail(
-          3,
-          1792,
-          42771010L,
-          529745L,
-          "HofD",
-          "\\#ffdc00***\\#9640ff=KOK+SPC+FLY=\\#ffdc00***\\#FF4040 All Welcome",
-          PlanetSideZoneID(7),
-          4983296,
-          List(
-            SquadPositionEntry(0, SquadPositionDetail("\\#ff0000 |||||||||||||||||||||||", "", Set(), 0, "")),
-            SquadPositionEntry(1, SquadPositionDetail("\\#ffdc00   C", "", Set(), 0, "")),
-            SquadPositionEntry(2, SquadPositionDetail("\\#ffdc00   H", "", Set(), 42644970L, "OpolE")),
-            SquadPositionEntry(3, SquadPositionDetail("\\#ffdc00    I", "", Set(), 41604210L, "BobaF3tt907")),
-            SquadPositionEntry(4, SquadPositionDetail("\\#ffdc00   N", "", Set(), 0, "")),
-            SquadPositionEntry(5, SquadPositionDetail("\\#ffdc00   A", "", Set(), 0, "")),
-            SquadPositionEntry(6, SquadPositionDetail("\\#ff0000 |||||||||||||||||||||||", "", Set(), 0, "")),
-            SquadPositionEntry(7, SquadPositionDetail("\\#9640ff   K", "", Set(), 0, "")),
-            SquadPositionEntry(8, SquadPositionDetail("\\#9640ff   O", "", Set(), 42771010L ,"HofD")),
-            SquadPositionEntry(9, SquadPositionDetail("\\#9640ff   K", "", Set(), 0, ""))
-          )
-        )
-      )
-    )
+    squadService ! Service.Join(s"${avatar.faction}") //channel will be player.Faction
   }
 
   def handleControlPkt(pkt : PlanetSideControlPacket) = {
@@ -3359,18 +3373,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
     case msg @ PlayerStateMessageUpstream(avatar_guid, pos, vel, yaw, pitch, yaw_upper, seq_time, unk3, is_crouching, is_jumping, unk4, is_cloaking, unk5, unk6) =>
       if(deadState == DeadState.Alive) {
         if(!player.Crouching && is_crouching) { //SQUAD TESTING CODE
-          sendRawResponse(hex"e80300818800015c5189004603408c000000012000ff")
-//          sendResponse(SquadMembershipResponse(SquadRequestType.Invite,0,0,42771010,Some(avatar.CharId),"HofD",false,None))
-//          sendResponse(SquadMembershipResponse(SquadRequestType.Accept,0,0,avatar.CharId,Some(42771010),"VirusGiver",true,Some(None)))
-//          sendResponse(SquadMemberEvent(0,7,42771010,0,Some("HofD"),Some(7),Some(529745)))
-//          sendResponse(SquadMemberEvent(0,7,42644970,1,Some("OpolE"),Some(7),Some(6418)))
-//          sendResponse(SquadMemberEvent(0,7,41604210,8,Some("BobaF3tt907"),Some(12),Some(8097)))
-//          sendResponse(PlanetsideAttributeMessage(player.GUID, 49, 7))
-//          sendResponse(PlanetsideAttributeMessage(player.GUID, 50, 2))
-//          sendResponse(PlanetsideAttributeMessage(player.GUID, 51, 8))
-//          sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(3), 0, SquadAction.Unknown(18, hex"".toBitVector)))
-//          sendResponse(PlanetsideAttributeMessage(player.GUID, 83, 0))
-//          sendResponse(SquadState(PlanetSideGUID(7),List(SquadStateInfo(avatar.CharId,64,64,Vector3(3464.0469f,4065.5703f,20.015625f),2,2,false,0,None,None))))
+          //...
         }
         player.Position = pos
         player.Velocity = vel
@@ -4850,7 +4853,29 @@ class WorldSessionActor extends Actor with MDCContextAware {
 
     case msg @ SquadDefinitionActionMessage(u1, u2, action) =>
       log.info(s"SquadDefinitionAction: $msg")
-      squadService ! SquadServiceMessage.SquadDefinitionAction(player, continent.Number, u1, u2, action)
+        squadService ! SquadServiceMessage(player, SquadServiceAction.Definition(player, continent.Number, u1, u2, action))
+
+    case msg @ SquadMembershipRequest(request_type, unk2, unk3, player_name, unk5) =>
+      log.info(s"$msg")
+      squadService ! SquadServiceMessage(player, SquadServiceAction.Membership(request_type, unk2, unk3, player_name, unk5))
+//      if(request_type == SquadRequestType.Accept) {
+//        sendResponse(SquadMembershipResponse(SquadRequestType.Accept, 0, 0, unk2, Some(30910985L), player.Name, true, Some(None)))
+//        sendResponse(SquadMemberEvent(0, 11, 30910985L, 0, Some("Wizkid45"), Some(5), Some(320036)))
+//        sendResponse(SquadMemberEvent(0, 11, 42781919L, 1, Some("xoBLADEox"), Some(5), Some(528805)))
+//        sendResponse(SquadMemberEvent(0, 11, avatar.CharId, 2, Some(player.Name), Some(13), Some(320036)))
+//        sendResponse(SquadMemberEvent(0, 11, 353380L, 3, Some("cabal0428") ,Some(5), Some(8156)))
+//        sendResponse(SquadMemberEvent(0, 11, 41588340L, 4 ,Some("xSkiku"), Some(5), Some(0)))
+//        sendResponse(SquadMemberEvent(0, 11, avatar.CharId, 2, Some(player.Name), Some(13), Some(320036)))
+//        sendResponse(PlanetsideAttributeMessage(player.GUID, 31, 11)) //associate with squad?
+//        sendResponse(PlanetsideAttributeMessage(player.GUID, 32, 2)) //associate with member position in squad?
+//        sendResponse(SquadState(PlanetSideGUID(11),List(
+//          SquadStateInfo(30910985L, 50,64, Vector3(5526.5234f,3818.7344f,54.59375f), 2,2, false, 429, None,None),
+//          SquadStateInfo(42781919L, 64,0, Vector3(4673.5312f,2604.8047f,40.015625f), 2,2, false, 149, None,None),
+//          SquadStateInfo(353380L, 64,64, Vector3(4727.492f,2613.5312f,51.390625f), 2,2, false, 0, None,None),
+//          SquadStateInfo(41588340L, 64,64, Vector3(3675.0f,4789.8047f,63.21875f), 2,2, false, 0, None,None)
+//        )))
+//        sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(3), 0, SquadAction.Unknown(18, hex"00".toBitVector.take(6))))
+//      }
 
     case msg @ GenericCollisionMsg(u1, p, t, php, thp, pv, tv, ppos, tpos, u2, u3, u4) =>
       log.info("Ouch! " + msg)

From 640a96ae9c84f936f179427e2fb3b8205ab192ad Mon Sep 17 00:00:00 2001
From: FateJH <Jason_DiDonato@yahoo.com>
Date: Wed, 10 Jul 2019 16:12:58 -0400
Subject: [PATCH 13/31] wrote comments for SquadState, SquadMembershipRequest,
 and SquadMembershipResponse; messaging for players joining and leaving squads
 in WSA and SquadService; char_id is now a required field for Avatars

---
 .../scala/net/psforever/objects/Avatar.scala  |  16 +-
 .../packet/game/SquadMemberEvent.scala        |  14 ++
 .../packet/game/SquadMembershipRequest.scala  |  16 +-
 .../packet/game/SquadMembershipResponse.scala |  30 ++--
 .../psforever/packet/game/SquadState.scala    |  57 +++++--
 .../scala/services/teamwork/SquadAction.scala |   5 +-
 .../services/teamwork/SquadResponse.scala     |   1 +
 .../services/teamwork/SquadService.scala      |  76 +++++++++-
 .../src/main/scala/WorldSessionActor.scala    | 140 ++++++++++--------
 9 files changed, 237 insertions(+), 118 deletions(-)

diff --git a/common/src/main/scala/net/psforever/objects/Avatar.scala b/common/src/main/scala/net/psforever/objects/Avatar.scala
index 01ce2d46..ceae122b 100644
--- a/common/src/main/scala/net/psforever/objects/Avatar.scala
+++ b/common/src/main/scala/net/psforever/objects/Avatar.scala
@@ -10,9 +10,8 @@ import net.psforever.types._
 import scala.annotation.tailrec
 import scala.collection.mutable
 
-class Avatar(val name : String, val faction : PlanetSideEmpire.Value, val sex : CharacterGender.Value, val head : Int, val voice : CharacterVoice.Value) {
-  /** Character ID; a unique identifier corresponding to a database table row index */
-  private var charId : Long = 0
+class Avatar(private val char_id : Long, val name : String, val faction : PlanetSideEmpire.Value, val sex : CharacterGender.Value, val head : Int, val voice : CharacterVoice.Value) {
+  /** char_id, Character ID; a unique identifier corresponding to a database table row index */
   /** Battle Experience Points */
   private var bep : Long = 0
   /** Command Experience Points */
@@ -48,14 +47,7 @@ class Avatar(val name : String, val faction : PlanetSideEmpire.Value, val sex :
 
   private val deployables : DeployableToolbox = new DeployableToolbox
 
-  def CharId : Long = charId
-
-  def CharId_=(id : Long) : Long = {
-    if(charId == 0) { //does not need to be set but can only set once
-      charId = id
-    }
-    CharId
-  }
+  def CharId : Long = char_id
 
   def BEP : Long = bep
 
@@ -226,7 +218,7 @@ class Avatar(val name : String, val faction : PlanetSideEmpire.Value, val sex :
 
 object Avatar {
   def apply(name : String, faction : PlanetSideEmpire.Value, sex : CharacterGender.Value, head : Int, voice : CharacterVoice.Value) : Avatar = {
-    new Avatar(name, faction, sex, head, voice)
+    new Avatar(0L, name, faction, sex, head, voice)
   }
 
   def toString(avatar : Avatar) : String = s"${avatar.faction} ${avatar.name}"
diff --git a/common/src/main/scala/net/psforever/packet/game/SquadMemberEvent.scala b/common/src/main/scala/net/psforever/packet/game/SquadMemberEvent.scala
index 50dec0f6..503254a5 100644
--- a/common/src/main/scala/net/psforever/packet/game/SquadMemberEvent.scala
+++ b/common/src/main/scala/net/psforever/packet/game/SquadMemberEvent.scala
@@ -6,6 +6,20 @@ import scodec.{Attempt, Codec, Err}
 import scodec.codecs._
 import shapeless.{::, HNil}
 
+object MemberEvent extends Enumeration {
+  type Type = Value
+
+  val
+  Add,
+  Remove,
+  Unknown2,
+  UpdateZone,
+  Unknown4
+    = Value
+
+  implicit val codec = PacketHelpers.createEnumerationCodec(this, uint(3))
+}
+
 final case class SquadMemberEvent(unk1 : Int,
                                   unk2 : Int,
                                   char_id : Long,
diff --git a/common/src/main/scala/net/psforever/packet/game/SquadMembershipRequest.scala b/common/src/main/scala/net/psforever/packet/game/SquadMembershipRequest.scala
index 27cf2b79..c5bd3550 100644
--- a/common/src/main/scala/net/psforever/packet/game/SquadMembershipRequest.scala
+++ b/common/src/main/scala/net/psforever/packet/game/SquadMembershipRequest.scala
@@ -7,15 +7,17 @@ import scodec.Codec
 import scodec.codecs._
 
 /**
-  * SquadMembershipRequest is a Client->Server message for manipulating squad and platoon members.
-  * @param request_type na
-  * @param unk2 na
-  * @param unk3 na
-  * @param player_name na
-  * @param unk5 na
+  * Dispatched by the client as manipulation protocol for squad and platoon members.
+  * Answerable by a `SquadMembershipResponse` packet.
+  * @param request_type the purpose of the request
+  * @param char_id      a squad member unique identifier;
+  *                     usually, the player being addresses by thie packet
+  * @param unk3         na
+  * @param player_name  name of the player being affected, if applicable
+  * @param unk5         na
   */
 final case class SquadMembershipRequest(request_type : SquadRequestType.Value,
-                                        unk2 : Long,
+                                        char_id : Long,
                                         unk3 : Option[Long],
                                         player_name : String,
                                         unk5 : Option[Option[String]])
diff --git a/common/src/main/scala/net/psforever/packet/game/SquadMembershipResponse.scala b/common/src/main/scala/net/psforever/packet/game/SquadMembershipResponse.scala
index 0d01a938..ee1746dd 100644
--- a/common/src/main/scala/net/psforever/packet/game/SquadMembershipResponse.scala
+++ b/common/src/main/scala/net/psforever/packet/game/SquadMembershipResponse.scala
@@ -7,31 +7,29 @@ import scodec.Codec
 import scodec.codecs._
 
 /**
-  * na
-  * @param request_type the type of request being answered
+  * Dispatched by the server as manipulation protocol for squad and platoon members.
+  * Prompted by and answers for a `SquadMembershipRequest` packet.
+  * @param request_type the purpose of the request
   * @param unk1 na
   * @param unk2 na
-  * @param unk3 na
-  * @param unk4 na
-  * @param player_name the player being affected, if applicable
+  * @param char_id a squad member unique identifier;
+  *                usually, the player being addresses by thie packet
+  * @param other_id another squad member's unique identifier;
+  *                 may be the same as `char_id`
+  * @param player_name name of the player being affected, if applicable
   * @param unk5 na
-  * @param unk6 na
+  * @param unk6 na;
+  *             the internal field, the `Option[String]`, never seems to be set
   */
 final case class SquadMembershipResponse(request_type : SquadRequestType.Value,
                                          unk1 : Int,
                                          unk2 : Int,
-                                         unk3 : Long,
-                                         unk4 : Option[Long],
+                                         char_id : Long,
+                                         other_id : Option[Long],
                                          player_name : String,
                                          unk5 : Boolean,
                                          unk6 : Option[Option[String]])
   extends PlanetSideGamePacket {
-  /*
-    if(response_type != 6 && response_type != 12)
-      assert(unk5.isDefined, "unk5 field required")
-    else
-      assert(!unk5.isDefined, "unk5 defined but unk1 invalid value")
-  */
   type Packet = SquadMembershipResponse
   def opcode = GamePacketOpcode.SquadMembershipResponse
   def encode = SquadMembershipResponse.encode(this)
@@ -42,8 +40,8 @@ object SquadMembershipResponse extends Marshallable[SquadMembershipResponse] {
     "request_type" | SquadRequestType.codec >>:~ { d =>
       ("unk1" | uint(5)) ::
         ("unk2" | uint2) ::
-        ("unk3" | uint32L) ::
-        ("unk4" | conditional(d != SquadRequestType.Promote && d != SquadRequestType.PlatoonLeave, uint32L)) ::
+        ("char_id" | uint32L) ::
+        ("other_id" | conditional(d != SquadRequestType.Promote && d != SquadRequestType.PlatoonLeave, uint32L)) ::
         ("player_name" | PacketHelpers.encodedWideStringAligned(5)) ::
         ("unk5" | bool) ::
         conditional(d != SquadRequestType.Invite, optional(bool, "unk6" | PacketHelpers.encodedWideStringAligned(6)))
diff --git a/common/src/main/scala/net/psforever/packet/game/SquadState.scala b/common/src/main/scala/net/psforever/packet/game/SquadState.scala
index ae745a74..dc092749 100644
--- a/common/src/main/scala/net/psforever/packet/game/SquadState.scala
+++ b/common/src/main/scala/net/psforever/packet/game/SquadState.scala
@@ -7,9 +7,27 @@ import scodec.{Attempt, Codec, Err}
 import scodec.codecs._
 import shapeless.{::, HNil}
 
+/**
+  * Information about a specific squad member.
+  * @param char_id the character's unique identifier
+  * @param health the character's health value percentage, divided into 64 units
+  * @param armor the character's armor value percentage, divided into 64 units
+  * @param pos the world coordinates of the character
+  * @param unk4 na;
+  *             usually, 2
+  * @param unk5 na;
+  *             usually, 2
+  * @param unk6 na;
+  *             usually, false
+  * @param unk7 na
+  * @param unk8 na;
+  *             if defined, will be defined with unk9
+  * @param unk9 na;
+  *             if defined, will be defined with unk8
+  */
 final case class SquadStateInfo(char_id : Long,
-                                unk2 : Int,
-                                unk3 : Int,
+                                health : Int,
+                                armor : Int,
                                 pos : Vector3,
                                 unk4 : Int,
                                 unk5 : Int,
@@ -18,12 +36,19 @@ final case class SquadStateInfo(char_id : Long,
                                 unk8 : Option[Int],
                                 unk9 : Option[Boolean])
 
-//7704001646c7a02810050a1a2bc97842280000
-// SquadState(PlanetSideGUID(4),List(SquadStateInfo(1684830722,64,64,Vector3(3152.1562,3045.0781,35.515625),2,2,false,0,None,None)))
-//770700342a28c028100420d60e9df8c2800000eab58a028100ce514b655ddc341400286d9130000021eb951539f4c4050800
-// SquadState(PlanetSideGUID(7),List(SquadStateInfo(1117948930,64,64,Vector3(2822.125,3919.0234,43.546875),0,0,false,0,None,None), SquadStateInfo(3937765890,64,64,Vector3(2856.3984,3882.3516,53.859375),0,0,false,416,None,None), SquadStateInfo(2262373120,0,0,Vector3(2909.0547,3740.539,67.296875),0,1,false,132,None,None)))
-//7704005dd9ccf01810132fdf9f9ef5c4a8000084de7a022c00e5898c8d5e4c429b004ed01d50181016395a4c364e08280001b901a070bd0140805308d59f90641f40000c001db11e280a00088d19e3a190f0ca6c0100
-// SquadState(PlanetSideGUID(4),List(SquadStateInfo(3718041345,64,64,Vector3(3966.5938,6095.8047,75.359375),2,2,false,0,None,None), SquadStateInfo(2229172738,22,0,Vector3(3268.4453,3690.3906,66.296875),2,2,false,728,None,None), SquadStateInfo(3976320257,64,64,Vector3(3530.6875,4635.1484,128.875),2,2,false,0,Some(441),Some(true)), SquadStateInfo(1088518658,64,64,Vector3(3336.3203,4601.3438,60.78125),2,2,false,0,Some(896),Some(true)), SquadStateInfo(1816627714,64,0,Vector3(5027.0625,4931.7734,48.234375),2,2,false,728,None,None)))
+/**
+  * Dispatched by the server to update a squad member's representative icons on the continental maps and the interstellar map.<br>
+  * <br>
+  * This packet must be preceded by the correct protocol
+  * to assign any character who is defined by `char_id` in `info_list`
+  * as a member of this client's player's assigned squad by means of associating that said `char_id`.
+  * The said preceding protocol also assigns the player's current zone (continent) and their ordinal position in the squad.
+  * @see `SquadMemberEvent`
+  * @param guid the squad's unique identifier;
+  *             must be consistent per packet on a given client;
+  *             does not have to be the global uid of the squad as according to the server
+  * @param info_list information about the members in this squad who will be updated
+  */
 final case class SquadState(guid : PlanetSideGUID,
                             info_list : List[SquadStateInfo])
   extends PlanetSideGamePacket {
@@ -43,8 +68,8 @@ object SquadStateInfo {
 object SquadState extends Marshallable[SquadState] {
   private val info_codec : Codec[SquadStateInfo] = (
     ("char_id" | uint32L) ::
-      ("unk2" | uint(7)) ::
-      ("unk3" | uint(7)) ::
+      ("health" | uint(7)) ::
+      ("armor" | uint(7)) ::
       ("pos" | Vector3.codec_pos) ::
       ("unk4" | uint2) ::
       ("unk5" | uint2) ::
@@ -56,14 +81,14 @@ object SquadState extends Marshallable[SquadState] {
       })
     ).exmap[SquadStateInfo] (
     {
-      case char_id :: u2 :: u3 :: pos :: u4 :: u5 :: u6 :: u7 :: _ :: u8 :: u9 :: HNil =>
-        Attempt.Successful(SquadStateInfo(char_id, u2, u3, pos, u4, u5, u6, u7, u8, u9))
+      case char_id :: health :: armor :: pos :: u4 :: u5 :: u6 :: u7 :: _ :: u8 :: u9 :: HNil =>
+        Attempt.Successful(SquadStateInfo(char_id, health, armor, pos, u4, u5, u6, u7, u8, u9))
     },
     {
-      case SquadStateInfo(char_id, u2, u3, pos, u4, u5, u6, u7, Some(u8), Some(u9)) =>
-        Attempt.Successful(char_id :: u2 :: u3 :: pos :: u4 :: u5 :: u6 :: u7 :: true :: Some(u8) :: Some(u9) :: HNil)
-      case SquadStateInfo(char_id, u2, u3, pos, u4, u5, u6, u7, None, None) =>
-        Attempt.Successful(char_id :: u2 :: u3 :: pos :: u4 :: u5 :: u6 :: u7 :: false :: None :: None :: HNil)
+      case SquadStateInfo(char_id, health, armor, pos, u4, u5, u6, u7, Some(u8), Some(u9)) =>
+        Attempt.Successful(char_id :: health :: armor :: pos :: u4 :: u5 :: u6 :: u7 :: true :: Some(u8) :: Some(u9) :: HNil)
+      case SquadStateInfo(char_id, health, armor, pos, u4, u5, u6, u7, None, None) =>
+        Attempt.Successful(char_id :: health :: armor :: pos :: u4 :: u5 :: u6 :: u7 :: false :: None :: None :: HNil)
       case data @ (SquadStateInfo(_, _, _, _, _, _, _, _, Some(_), None) | SquadStateInfo(_, _, _, _, _, _, _, _, None, Some(_))) =>
         Attempt.Failure(Err(s"SquadStateInfo requires both unk8 and unk9 to be either defined or undefined at the same time - $data"))
     }
diff --git a/common/src/main/scala/services/teamwork/SquadAction.scala b/common/src/main/scala/services/teamwork/SquadAction.scala
index 744a4940..1677cf75 100644
--- a/common/src/main/scala/services/teamwork/SquadAction.scala
+++ b/common/src/main/scala/services/teamwork/SquadAction.scala
@@ -3,11 +3,12 @@ package services.teamwork
 
 import net.psforever.objects.Player
 import net.psforever.packet.game._
-import net.psforever.types.SquadRequestType
+import net.psforever.types.{SquadRequestType, Vector3}
 
 object SquadAction {
   trait Action
 
+  final case class Definition(player : Player, zone_ordinal_number : Int, guid : PlanetSideGUID, line : Int, action : SquadAction) extends Action
   final case class Membership(request_type : SquadRequestType.Value, unk2 : Long, unk3 : Option[Long], player_name : String, unk5 : Option[Option[String]]) extends Action
-  final case class Definition(player : Player, zone_ordinal_number : Int, guid : PlanetSideGUID, line : Int, action : SquadAction)
+  final case class Update(char_id : Long, health : Int, max_health : Int, armor : Int, max_armor : Int, pos : Vector3, zone_number : Int) extends Action
 }
diff --git a/common/src/main/scala/services/teamwork/SquadResponse.scala b/common/src/main/scala/services/teamwork/SquadResponse.scala
index 773b4067..e1ab03cb 100644
--- a/common/src/main/scala/services/teamwork/SquadResponse.scala
+++ b/common/src/main/scala/services/teamwork/SquadResponse.scala
@@ -15,6 +15,7 @@ object SquadResponse {
   final case class Membership(request_type : SquadRequestType.Value, unk1 : Int, unk2 : Int, unk3 : Long, unk4 : Option[Long], player_name : String, unk5 : Boolean, unk6 : Option[Option[String]]) extends Response //see SquadMembershipResponse
   final case class Join(squad : Squad, positionsToUpdate : List[Int]) 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 Detail(guid : PlanetSideGUID, leader : String, task : String, zone : PlanetSideZoneID, member_info : List[SquadPositionDetail]) extends Response
 }
diff --git a/common/src/main/scala/services/teamwork/SquadService.scala b/common/src/main/scala/services/teamwork/SquadService.scala
index 070cdacc..9aa5954b 100644
--- a/common/src/main/scala/services/teamwork/SquadService.scala
+++ b/common/src/main/scala/services/teamwork/SquadService.scala
@@ -3,6 +3,7 @@ package services.teamwork
 
 import akka.actor.Actor
 import net.psforever.objects.Player
+import net.psforever.objects.definition.converter.StatConverter
 import net.psforever.objects.teamwork.Squad
 import net.psforever.packet.game._
 import net.psforever.types.{PlanetSideEmpire, SquadRequestType, Vector3}
@@ -36,18 +37,26 @@ class SquadService extends Actor {
     testSquad.Membership(0).Name = "Wizkid45"
     testSquad.Membership(0).CharId = 30910985L
     testSquad.Membership(0).ZoneId = 5
+    testSquad.Membership(0).Health = 64
+    testSquad.Membership(0).Armor = 34
     testSquad.Membership(0).Position = Vector3(5526.5234f, 3818.7344f, 54.59375f)
     testSquad.Membership(1).Name = "xoBLADEox"
     testSquad.Membership(1).CharId = 42781919L
     testSquad.Membership(1).ZoneId = 5
+    testSquad.Membership(1).Health = 54
+    testSquad.Membership(1).Armor = 44
     testSquad.Membership(1).Position = Vector3(4673.5312f, 2604.8047f, 40.015625f)
     testSquad.Membership(3).Name = "cabal0428"
     testSquad.Membership(3).CharId = 353380L
     testSquad.Membership(3).ZoneId = 5
+    testSquad.Membership(3).Health = 44
+    testSquad.Membership(3).Armor = 54
     testSquad.Membership(3).Position = Vector3(4727.492f, 2613.5312f, 51.390625f)
     testSquad.Membership(4).Name = "xSkiku"
     testSquad.Membership(4).CharId = 41588340L
     testSquad.Membership(4).ZoneId = 5
+    testSquad.Membership(4).Health = 34
+    testSquad.Membership(4).Armor = 64
     testSquad.Membership(4).Position = Vector3(3675.0f, 4789.8047f, 63.21875f)
     idToSquad(PlanetSideGUID(3)) = testSquad
     testSquad.Listed = true
@@ -174,7 +183,7 @@ class SquadService extends Actor {
     case Service.Leave(None) | Service.LeaveAll() => ;
 
     case SquadServiceMessage(tplayer, squad_action) => squad_action match {
-      case SquadAction.Membership(request_type, char_id, optional_char_id, name, unk5) => request_type match {
+      case SquadAction.Membership(request_type, char_id, optional_char_id, _, _) => request_type match {
         case SquadRequestType.Accept =>
           bids.get(char_id) match {
             case Some((squadGUID, line)) if idToSquad.get(squadGUID).nonEmpty =>
@@ -190,10 +199,18 @@ class SquadService extends Actor {
                 position.Position = tplayer.Position
                 position.ZoneId = 13
                 memberToSquad(tplayer.Name) = squad
+                //joining the squad
                 sender ! SquadServiceResponse("", SquadResponse.Join(
                   squad,
                   squad.Membership.zipWithIndex.collect({ case (member, index) if member.CharId != 0 => index }).toList
                 ))
+                //other squad members see us joining the squad
+                val updatedIndex = List(line)
+                squad.Membership
+                  .collect({ case member if member.CharId != 0 && member.CharId != char_id => member.Name })
+                  .foreach { name =>
+                    SquadEvents.publish( SquadServiceResponse(s"$name/Squad", SquadResponse.Join(squad, updatedIndex)) )
+                  }
               }
               bids.remove(char_id)
             case _ => ;
@@ -208,17 +225,66 @@ class SquadService extends Actor {
             val (member, index) = membership
               .find { case (_member, _) => _member.Name == name }
               .get
-            val updateList = membership.collect({ case (_member, _) if _member.CharId > 0 => (_member.CharId, index) }).toList
+            val updateList = membership.collect({ case (_member, _index) if _member.CharId > 0 => (_member.CharId, _index) }).toList
             memberToSquad.remove(name)
             member.Name = ""
             member.CharId = 0
+            //leaving the squad completely
             sender ! SquadServiceResponse("", SquadResponse.Leave(squad, updateList))
+            //other squad members see us leaving the squad
+            val leavingMember = List((char_id, index))
+            membership
+              .collect({ case (_member, _) if _member.CharId > 0 => _member.Name })
+              .foreach { name =>
+                SquadEvents.publish( SquadServiceResponse(s"$name/Squad", SquadResponse.Leave(squad, leavingMember)) )
+            }
           }
 
         case _ => ;
       }
 
-      case SquadAction.Definition(tplayer : Player, zone_ordinal_number : Int, guid : PlanetSideGUID, line : Int, action : SquadAction) =>
+      case SquadAction.Update(char_id, health, max_health, armor, max_armor, pos, zone_number) =>
+        memberToSquad.get(tplayer.Name) match {
+          case Some(squad) =>
+            squad.Membership.find(_.CharId == char_id) match {
+              case Some(member) =>
+                val newHealth = StatConverter.Health(health, max_health, min=1, max=64)
+                val newArmor = StatConverter.Health(armor, max_armor, min=1, max=64)
+                member.Health = newHealth
+                member.Armor = newArmor
+                member.Position = pos
+                member.ZoneId = zone_number
+                sender ! SquadServiceResponse("", 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 _ => ;
+            }
+//            val (self, others) = squad.Membership.partition(_.CharId == char_id)
+//            self match {
+//              case Array(member) =>
+//                val newHealth = StatConverter.Health(health, max_health, min=1, max=64)
+//                val newArmor = StatConverter.Health(armor, max_armor, min=1, max=64)
+//                member.Health = newHealth
+//                member.Armor = newArmor
+//                member.Position = pos
+//                member.ZoneId = zone_number
+//                sender ! SquadServiceResponse("", SquadResponse.UpdateMembers(
+//                  squad,
+//                  others
+//                    .map { member => SquadAction.Update(member.CharId, member.Health, 0, member.Armor, 0, member.Position, member.ZoneId) }
+//                    .toList
+//                ))
+//              case _ => ;
+//            }
+
+          case None => ;
+        }
+
+      case SquadAction.Definition(tplayer : Player, zone_ordinal_number : Int, guid : PlanetSideGUID, _ : Int, action : SquadAction) =>
         import net.psforever.packet.game.SquadAction._
         val squadOpt = GetParticipatingSquad(tplayer, zone_ordinal_number)
         action match {
@@ -320,11 +386,11 @@ class SquadService extends Actor {
 
           case SelectRoleForYourself(position) =>
             //TODO need to ask permission from the squad leader, unless our character is the squad leader or already currently in the squad
-            val name = tplayer.Name
+            //val name = tplayer.Name
             squadOpt match {
               case Some(squad) if squad.GUID == guid =>
               //already a member of this squad; swap positions freely
-              case Some(squad) =>
+              case Some(_) =>
               //not a member of the requesting squad; do nothing
               case None =>
                 //not a member of any squad; consider request of joining the target squad
diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala
index 0499a0aa..6064e729 100644
--- a/pslogin/src/main/scala/WorldSessionActor.scala
+++ b/pslogin/src/main/scala/WorldSessionActor.scala
@@ -54,6 +54,7 @@ import services.vehicle.support.TurretUpgrader
 import services.vehicle.{VehicleAction, VehicleResponse, VehicleServiceMessage, VehicleServiceResponse}
 import services.teamwork.{SquadAction => SquadServiceAction, SquadServiceMessage, SquadServiceResponse, SquadResponse, SquadService}
 
+import scala.collection.mutable.LongMap
 import scala.concurrent.duration._
 import scala.concurrent.ExecutionContext.Implicits.global
 import scala.annotation.tailrec
@@ -115,6 +116,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
     * no harm should come from leaving the field set to an old unique identifier value after the transfer period
     */
   var interstellarFerryTopLevelGUID : Option[PlanetSideGUID] = None
+  val squadUI : LongMap[SquadUIElement] = new LongMap[SquadUIElement]()
 
   var amsSpawnPoints : List[SpawnPoint] = Nil
   var clientKeepAlive : Cancellable = DefaultCancellable.obj
@@ -394,47 +396,92 @@ class WorldSessionActor extends Actor with MDCContextAware {
           val membershipPositions = squad.Membership
             .zipWithIndex
             .filter { case (_, index ) => positionsToUpdate.contains(index) }
-          sendResponse(SquadMembershipResponse(SquadRequestType.Accept, 0, 0, player.CharId, Some(leader.CharId), player.Name, true, Some(None)))
-          //load each member's entry
-          membershipPositions.foreach { case(member, index) =>
-            sendResponse(SquadMemberEvent(0, id, member.CharId, index, Some(member.Name), Some(member.ZoneId), Some(0)))
-          }
-          //repeat own entry and initialize connection to squad
           membershipPositions.find({ case(member, _) => member.CharId == avatar.CharId }) match {
-            case Some((member, index)) =>
-              sendResponse(SquadMemberEvent(0, id, member.CharId, index, Some(member.Name), Some(member.ZoneId), Some(0)))
+            case Some((ourMember, ourIndex)) =>
+              //we are joining the squad
+              sendResponse(SquadMembershipResponse(SquadRequestType.Accept, 0, 0, player.CharId, Some(leader.CharId), player.Name, true, Some(None)))
+              //load each member's entry (our own too)
+              membershipPositions.foreach { case(member, index) =>
+                sendResponse(SquadMemberEvent(0, id, member.CharId, index, Some(member.Name), Some(member.ZoneId), Some(0)))
+                squadUI(member.CharId) = SquadUIElement(member.Name, index, member.ZoneId, member.Health, member.Armor, member.Position)
+              }
+              //initialization
+              sendResponse(SquadMemberEvent(0, id, ourMember.CharId, ourIndex, Some(ourMember.Name), Some(ourMember.ZoneId), Some(0))) //repeat of our entry
               sendResponse(PlanetsideAttributeMessage(player.GUID, 31, id)) //associate with squad?
-              sendResponse(PlanetsideAttributeMessage(player.GUID, 32, index)) //associate with member position in squad?
-            case _ => ;
+              sendResponse(PlanetsideAttributeMessage(player.GUID, 32, ourIndex)) //associate with member position in squad?
+              //a finalization? what does this do?
+              sendResponse(SquadDefinitionActionMessage(squad.GUID, 0, SquadAction.Unknown(18, hex"00".toBitVector.take(6))))
+            case _ =>
+              //other player is joining our squad
+              //load each member's entry
+              membershipPositions.foreach { case(member, index) =>
+                sendResponse(SquadMemberEvent(0, id, member.CharId, index, Some(member.Name), Some(member.ZoneId), Some(0)))
+                squadUI(member.CharId) = SquadUIElement(member.Name, index, member.ZoneId, member.Health, member.Armor, member.Position)
+              }
           }
-          //send first update for map icons
+          //send an initial dummy update for map icon(s)
           sendResponse(SquadState(PlanetSideGUID(id),
             membershipPositions
               .filterNot { case (member, _) => member.CharId == avatar.CharId }
-              .map{ case (member, _) => SquadStateInfo(member.CharId, 64,64, member.Position, 2,2, false, 429, None,None) }
+              .map{ case (member, _) => SquadStateInfo(member.CharId, member.Health, member.Armor, member.Position, 2,2, false, 429, None,None) }
               .toList
           ))
-          //a finalization? what does this do?
-          sendResponse(SquadDefinitionActionMessage(squad.GUID, 0, SquadAction.Unknown(18, hex"00".toBitVector.take(6))))
 
         case SquadResponse.Leave(_, positionsToUpdate) =>
           val id = 11
-          sendResponse(SquadMembershipResponse(SquadRequestType.Leave, 0,1, avatar.CharId, Some(avatar.CharId), avatar.name, true, Some(None)))
-          //remove each member's entry (our own too)
-          positionsToUpdate.foreach { case(member, index) =>
-            sendResponse(SquadMemberEvent(1, id, member, index, None, None, None))
-          }
-          //repeat own entry and rescind connection to squad
           positionsToUpdate.find({ case(member, _) => member == avatar.CharId }) match {
-            case Some((member, index)) =>
-              sendResponse(SquadMemberEvent(1, id, member, index, None, None, None))
+            case Some((ourMember, ourIndex)) =>
+              //we are leaving the squad
+              sendResponse(SquadMembershipResponse(SquadRequestType.Leave, 0,1, avatar.CharId, Some(avatar.CharId), avatar.name, true, Some(None)))
+              //remove each member's entry (our own too)
+              positionsToUpdate.foreach { case(member, index) =>
+                sendResponse(SquadMemberEvent(1, id, member, index, None, None, None))
+                squadUI.remove(member)
+              }
+              //uninitialize
+              sendResponse(SquadMemberEvent(1, id, ourMember, ourIndex, None, None, None)) //repeat of our entry
               sendResponse(PlanetsideAttributeMessage(player.GUID, 31, 0)) //disassociate with squad?
               sendResponse(PlanetsideAttributeMessage(player.GUID, 32, 0)) //disassociate with member position in squad?
-              sendResponse(PlanetsideAttributeMessage(player.GUID, 34, 4294967295L)) //unknown
-            case _ => ;
+              sendResponse(PlanetsideAttributeMessage(player.GUID, 34, 4294967295L)) //unknown, perhaps unrelated?
+              //a finalization? what does this do?
+              sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(18, hex"00".toBitVector.take(6))))
+            case _ =>
+              //remove each member's entry
+              positionsToUpdate.foreach { case(member, index) =>
+                sendResponse(SquadMemberEvent(1, id, member, index, None, None, None))
+                squadUI.remove(member)
+              }
           }
-          //a finalization? what does this do?
-          sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(18, hex"00".toBitVector.take(6))))
+
+        case SquadResponse.UpdateMembers(squad, positions) =>
+          import services.teamwork.SquadAction.{Update => SAUpdate}
+          val pairedEntries = positions.collect {
+            case entry if squadUI.contains(entry.char_id) =>
+              (entry, squadUI(entry.char_id))
+          }
+          //prune entries
+          val updatedEntries = pairedEntries
+            .collect({
+              case (entry, element) if entry.zone_number != element.zone =>
+                //zone gets updated for these entries
+                sendResponse(SquadMemberEvent(3, 11, entry.char_id, element.index, None, Some(entry.zone_number), None))
+                squadUI(entry.char_id) = SquadUIElement(element.name, 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)
+                entry
+            })
+            .filterNot(_.char_id == avatar.CharId) //we want to update our backend, but not our frontend
+          if(updatedEntries.nonEmpty) {
+            sendResponse(
+              SquadState(
+                PlanetSideGUID(11),
+                updatedEntries.map { entry => SquadStateInfo(entry.char_id, entry.health, entry.armor, entry.pos, 2,2, false, 429, None,None)}
+              )
+            )
+          }
+
         case _ => ;
       }
 
@@ -586,7 +633,6 @@ class WorldSessionActor extends Actor with MDCContextAware {
         player.Stamina = stamina
         player.Armor = armor
       }
-      avatar.CharId = 41605313L
       sendResponse(CharacterInfoMessage(15, PlanetSideZoneID(10000), avatar.CharId, player.GUID, false, 6404428))
       RemoveCharacterSelectScreenGUID(player)
       sendResponse(CharacterInfoMessage(0, PlanetSideZoneID(1), 0, PlanetSideGUID(0), true, 0))
@@ -2986,7 +3032,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
       import net.psforever.objects.GlobalDefinitions._
       import net.psforever.types.CertificationType._
       val faction = PlanetSideEmpire.VS
-      val avatar = Avatar(s"TestCharacter$sessionId", faction, CharacterGender.Female, 41, CharacterVoice.Voice1)
+      val avatar = new Avatar(41605313L+sessionId, s"TestCharacter$sessionId", faction, CharacterGender.Female, 41, CharacterVoice.Voice1)
       avatar.Certifications += StandardAssault
       avatar.Certifications += MediumAssault
       avatar.Certifications += StandardExoSuit
@@ -3373,7 +3419,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
     case msg @ PlayerStateMessageUpstream(avatar_guid, pos, vel, yaw, pitch, yaw_upper, seq_time, unk3, is_crouching, is_jumping, unk4, is_cloaking, unk5, unk6) =>
       if(deadState == DeadState.Alive) {
         if(!player.Crouching && is_crouching) { //SQUAD TESTING CODE
-          //...
+          sendResponse(SquadMemberEvent(3, 11, 353380L, 3, None, Some(13), None))
         }
         player.Position = pos
         player.Velocity = vel
@@ -3416,6 +3462,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
           case None => false
         }
         avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlayerState(avatar_guid, msg, spectator, wepInHand))
+        //squadService ! SquadServiceMessage(tplayer, SquadAction.Update(tplayer.CharId, tplayer.Health, tplayer.MaxHealth, tplayer.Armor, tplayer.MaxArmor, pos, zone.Number))
       }
 
     case msg @ ChildObjectStateMessage(object_guid, pitch, yaw) =>
@@ -4858,24 +4905,6 @@ class WorldSessionActor extends Actor with MDCContextAware {
     case msg @ SquadMembershipRequest(request_type, unk2, unk3, player_name, unk5) =>
       log.info(s"$msg")
       squadService ! SquadServiceMessage(player, SquadServiceAction.Membership(request_type, unk2, unk3, player_name, unk5))
-//      if(request_type == SquadRequestType.Accept) {
-//        sendResponse(SquadMembershipResponse(SquadRequestType.Accept, 0, 0, unk2, Some(30910985L), player.Name, true, Some(None)))
-//        sendResponse(SquadMemberEvent(0, 11, 30910985L, 0, Some("Wizkid45"), Some(5), Some(320036)))
-//        sendResponse(SquadMemberEvent(0, 11, 42781919L, 1, Some("xoBLADEox"), Some(5), Some(528805)))
-//        sendResponse(SquadMemberEvent(0, 11, avatar.CharId, 2, Some(player.Name), Some(13), Some(320036)))
-//        sendResponse(SquadMemberEvent(0, 11, 353380L, 3, Some("cabal0428") ,Some(5), Some(8156)))
-//        sendResponse(SquadMemberEvent(0, 11, 41588340L, 4 ,Some("xSkiku"), Some(5), Some(0)))
-//        sendResponse(SquadMemberEvent(0, 11, avatar.CharId, 2, Some(player.Name), Some(13), Some(320036)))
-//        sendResponse(PlanetsideAttributeMessage(player.GUID, 31, 11)) //associate with squad?
-//        sendResponse(PlanetsideAttributeMessage(player.GUID, 32, 2)) //associate with member position in squad?
-//        sendResponse(SquadState(PlanetSideGUID(11),List(
-//          SquadStateInfo(30910985L, 50,64, Vector3(5526.5234f,3818.7344f,54.59375f), 2,2, false, 429, None,None),
-//          SquadStateInfo(42781919L, 64,0, Vector3(4673.5312f,2604.8047f,40.015625f), 2,2, false, 149, None,None),
-//          SquadStateInfo(353380L, 64,64, Vector3(4727.492f,2613.5312f,51.390625f), 2,2, false, 0, None,None),
-//          SquadStateInfo(41588340L, 64,64, Vector3(3675.0f,4789.8047f,63.21875f), 2,2, false, 0, None,None)
-//        )))
-//        sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(3), 0, SquadAction.Unknown(18, hex"00".toBitVector.take(6))))
-//      }
 
     case msg @ GenericCollisionMsg(u1, p, t, php, thp, pv, tv, ppos, tpos, u2, u3, u4) =>
       log.info("Ouch! " + msg)
@@ -7621,17 +7650,6 @@ class WorldSessionActor extends Actor with MDCContextAware {
     * @return a `DestroyDisplayMessage` packet that is properly formatted
     */
   def DestroyDisplayMessage(killer : SourceEntry, victim : SourceEntry, method : Int, unk : Int = 121) : DestroyDisplayMessage = {
-    val killerCharId : Long = killer.CharId
-    val victimCharId : Long = {
-      //TODO this block of code is only necessary for this particular dev build; use "victim.CharId" normallyLoado
-      val id = victim.CharId
-      if(killerCharId == id) {
-        Int.MaxValue - id + 1
-      }
-      else {
-        id
-      }
-    }
     val killer_seated = killer match {
       case obj : PlayerSource => obj.Seated
       case _ => false
@@ -7641,9 +7659,9 @@ class WorldSessionActor extends Actor with MDCContextAware {
       case _ => false
     }
     new DestroyDisplayMessage(
-      killer.Name, killerCharId, killer.Faction, killer_seated,
+      killer.Name, killer.CharId, killer.Faction, killer_seated,
       unk, method,
-      victim.Name, victimCharId, victim.Faction, victim_seated
+      victim.Name, victim.CharId, victim.Faction, victim_seated
     )
   }
 
@@ -9197,6 +9215,8 @@ object WorldSessionActor {
                                            completeAction : () => Unit,
                                            tickAction : Option[() => Unit] = None)
 
+  protected final case class SquadUIElement(name : String, index : Int, zone : Int, health : Int, armor : Int, position : Vector3)
+
   private final case class NtuCharging(tplayer: Player,
                                        vehicle: Vehicle)
   private final case class NtuDischarging(tplayer: Player, vehicle: Vehicle, silo_guid: PlanetSideGUID)

From f81c87ce2203eda8a5f536419e15899fb128980c Mon Sep 17 00:00:00 2001
From: FateJH <Jason_DiDonato@yahoo.com>
Date: Mon, 22 Jul 2019 17:56:09 -0400
Subject: [PATCH 14/31] split original aphedox SquadRequestType's into two
 different enumerations, one for Request and the other for Response; added
 functionality for more squad interactions; squad favorites, excepting the
 ability to actually load them onto one's client; extensive comments about
 SquadResponseMessage events

---
 .../psforever/objects/loadouts/Loadout.scala  |   3 +-
 .../objects/loadouts/SquadLoadout.scala       |   5 +-
 .../psforever/objects/teamwork/Squad.scala    |   2 +-
 .../game/ReplicationStreamMessage.scala       |  20 +-
 .../game/SquadDefinitionActionMessage.scala   | 110 ++-
 .../SquadDetailDefinitionUpdateMessage.scala  |  36 +-
 .../packet/game/SquadMembershipResponse.scala |  60 +-
 .../psforever/types/SquadRequestType.scala    |  37 +-
 .../psforever/types/SquadResponseType.scala   |  33 +
 .../services/teamwork/SquadResponse.scala     |  18 +-
 .../services/teamwork/SquadService.scala      | 708 +++++++++++++-----
 .../SquadDefinitionActionMessageTest.scala    |  16 +-
 .../src/main/scala/WorldSessionActor.scala    |  65 +-
 13 files changed, 820 insertions(+), 293 deletions(-)
 create mode 100644 common/src/main/scala/net/psforever/types/SquadResponseType.scala

diff --git a/common/src/main/scala/net/psforever/objects/loadouts/Loadout.scala b/common/src/main/scala/net/psforever/objects/loadouts/Loadout.scala
index 58490f60..64668c4e 100644
--- a/common/src/main/scala/net/psforever/objects/loadouts/Loadout.scala
+++ b/common/src/main/scala/net/psforever/objects/loadouts/Loadout.scala
@@ -23,7 +23,7 @@ object Loadout {
     owner match {
       case p : Player => Success(Create(p, label))
       case v : Vehicle => Success(Create(v, label))
-      case s : Squad => Success(Create(s, label))
+      case s : Squad => Success(Create(s, s.Task))
       case _ => Failure(new MatchError(s"can not create a loadout based on the (current status of) $owner"))
     }
   }
@@ -65,7 +65,6 @@ object Loadout {
   def Create(squad : Squad, label : String) : Loadout = {
     SquadLoadout(
       label,
-      squad.Task,
       if(squad.CustomZoneId) { Some(squad.ZoneId) } else { None },
       squad.Membership
         .zipWithIndex
diff --git a/common/src/main/scala/net/psforever/objects/loadouts/SquadLoadout.scala b/common/src/main/scala/net/psforever/objects/loadouts/SquadLoadout.scala
index 9039c5a0..029c58b7 100644
--- a/common/src/main/scala/net/psforever/objects/loadouts/SquadLoadout.scala
+++ b/common/src/main/scala/net/psforever/objects/loadouts/SquadLoadout.scala
@@ -5,7 +5,6 @@ import net.psforever.types.CertificationType
 
 final case class SquadPositionLoadout(index : Int, role : String, orders : String, requirements : Set[CertificationType.Value])
 
-final case class SquadLoadout(label : String,
-                              task : String,
+final case class SquadLoadout(task : String,
                               zone_id : Option[Int],
-                              members : List[SquadPositionLoadout]) extends Loadout(label)
+                              members : List[SquadPositionLoadout]) extends Loadout(task)
diff --git a/common/src/main/scala/net/psforever/objects/teamwork/Squad.scala b/common/src/main/scala/net/psforever/objects/teamwork/Squad.scala
index b5e5d81d..5f653dcd 100644
--- a/common/src/main/scala/net/psforever/objects/teamwork/Squad.scala
+++ b/common/src/main/scala/net/psforever/objects/teamwork/Squad.scala
@@ -91,7 +91,7 @@ class Squad(squadId : PlanetSideGUID, alignment : PlanetSideEmpire.Value) extend
     }
   }
 
-  def Size : Int = membership.count(member => !member.Name.equals(""))
+  def Size : Int = membership.count(member => member.CharId != 0)
 
   def Capacity : Int = availability.count(open => open)
 }
diff --git a/common/src/main/scala/net/psforever/packet/game/ReplicationStreamMessage.scala b/common/src/main/scala/net/psforever/packet/game/ReplicationStreamMessage.scala
index 233f5a59..c34a057c 100644
--- a/common/src/main/scala/net/psforever/packet/game/ReplicationStreamMessage.scala
+++ b/common/src/main/scala/net/psforever/packet/game/ReplicationStreamMessage.scala
@@ -51,6 +51,22 @@ final case class SquadInfo(leader : Option[String],
       squad_guid.orElse(info.squad_guid)
     )
   }
+
+  //methods intended to combine the fields of itself and another object
+  def Leader(leader : String) : SquadInfo =
+    this And SquadInfo(Some(leader), None, None, None, None, None)
+  def Task(task : String) : SquadInfo =
+    this And SquadInfo(None, Some(task), None, None, None, None)
+  def ZoneId(zone : PlanetSideZoneID) : 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)
+  }
+  def Size(sz : Int) : SquadInfo =
+    this And SquadInfo(None, None, None, Some(sz), None, None)
+  def Capacity(cap : Int) : SquadInfo =
+    this And SquadInfo(None, None, None, None, Some(cap), None)
 }
 
 /**
@@ -117,7 +133,9 @@ object SquadInfo {
   /**
     * An entry where no fields are defined.
     */
-  final val Blank = SquadInfo(None, None, None, None, None)
+  final val Blank = SquadInfo()
+
+  def apply() : SquadInfo = SquadInfo(None, None, None, None, None, None)
 
   /**
     * Alternate constructor for `SquadInfo` that ignores the `Option` requirement for the fields.<br>
diff --git a/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala b/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala
index 7b9b4647..32bbc68e 100644
--- a/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala
+++ b/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala
@@ -20,18 +20,24 @@ object SquadAction{
 
   final case class AnswerSquadJoinRequest() extends SquadAction(1)
 
-  final case class SaveSquadDefinition() extends SquadAction(3)
+  final case class SaveSquadFavorite() extends SquadAction(3)
 
-  final case class LoadSquadDefinition() extends SquadAction(4)
+  final case class LoadSquadFavorite() extends SquadAction(4)
 
-  final case class ListSquadDefinition(name : String) extends SquadAction(7)
+  final case class DeleteSquadFavorite() extends SquadAction(5)
 
-  final case class ListSquad() extends SquadAction(8)
+  final case class ListSquadFavorite(name : String) extends SquadAction(7)
+
+  final case class RequestListSquad() extends SquadAction(8)
+
+  final case class StopListSquad() extends SquadAction(9)
 
   final case class SelectRoleForYourself(state : Int) extends SquadAction(10)
 
   final case class CancelSelectRoleForYourself(value: Long = 0) extends SquadAction(15)
 
+  final case class SetListSquad() extends SquadAction(17)
+
   final case class ChangeSquadPurpose(purpose : String) extends SquadAction(19)
 
   final case class ChangeSquadZone(zone : PlanetSideZoneID) extends SquadAction(20)
@@ -56,12 +62,21 @@ object SquadAction{
 
   final case class CancelSquadSearch() extends SquadAction(35)
 
+  final case class AssignSquadMemberToRole(position : Int, char_id : Long) extends SquadAction(38)
+
   final case class FindLfsSoldiersForRole(state : Int) extends SquadAction(40)
 
   final case class CancelFind() extends SquadAction(41)
 
   final case class Unknown(badCode : Int, data : BitVector) extends SquadAction(badCode)
 
+  object Unknown {
+    import scodec.bits._
+    val StandardBits : BitVector = hex"00".toBitVector.take(6)
+
+    def apply(badCode : Int) : Unknown = Unknown(badCode, StandardBits)
+  }
+
   /**
     * The `Codec`s used to transform the input stream into the context of a specific action
     * and extract the field data from that stream.
@@ -83,31 +98,45 @@ object SquadAction{
       }
     )
 
-    val saveSquadDefinitionCodec = everFailCondition.xmap[SaveSquadDefinition] (
-      _ => SaveSquadDefinition(),
+    val saveSquadFavoriteCodec = everFailCondition.xmap[SaveSquadFavorite] (
+      _ => SaveSquadFavorite(),
       {
-        case SaveSquadDefinition() => None
+        case SaveSquadFavorite() => None
       }
     )
 
-    val loadSquadDefinitionCodec = everFailCondition.xmap[LoadSquadDefinition] (
-      _ => LoadSquadDefinition(),
+    val loadSquadFavoriteCodec = everFailCondition.xmap[LoadSquadFavorite] (
+      _ => LoadSquadFavorite(),
       {
-        case LoadSquadDefinition() => None
+        case LoadSquadFavorite() => None
       }
     )
 
-    val listSquadDefinitionCodec = PacketHelpers.encodedWideStringAligned(6).xmap[ListSquadDefinition] (
-      text => ListSquadDefinition(text),
+    val deleteSquadFavoriteCodec = everFailCondition.xmap[DeleteSquadFavorite] (
+      _ => DeleteSquadFavorite(),
       {
-        case ListSquadDefinition(text) => text
+        case DeleteSquadFavorite() => None
       }
     )
 
-    val listSquadCodec = everFailCondition.xmap[ListSquad] (
-      _ => ListSquad(),
+    val listSquadFavoriteCodec = PacketHelpers.encodedWideStringAligned(6).xmap[ListSquadFavorite] (
+      text => ListSquadFavorite(text),
       {
-        case ListSquad() => None
+        case ListSquadFavorite(text) => text
+      }
+    )
+
+    val requestListSquadCodec = everFailCondition.xmap[RequestListSquad] (
+      _ => RequestListSquad(),
+      {
+        case RequestListSquad() => None
+      }
+    )
+
+    val stopListSquadCodec = everFailCondition.xmap[StopListSquad] (
+      _ => StopListSquad(),
+      {
+        case StopListSquad() => None
       }
     )
 
@@ -125,6 +154,13 @@ object SquadAction{
       }
     )
 
+    val setListSquadCodec = everFailCondition.xmap[SetListSquad] (
+      _ => SetListSquad(),
+      {
+        case SetListSquad() => None
+      }
+    )
+
     val changeSquadPurposeCodec = PacketHelpers.encodedWideStringAligned(6).xmap[ChangeSquadPurpose] (
       purpose => ChangeSquadPurpose(purpose),
       {
@@ -223,6 +259,15 @@ object SquadAction{
       }
     )
 
+    val assignSquadMemberToRoleCodec = (uint4 :: uint32L).xmap[AssignSquadMemberToRole] (
+      {
+        case u1 :: u2 :: HNil => AssignSquadMemberToRole(u1, u2)
+      },
+      {
+        case AssignSquadMemberToRole(u1, u2) => u1 :: u2 :: HNil
+      }
+    )
+
     val findLfsSoldiersForRoleCodec = uint4.xmap[FindLfsSoldiersForRole] (
       state => FindLfsSoldiersForRole(state),
       {
@@ -270,13 +315,14 @@ object SquadAction{
   * &nbsp;&nbsp;&nbsp;&nbsp;`0 ` - Display Squad<br>
   * &nbsp;&nbsp;&nbsp;&nbsp;`1 ` - Answer Squad Join Request<br>
   * &nbsp;&nbsp;&nbsp;&nbsp;`2 ` - UNKNOWN<br>
-  * &nbsp;&nbsp;&nbsp;&nbsp;`3 ` - Save Squad Definition<br>
-  * &nbsp;&nbsp;&nbsp;&nbsp;`4 ` - Load Squad Definition<br>
+  * &nbsp;&nbsp;&nbsp;&nbsp;`3 ` - Save Squad Favorite<br>
+  * &nbsp;&nbsp;&nbsp;&nbsp;`4 ` - Load Squad Favorite<br>
+  * &nbsp;&nbsp;&nbsp;&nbsp;`5 ` - Delete Squad Favorite<br>
   * &nbsp;&nbsp;&nbsp;&nbsp;`6 ` - UNKNOWN<br>
-  * &nbsp;&nbsp;&nbsp;&nbsp;`8 ` - List Squad<br>
-  * &nbsp;&nbsp;&nbsp;&nbsp;`9 ` - UNKNOWN<br>
+  * &nbsp;&nbsp;&nbsp;&nbsp;`8 ` - Request List Squad<br>
+  * &nbsp;&nbsp;&nbsp;&nbsp;`9 ` - Stop List Squad<br>
   * &nbsp;&nbsp;&nbsp;&nbsp;`16` - UNKNOWN<br>
-  * &nbsp;&nbsp;&nbsp;&nbsp;`17` - UNKNOWN<br>
+  * &nbsp;&nbsp;&nbsp;&nbsp;`17` - Set List Squad (ui)<br>
   * &nbsp;&nbsp;&nbsp;&nbsp;`18` - UNKNOWN<br>
   * &nbsp;&nbsp;&nbsp;&nbsp;`26` - Reset All<br>
   * &nbsp;&nbsp;&nbsp;&nbsp;`35` - Cancel Squad Search<br>
@@ -302,12 +348,12 @@ object SquadAction{
   * &nbsp;&nbsp;&nbsp;&nbsp;`15` - Select this Role for Yourself<br>
   * &nbsp;&nbsp;&nbsp;&nbsp;`37` - UNKNOWN<br>
   * &nbsp;&nbsp;`String`<br>
-  * &nbsp;&nbsp;&nbsp;&nbsp;`7 ` - List Squad Definition<br>
+  * &nbsp;&nbsp;&nbsp;&nbsp;`7 ` - List Squad Favorite<br>
   * &nbsp;&nbsp;&nbsp;&nbsp;`19` - (Squad leader) Change Squad Purpose<br>
   * &nbsp;&nbsp;`Int :: Long`<br>
   * &nbsp;&nbsp;&nbsp;&nbsp;`12` - UNKNOWN<br>
   * &nbsp;&nbsp;&nbsp;&nbsp;`25` - (Squad leader) Change Squad Member Requirements - Weapons<br>
-  * &nbsp;&nbsp;&nbsp;&nbsp;`38` - UNKNOWN<br>
+  * &nbsp;&nbsp;&nbsp;&nbsp;`38` - Assign Squad Member To Role<br>
   * &nbsp;&nbsp;`Int :: String`<br>
   * &nbsp;&nbsp;&nbsp;&nbsp;`23` - (Squad leader) Change Squad Member Requirements - Role<br>
   * &nbsp;&nbsp;&nbsp;&nbsp;`24` - (Squad leader) Change Squad Member Requirements - Detailed Orders<br>
@@ -343,12 +389,15 @@ object SquadDefinitionActionMessage extends Marshallable[SquadDefinitionActionMe
     ((code : @switch) match {
       case 0 => displaySquadCodec
       case 1 => answerSquadJoinRequestCodec
-      case 3 => saveSquadDefinitionCodec
-      case 4 => loadSquadDefinitionCodec
-      case 7 => listSquadDefinitionCodec
-      case 8 => listSquadCodec
+      case 3 => saveSquadFavoriteCodec
+      case 4 => loadSquadFavoriteCodec
+      case 5 => deleteSquadFavoriteCodec
+      case 7 => listSquadFavoriteCodec
+      case 8 => requestListSquadCodec
+      case 9 => stopListSquadCodec
       case 10 => selectRoleForYourselfCodec
       case 15 => cancelSelectRoleForYourselfCodec
+      case 17 => setListSquadCodec
       case 19 => changeSquadPurposeCodec
       case 20 => changeSquadZoneCodec
       case 21 => closeSquadMemberPositionCodec
@@ -361,12 +410,13 @@ object SquadDefinitionActionMessage extends Marshallable[SquadDefinitionActionMe
       case 31 => locationFollowsSquadLeadCodec
       case 34 => searchForSquadsWithParticularRoleCodec
       case 35 => cancelSquadSearchCodec
+      case 38 => assignSquadMemberToRoleCodec
       case 40 => findLfsSoldiersForRoleCodec
       case 41 => cancelFindCodec
-      case 2 | 6 | 9 | 11 |
-           12 | 13 | 14 | 16 | 17 |
+      case 2 | 6 | 11 |
+           12 | 13 | 14 | 16 |
            18 | 29 | 30 | 32 | 33 |
-           36 | 37 | 38 | 39 | 42 | 43 => unknownCodec(code)
+           36 | 37 | 39 | 42 | 43 => unknownCodec(code)
       case _ => failureCodec(code)
     }).asInstanceOf[Codec[SquadAction]]
   }
diff --git a/common/src/main/scala/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala b/common/src/main/scala/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala
index 9bda7527..225489b7 100644
--- a/common/src/main/scala/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala
+++ b/common/src/main/scala/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala
@@ -161,7 +161,21 @@ final case class SquadDetail(unk1 : Option[Int],
       task.orElse(info.task),
       zone_id.orElse(info.zone_id),
       unk7.orElse(info.unk7),
-      member_info.orElse(info.member_info)
+      {
+        (member_info, info.member_info) match {
+          case (Some(info1), Some(info2)) =>
+            //combine the first list with the elements of the second list whose indices not found in the first list
+            val indices = info1.map { _.index }
+            Some(info1 ++ (for {
+              position <- info2
+              if !indices.contains(position.index)
+            } yield position).sortBy(_.index))
+          case (Some(info1), None) =>
+            Some(info1)
+          case (None, _) =>
+            info.member_info
+        }
+      }
     )
   }
 
@@ -269,6 +283,15 @@ object SquadPositionDetail {
     * @return a `SquadPositionDetail` object
     */
   def apply(role : String, detailed_orders : String, requirements : Set[CertificationType.Value], char_id : Long, name : String) : SquadPositionDetail = SquadPositionDetail(Some(false), Some(role), Some(detailed_orders), Some(requirements), Some(char_id), Some(name))
+
+  object Fields {
+    final val Closed = 0
+    final val Role = 1
+    final val Orders = 2
+    final val CharId = 3
+    final val Name = 4
+    final val Requirements = 5
+  }
 }
 
 object SquadPositionEntry {
@@ -318,6 +341,17 @@ object SquadDetail {
     SquadDetail(None, None, None, None, None, None, None, Some(unk7), None)
   def Members(list : List[SquadPositionEntry]) : SquadDetail =
     SquadDetail(None, None, None, None, None, None, None, None, Some(list))
+
+  object Fields {
+    final val Field1 = 1
+    final val CharId = 2
+    final val Field3 = 3
+    final val Leader = 4
+    final val Task = 5
+    final val ZoneId = 6
+    final val Field7 = 7
+    final val Members = 8
+  }
 }
 
 object SquadDetailDefinitionUpdateMessage extends Marshallable[SquadDetailDefinitionUpdateMessage] {
diff --git a/common/src/main/scala/net/psforever/packet/game/SquadMembershipResponse.scala b/common/src/main/scala/net/psforever/packet/game/SquadMembershipResponse.scala
index ee1746dd..a9f03518 100644
--- a/common/src/main/scala/net/psforever/packet/game/SquadMembershipResponse.scala
+++ b/common/src/main/scala/net/psforever/packet/game/SquadMembershipResponse.scala
@@ -2,12 +2,12 @@
 package net.psforever.packet.game
 
 import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
-import net.psforever.types.SquadRequestType
+import net.psforever.types.SquadResponseType
 import scodec.Codec
 import scodec.codecs._
 
 /**
-  * Dispatched by the server as manipulation protocol for squad and platoon members.
+  * Dispatched by the server as message generation protocol for squad and platoon members.
   * Prompted by and answers for a `SquadMembershipRequest` packet.
   * @param request_type the purpose of the request
   * @param unk1 na
@@ -17,11 +17,57 @@ import scodec.codecs._
   * @param other_id another squad member's unique identifier;
   *                 may be the same as `char_id`
   * @param player_name name of the player being affected, if applicable
-  * @param unk5 na
+  * @param unk5 adjusts the nature of the request-type response based on the message recipient
   * @param unk6 na;
   *             the internal field, the `Option[String]`, never seems to be set
+  * <br>
+  * `request_type` (enum value) / `unk5` state (`false`/`true`)<br>
+  * ----------------------------------------<br>
+  *  - `Invite` (0)<br>
+  * false => [PROMPT] "`player_name` has invited you into a squad." [YES/NO]<br>
+  * true => "You have invited `player_name` to join your squad."<br>
+  *  - `Unk01` (1)<br>
+  * false => n/a<br>
+  * true => n/a<br>
+  *  - `Accept` (2)<br>
+  * false => "`player_name` has accepted your invitation to join into your squad.<br>
+  *   "You have formed a squad and are now that squad's commander." (if first time)<br>
+  * true => "You have accepted an invitation to join a squad."<br>
+  *   "You have successfully joined a squad for the first time." (if first time)<br>
+  *  - `Reject` (3)<br>
+  * false => "`player_name` does not want to join your squad at this time."<br>
+  * true => "You have declined an invitation to join a squad."<br>
+  *  - `Cancel` (4)<br>
+  * false => "`player_name` has withdrawn his invitation."<br>
+  * true => "You have canceled your invitation to `player_name`."<br>
+  *  - `Leave` (5)<br>
+  * false => "The Squad Leader has kicked you out of the squad."<br>
+  * true => "You have kicked `player_name` out of the squad."<br>
+  *  - `Disband` (6)<br>
+  * false => "The squad has been disbanded."<br>
+  * true => "You have disbanded the squad."<br>
+  *  - `PlatoonInvite` (7)<br>
+  * false => [PROMPT] "`player_name` has invited you into a platoon." [YES/NO]<br>
+  * true => "You have invited `player_name`'s squad to join your platoon."<br>
+  *  - `PlatoonAccept` (8)
+  * false => "`player_name` has accepted your invitation to join into your platoon.<br>
+  *   "You have formed a platoon and are now that platoon commander." (if first time)<br>
+  * true => "You have accepted an invitation to join a platoon."<br>
+  *   "You have successfully joined a platoon for the first time." (if first time)<br>
+  *  - `PlatoonReject` (9)<br>
+  * false => "`player_name` does not want to join your platoon at this time."<br>
+  * true => "You have declined an invitation to join a platoon."<br>
+  *  - `PlatoonCancel` (10)<br>
+  * false => "`player_name` has withdrawn his invitation."<br>
+  * true => "You have declined your invitation to `player_name`." (nonsense?)<br>
+  *  - `PlatoonLeave` (11)<br>
+  * false => "The Platoon Leader has kicked you out of the platoon."<br>
+  * true => "You have kicked `player_name`'s squad out of the platoon."<br>
+  *  - `PlatoonDisband` (12)<br>
+  * false => "The platoon has been disbanded."<br>
+  * true => "You have disbanded the platoon."
   */
-final case class SquadMembershipResponse(request_type : SquadRequestType.Value,
+final case class SquadMembershipResponse(request_type : SquadResponseType.Value,
                                          unk1 : Int,
                                          unk2 : Int,
                                          char_id : Long,
@@ -37,14 +83,14 @@ final case class SquadMembershipResponse(request_type : SquadRequestType.Value,
 
 object SquadMembershipResponse extends Marshallable[SquadMembershipResponse] {
   implicit val codec : Codec[SquadMembershipResponse] = (
-    "request_type" | SquadRequestType.codec >>:~ { d =>
+    "request_type" | SquadResponseType.codec >>:~ { d =>
       ("unk1" | uint(5)) ::
         ("unk2" | uint2) ::
         ("char_id" | uint32L) ::
-        ("other_id" | conditional(d != SquadRequestType.Promote && d != SquadRequestType.PlatoonLeave, uint32L)) ::
+        ("other_id" | conditional(d != SquadResponseType.Disband && d != SquadResponseType.PlatoonDisband, uint32L)) ::
         ("player_name" | PacketHelpers.encodedWideStringAligned(5)) ::
         ("unk5" | bool) ::
-        conditional(d != SquadRequestType.Invite, optional(bool, "unk6" | PacketHelpers.encodedWideStringAligned(6)))
+        conditional(d != SquadResponseType.Invite, optional(bool, "unk6" | PacketHelpers.encodedWideStringAligned(6)))
     }
     ).as[SquadMembershipResponse]
 }
diff --git a/common/src/main/scala/net/psforever/types/SquadRequestType.scala b/common/src/main/scala/net/psforever/types/SquadRequestType.scala
index 8d7e2435..1b22bc38 100644
--- a/common/src/main/scala/net/psforever/types/SquadRequestType.scala
+++ b/common/src/main/scala/net/psforever/types/SquadRequestType.scala
@@ -7,23 +7,28 @@ import scodec.codecs._
 object SquadRequestType extends Enumeration {
   type Type = Value
   val
-  Invite,   //00
-  Unk01,    //01
-  Accept,   //02
-  Reject,   //03
-  Cancel,   //04
-  Leave,    //05
-  Promote,  //06
-  Disband,  //07
-  PlatoonInvite,  //08
-  PlatoonAccept,  //09
-  PlatoonReject,  //10
-  PlatoonCancel,  //11
-  PlatoonLeave,   //12
-  PlatoonDisband, //13
-  Unk14,    //14
-  Unk15     //15
+  Invite,
+  Unk01,
+  Accept,
+  Reject,
+  Cancel,
+  Leave,
+  Promote,
+  Disband,
+  PlatoonInvite,
+  PlatoonAccept,
+  PlatoonReject,
+  PlatoonCancel,
+  PlatoonLeave,
+  PlatoonDisband
   = Value
 
   implicit val codec = PacketHelpers.createEnumerationCodec(this, uint4L)
+
+  def toResponse(request : SquadRequestType.Value) : SquadResponseType.Value = {
+    val id = request.id
+    if(id < 6) SquadResponseType(id)
+    else if(id > 6) SquadResponseType(id - 1)
+    else throw new NoSuchElementException("request does not have an applicable response")
+  }
 }
diff --git a/common/src/main/scala/net/psforever/types/SquadResponseType.scala b/common/src/main/scala/net/psforever/types/SquadResponseType.scala
new file mode 100644
index 00000000..1f73ccc4
--- /dev/null
+++ b/common/src/main/scala/net/psforever/types/SquadResponseType.scala
@@ -0,0 +1,33 @@
+// Copyright (c) 2019 PSForever
+package net.psforever.types
+
+import net.psforever.packet.PacketHelpers
+import scodec.codecs._
+
+object SquadResponseType extends Enumeration {
+  type Type = Value
+  val
+  Invite,
+  Unk01,
+  Accept,
+  Reject,
+  Cancel,
+  Leave,
+  Disband,
+  PlatoonInvite,
+  PlatoonAccept,
+  PlatoonReject,
+  PlatoonCancel,
+  PlatoonLeave,
+  PlatoonDisband
+  = Value
+
+  implicit val codec = PacketHelpers.createEnumerationCodec(this, uint4L)
+
+  def fromRequest(response : SquadResponseType.Value) : SquadRequestType.Value = {
+    val id = response.id
+    if(id < 6) SquadRequestType(id)
+    else if(id > 5) SquadRequestType(id + 1)
+    else throw new NoSuchElementException("response does not stem from an applicable request")
+  }
+}
diff --git a/common/src/main/scala/services/teamwork/SquadResponse.scala b/common/src/main/scala/services/teamwork/SquadResponse.scala
index e1ab03cb..b74f153f 100644
--- a/common/src/main/scala/services/teamwork/SquadResponse.scala
+++ b/common/src/main/scala/services/teamwork/SquadResponse.scala
@@ -1,21 +1,27 @@
 // Copyright (c) 2019 PSForever
 package services.teamwork
 
-import net.psforever.objects.teamwork.{Member, Squad}
+import net.psforever.objects.teamwork.Squad
 import net.psforever.packet.game._
-import net.psforever.types.SquadRequestType
+import net.psforever.types.SquadResponseType
 
 object SquadResponse {
   trait Response
 
-  final case class Init(info : Vector[SquadInfo]) extends Response
-  final case class Update(infos : Iterable[(Int, SquadInfo)]) extends Response
-  final case class Remove(infos : Iterable[Int]) extends Response
+  final case class ListSquadFavorite(line : Int, task : String) extends Response
 
-  final case class Membership(request_type : SquadRequestType.Value, unk1 : Int, unk2 : Int, unk3 : Long, unk4 : Option[Long], player_name : String, unk5 : Boolean, unk6 : Option[Option[String]]) extends Response //see SquadMembershipResponse
+  final case class InitList(info : Vector[SquadInfo]) extends Response
+  final case class UpdateList(infos : Iterable[(Int, SquadInfo)]) extends Response
+  final case class RemoveFromList(infos : Iterable[Int]) extends Response
+
+  final case class InitSquad(squad_guid : PlanetSideGUID) extends Response
+  final case class Unknown17(squad : Squad, char_id : Long) extends Response
+  final case class Membership(request_type : SquadResponseType.Value, unk1 : Int, unk2 : Int, unk3 : Long, unk4 : Option[Long], player_name : String, unk5 : Boolean, unk6 : Option[Option[String]]) extends Response //see SquadMembershipResponse
+  final case class Invite(from_char_id : Long, to_char_id : Long, name : String) extends Response
   final case class Join(squad : Squad, positionsToUpdate : List[Int]) 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 SwapMember(squad : Squad, from_index : Int, to_index : Int) extends Response
 
   final case class Detail(guid : PlanetSideGUID, leader : String, task : String, zone : PlanetSideZoneID, member_info : List[SquadPositionDetail]) extends Response
 }
diff --git a/common/src/main/scala/services/teamwork/SquadService.scala b/common/src/main/scala/services/teamwork/SquadService.scala
index 9aa5954b..0f8ae794 100644
--- a/common/src/main/scala/services/teamwork/SquadService.scala
+++ b/common/src/main/scala/services/teamwork/SquadService.scala
@@ -4,9 +4,10 @@ package services.teamwork
 import akka.actor.Actor
 import net.psforever.objects.Player
 import net.psforever.objects.definition.converter.StatConverter
-import net.psforever.objects.teamwork.Squad
+import net.psforever.objects.loadouts.SquadLoadout
+import net.psforever.objects.teamwork.{Member, Squad}
 import net.psforever.packet.game._
-import net.psforever.types.{PlanetSideEmpire, SquadRequestType, Vector3}
+import net.psforever.types._
 import services.{GenericEventBus, Service}
 
 import scala.collection.concurrent.TrieMap
@@ -16,7 +17,9 @@ import scala.collection.mutable.ListBuffer
 //import scala.concurrent.duration._
 
 class SquadService extends Actor {
-  private var memberToSquad : TrieMap[String, Squad] = new TrieMap[String, Squad]()
+  import SquadService._
+
+  private var memberToSquad : mutable.LongMap[Squad] = mutable.LongMap[Squad]()
   private var idToSquad : TrieMap[PlanetSideGUID, Squad] = new TrieMap[PlanetSideGUID, Squad]()
   private var i : Int = 1
   private val publishedLists : TrieMap[PlanetSideEmpire.Value, ListBuffer[SquadInfo]] = TrieMap[PlanetSideEmpire.Value, ListBuffer[SquadInfo]](
@@ -24,35 +27,39 @@ class SquadService extends Actor {
     PlanetSideEmpire.NC -> ListBuffer.empty,
     PlanetSideEmpire.VS -> ListBuffer.empty
   )
-  private val bids : mutable.LongMap[(PlanetSideGUID, Int)] = mutable.LongMap[(PlanetSideGUID, Int)]()
+  private val bids : mutable.LongMap[PositionBid] = mutable.LongMap[PositionBid]()
 
   private [this] val log = org.log4s.getLogger
 
-  override def preStart = {
+  override def preStart : Unit = {
     log.info("Starting...")
 
     val testSquad = new Squad(PlanetSideGUID(3), PlanetSideEmpire.VS)
     testSquad.Task = "\\#66CCFF Sentinels of Auraxis\\#FFFFFF ... \\#40FF40 Squad Up!!"
     testSquad.ZoneId = 5
     testSquad.Membership(0).Name = "Wizkid45"
+    testSquad.Membership(0).Role = "Fearless Leader"
     testSquad.Membership(0).CharId = 30910985L
     testSquad.Membership(0).ZoneId = 5
     testSquad.Membership(0).Health = 64
     testSquad.Membership(0).Armor = 34
     testSquad.Membership(0).Position = Vector3(5526.5234f, 3818.7344f, 54.59375f)
     testSquad.Membership(1).Name = "xoBLADEox"
+    testSquad.Membership(1).Role = "Right Hand"
     testSquad.Membership(1).CharId = 42781919L
     testSquad.Membership(1).ZoneId = 5
     testSquad.Membership(1).Health = 54
     testSquad.Membership(1).Armor = 44
     testSquad.Membership(1).Position = Vector3(4673.5312f, 2604.8047f, 40.015625f)
     testSquad.Membership(3).Name = "cabal0428"
+    testSquad.Membership(3).Role = "Left Hand"
     testSquad.Membership(3).CharId = 353380L
     testSquad.Membership(3).ZoneId = 5
     testSquad.Membership(3).Health = 44
     testSquad.Membership(3).Armor = 54
     testSquad.Membership(3).Position = Vector3(4727.492f, 2613.5312f, 51.390625f)
     testSquad.Membership(4).Name = "xSkiku"
+    testSquad.Membership(4).Role = "Right Foot's Middle Toe's Nail"
     testSquad.Membership(4).CharId = 41588340L
     testSquad.Membership(4).ZoneId = 5
     testSquad.Membership(4).Health = 34
@@ -60,7 +67,7 @@ class SquadService extends Actor {
     testSquad.Membership(4).Position = Vector3(3675.0f, 4789.8047f, 63.21875f)
     idToSquad(PlanetSideGUID(3)) = testSquad
     testSquad.Listed = true
-    UpdateSquadList(testSquad, List())
+    UpdateSquadList(testSquad, None)
   }
 
   def GetNextSquadId() : PlanetSideGUID = {
@@ -76,7 +83,7 @@ class SquadService extends Actor {
   }
 
   def GetParticipatingSquad(player : Player, zone : Int) : Option[Squad] = {
-    memberToSquad.get(player.Name) match {
+    memberToSquad.get(player.CharId) match {
       case opt @ Some(squad) =>
         squad.Membership.find(_.Name == player.Name).get.ZoneId = zone
         opt
@@ -86,10 +93,10 @@ class SquadService extends Actor {
   }
 
   def GetLeadingSquad(player : Player, zone : Int, opt : Option[Squad]) : Squad = {
-    val name = player.Name
+    val charId = player.CharId
     val squadOut = opt match {
       case Some(squad) =>
-        if(squad.Leader.Name.equals(name)) {
+        if(squad.Leader.CharId == charId) {
           squad
         }
         else {
@@ -97,21 +104,23 @@ class SquadService extends Actor {
         }
 
       case None =>
-        memberToSquad.get(name) match {
-          case Some(squad) if squad.Leader.Name.equals(name) =>
+        memberToSquad.get(charId) match {
+          case Some(squad) if squad.Leader.CharId.equals(charId) =>
             squad
           case _ =>
             val faction = player.Faction
             val id = GetNextSquadId()
             val squad = new Squad(id, faction)
             val leadPosition = squad.Membership(squad.LeaderPositionIndex)
+            val name = player.Name
             leadPosition.Name = name
+            leadPosition.CharId = charId
             leadPosition.Health = player.Health
             leadPosition.Armor = player.Armor
             leadPosition.Position = player.Position
             leadPosition.ZoneId = zone
             log.info(s"$name-$faction has started a new squad")
-            memberToSquad += name -> squad
+            memberToSquad += charId -> squad
             idToSquad += id -> squad
             squad
         }
@@ -130,26 +139,27 @@ class SquadService extends Actor {
       log.info(s"$who has joined $path")
       SquadEvents.subscribe(who, path)
       //send initial squad catalog
-      sender ! SquadServiceResponse(s"$faction/Squad", SquadResponse.Init(publishedLists(PlanetSideEmpire(faction)).toVector))
+      sender ! SquadServiceResponse(s"$faction/Squad", SquadResponse.InitList(publishedLists(PlanetSideEmpire(faction)).toVector))
 
     //subscribe to the player's personal channel - necessary only to inform about any previous squad association
-    case Service.Join(name) =>
-      val path = s"$name/Squad"
+    case Service.Join(char_id) =>
+      val path = s"$char_id/Squad"
       val who = sender()
       log.info(s"$who has joined $path")
       SquadEvents.subscribe(who, path) //TODO squad-specific switchboard
       //check for renewable squad information
-      memberToSquad.get(name) match {
+      memberToSquad.get(char_id.toLong) match {
         case None => ;
         case Some(_) =>
           sender ! SquadServiceMessage.RecoverSquadMembership() //TODO?
       }
 
-    case Service.Leave(Some(name)) => ;
+    case Service.Leave(Some(char_id)) => ;
       SquadEvents.unsubscribe(sender())
-      memberToSquad.get(name) match {
+      val longCharId = char_id.toLong
+      memberToSquad.get(longCharId) match {
         case Some(squad) =>
-          if(squad.Leader.Name.equals(name)) {
+          if(squad.Leader.Name.equals(char_id)) {
             //we were the leader
             if(squad.Membership.count(p => p.Name.equals("")) > 1) {
               //other players were in the squad; publicly disband it
@@ -162,19 +172,19 @@ class SquadService extends Actor {
                 position.Armor = 0
               })
             }
-            memberToSquad.remove(name)
+            memberToSquad.remove(longCharId)
             idToSquad.remove(squad.GUID)
-            UpdateSquadList(squad, List())
+            UpdateSquadList(squad, None)
           }
           else {
             //we were just a grunt in the squad
-            val position = squad.Membership.find(_.Name == name).get
+            val position = squad.Membership.find(_.CharId == longCharId).get
             position.Name = ""
             position.ZoneId = 0
             position.Position = Vector3.Zero
             position.Health = 0
             position.Armor = 0
-            UpdateSquadDetail(squad)
+            UpdateSquadDetail(squad.GUID, squad)
           }
         case None => ;
       }
@@ -184,67 +194,116 @@ class SquadService extends Actor {
 
     case SquadServiceMessage(tplayer, squad_action) => squad_action match {
       case SquadAction.Membership(request_type, char_id, optional_char_id, _, _) => request_type match {
-        case SquadRequestType.Accept =>
-          bids.get(char_id) match {
-            case Some((squadGUID, line)) if idToSquad.get(squadGUID).nonEmpty =>
-              //join squad
-              val squad = idToSquad(squadGUID)
-              val position = squad.Membership(line)
-              if(squad.Availability(line) && position.CharId == 0 &&
-                tplayer.Certifications.intersect(position.Requirements) == position.Requirements) {
-                position.Name = tplayer.Name
-                position.CharId = char_id
-                position.Health = tplayer.Health
-                position.Armor = tplayer.Armor
-                position.Position = tplayer.Position
-                position.ZoneId = 13
-                memberToSquad(tplayer.Name) = squad
-                //joining the squad
-                sender ! SquadServiceResponse("", SquadResponse.Join(
-                  squad,
-                  squad.Membership.zipWithIndex.collect({ case (member, index) if member.CharId != 0 => index }).toList
-                ))
-                //other squad members see us joining the squad
-                val updatedIndex = List(line)
-                squad.Membership
-                  .collect({ case member if member.CharId != 0 && member.CharId != char_id => member.Name })
-                  .foreach { name =>
-                    SquadEvents.publish( SquadServiceResponse(s"$name/Squad", SquadResponse.Join(squad, updatedIndex)) )
-                  }
-              }
-              bids.remove(char_id)
+        case SquadRequestType.Invite =>
+          (optional_char_id, memberToSquad.get(char_id)) match {
+            case (Some(toCharId), Some(squad)) =>
+              bids(toCharId) = VacancyBid(char_id, squad.GUID)
+              SquadEvents.publish( SquadServiceResponse(s"$toCharId/Squad", SquadResponse.Invite(char_id, toCharId, tplayer.Name)) )
+            case (Some(toCharId), None) =>
+              val ourSquad = GetLeadingSquad(tplayer, 1, None)
+              memberToSquad.remove(char_id)
+              bids(toCharId) = CamraderieBid(char_id, ourSquad)
+              SquadEvents.publish( SquadServiceResponse(s"$toCharId/Squad", SquadResponse.Invite(char_id, toCharId, tplayer.Name)) )
             case _ => ;
           }
 
         case SquadRequestType.Leave =>
+          val squad = memberToSquad(char_id)
           if(optional_char_id.contains(char_id)) {
-            //we're leaving the squad on our own
-            val name = tplayer.Name
-            val squad = memberToSquad(name)
-            val membership = squad.Membership.zipWithIndex
-            val (member, index) = membership
-              .find { case (_member, _) => _member.Name == name }
-              .get
-            val updateList = membership.collect({ case (_member, _index) if _member.CharId > 0 => (_member.CharId, _index) }).toList
-            memberToSquad.remove(name)
-            member.Name = ""
-            member.CharId = 0
-            //leaving the squad completely
+            //leaving the squad of our own accord
+          }
+          else {
+            val leader = squad.Leader
+            if(optional_char_id.contains(leader.CharId)) {
+              //kicked by the squad leader
+            }
+          }
+          val membership = squad.Membership.zipWithIndex
+          val (member, index) = membership
+            .find { case (_member, _) => _member.CharId == char_id }
+            .get
+          val updateList = membership.collect({ case (_member, _index) if _member.CharId > 0 => (_member.CharId, _index) }).toList
+          memberToSquad.remove(char_id)
+          member.Name = ""
+          member.CharId = 0
+
+          val size = squad.Size
+          if(size == 1) {
+            //squad is rendered to just one person; collapse it
+            (membership.collect({ case (_member, _) if _member.CharId > 0 => _member.CharId }) :+ char_id)
+              .foreach { charId =>
+                SquadEvents.publish( SquadServiceResponse(s"$charId/Squad", SquadResponse.Leave(squad, updateList)) )
+                memberToSquad.remove(charId)
+              }
+            idToSquad.remove(squad.GUID)
+          }
+          else {
+            //squad continues, despite player's parting
+            //member leaves the squad completely
             sender ! SquadServiceResponse("", SquadResponse.Leave(squad, updateList))
-            //other squad members see us leaving the squad
+            //other squad members see the member leaving only
             val leavingMember = List((char_id, index))
             membership
-              .collect({ case (_member, _) if _member.CharId > 0 => _member.Name })
-              .foreach { name =>
-                SquadEvents.publish( SquadServiceResponse(s"$name/Squad", SquadResponse.Leave(squad, leavingMember)) )
-            }
+              .collect({ case (_member, _) if _member.CharId > 0 => _member.CharId })
+              .foreach { charId =>
+                SquadEvents.publish( SquadServiceResponse(s"$charId/Squad", SquadResponse.Leave(squad, leavingMember)) )
+              }
+          }
+
+        case SquadRequestType.Accept =>
+          bids.remove(char_id) match {
+            case Some(NormalBid(inviterCharId, squadGUID, line)) if idToSquad.get(squadGUID).nonEmpty =>
+              JoinSquad(tplayer, idToSquad(squadGUID), line)
+
+            case Some(VacancyBid(inviterCharId, squadGUID)) if idToSquad.get(squadGUID).nonEmpty =>
+              val squad = idToSquad(squadGUID)
+              squad.Membership.zipWithIndex.find({ case (member, index) =>
+                ValidOpenSquadPosition(squad, index, squad.Membership(index), tplayer.Certifications)
+              }) match {
+                case Some((_, line)) =>
+                  JoinSquad(tplayer, squad, line)
+                case _ => ;
+              }
+
+            case Some(CamraderieBid(inviterCharId, placeholderSquad)) =>
+              (GetParticipatingSquad(tplayer, 1) match {
+                case Some(participating) =>
+                  if(participating.Leader.CharId == inviterCharId) {
+                    Some(participating)
+                  }
+                  else {
+                    //inviter is not leader of squad; bounce this request off of the squad leader
+                    val leaderCharId = participating.Leader.CharId
+                    bids(char_id) = VacancyBid(leaderCharId, participating.GUID) //reframed request
+                    //TODO squad leader receives "<player> wants to join squad" prompt
+                    //SquadEvents.publish(SquadServiceResponse(s"$leaderCharId/Squad", SquadResponse.Invite(char_id, leaderCharId, tplayer.Name)))
+                    None
+                  }
+                case None =>
+                  placeholderSquad.Task = s"${tplayer.Name}'s Squad"
+                  memberToSquad(inviterCharId) = placeholderSquad
+                  SquadEvents.publish(SquadServiceResponse(s"$inviterCharId/Squad", SquadResponse.InitSquad(placeholderSquad.GUID)))
+                  Some(placeholderSquad)
+              }) match {
+              case Some(squad) =>
+                squad.Membership.zipWithIndex.find({ case (member, index) =>
+                  ValidOpenSquadPosition(squad, index, squad.Membership(index), tplayer.Certifications)
+                }) match {
+                  case Some((_, line)) =>
+                    JoinSquad(tplayer, squad, line)
+                  case _ => ;
+                }
+              case None => ;
+              }
+
+            case _ => ;
           }
 
         case _ => ;
       }
 
       case SquadAction.Update(char_id, health, max_health, armor, max_armor, pos, zone_number) =>
-        memberToSquad.get(tplayer.Name) match {
+        memberToSquad.get(char_id) match {
           case Some(squad) =>
             squad.Membership.find(_.CharId == char_id) match {
               case Some(member) =>
@@ -263,46 +322,64 @@ class SquadService extends Actor {
                 ))
               case _ => ;
             }
-//            val (self, others) = squad.Membership.partition(_.CharId == char_id)
-//            self match {
-//              case Array(member) =>
-//                val newHealth = StatConverter.Health(health, max_health, min=1, max=64)
-//                val newArmor = StatConverter.Health(armor, max_armor, min=1, max=64)
-//                member.Health = newHealth
-//                member.Armor = newArmor
-//                member.Position = pos
-//                member.ZoneId = zone_number
-//                sender ! SquadServiceResponse("", SquadResponse.UpdateMembers(
-//                  squad,
-//                  others
-//                    .map { member => SquadAction.Update(member.CharId, member.Health, 0, member.Armor, 0, member.Position, member.ZoneId) }
-//                    .toList
-//                ))
-//              case _ => ;
-//            }
 
           case None => ;
         }
 
-      case SquadAction.Definition(tplayer : Player, zone_ordinal_number : Int, guid : PlanetSideGUID, _ : Int, action : SquadAction) =>
+      case SquadAction.Definition(tplayer : Player, zone_ordinal_number : Int, guid : PlanetSideGUID, line : Int, action : SquadAction) =>
         import net.psforever.packet.game.SquadAction._
         val squadOpt = GetParticipatingSquad(tplayer, zone_ordinal_number)
         action match {
-          case SaveSquadDefinition() =>
+          case SaveSquadFavorite() =>
+            squadOpt match {
+              case Some(squad) if squad.Leader.CharId == tplayer.CharId && squad.Task.nonEmpty && squad.ZoneId > 0 =>
+                tplayer.SquadLoadouts.SaveLoadout(squad, squad.Task, line)
+                sender ! SquadServiceResponse("", SquadResponse.ListSquadFavorite(line, squad.Task))
+              case _ => ;
+            }
+
+          case LoadSquadFavorite() =>
+            (squadOpt match {
+              case Some(squad) if squad.Size == 1 =>
+                //we are the leader of our own squad, but no one else has joined yet
+                Some(squad)
+              case None => ;
+                //we are not yet member of a squad; start a squad to support our favorite definition
+                Some(GetLeadingSquad(tplayer, zone_ordinal_number, None))
+              case _ => ;
+                //player is member of populated squad; should not overwrite squad definition with favorite
+                None
+            }, tplayer.SquadLoadouts.LoadLoadout(line)) match {
+              case (Some(squad : Squad), Some(loadout : SquadLoadout)) =>
+                log.info(s"${tplayer.Name} is loading a squad composition: $loadout")
+                SquadService.LoadSquadDefinition(squad, loadout)
+                sender ! SquadServiceResponse("", SquadResponse.InitSquad(squad.GUID))
+                UpdateSquadList(squad, SquadInfo().Task(squad.Task).ZoneId(PlanetSideZoneID(squad.ZoneId)).Capacity(squad.Capacity))
+                UpdateSquadDetail(PlanetSideGUID(0), squad)
+
+              case _ => ;
+            }
+
+          case DeleteSquadFavorite() =>
+            tplayer.SquadLoadouts.DeleteLoadout(line)
+            sender ! SquadServiceResponse("", SquadResponse.ListSquadFavorite(line, ""))
 
           case ChangeSquadPurpose(purpose) =>
             log.info(s"${tplayer.Name}-${tplayer.Faction} has changed his squad's task to $purpose")
             val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt)
             squad.Task = purpose
-            UpdateSquadList(squad, List(SquadInfo.Field.Task))
-            UpdateSquadDetail(squad)
+
+            UpdateSquadListWhenListed(squad, SquadInfo().Task(purpose))
+            SquadDetail().Task(purpose)
+            UpdateListedSquadDetail(squad.GUID, squad)
 
           case ChangeSquadZone(zone) =>
             log.info(s"${tplayer.Name}-${tplayer.Faction} has changed squad's ops zone to $zone")
             val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt)
             squad.ZoneId = zone.zoneId.toInt
-            UpdateSquadList(squad, List(SquadInfo.Field.ZoneId))
-            UpdateSquadDetail(squad)
+            UpdateSquadListWhenListed(squad, SquadInfo().ZoneId(zone))
+            SquadDetail().ZoneId(zone)
+            UpdateListedSquadDetail(squad.GUID, squad)
 
           case CloseSquadMemberPosition(position) =>
             val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt)
@@ -312,14 +389,15 @@ class SquadService extends Actor {
                 log.info(s"${tplayer.Name}-${tplayer.Faction} has closed the #$position position in squad")
                 val memberPosition = squad.Membership(position)
                 val listingChanged = if(memberPosition.Name.nonEmpty) {
-                  List(SquadInfo.Field.Size, SquadInfo.Field.Capacity)
+                  SquadInfo().Size(squad.Size).Capacity(squad.Capacity)
                 }
                 else {
-                  List(SquadInfo.Field.Capacity)
+                  SquadInfo().Capacity(squad.Capacity)
                 }
                 memberPosition.Close()
-                UpdateSquadList(squad, listingChanged)
-                UpdateSquadDetail(squad)
+                UpdateSquadListWhenListed(squad, listingChanged)
+                SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail.Closed)))
+                UpdateListedSquadDetail(squad.GUID, squad)
               case Some(false) | None => ;
             }
 
@@ -329,8 +407,9 @@ class SquadService extends Actor {
               case Some(false) =>
                 log.info(s"${tplayer.Name}-${tplayer.Faction} has opened the #$position position in squad")
                 squad.Availability.update(position, true)
-                UpdateSquadList(squad, List(SquadInfo.Field.Capacity))
-                UpdateSquadDetail(squad)
+                UpdateSquadListWhenListed(squad, SquadInfo().Capacity(squad.Capacity))
+                SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail.Open)))
+                UpdateListedSquadDetail(squad.GUID, squad)
               case Some(true) | None => ;
             }
 
@@ -340,7 +419,8 @@ class SquadService extends Actor {
               case Some(true) =>
                 log.info(s"${tplayer.Name}-${tplayer.Faction} has changed the role of squad position #$position")
                 squad.Membership(position).Role = role
-                UpdateSquadDetail(squad)
+                SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail().Role(role))))
+                UpdateListedSquadDetail(squad.GUID, squad)
               case Some(false) | None => ;
             }
 
@@ -350,7 +430,8 @@ class SquadService extends Actor {
               case Some(true) =>
                 log.info(s"${tplayer.Name}-${tplayer.Faction} has changed the orders for squad position #$position")
                 squad.Membership(position).Orders = orders
-                UpdateSquadDetail(squad)
+                SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail().DetailedOrders(orders))))
+                UpdateListedSquadDetail(squad.GUID, squad)
               case Some(false) | None => ;
             }
 
@@ -360,7 +441,8 @@ class SquadService extends Actor {
               case Some(true) =>
                 log.info(s"${tplayer.Name}-${tplayer.Faction} has changed the requirements for squad position #$position")
                 squad.Membership(position).Requirements = certs
-                UpdateSquadDetail(squad)
+                SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail().Requirements(certs))))
+                UpdateListedSquadDetail(squad.GUID, squad)
               case Some(false) | None => ;
             }
 
@@ -386,12 +468,47 @@ class SquadService extends Actor {
 
           case SelectRoleForYourself(position) =>
             //TODO need to ask permission from the squad leader, unless our character is the squad leader or already currently in the squad
-            //val name = tplayer.Name
             squadOpt match {
-              case Some(squad) if squad.GUID == guid =>
-              //already a member of this squad; swap positions freely
+              case Some(squad) if squad.GUID == guid || squad.Leader.Name == tplayer.Name =>
+                //already a member of this squad, or the leader; swap positions freely
+                val membership = squad.Membership.zipWithIndex
+                val toMember = squad.Membership(position)
+                if(ValidOpenSquadPosition(squad, position, toMember, tplayer.Certifications)) {
+                  //acquire this membership position
+                  membership.find { case (member, _) => member.Name == tplayer.Name } match {
+                    case Some((fromMember, fromIndex)) =>
+                      //copy member details
+                      toMember.Name = fromMember.Name
+                      toMember.CharId = fromMember.CharId
+                      toMember.Health = fromMember.Health
+                      toMember.Armor = fromMember.Armor
+                      toMember.Position = fromMember.Position
+                      toMember.ZoneId = fromMember.ZoneId
+                      //old membership no longer valid
+                      fromMember.Name = ""
+                      fromMember.CharId = 0L
+                      if(fromIndex == squad.LeaderPositionIndex) {
+                        squad.LeaderPositionIndex = position
+                      }
+                      membership
+                        .collect({ case (_member, _) if _member.CharId > 0 => _member.Name })
+                        .foreach { name =>
+                          SquadEvents.publish(SquadServiceResponse(s"$name/Squad", SquadResponse.SwapMember(squad, fromIndex, position)))
+                        }
+                    case None =>
+                      toMember.Name = tplayer.Name
+                      toMember.CharId = tplayer.CharId
+                      toMember.Health = StatConverter.Health(tplayer.Health, tplayer.MaxHealth, min = 1, max = 64)
+                      toMember.Armor = StatConverter.Health(tplayer.Armor, tplayer.MaxArmor, min = 1, max = 64)
+                      toMember.Position = tplayer.Position
+                      toMember.ZoneId = 13
+                      memberToSquad(tplayer.CharId) = squad
+                  }
+                }
+                UpdateSquadDetail(squad.GUID, squad)
+
               case Some(_) =>
-              //not a member of the requesting squad; do nothing
+                //not a member of the requesting squad; do nothing
               case None =>
                 //not a member of any squad; consider request of joining the target squad
                 idToSquad.get(guid) match {
@@ -399,10 +516,10 @@ class SquadService extends Actor {
                     val member = squad.Membership(position)
                     if(squad.Availability(position) && member.CharId == 0 &&
                       tplayer.Certifications.intersect(member.Requirements) == member.Requirements) {
-                      bids(tplayer.CharId) = (guid, position)
+                      bids(tplayer.CharId) = NormalBid(squad.Leader.CharId, guid, position)
                       val leader = squad.Leader
                       //TODO need to ask permission from the squad leader, unless auto-approve is in effect
-                      sender ! SquadServiceResponse("", SquadResponse.Membership(SquadRequestType.Invite, 0, 0, leader.CharId, Some(tplayer.CharId), leader.Name, false, None))
+                      sender ! SquadServiceResponse("", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, leader.CharId, Some(tplayer.CharId), leader.Name, false, None))
                     }
 
                   case None =>
@@ -411,36 +528,62 @@ class SquadService extends Actor {
                 }
             }
 
-          case ListSquad() =>
+          case RequestListSquad() =>
             val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt)
             if(!squad.Listed) {
-              log.info(s"${tplayer.Name}-${tplayer.Faction} has opened recruitment for this squad")
+              log.info(s"${tplayer.Name}-${tplayer.Faction} has opened public recruitment for squad ${squad.Task}")
               squad.Listed = true
+              sender ! SquadServiceResponse("", SquadResponse.InitSquad(squad.GUID))
+              UpdateSquadList(squad, None)
+              sender ! SquadServiceResponse("", SquadResponse.Unknown17(squad, tplayer.CharId))
+            }
+
+          case StopListSquad() =>
+            val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt)
+            if(!squad.Listed) {
+              log.info(s"${tplayer.Name}-${tplayer.Faction} has closed public recruitment for squad ${squad.Task}")
+              squad.Listed = false
+              sender ! SquadServiceResponse("", SquadResponse.InitSquad(PlanetSideGUID(0)))
+              UpdateSquadList(squad, None)
             }
-            UpdateSquadList(squad, List())
 
           case ResetAll() =>
-            val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt)
-            squad.Task = ""
-            squad.ZoneId = None
-            squad.Availability.indices.foreach { i =>
-              squad.Availability.update(i, true)
+            squadOpt match {
+              case Some(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()
+                })
+                squad.LocationFollowsSquadLead = false
+                squad.AutoApproveInvitationRequests = false
+                UpdateSquadList(squad, SquadInfo().Task("").ZoneId(None).Capacity(squad.Capacity))
+                UpdateSquadDetail(squad.GUID, squad)
+              case None => ;
             }
-            squad.Membership.foreach(position => {
-              position.Role = ""
-              position.Orders = ""
-              position.Requirements = Set()
-            })
-            UpdateSquadList(squad, List(SquadInfo.Field.Task, SquadInfo.Field.ZoneId, SquadInfo.Field.Size, SquadInfo.Field.Capacity))
-            UpdateSquadDetail(squad)
 
           case DisplaySquad() =>
             idToSquad.get(guid) match {
               case Some(squad) =>
-                sender ! SquadServiceResponse(s"${tplayer.Name}/Squad", GenSquadDetail(squad))
+                sender ! SquadServiceResponse(s"${tplayer.Name}/Squad", GenSquadDetail(squad.GUID, squad))
               case None => ;
             }
 
+//          case AnswerSquadJoinRequest() =>
+//            idToSquad.get(guid) match {
+//              case Some(squad) =>
+//                if(squad.Leader.Name == tplayer.Name && squad.Listed) {
+//                  //squad was just listed but we have not yet declared ourselves as the leader
+//                  UpdateSquadDetail(squad)
+//                }
+//              case None => ;
+//            }
+
           case _ => ;
         }
 
@@ -449,54 +592,109 @@ class SquadService extends Actor {
     }
   }
 
-  def UpdateSquadList(squad : Squad, listingChanged : List[Int]) : Unit = {
-    //queue updates
+  def JoinSquad(player : Player, squad : Squad, line : Int) : Boolean = {
+    val charId = player.CharId
+    val position = squad.Membership(line)
+    if(squad.Availability(line) && position.CharId == 0 &&
+      player.Certifications.intersect(position.Requirements) == position.Requirements) {
+      position.Name = player.Name
+      position.CharId = charId
+      position.Health = StatConverter.Health(player.Health, player.MaxHealth, min=1, max=64)
+      position.Armor = StatConverter.Health(player.Armor, player.MaxArmor, min=1, max=64)
+      position.Position = player.Position
+      position.ZoneId = 13
+      memberToSquad(player.CharId) = squad
+
+      val size = squad.Size
+      if(size == 1) {
+        //leader joins the squad? do nothing?
+        squad.LeaderPositionIndex = line
+      }
+      else if(size == 2) {
+        //first squad member after leader; both members fully initialize
+        val indices = squad.Membership.zipWithIndex.collect({ case (member, index) if member.CharId != 0 => index }).toList
+        squad.Membership
+          .collect({ case member if member.CharId != 0 => member.CharId })
+          .foreach { charId =>
+            SquadEvents.publish(SquadServiceResponse(s"$charId/Squad", SquadResponse.Join(squad, indices)))
+          }
+      }
+      else {
+        //joining an active squad; everybody updates differently
+        //new member gets full UI updates
+        sender ! SquadServiceResponse("", SquadResponse.Join(
+          squad,
+          squad.Membership.zipWithIndex.collect({ case (member, index) if member.CharId != 0 => index }).toList
+        ))
+        //other squad members see us joining the squad
+        val updatedIndex = List(line)
+        squad.Membership
+          .collect({ case member if member.CharId != 0 && member.CharId != charId => member.CharId })
+          .foreach { charId =>
+            SquadEvents.publish(SquadServiceResponse(s"$charId/Squad", SquadResponse.Join(squad, updatedIndex)))
+          }
+      }
+      true
+    }
+    else {
+      false
+    }
+  }
+
+  def UpdateSquadList(squad : Squad, changes : SquadInfo) : Unit = {
+    UpdateSquadList(squad, Some(changes))
+  }
+
+  def UpdateSquadListWhenListed(squad : Squad, changes : SquadInfo) : Unit = {
+    UpdateSquadListWhenListed(squad, Some(changes))
+  }
+
+  def UpdateSquadListWhenListed(squad : Squad, changes : Option[SquadInfo]) : Unit = {
     if(squad.Listed) {
-      val entry = SquadService.Publish(squad)
-      val faction = squad.Faction
-      val factionListings = publishedLists(faction)
-      factionListings.find(info => {
-        info.squad_guid match {
-          case Some(sguid) => sguid == squad.GUID
-          case _ => false
-        }
-      }) match {
-        case Some(listedSquad) =>
-          val index = factionListings.indexOf(listedSquad)
-          val changes = if(listingChanged.nonEmpty) {
-            SquadService.Differences(listingChanged, entry)
-          }
-          else {
-            SquadService.Differences(listedSquad, entry)
-          }
-          if(changes != SquadInfo.Blank) {
+      UpdateSquadList(squad, changes)
+    }
+  }
+
+  def UpdateSquadList(squad : Squad, changes : Option[SquadInfo]) : Unit = {
+    val entry = SquadService.SquadList.Publish(squad)
+    val faction = squad.Faction
+    val factionListings = publishedLists(faction)
+    factionListings.find(info => {
+      info.squad_guid match {
+        case Some(sguid) => sguid == squad.GUID
+        case _ => false
+      }
+    }) match {
+      case Some(listedSquad) =>
+        val index = factionListings.indexOf(listedSquad)
+        changes match {
+          case Some(changedFields) =>
             //squad information update
             log.info(s"Squad will be updated")
             factionListings(index) = entry
             SquadEvents.publish(
-              SquadServiceResponse(s"$faction/Squad", SquadResponse.Update(Seq((index, changes))))
+              SquadServiceResponse(s"$faction/Squad", SquadResponse.UpdateList(Seq((index, changedFields))))
             )
-          }
-          else {
+          case None =>
             //remove squad from listing
             log.info(s"Squad will be removed")
             factionListings.remove(index)
             SquadEvents.publish(
-              SquadServiceResponse(s"$faction/Squad", SquadResponse.Remove(Seq(index)))
+              //SquadServiceResponse(s"$faction/Squad", SquadResponse.RemoveFromList(Seq(index)))
+              SquadServiceResponse(s"$faction/Squad", SquadResponse.InitList(factionListings.toVector))
             )
-          }
-        case None =>
-          //first time being published
-          log.info(s"Squad will be introduced")
-          factionListings += SquadService.Publish(squad)
-          SquadEvents.publish(
-            SquadServiceResponse(s"$faction/Squad", SquadResponse.Init(factionListings.toVector))
-          )
-      }
+        }
+      case None =>
+        //first time being published
+        log.info(s"Squad will be introduced")
+        factionListings += entry
+        SquadEvents.publish(
+          SquadServiceResponse(s"$faction/Squad", SquadResponse.InitList(factionListings.toVector))
+        )
     }
   }
 
-  def GenSquadDetail(squad : Squad) : SquadResponse.Detail = {
+  def GenSquadDetail(guid : PlanetSideGUID, squad : Squad) : SquadResponse.Detail = {
     SquadResponse.Detail(
       squad.GUID,
       squad.Leader.Name,
@@ -513,60 +711,164 @@ class SquadService extends Actor {
     )
   }
 
-  def UpdateSquadDetail(squad : Squad) : Unit = {
-    val detail = GenSquadDetail(squad)
-    squad.Membership.collect {
-      case member if !member.Name.equals("") =>
-        member.Name
-    }.foreach { name =>
-      SquadEvents.publish(
-        SquadServiceResponse(s"$name/Squad", detail)
-      )
+  def UpdateListedSquadDetail(guid : PlanetSideGUID, squad : Squad) : Unit = {
+    if(squad.Listed) {
+      UpdateSquadDetail(guid, squad)
     }
   }
+
+  def UpdateSquadDetail(guid : PlanetSideGUID, squad : Squad) : Unit = {
+    val detail = GenSquadDetail(guid, squad)
+    squad.Membership
+      .collect { case member if member.CharId > 0L => member.CharId }
+      .foreach { charId =>
+        SquadEvents.publish(SquadServiceResponse(s"$charId/Squad", detail))
+      }
+  }
 }
 
 object SquadService {
-  def Publish(squad : Squad) : SquadInfo = {
-    SquadInfo(
-      squad.Leader.Name,
-      squad.Task,
-      PlanetSideZoneID(squad.ZoneId),
-      squad.Size,
-      squad.Capacity,
-      squad.GUID
-    )
-  }
+  trait PositionBid
 
-  def Differences(updates : List[Int], info : SquadInfo) : SquadInfo = {
-    if(updates.nonEmpty) {
-      val list = Seq(
-        SquadInfo.Blank, //must be index-0
-        SquadInfo(info.leader, None, None, None, None),
-        SquadInfo(None, info.task, None, None, None),
-        SquadInfo(None, None, info.zone_id, None, None),
-        SquadInfo(None, None, None, info.size, None),
-        SquadInfo(None, None, None, None, info.capacity)
+  final case class NormalBid(char_id : Long, squad_guid : PlanetSideGUID, position : Int) extends PositionBid
+
+  final case class VacancyBid(char_id : Long, squad_guid : PlanetSideGUID) extends PositionBid
+
+  final case class CamraderieBid(char_id : Long, placeholder : Squad) extends PositionBid
+
+  object SquadList {
+    def Publish(squad : Squad) : SquadInfo = {
+      SquadInfo(
+        squad.Leader.Name,
+        squad.Task,
+        PlanetSideZoneID(squad.ZoneId),
+        squad.Size,
+        squad.Capacity,
+        squad.GUID
       )
-      var out = SquadInfo.Blank
-      updates
-        .map(i => list(i))
-        .filterNot { _ == SquadInfo.Blank }
-        .foreach(sinfo => out = out And sinfo )
-      out
-    }
-    else {
-      SquadInfo.Blank
     }
   }
 
-  def Differences(before : SquadInfo, after : SquadInfo) : SquadInfo = {
-    SquadInfo(
-      if(!before.leader.equals(after.leader)) after.leader else None,
-      if(!before.task.equals(after.task)) after.task else None,
-      if(!before.zone_id.equals(after.zone_id)) after.zone_id else None,
-      if(!before.size.equals(after.size)) after.size else None,
-      if(!before.capacity.equals(after.capacity)) after.capacity else None
-    )
+  object Detail {
+    def Publish(squad : Squad) : SquadDetail = {
+      SquadDetail()
+        .LeaderCharId(squad.Leader.CharId)
+        .LeaderName(squad.Leader.Name)
+        .Task(squad.Task)
+        .ZoneId(PlanetSideZoneID(squad.ZoneId))
+        .Members(
+          squad.Membership.zipWithIndex.map({ case (p, index) =>
+            SquadPositionEntry(index, if(squad.Availability(index)) {
+              SquadPositionDetail(p.Role, p.Orders, p.Requirements, p.CharId, p.Name)
+            }
+            else {
+              SquadPositionDetail.Closed
+            })
+          }).toList
+        )
+        .Complete
+    }
+
+    def Differences(updates : List[Int], info : SquadDetail) : SquadDetail = {
+      if(updates.nonEmpty) {
+        val list = Seq(
+          SquadDetail.Blank, //must be index-0
+          SquadDetail(info.unk1, None, None, None, None, None, None, None, None),
+          SquadDetail(None, None, info.leader_char_id, None, None, None, None, None, None),
+          SquadDetail(None, None, None, info.unk3, None, None, None, None, None),
+          SquadDetail(None, None, None, None, info.leader_name, None, None, None, None),
+          SquadDetail(None, None, None, None, None, info.task, None, None, None),
+          SquadDetail(None, None, None, None, None, None, info.zone_id, None, None),
+          SquadDetail(None, None, None, None, None, None, None, info.unk7, None),
+          SquadDetail(None, None, None, None, None, None, None, None, info.member_info)
+        )
+        var out = SquadDetail.Blank
+        updates
+          .map(i => list(i))
+          .filterNot { _ == SquadDetail.Blank }
+          .foreach(sinfo => out = out And sinfo )
+        out
+      }
+      else {
+        SquadDetail.Blank
+      }
+    }
+
+    def Differences(before : SquadDetail, after : SquadDetail) : SquadDetail = {
+      SquadDetail(
+        if(!before.unk1.equals(after.unk1)) after.unk1 else None,
+        None,
+        if(!before.leader_char_id.equals(after.leader_char_id)) after.leader_char_id else None,
+        if(!before.unk3.equals(after.unk3)) after.unk3 else None,
+        if(!before.leader_name.equals(after.leader_name)) after.leader_name else None,
+        if(!before.task.equals(after.task)) after.task else None,
+        if(!before.zone_id.equals(after.zone_id)) after.zone_id else None,
+        if(!before.unk7.equals(after.unk7)) after.unk7 else None,
+        {
+          (before.member_info, after.member_info) match {
+            case (Some(beforeEntry), Some(afterEntry)) =>
+              val out = beforeEntry.zip(afterEntry)
+                .map { case (b, a) => PositionEquality(b, a) }
+                .collect { case Some(entry) => entry }
+              if(out.nonEmpty) {
+                Some(out)
+              }
+              else {
+                None
+              }
+            case _ =>
+              None
+          }
+        }
+      )
+    }
+
+    private def PositionEquality(before : SquadPositionEntry, after : SquadPositionEntry) : Option[SquadPositionEntry] = {
+      (before.info, after.info) match {
+        case (Some(binfo), Some(ainfo)) =>
+          val out = MemberEquality(binfo, ainfo)
+          if(out == SquadPositionDetail.Blank) {
+            None
+          }
+          else {
+            Some(SquadPositionEntry(before.index, out))
+          }
+        case _ =>
+          None
+      }
+    }
+
+    private def MemberEquality(before : SquadPositionDetail, after : SquadPositionDetail) : SquadPositionDetail = {
+      SquadPositionDetail(
+        if(!before.is_closed.equals(after.is_closed)) after.is_closed else None,
+        if(!before.role.equals(after.role)) after.role else None,
+        if(!before.detailed_orders.equals(after.detailed_orders)) after.detailed_orders else None,
+        if(!before.requirements.equals(after.requirements)) after.requirements else None,
+        if(!before.char_id.equals(after.char_id)) after.char_id else None,
+        if(!before.name.equals(after.name)) after.name else None
+      )
+    }
+  }
+
+  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
+    }
+  }
+
+  def ValidOpenSquadPosition(squad : Squad, position : Int, member : Member, reqs : Set[CertificationType.Value]) : Boolean = {
+    squad.Availability(position) && member.CharId == 0 && reqs.intersect(member.Requirements) == member.Requirements
   }
 }
diff --git a/common/src/test/scala/game/SquadDefinitionActionMessageTest.scala b/common/src/test/scala/game/SquadDefinitionActionMessageTest.scala
index f36b61c5..09e2cd42 100644
--- a/common/src/test/scala/game/SquadDefinitionActionMessageTest.scala
+++ b/common/src/test/scala/game/SquadDefinitionActionMessageTest.scala
@@ -54,7 +54,7 @@ class SquadDefinitionActionMessageTest extends Specification {
       case SquadDefinitionActionMessage(unk1, unk2, action) =>
         unk1 mustEqual PlanetSideGUID(0)
         unk2 mustEqual 3
-        action mustEqual SaveSquadDefinition()
+        action mustEqual SaveSquadFavorite()
       case _ =>
         ko
     }
@@ -65,7 +65,7 @@ class SquadDefinitionActionMessageTest extends Specification {
       case SquadDefinitionActionMessage(unk1, unk2, action) =>
         unk1 mustEqual PlanetSideGUID(0)
         unk2 mustEqual 3
-        action mustEqual LoadSquadDefinition()
+        action mustEqual LoadSquadFavorite()
       case _ =>
         ko
     }
@@ -76,7 +76,7 @@ class SquadDefinitionActionMessageTest extends Specification {
       case SquadDefinitionActionMessage(unk1, unk2, action) =>
         unk1 mustEqual PlanetSideGUID(0)
         unk2 mustEqual 3
-        action mustEqual ListSquadDefinition("Cops and Military Officers")
+        action mustEqual ListSquadFavorite("Cops and Military Officers")
       case _ =>
         ko
     }
@@ -87,7 +87,7 @@ class SquadDefinitionActionMessageTest extends Specification {
       case SquadDefinitionActionMessage(unk1, unk2, action) =>
         unk1 mustEqual PlanetSideGUID(0)
         unk2 mustEqual 0
-        action mustEqual ListSquad()
+        action mustEqual RequestListSquad()
       case _ =>
         ko
     }
@@ -328,28 +328,28 @@ class SquadDefinitionActionMessageTest extends Specification {
   }
 
   "encode (03)" in {
-    val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 3, SaveSquadDefinition())
+    val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 3, SaveSquadFavorite())
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_03
   }
 
   "encode (03)" in {
-    val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 3, LoadSquadDefinition())
+    val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 3, LoadSquadFavorite())
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_04
   }
 
   "encode (07)" in {
-    val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 3, ListSquadDefinition("Cops and Military Officers"))
+    val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 3, ListSquadFavorite("Cops and Military Officers"))
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_07
   }
 
   "encode (08)" in {
-    val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, ListSquad())
+    val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, RequestListSquad())
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_08
diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala
index 6064e729..37619aec 100644
--- a/pslogin/src/main/scala/WorldSessionActor.scala
+++ b/pslogin/src/main/scala/WorldSessionActor.scala
@@ -346,10 +346,13 @@ class WorldSessionActor extends Actor with MDCContextAware {
 
     case SquadServiceResponse(toChannel, response) =>
       response match {
-        case SquadResponse.Init(infos) if infos.nonEmpty =>
+        case SquadResponse.ListSquadFavorite(line, task) =>
+          sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), line, SquadAction.ListSquadFavorite(task)))
+
+        case SquadResponse.InitList(infos) if infos.nonEmpty =>
           sendResponse(ReplicationStreamMessage(infos))
 
-        case SquadResponse.Update(infos) if infos.nonEmpty =>
+        case SquadResponse.UpdateList(infos) if infos.nonEmpty =>
           val o = ReplicationStreamMessage(6, None,
             infos.map { case (index, squadInfo) =>
               SquadListing(index, squadInfo)
@@ -364,7 +367,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
             )
           )
 
-        case SquadResponse.Remove(infos) if infos.nonEmpty =>
+        case SquadResponse.RemoveFromList(infos) if infos.nonEmpty =>
           sendResponse(
             ReplicationStreamMessage(1, None,
               infos.map { index =>
@@ -387,9 +390,21 @@ class WorldSessionActor extends Actor with MDCContextAware {
             )
           )
 
+        case SquadResponse.InitSquad(squad_guid) =>
+          sendResponse(SquadDefinitionActionMessage(squad_guid, 0, SquadAction.Unknown(16)))
+          sendResponse(SquadDefinitionActionMessage(squad_guid, 0, SquadAction.SetListSquad()))
+
+        case SquadResponse.Unknown17(squad, char_id) =>
+          sendResponse(
+            SquadDefinitionActionMessage(squad.GUID, 0, SquadAction.Unknown(33))
+          )
+
         case SquadResponse.Membership(request_type, unk1, unk2, unk3, unk4, player_name, unk5, unk6) =>
           sendResponse(SquadMembershipResponse(request_type, unk1, unk2, unk3, unk4, player_name, unk5, unk6))
 
+        case SquadResponse.Invite(from_char_id, to_char_id, name) =>
+          sendResponse(SquadMembershipResponse(SquadResponseType.Invite, 0, 0, from_char_id, Some(to_char_id), s"$name", false, Some(None)))
+
         case SquadResponse.Join(squad, positionsToUpdate) =>
           val leader = squad.Leader
           val id = 11
@@ -399,7 +414,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
           membershipPositions.find({ case(member, _) => member.CharId == avatar.CharId }) match {
             case Some((ourMember, ourIndex)) =>
               //we are joining the squad
-              sendResponse(SquadMembershipResponse(SquadRequestType.Accept, 0, 0, player.CharId, Some(leader.CharId), player.Name, true, Some(None)))
+              sendResponse(SquadMembershipResponse(SquadResponseType.Accept, 0, 0, player.CharId, Some(leader.CharId), player.Name, true, Some(None)))
               //load each member's entry (our own too)
               membershipPositions.foreach { case(member, index) =>
                 sendResponse(SquadMemberEvent(0, id, member.CharId, index, Some(member.Name), Some(member.ZoneId), Some(0)))
@@ -410,7 +425,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
               sendResponse(PlanetsideAttributeMessage(player.GUID, 31, id)) //associate with squad?
               sendResponse(PlanetsideAttributeMessage(player.GUID, 32, ourIndex)) //associate with member position in squad?
               //a finalization? what does this do?
-              sendResponse(SquadDefinitionActionMessage(squad.GUID, 0, SquadAction.Unknown(18, hex"00".toBitVector.take(6))))
+              sendResponse(SquadDefinitionActionMessage(squad.GUID, 0, SquadAction.Unknown(18)))
             case _ =>
               //other player is joining our squad
               //load each member's entry
@@ -432,7 +447,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
           positionsToUpdate.find({ case(member, _) => member == avatar.CharId }) match {
             case Some((ourMember, ourIndex)) =>
               //we are leaving the squad
-              sendResponse(SquadMembershipResponse(SquadRequestType.Leave, 0,1, avatar.CharId, Some(avatar.CharId), avatar.name, true, Some(None)))
+              sendResponse(SquadMembershipResponse(SquadResponseType.Leave, 0,1, avatar.CharId, Some(avatar.CharId), avatar.name, true, Some(None)))
               //remove each member's entry (our own too)
               positionsToUpdate.foreach { case(member, index) =>
                 sendResponse(SquadMemberEvent(1, id, member, index, None, None, None))
@@ -444,7 +459,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
               sendResponse(PlanetsideAttributeMessage(player.GUID, 32, 0)) //disassociate with member position in squad?
               sendResponse(PlanetsideAttributeMessage(player.GUID, 34, 4294967295L)) //unknown, perhaps unrelated?
               //a finalization? what does this do?
-              sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(18, hex"00".toBitVector.take(6))))
+              sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(18)))
             case _ =>
               //remove each member's entry
               positionsToUpdate.foreach { case(member, index) =>
@@ -455,6 +470,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
 
         case SquadResponse.UpdateMembers(squad, positions) =>
           import services.teamwork.SquadAction.{Update => SAUpdate}
+          val id = 11
           val pairedEntries = positions.collect {
             case entry if squadUI.contains(entry.char_id) =>
               (entry, squadUI(entry.char_id))
@@ -476,12 +492,31 @@ class WorldSessionActor extends Actor with MDCContextAware {
           if(updatedEntries.nonEmpty) {
             sendResponse(
               SquadState(
-                PlanetSideGUID(11),
+                PlanetSideGUID(id),
                 updatedEntries.map { entry => SquadStateInfo(entry.char_id, entry.health, entry.armor, entry.pos, 2,2, false, 429, None,None)}
               )
             )
           }
 
+        case SquadResponse.SwapMember(squad, to_index, from_index) =>
+          //this failsafe is not supported by normal squad member operations
+          val member = squad.Membership(to_index)
+          val charId = member.CharId
+          val elem = squadUI(charId)
+          val id = 11
+          squadUI(charId) = SquadUIElement(elem.name, to_index, elem.zone, elem.health, elem.armor, elem.position)
+          sendResponse(SquadMemberEvent(1, id, charId, from_index, None, None, None))
+          sendResponse(SquadMemberEvent(0, id, charId, to_index, Some(elem.name), Some(elem.zone), Some(0)))
+          sendResponse(
+            SquadState(
+              PlanetSideGUID(id),
+              List(SquadStateInfo(charId, elem.health, elem.armor, elem.position, 2,2, false, 429, None,None))
+            )
+          )
+          if(charId == avatar.CharId) {
+            sendResponse(PlanetsideAttributeMessage(player.GUID, 32, to_index))
+          }
+
         case _ => ;
       }
 
@@ -956,7 +991,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
       galaxyService ! Service.Join("galaxy") //for galaxy-wide messages
       galaxyService ! Service.Join(s"${avatar.faction}") //for hotspots
       squadService ! Service.Join(s"${avatar.faction}") //channel will be player.Faction
-      squadService ! Service.Join(avatar.name) //management of any lingering squad information connected to this player
+      squadService ! Service.Join(s"${avatar.CharId}") //channel will be player.CharId (in order to work with packets)
       cluster ! InterstellarCluster.GetWorld("home3")
 
     case InterstellarCluster.GiveWorld(zoneId, zone) =>
@@ -2964,14 +2999,14 @@ class WorldSessionActor extends Actor with MDCContextAware {
     //AvatarAwardMessage
     //DisplayAwardMessage
     sendResponse(PlanetsideStringAttributeMessage(guid, 0, "Outfit Name"))
-    sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(6, hex"".toBitVector)))
+    sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(6)))
     (0 to 9).foreach(line => {
-      sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), line, SquadAction.ListSquadDefinition("")))
+      sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), line, SquadAction.ListSquadFavorite("")))
     })
     sendResponse(SquadDetailDefinitionUpdateMessage.Init)
-    sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0,SquadAction.Unknown(16, hex"".toBitVector)))
-    sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0,SquadAction.Unknown(17, hex"".toBitVector)))
-    sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0,SquadAction.Unknown(18, hex"".toBitVector)))
+    sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0,SquadAction.Unknown(16)))
+    sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0,SquadAction.SetListSquad()))
+    sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0,SquadAction.Unknown(18)))
     //MapObjectStateBlockMessage and ObjectCreateMessage?
     //TacticsMessage?
     //change the owner on our deployables (re-draw the icons for our deployables too)
@@ -3419,7 +3454,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
     case msg @ PlayerStateMessageUpstream(avatar_guid, pos, vel, yaw, pitch, yaw_upper, seq_time, unk3, is_crouching, is_jumping, unk4, is_cloaking, unk5, unk6) =>
       if(deadState == DeadState.Alive) {
         if(!player.Crouching && is_crouching) { //SQUAD TESTING CODE
-          sendResponse(SquadMemberEvent(3, 11, 353380L, 3, None, Some(13), None))
+          sendResponse(SquadMembershipResponse(SquadResponseType.Unk01, 0, 0, player.CharId, None, "Dummy", false, Some(None)))
         }
         player.Position = pos
         player.Velocity = vel

From 24691ec239b3b1a93e8a120e84dfd404f4914071 Mon Sep 17 00:00:00 2001
From: FateJH <Jason_DiDonato@yahoo.com>
Date: Tue, 30 Jul 2019 11:30:57 -0400
Subject: [PATCH 15/31] initial work on SquadInvitationRequestMessage packet,
 originally by aphedox; initial work on packet and tests for
 CharacterKnowledgeMessage; re-organization of SquadService workflow;
 prototyping for SquadSwitchboard

---
 .../psforever/packet/GamePacketOpcode.scala   |   4 +-
 .../game/CharacterKnowledgeMessage.scala      |  57 ++
 .../game/SquadInvitationRequestMessage.scala  |  41 +
 .../services/teamwork/SquadResponse.scala     |   4 +-
 .../services/teamwork/SquadService.scala      | 732 +++++++++---------
 .../services/teamwork/SquadSwitchboard.scala  | 142 ++++
 .../game/CharacterKnowledgeMessageTest.scala  |  70 ++
 .../src/main/scala/WorldSessionActor.scala    | 108 ++-
 8 files changed, 765 insertions(+), 393 deletions(-)
 create mode 100644 common/src/main/scala/net/psforever/packet/game/CharacterKnowledgeMessage.scala
 create mode 100644 common/src/main/scala/net/psforever/packet/game/SquadInvitationRequestMessage.scala
 create mode 100644 common/src/main/scala/services/teamwork/SquadSwitchboard.scala
 create mode 100644 common/src/test/scala/game/CharacterKnowledgeMessageTest.scala

diff --git a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
index 03ed5e25..54452c55 100644
--- a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
+++ b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
@@ -595,8 +595,8 @@ object GamePacketOpcode extends Enumeration {
     case 0xe8 => game.SquadDetailDefinitionUpdateMessage.decode
     case 0xe9 => noDecoder(TacticsMessage)
     case 0xea => noDecoder(RabbitUpdateMessage)
-    case 0xeb => noDecoder(SquadInvitationRequestMessage)
-    case 0xec => noDecoder(CharacterKnowledgeMessage)
+    case 0xeb => game.SquadInvitationRequestMessage.decode
+    case 0xec => game.CharacterKnowledgeMessage.decode
     case 0xed => noDecoder(GameScoreUpdateMessage)
     case 0xee => noDecoder(UnknownMessage238)
     case 0xef => noDecoder(OrderTerminalBugMessage)
diff --git a/common/src/main/scala/net/psforever/packet/game/CharacterKnowledgeMessage.scala b/common/src/main/scala/net/psforever/packet/game/CharacterKnowledgeMessage.scala
new file mode 100644
index 00000000..3b9710a3
--- /dev/null
+++ b/common/src/main/scala/net/psforever/packet/game/CharacterKnowledgeMessage.scala
@@ -0,0 +1,57 @@
+// Copyright (c) 2019 PSForever
+package net.psforever.packet.game
+
+import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
+import net.psforever.types.CertificationType
+import scodec.Codec
+import scodec.codecs._
+import shapeless.{::, HNil}
+
+final case class CharacterKnowledgeInfo(name : String,
+                                        permissions : Set[CertificationType.Value],
+                                        unk1 : Int,
+                                        unk2 : Int,
+                                        unk3 : PlanetSideGUID)
+
+final case class CharacterKnowledgeMessage(char_id : Long,
+                                           info : Option[CharacterKnowledgeInfo])
+  extends PlanetSideGamePacket {
+  type Packet = CharacterKnowledgeMessage
+  def opcode = GamePacketOpcode.CharacterKnowledgeMessage
+  def encode = CharacterKnowledgeMessage.encode(this)
+}
+
+object CharacterKnowledgeMessage extends Marshallable[CharacterKnowledgeMessage] {
+  def apply(char_id : Long) : CharacterKnowledgeMessage =
+    CharacterKnowledgeMessage(char_id, None)
+
+  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 info_codec : Codec[CharacterKnowledgeInfo] = (
+    ("name" | PacketHelpers.encodedWideStringAligned(adjustment = 7)) ::
+      ("permissions" | ulongL(bits = 46)) ::
+      ("unk1" | uint(bits = 6)) ::
+      ("unk2" | uint(bits = 3)) ::
+      ("unk3" | PlanetSideGUID.codec)
+  ).xmap[CharacterKnowledgeInfo] (
+    {
+      case name :: permissions :: u1 :: u2 :: u3 :: HNil =>
+        CharacterKnowledgeInfo(name, CertificationType.fromEncodedLong(permissions), u1, u2, u3)
+    },
+    {
+      case CharacterKnowledgeInfo(name, permissions, u1, u2, u3) =>
+        name :: CertificationType.toEncodedLong(permissions) :: u1 :: u2 :: u3 :: HNil
+    }
+  )
+
+  implicit val codec : Codec[CharacterKnowledgeMessage] = (
+    ("char_id" | uint32L) ::
+      ("info" | optional(inverter, info_codec))
+    ).as[CharacterKnowledgeMessage]
+}
diff --git a/common/src/main/scala/net/psforever/packet/game/SquadInvitationRequestMessage.scala b/common/src/main/scala/net/psforever/packet/game/SquadInvitationRequestMessage.scala
new file mode 100644
index 00000000..60de66e1
--- /dev/null
+++ b/common/src/main/scala/net/psforever/packet/game/SquadInvitationRequestMessage.scala
@@ -0,0 +1,41 @@
+// Copyright (c) 2019 PSForever
+package net.psforever.packet.game
+
+import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
+import scodec.Codec
+import scodec.codecs._
+
+/**
+  * A message for communicating squad invitation.
+  * When received by a client, the event message "You have invited `name` to join your squad" is produced
+  * and a `SquadMembershipRequest` packet of type `Invite`
+  * using `char_id` as the optional unique character identifier field is dispatched to the server.
+  * The message is equivalent to a dispatched packet of type `SquadMembershipResponse`
+  * with an `Invite` event with the referral field set to `true`.
+  * @see `SquadMembershipResponse`
+  * @param squad_guid the squad's GUID
+  * @param slot a potentially valid slot index;
+  *             0-9; higher numbers produce no response
+  * @param char_id the unique character identifier
+  * @param name the character's name;
+  *             frequently, though that does not produce a coherent message,
+  *             the avatar's own name is supplied in the event message instead of the name of another player
+  */
+final case class SquadInvitationRequestMessage(squad_guid : PlanetSideGUID,
+                                               slot : Int,
+                                               char_id : Long,
+                                               name : String)
+  extends PlanetSideGamePacket {
+  type Packet = SquadInvitationRequestMessage
+  def opcode = GamePacketOpcode.SquadInvitationRequestMessage
+  def encode = SquadInvitationRequestMessage.encode(this)
+}
+
+object SquadInvitationRequestMessage extends Marshallable[SquadInvitationRequestMessage] {
+  implicit val codec : Codec[SquadInvitationRequestMessage] = (
+    ("squad_guid" | PlanetSideGUID.codec) ::
+      ("slot" | uint4) ::
+      ("char_id" | uint32L) ::
+      ("name" | PacketHelpers.encodedWideStringAligned(adjustment = 4))
+    ).as[SquadInvitationRequestMessage]
+}
diff --git a/common/src/main/scala/services/teamwork/SquadResponse.scala b/common/src/main/scala/services/teamwork/SquadResponse.scala
index b74f153f..26215f02 100644
--- a/common/src/main/scala/services/teamwork/SquadResponse.scala
+++ b/common/src/main/scala/services/teamwork/SquadResponse.scala
@@ -21,7 +21,7 @@ object SquadResponse {
   final case class Join(squad : Squad, positionsToUpdate : List[Int]) 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 SwapMember(squad : Squad, from_index : Int, to_index : Int) extends Response
+  final case class AssignMember(squad : Squad, from_index : Int, to_index : Int) extends Response
 
-  final case class Detail(guid : PlanetSideGUID, leader : String, task : String, zone : PlanetSideZoneID, member_info : List[SquadPositionDetail]) extends Response
+  final case class Detail(guid : PlanetSideGUID, squad_detail : SquadDetail) extends Response
 }
diff --git a/common/src/main/scala/services/teamwork/SquadService.scala b/common/src/main/scala/services/teamwork/SquadService.scala
index 0f8ae794..e3dbe592 100644
--- a/common/src/main/scala/services/teamwork/SquadService.scala
+++ b/common/src/main/scala/services/teamwork/SquadService.scala
@@ -1,7 +1,7 @@
 // Copyright (c) 2019 PSForever
 package services.teamwork
 
-import akka.actor.Actor
+import akka.actor.{Actor, ActorRef, Props}
 import net.psforever.objects.Player
 import net.psforever.objects.definition.converter.StatConverter
 import net.psforever.objects.loadouts.SquadLoadout
@@ -21,6 +21,7 @@ class SquadService extends Actor {
 
   private var memberToSquad : mutable.LongMap[Squad] = mutable.LongMap[Squad]()
   private var idToSquad : TrieMap[PlanetSideGUID, Squad] = new TrieMap[PlanetSideGUID, Squad]()
+  private var idToSwitchboard : TrieMap[PlanetSideGUID, ActorRef] = new TrieMap[PlanetSideGUID, ActorRef]()
   private var i : Int = 1
   private val publishedLists : TrieMap[PlanetSideEmpire.Value, ListBuffer[SquadInfo]] = TrieMap[PlanetSideEmpire.Value, ListBuffer[SquadInfo]](
     PlanetSideEmpire.TR -> ListBuffer.empty,
@@ -28,6 +29,7 @@ class SquadService extends Actor {
     PlanetSideEmpire.VS -> ListBuffer.empty
   )
   private val bids : mutable.LongMap[PositionBid] = mutable.LongMap[PositionBid]()
+  private val viewDetails : mutable.LongMap[PlanetSideGUID] = mutable.LongMap[PlanetSideGUID]()
 
   private [this] val log = org.log4s.getLogger
 
@@ -82,51 +84,55 @@ class SquadService extends Actor {
     PlanetSideGUID(out)
   }
 
-  def GetParticipatingSquad(player : Player, zone : Int) : Option[Squad] = {
+  def GetParticipatingSquad(player : Player) : Option[Squad] = {
     memberToSquad.get(player.CharId) match {
-      case opt @ Some(squad) =>
-        squad.Membership.find(_.Name == player.Name).get.ZoneId = zone
+      case opt @ Some(_) =>
         opt
       case None =>
         None
     }
   }
 
-  def GetLeadingSquad(player : Player, zone : Int, opt : Option[Squad]) : Squad = {
+  def GetLeadingSquad(player : Player, zone : Int, opt : Option[Squad]) : Option[Squad] = {
     val charId = player.CharId
-    val squadOut = opt match {
+    opt match {
       case Some(squad) =>
         if(squad.Leader.CharId == charId) {
-          squad
+          Some(squad)
         }
         else {
-          GetLeadingSquad(player, zone, None)
+          None
         }
 
       case None =>
         memberToSquad.get(charId) match {
-          case Some(squad) if squad.Leader.CharId.equals(charId) =>
-            squad
+          case Some(squad) if squad.Leader.CharId == charId =>
+            Some(squad)
           case _ =>
-            val faction = player.Faction
-            val id = GetNextSquadId()
-            val squad = new Squad(id, faction)
-            val leadPosition = squad.Membership(squad.LeaderPositionIndex)
-            val name = player.Name
-            leadPosition.Name = name
-            leadPosition.CharId = charId
-            leadPosition.Health = player.Health
-            leadPosition.Armor = player.Armor
-            leadPosition.Position = player.Position
-            leadPosition.ZoneId = zone
-            log.info(s"$name-$faction has started a new squad")
-            memberToSquad += charId -> squad
-            idToSquad += id -> squad
-            squad
+            None
         }
     }
-    squadOut.Membership(squadOut.LeaderPositionIndex).ZoneId = zone
-    squadOut
+  }
+
+  def StartSquad(player : Player) : Squad = {
+    val charId = player.CharId
+    val faction = player.Faction
+    val id = GetNextSquadId()
+    val name = player.Name
+    val squad = new Squad(id, faction)
+    val leadPosition = squad.Membership(squad.LeaderPositionIndex)
+    leadPosition.Name = name
+    leadPosition.CharId = charId
+    leadPosition.Health = player.Health
+    leadPosition.Armor = player.Armor
+    leadPosition.Position = player.Position
+    leadPosition.ZoneId = 1
+    val switchboard = context.actorOf(Props[SquadSwitchboard], s"squad${id.guid}")
+    memberToSquad += charId -> squad
+    idToSquad += id -> squad
+    idToSwitchboard += id -> switchboard
+    log.info(s"$name-$faction has started a new squad")
+    squad
   }
 
   val SquadEvents = new GenericEventBus[SquadServiceResponse]
@@ -134,16 +140,16 @@ class SquadService extends Actor {
   def receive : Receive = {
     //subscribe to a faction's channel - necessary to receive updates about listed squads
     case Service.Join(faction) if "TRNCVS".indexOf(faction) > -1 =>
-      val path = s"$faction/Squad"
+      val path = s"/$faction/Squad"
       val who = sender()
       log.info(s"$who has joined $path")
       SquadEvents.subscribe(who, path)
       //send initial squad catalog
-      sender ! SquadServiceResponse(s"$faction/Squad", SquadResponse.InitList(publishedLists(PlanetSideEmpire(faction)).toVector))
+      sender ! SquadServiceResponse(s"/$faction/Squad", SquadResponse.InitList(publishedLists(PlanetSideEmpire(faction)).toVector))
 
     //subscribe to the player's personal channel - necessary only to inform about any previous squad association
     case Service.Join(char_id) =>
-      val path = s"$char_id/Squad"
+      val path = s"/$char_id/Squad"
       val who = sender()
       log.info(s"$who has joined $path")
       SquadEvents.subscribe(who, path) //TODO squad-specific switchboard
@@ -195,101 +201,80 @@ class SquadService extends Actor {
     case SquadServiceMessage(tplayer, squad_action) => squad_action match {
       case SquadAction.Membership(request_type, char_id, optional_char_id, _, _) => request_type match {
         case SquadRequestType.Invite =>
+          //char_id is the inviter, e.g., the (prospective) squad leader
+          //this is just busy work; for actual joining operations, see SquadRequestType.Accept
           (optional_char_id, memberToSquad.get(char_id)) match {
-            case (Some(toCharId), Some(squad)) =>
-              bids(toCharId) = VacancyBid(char_id, squad.GUID)
-              SquadEvents.publish( SquadServiceResponse(s"$toCharId/Squad", SquadResponse.Invite(char_id, toCharId, tplayer.Name)) )
-            case (Some(toCharId), None) =>
-              val ourSquad = GetLeadingSquad(tplayer, 1, None)
-              memberToSquad.remove(char_id)
-              bids(toCharId) = CamraderieBid(char_id, ourSquad)
-              SquadEvents.publish( SquadServiceResponse(s"$toCharId/Squad", SquadResponse.Invite(char_id, toCharId, tplayer.Name)) )
+            case (Some(invitee), Some(squad)) =>
+              bids(invitee) = VacancyBid(char_id, squad.GUID)
+              log.info(s"$invitee has been invited to squad ${squad.Task} by $char_id")
+              SquadEvents.publish( SquadServiceResponse(s"/$invitee/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, char_id, Some(invitee), tplayer.Name, false, Some(None))) )
+              SquadEvents.publish( SquadServiceResponse(s"/$char_id/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, invitee, Some(char_id), tplayer.Name, true, Some(None))) )
+            case (Some(invitee), None) =>
+              //the inviter does not currently belong to a squad; check for an existing placeholder, or create a new one
+              val ourSquad = {
+                bids.find { case (inviter, _) => inviter == char_id } match {
+                  case Some((_, SpontaneousBid(_, _squad))) =>
+                    _squad //borrow
+                  case _ =>
+                    val _squad = StartSquad(tplayer)
+                    memberToSquad.remove(char_id) //completely unlist until assured the squad is necessary
+                    _squad
+                }
+              }
+              bids(invitee) = SpontaneousBid(char_id, ourSquad)
+              SquadEvents.publish( SquadServiceResponse(s"/$invitee/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, char_id, Some(invitee), tplayer.Name, false, Some(None))) )
+              SquadEvents.publish( SquadServiceResponse(s"/$char_id/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, invitee, Some(char_id), tplayer.Name, true, Some(None))) )
             case _ => ;
           }
 
-        case SquadRequestType.Leave =>
-          val squad = memberToSquad(char_id)
-          if(optional_char_id.contains(char_id)) {
-            //leaving the squad of our own accord
-          }
-          else {
-            val leader = squad.Leader
-            if(optional_char_id.contains(leader.CharId)) {
-              //kicked by the squad leader
-            }
-          }
-          val membership = squad.Membership.zipWithIndex
-          val (member, index) = membership
-            .find { case (_member, _) => _member.CharId == char_id }
-            .get
-          val updateList = membership.collect({ case (_member, _index) if _member.CharId > 0 => (_member.CharId, _index) }).toList
-          memberToSquad.remove(char_id)
-          member.Name = ""
-          member.CharId = 0
-
-          val size = squad.Size
-          if(size == 1) {
-            //squad is rendered to just one person; collapse it
-            (membership.collect({ case (_member, _) if _member.CharId > 0 => _member.CharId }) :+ char_id)
-              .foreach { charId =>
-                SquadEvents.publish( SquadServiceResponse(s"$charId/Squad", SquadResponse.Leave(squad, updateList)) )
-                memberToSquad.remove(charId)
-              }
-            idToSquad.remove(squad.GUID)
-          }
-          else {
-            //squad continues, despite player's parting
-            //member leaves the squad completely
-            sender ! SquadServiceResponse("", SquadResponse.Leave(squad, updateList))
-            //other squad members see the member leaving only
-            val leavingMember = List((char_id, index))
-            membership
-              .collect({ case (_member, _) if _member.CharId > 0 => _member.CharId })
-              .foreach { charId =>
-                SquadEvents.publish( SquadServiceResponse(s"$charId/Squad", SquadResponse.Leave(squad, leavingMember)) )
-              }
-          }
-
         case SquadRequestType.Accept =>
+          //char_id is the invitee, e.g., the person joining the squad
           bids.remove(char_id) match {
-            case Some(NormalBid(inviterCharId, squadGUID, line)) if idToSquad.get(squadGUID).nonEmpty =>
+            case Some(NormalBid(_/*inviterCharId*/, squadGUID, line)) if idToSquad.get(squadGUID).nonEmpty =>
+              //player requested to join a squad's specific position
               JoinSquad(tplayer, idToSquad(squadGUID), line)
 
             case Some(VacancyBid(inviterCharId, squadGUID)) if idToSquad.get(squadGUID).nonEmpty =>
+              //we were invited by the squad leader into an existing squad
               val squad = idToSquad(squadGUID)
               squad.Membership.zipWithIndex.find({ case (member, index) =>
-                ValidOpenSquadPosition(squad, index, squad.Membership(index), tplayer.Certifications)
+                ValidOpenSquadPosition(squad, index, member, tplayer.Certifications)
               }) match {
                 case Some((_, line)) =>
+                  SquadEvents.publish( SquadServiceResponse(s"/$inviterCharId/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, inviterCharId, Some(char_id), tplayer.Name, false, Some(None))) )
+                  SquadEvents.publish( SquadServiceResponse(s"/$char_id/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, char_id, Some(inviterCharId), "", true, Some(None))) )
                   JoinSquad(tplayer, squad, line)
                 case _ => ;
               }
 
-            case Some(CamraderieBid(inviterCharId, placeholderSquad)) =>
-              (GetParticipatingSquad(tplayer, 1) match {
+            case Some(SpontaneousBid(inviterCharId, placeholderSquad)) =>
+              //we were invited by someone into a new squad they would form
+              (GetParticipatingSquad(tplayer) match {
                 case Some(participating) =>
                   if(participating.Leader.CharId == inviterCharId) {
                     Some(participating)
                   }
                   else {
-                    //inviter is not leader of squad; bounce this request off of the squad leader
+                    //inviter joined a squad and is not its leader; bounce this request off of the squad leader
+                    //TODO squad leader receives "<player> wants to join squad" prompt
                     val leaderCharId = participating.Leader.CharId
                     bids(char_id) = VacancyBid(leaderCharId, participating.GUID) //reframed request
-                    //TODO squad leader receives "<player> wants to join squad" prompt
-                    //SquadEvents.publish(SquadServiceResponse(s"$leaderCharId/Squad", SquadResponse.Invite(char_id, leaderCharId, tplayer.Name)))
+                    SquadEvents.publish(SquadServiceResponse(s"/$leaderCharId/Squad", SquadResponse.Invite(char_id, leaderCharId, tplayer.Name)))
                     None
                   }
                 case None =>
                   placeholderSquad.Task = s"${tplayer.Name}'s Squad"
                   memberToSquad(inviterCharId) = placeholderSquad
-                  SquadEvents.publish(SquadServiceResponse(s"$inviterCharId/Squad", SquadResponse.InitSquad(placeholderSquad.GUID)))
+                  SquadEvents.publish(SquadServiceResponse(s"/$inviterCharId/Squad", SquadResponse.InitSquad(placeholderSquad.GUID)))
                   Some(placeholderSquad)
               }) match {
               case Some(squad) =>
                 squad.Membership.zipWithIndex.find({ case (member, index) =>
-                  ValidOpenSquadPosition(squad, index, squad.Membership(index), tplayer.Certifications)
+                  ValidOpenSquadPosition(squad, index, member, tplayer.Certifications)
                 }) match {
                   case Some((_, line)) =>
+                    SquadEvents.publish( SquadServiceResponse(s"/$inviterCharId/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, inviterCharId, Some(char_id), tplayer.Name, false, Some(None))) )
+                    SquadEvents.publish( SquadServiceResponse(s"/$char_id/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, char_id, Some(inviterCharId), "", true, Some(None))) )
                     JoinSquad(tplayer, squad, line)
                   case _ => ;
                 }
@@ -299,6 +284,50 @@ class SquadService extends Actor {
             case _ => ;
           }
 
+        case SquadRequestType.Leave =>
+          //char_id is the player leaving
+          val squad = memberToSquad(char_id)
+          val leader = squad.Leader.CharId
+          if(char_id == leader) {
+            //squad leader is leaving his own squad, so it will be disbanded
+            squad.Membership
+              .filterNot { _.CharId == leader }
+              .foreach { member =>
+                val charId = member.CharId
+                SquadEvents.publish(SquadServiceResponse(s"/$charId/Squad", SquadResponse.Membership(SquadResponseType.Disband, 0, 0, charId, None, "", false, Some(None))))
+              }
+            CloseOutSquad(squad)
+            SquadEvents.publish(SquadServiceResponse(s"/$leader/Squad", SquadResponse.Membership(SquadResponseType.Disband, 0, 0, leader, None, "", true, Some(None))))
+          }
+          else {
+            if(optional_char_id.contains(char_id)) {
+              //leaving the squad of our own accord
+              LeaveSquad(tplayer, squad)
+            }
+            else if(optional_char_id.contains(leader)) {
+              //kicked by the squad leader
+              SquadEvents.publish( SquadServiceResponse(s"/$char_id/Squad", SquadResponse.Membership(SquadResponseType.Leave, 0, 0, char_id, Some(leader), tplayer.Name, false, Some(None))) )
+              SquadEvents.publish( SquadServiceResponse(s"/$leader/Squad", SquadResponse.Membership(SquadResponseType.Leave, 0, 0, leader, Some(char_id), "", true, Some(None))) )
+              LeaveSquad(tplayer, squad)
+            }
+          }
+
+        case SquadRequestType.Reject =>
+          //a player has opted out of joining a squad
+          (bids.remove(char_id) match {
+            case Some(SpontaneousBid(inviterCharId, squad)) if squad.Leader.CharId != char_id =>
+              (inviterCharId, squad)
+            case Some(VacancyBid(inviterCharId, guid)) if idToSquad(guid).Leader.CharId != char_id =>
+              (inviterCharId, idToSquad.get(guid))
+            case _ => ;
+              (0L, None)
+          }) match {
+            case (inviterCharId, Some(squad)) =>
+              SquadEvents.publish( SquadServiceResponse(s"/$inviterCharId/Squad", SquadResponse.Membership(SquadResponseType.Reject, 0, 0, inviterCharId, Some(char_id), tplayer.Name, false, Some(None))) )
+              SquadEvents.publish( SquadServiceResponse(s"/$char_id/Squad", SquadResponse.Membership(SquadResponseType.Reject, 0, 0, char_id, Some(inviterCharId), "", true, Some(None))) )
+            case _ => ;
+          }
+
         case _ => ;
       }
 
@@ -307,10 +336,8 @@ class SquadService extends Actor {
           case Some(squad) =>
             squad.Membership.find(_.CharId == char_id) match {
               case Some(member) =>
-                val newHealth = StatConverter.Health(health, max_health, min=1, max=64)
-                val newArmor = StatConverter.Health(armor, max_armor, min=1, max=64)
-                member.Health = newHealth
-                member.Armor = newArmor
+                member.Health = StatConverter.Health(health, max_health, min=1, max=64)
+                member.Armor = StatConverter.Health(armor, max_armor, min=1, max=64)
                 member.Position = pos
                 member.ZoneId = zone_number
                 sender ! SquadServiceResponse("", SquadResponse.UpdateMembers(
@@ -328,36 +355,27 @@ class SquadService extends Actor {
 
       case SquadAction.Definition(tplayer : Player, zone_ordinal_number : Int, guid : PlanetSideGUID, line : Int, action : SquadAction) =>
         import net.psforever.packet.game.SquadAction._
-        val squadOpt = GetParticipatingSquad(tplayer, zone_ordinal_number)
+        val pSquadOpt = GetParticipatingSquad(tplayer)
+        val lSquadOpt = GetLeadingSquad(tplayer, zone_ordinal_number, pSquadOpt)
+        //the following actions can only be performed by a squad's leader
         action match {
           case SaveSquadFavorite() =>
-            squadOpt match {
-              case Some(squad) if squad.Leader.CharId == tplayer.CharId && squad.Task.nonEmpty && squad.ZoneId > 0 =>
-                tplayer.SquadLoadouts.SaveLoadout(squad, squad.Task, line)
-                sender ! SquadServiceResponse("", SquadResponse.ListSquadFavorite(line, squad.Task))
-              case _ => ;
+            val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
+            if(squad.Task.nonEmpty && squad.ZoneId > 0) {
+              tplayer.SquadLoadouts.SaveLoadout(squad, squad.Task, line)
+              sender ! SquadServiceResponse("", SquadResponse.ListSquadFavorite(line, squad.Task))
             }
 
           case LoadSquadFavorite() =>
-            (squadOpt match {
-              case Some(squad) if squad.Size == 1 =>
-                //we are the leader of our own squad, but no one else has joined yet
-                Some(squad)
-              case None => ;
-                //we are not yet member of a squad; start a squad to support our favorite definition
-                Some(GetLeadingSquad(tplayer, zone_ordinal_number, None))
-              case _ => ;
-                //player is member of populated squad; should not overwrite squad definition with favorite
-                None
-            }, tplayer.SquadLoadouts.LoadLoadout(line)) match {
-              case (Some(squad : Squad), Some(loadout : SquadLoadout)) =>
+            val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
+            tplayer.SquadLoadouts.LoadLoadout(line) match {
+              case Some(loadout : SquadLoadout) if squad.Size == 1 =>
                 log.info(s"${tplayer.Name} is loading a squad composition: $loadout")
                 SquadService.LoadSquadDefinition(squad, loadout)
                 sender ! SquadServiceResponse("", SquadResponse.InitSquad(squad.GUID))
                 UpdateSquadList(squad, SquadInfo().Task(squad.Task).ZoneId(PlanetSideZoneID(squad.ZoneId)).Capacity(squad.Capacity))
                 UpdateSquadDetail(PlanetSideGUID(0), squad)
-
-              case _ => ;
+              case _ =>
             }
 
           case DeleteSquadFavorite() =>
@@ -366,23 +384,20 @@ class SquadService extends Actor {
 
           case ChangeSquadPurpose(purpose) =>
             log.info(s"${tplayer.Name}-${tplayer.Faction} has changed his squad's task to $purpose")
-            val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt)
+            val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
             squad.Task = purpose
-
             UpdateSquadListWhenListed(squad, SquadInfo().Task(purpose))
-            SquadDetail().Task(purpose)
-            UpdateListedSquadDetail(squad.GUID, squad)
+            UpdateSquadDetailWhenListed(squad.GUID, squad, SquadDetail().Task(purpose))
 
           case ChangeSquadZone(zone) =>
             log.info(s"${tplayer.Name}-${tplayer.Faction} has changed squad's ops zone to $zone")
-            val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt)
+            val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
             squad.ZoneId = zone.zoneId.toInt
             UpdateSquadListWhenListed(squad, SquadInfo().ZoneId(zone))
-            SquadDetail().ZoneId(zone)
-            UpdateListedSquadDetail(squad.GUID, squad)
+            UpdateSquadDetailWhenListed(squad.GUID, squad, SquadDetail().ZoneId(zone))
 
           case CloseSquadMemberPosition(position) =>
-            val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt)
+            val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
             squad.Availability.lift(position) match {
               case Some(true) =>
                 squad.Availability.update(position, false)
@@ -396,151 +411,93 @@ class SquadService extends Actor {
                 }
                 memberPosition.Close()
                 UpdateSquadListWhenListed(squad, listingChanged)
-                SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail.Closed)))
-                UpdateListedSquadDetail(squad.GUID, squad)
+                UpdateSquadDetailWhenListed(squad.GUID, squad,
+                  SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail.Closed)))
+                )
               case Some(false) | None => ;
             }
 
           case AddSquadMemberPosition(position) =>
-            val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt)
+            val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
             squad.Availability.lift(position) match {
               case Some(false) =>
                 log.info(s"${tplayer.Name}-${tplayer.Faction} has opened the #$position position in squad")
                 squad.Availability.update(position, true)
                 UpdateSquadListWhenListed(squad, SquadInfo().Capacity(squad.Capacity))
-                SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail.Open)))
-                UpdateListedSquadDetail(squad.GUID, squad)
+                UpdateSquadDetailWhenListed(squad.GUID, squad,
+                  SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail.Open)))
+                )
               case Some(true) | None => ;
             }
 
           case ChangeSquadMemberRequirementsRole(position, role) =>
-            val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt)
+            val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
             squad.Availability.lift(position) match {
               case Some(true) =>
                 log.info(s"${tplayer.Name}-${tplayer.Faction} has changed the role of squad position #$position")
                 squad.Membership(position).Role = role
-                SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail().Role(role))))
-                UpdateListedSquadDetail(squad.GUID, squad)
+                UpdateSquadDetailWhenListed(squad.GUID, squad,
+                  SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail().Role(role))))
+                )
               case Some(false) | None => ;
             }
 
           case ChangeSquadMemberRequirementsDetailedOrders(position, orders) =>
-            val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt)
+            val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
             squad.Availability.lift(position) match {
               case Some(true) =>
                 log.info(s"${tplayer.Name}-${tplayer.Faction} has changed the orders for squad position #$position")
                 squad.Membership(position).Orders = orders
-                SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail().DetailedOrders(orders))))
-                UpdateListedSquadDetail(squad.GUID, squad)
+                UpdateSquadDetailWhenListed(squad.GUID, squad,
+                  SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail().DetailedOrders(orders))))
+                )
               case Some(false) | None => ;
             }
 
           case ChangeSquadMemberRequirementsCertifications(position, certs) =>
-            val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt)
+            val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
             squad.Availability.lift(position) match {
               case Some(true) =>
                 log.info(s"${tplayer.Name}-${tplayer.Faction} has changed the requirements for squad position #$position")
                 squad.Membership(position).Requirements = certs
-                SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail().Requirements(certs))))
-                UpdateListedSquadDetail(squad.GUID, squad)
+                UpdateSquadDetailWhenListed(squad.GUID, squad,
+                  SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail().Requirements(certs))))
+                )
               case Some(false) | None => ;
             }
 
           case LocationFollowsSquadLead(state) =>
-            val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt)
             if(state) {
               log.info(s"${tplayer.Name}-${tplayer.Faction} has moves the rally to the leader's position")
             }
             else {
               log.info(s"${tplayer.Name}-${tplayer.Faction} has let the rally move freely")
             }
+            val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
             squad.LocationFollowsSquadLead = state
 
           case AutoApproveInvitationRequests(state) =>
-            val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt)
             if(state) {
               log.info(s"${tplayer.Name}-${tplayer.Faction} is allowing all requests to join the squad")
             }
             else {
               log.info(s"${tplayer.Name}-${tplayer.Faction} has started screening invitation requests")
             }
+            val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
             squad.AutoApproveInvitationRequests = state
 
-          case SelectRoleForYourself(position) =>
-            //TODO need to ask permission from the squad leader, unless our character is the squad leader or already currently in the squad
-            squadOpt match {
-              case Some(squad) if squad.GUID == guid || squad.Leader.Name == tplayer.Name =>
-                //already a member of this squad, or the leader; swap positions freely
-                val membership = squad.Membership.zipWithIndex
-                val toMember = squad.Membership(position)
-                if(ValidOpenSquadPosition(squad, position, toMember, tplayer.Certifications)) {
-                  //acquire this membership position
-                  membership.find { case (member, _) => member.Name == tplayer.Name } match {
-                    case Some((fromMember, fromIndex)) =>
-                      //copy member details
-                      toMember.Name = fromMember.Name
-                      toMember.CharId = fromMember.CharId
-                      toMember.Health = fromMember.Health
-                      toMember.Armor = fromMember.Armor
-                      toMember.Position = fromMember.Position
-                      toMember.ZoneId = fromMember.ZoneId
-                      //old membership no longer valid
-                      fromMember.Name = ""
-                      fromMember.CharId = 0L
-                      if(fromIndex == squad.LeaderPositionIndex) {
-                        squad.LeaderPositionIndex = position
-                      }
-                      membership
-                        .collect({ case (_member, _) if _member.CharId > 0 => _member.Name })
-                        .foreach { name =>
-                          SquadEvents.publish(SquadServiceResponse(s"$name/Squad", SquadResponse.SwapMember(squad, fromIndex, position)))
-                        }
-                    case None =>
-                      toMember.Name = tplayer.Name
-                      toMember.CharId = tplayer.CharId
-                      toMember.Health = StatConverter.Health(tplayer.Health, tplayer.MaxHealth, min = 1, max = 64)
-                      toMember.Armor = StatConverter.Health(tplayer.Armor, tplayer.MaxArmor, min = 1, max = 64)
-                      toMember.Position = tplayer.Position
-                      toMember.ZoneId = 13
-                      memberToSquad(tplayer.CharId) = squad
-                  }
-                }
-                UpdateSquadDetail(squad.GUID, squad)
-
-              case Some(_) =>
-                //not a member of the requesting squad; do nothing
-              case None =>
-                //not a member of any squad; consider request of joining the target squad
-                idToSquad.get(guid) match {
-                  case Some(squad) =>
-                    val member = squad.Membership(position)
-                    if(squad.Availability(position) && member.CharId == 0 &&
-                      tplayer.Certifications.intersect(member.Requirements) == member.Requirements) {
-                      bids(tplayer.CharId) = NormalBid(squad.Leader.CharId, guid, position)
-                      val leader = squad.Leader
-                      //TODO need to ask permission from the squad leader, unless auto-approve is in effect
-                      sender ! SquadServiceResponse("", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, leader.CharId, Some(tplayer.CharId), leader.Name, false, None))
-                    }
-
-                  case None =>
-                  //squad does not exist!? assume old data
-                  //reload squad list data and blank the squad definition the user is looking at
-                }
-            }
-
           case RequestListSquad() =>
-            val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt)
-            if(!squad.Listed) {
+            val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
+            if(!squad.Listed && squad.Task.nonEmpty && squad.ZoneId > 0) {
               log.info(s"${tplayer.Name}-${tplayer.Faction} has opened public recruitment for squad ${squad.Task}")
               squad.Listed = true
               sender ! SquadServiceResponse("", SquadResponse.InitSquad(squad.GUID))
               UpdateSquadList(squad, None)
-              sender ! SquadServiceResponse("", SquadResponse.Unknown17(squad, tplayer.CharId))
             }
 
           case StopListSquad() =>
-            val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt)
-            if(!squad.Listed) {
+            val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
+            if(squad.Listed) {
               log.info(s"${tplayer.Name}-${tplayer.Faction} has closed public recruitment for squad ${squad.Task}")
               squad.Listed = false
               sender ! SquadServiceResponse("", SquadResponse.InitSquad(PlanetSideGUID(0)))
@@ -548,41 +505,101 @@ class SquadService extends Actor {
             }
 
           case ResetAll() =>
-            squadOpt match {
-              case Some(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()
-                })
-                squad.LocationFollowsSquadLead = false
-                squad.AutoApproveInvitationRequests = false
-                UpdateSquadList(squad, SquadInfo().Task("").ZoneId(None).Capacity(squad.Capacity))
-                UpdateSquadDetail(squad.GUID, squad)
-              case None => ;
+            val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
+            squad.Task = ""
+            squad.ZoneId = None
+            squad.Availability.indices.foreach { i =>
+              squad.Availability.update(i, true)
+            }
+            squad.Membership.foreach(position => {
+              position.Role = ""
+              position.Orders = ""
+              position.Requirements = Set()
+            })
+            squad.LocationFollowsSquadLead = false
+            squad.AutoApproveInvitationRequests = false
+            UpdateSquadListWhenListed(squad, SquadInfo().Task("").ZoneId(None).Capacity(squad.Capacity))
+            UpdateSquadDetailWhenListed(squad.GUID, squad)
+
+          case _ =>
+        }
+        // etc..
+        (pSquadOpt, action) match {
+          //the following action can be performed by the squad leader and maybe an unaffiliated player
+          case (Some(squad), SelectRoleForYourself(position)) =>
+            log.info(s"${tplayer.Name} would like the #${position+1} spot in this squad")
+            val membership = squad.Membership.zipWithIndex
+            val toMember = squad.Membership(position)
+            //the squad leader may swap to any open position; a normal member has to validate against requirements
+            if((squad.Leader.CharId == tplayer.CharId && toMember.CharId == 0) || ValidOpenSquadPosition(squad, position, toMember, tplayer.Certifications)) {
+              membership.find { case (member, _) => member.CharId == tplayer.CharId } match {
+                case Some((fromMember, fromIndex)) =>
+                  SwapMemberPosition(squad, toMember, fromMember)
+                  if(fromIndex == squad.LeaderPositionIndex) {
+                    squad.LeaderPositionIndex = position
+                  }
+                  membership
+                    .filter { case (_member, _) => _member.CharId > 0 }
+                    .foreach { case (_member, _) =>
+                      SquadEvents.publish(SquadServiceResponse(s"/${_member.CharId}/Squad", SquadResponse.AssignMember(squad, fromIndex, position)))
+                    }
+                  UpdateSquadDetailWhenListed(squad.GUID, squad)
+                case _ => ;
+                //somehow, this is not our squad; do nothing, for now
+              }
+            }
+            else {
+              //not qualified for requested position
             }
 
-          case DisplaySquad() =>
+          //the following action can be performed by an unaffiliated player
+          case (None, SelectRoleForYourself(position)) =>
+            //not a member of any squad, but we might become a member of this one
             idToSquad.get(guid) match {
               case Some(squad) =>
-                sender ! SquadServiceResponse(s"${tplayer.Name}/Squad", GenSquadDetail(squad.GUID, squad))
+                val toMember = squad.Membership(position)
+                if(ValidOpenSquadPosition(squad, position, toMember, tplayer.Certifications)) {
+                  //we could join but we may need permission from the squad leader first
+                  log.info(s"Player ${tplayer.Name} would like to join the squad ${squad.Task}.")
+                  bids(tplayer.CharId) = NormalBid(squad.Leader.CharId, guid, position)
+                  //val leader = squad.Leader
+                  //TODO ask permission from the squad leader, unless auto-approve is in effect
+                  self ! SquadServiceMessage(tplayer, SquadAction.Membership(SquadRequestType.Accept, tplayer.CharId, None, "", None))
+                }
               case None => ;
+              //squad does not exist? assume old local data; force update to correct discrepancy
             }
 
-//          case AnswerSquadJoinRequest() =>
-//            idToSquad.get(guid) match {
-//              case Some(squad) =>
-//                if(squad.Leader.Name == tplayer.Name && squad.Listed) {
-//                  //squad was just listed but we have not yet declared ourselves as the leader
-//                  UpdateSquadDetail(squad)
-//                }
-//              case None => ;
-//            }
+          //the following action can be performed by ???
+          case (Some(squad), AssignSquadMemberToRole(position, char_id)) =>
+            val membership = squad.Membership.zipWithIndex
+            (membership.find({ case (member, _) => member.CharId == char_id}), membership(position)) match {
+              case (Some((fromMember, fromPosition)), (toMember, _)) =>
+                val name = fromMember.Name
+                SwapMemberPosition(squad, toMember, fromMember)
+                //
+                membership
+                  .filter({ case (_member, _) => _member.CharId > 0 })
+                  .foreach { case (_member, _) =>
+                    SquadEvents.publish(SquadServiceResponse(s"/${_member.CharId}/Squad", SquadResponse.AssignMember(squad, fromPosition, position)))
+                  }
+                UpdateSquadDetailWhenListed(squad.GUID, squad,
+                  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
+          case (_, DisplaySquad()) =>
+            idToSquad.get(guid) match {
+              case Some(squad) =>
+                viewDetails(tplayer.CharId) = guid
+                sender ! SquadServiceResponse(s"/${tplayer.CharId}/Squad", SquadResponse.Detail(squad.GUID, SquadService.Detail.Publish(squad)))
+              case None => ;
+            }
 
           case _ => ;
         }
@@ -595,15 +612,15 @@ class SquadService extends Actor {
   def JoinSquad(player : Player, squad : Squad, line : Int) : Boolean = {
     val charId = player.CharId
     val position = squad.Membership(line)
-    if(squad.Availability(line) && position.CharId == 0 &&
-      player.Certifications.intersect(position.Requirements) == position.Requirements) {
+    if(ValidOpenSquadPosition(squad, line, position, player.Certifications)) {
+      log.info(s"Player ${player.Name} will join the squad ${squad.Task} at position ${line+1}!")
       position.Name = player.Name
       position.CharId = charId
       position.Health = StatConverter.Health(player.Health, player.MaxHealth, min=1, max=64)
       position.Armor = StatConverter.Health(player.Armor, player.MaxArmor, min=1, max=64)
       position.Position = player.Position
       position.ZoneId = 13
-      memberToSquad(player.CharId) = squad
+      memberToSquad(charId) = squad
 
       val size = squad.Size
       if(size == 1) {
@@ -612,28 +629,34 @@ class SquadService extends Actor {
       }
       else if(size == 2) {
         //first squad member after leader; both members fully initialize
-        val indices = squad.Membership.zipWithIndex.collect({ case (member, index) if member.CharId != 0 => index }).toList
+        val indices = squad.Membership.zipWithIndex
+          .collect({ case (member, index) if member.CharId != 0 => index }).toList
         squad.Membership
-          .collect({ case member if member.CharId != 0 => member.CharId })
-          .foreach { charId =>
-            SquadEvents.publish(SquadServiceResponse(s"$charId/Squad", SquadResponse.Join(squad, indices)))
+          .filterNot { _.CharId == 0 }
+          .foreach { member =>
+            SquadEvents.publish(SquadServiceResponse(s"/${member.CharId}/Squad", SquadResponse.Join(squad, indices)))
           }
+        //fully update for all users
+        UpdateSquadDetail(squad.GUID, squad)
       }
       else {
         //joining an active squad; everybody updates differently
         //new member gets full UI updates
-        sender ! SquadServiceResponse("", SquadResponse.Join(
-          squad,
-          squad.Membership.zipWithIndex.collect({ case (member, index) if member.CharId != 0 => index }).toList
-        ))
+        val indices = squad.Membership.zipWithIndex
+          .collect({ case (member, index) if member.CharId != 0 => index }).toList
+        SquadEvents.publish(SquadServiceResponse(s"/$charId/Squad", SquadResponse.Join(squad, indices)))
         //other squad members see us joining the squad
         val updatedIndex = List(line)
         squad.Membership
-          .collect({ case member if member.CharId != 0 && member.CharId != charId => member.CharId })
-          .foreach { charId =>
-            SquadEvents.publish(SquadServiceResponse(s"$charId/Squad", SquadResponse.Join(squad, updatedIndex)))
+          .filterNot { member => member.CharId == 0 || member.CharId == charId }
+          .foreach { member =>
+            SquadEvents.publish(SquadServiceResponse(s"/${member.CharId}/Squad", SquadResponse.Join(squad, updatedIndex)))
           }
+        UpdateSquadDetailWhenListed(squad.GUID, squad,
+          SquadDetail().Members(List(SquadPositionEntry(line, SquadPositionDetail().CharId(charId).Name(player.Name))))
+        )
       }
+      UpdateSquadListWhenListed(squad, SquadInfo().Size(size))
       true
     }
     else {
@@ -641,6 +664,86 @@ class SquadService extends Actor {
     }
   }
 
+  def LeaveSquad(player : Player, squad : Squad) : Boolean = {
+    val charId = player.CharId
+    val membership = squad.Membership.zipWithIndex
+    membership.find { case (_member, _) => _member.CharId == charId } match {
+      case Some((member, index)) =>
+        val updateList = membership.collect({ case (_member, _index) if _member.CharId > 0 => (_member.CharId, _index) }).toList
+        memberToSquad.remove(charId)
+        member.Name = ""
+        member.CharId = 0
+
+        val size = squad.Size
+        if(size < 2) {
+          //squad is rendered to just one person or less; collapse it
+          squad.Membership.foreach { _member =>
+              SquadEvents.publish(SquadServiceResponse(s"/${_member.CharId}/Squad", SquadResponse.Membership(SquadResponseType.Disband, 0, 0, charId, None, "", false, Some(None))))
+          }
+          CloseOutSquad(squad, membership, updateList)
+        }
+        else {
+          //squad continues, despite player's parting
+          //member leaves the squad completely
+          sender ! SquadServiceResponse("", SquadResponse.Leave(squad, updateList))
+          //other squad members see the member leaving
+          val leavingMember = List((charId, index))
+          membership
+            .filter { case (_member, _) => _member.CharId > 0 }
+            .foreach { case (_member, _) =>
+              SquadEvents.publish( SquadServiceResponse(s"/${_member.CharId}/Squad", SquadResponse.Leave(squad, leavingMember)) )
+            }
+        }
+        true
+      case None =>
+        false
+    }
+  }
+
+  def CloseOutSquad(squad : Squad) : Unit = {
+    val membership = squad.Membership.zipWithIndex
+    CloseOutSquad(
+      squad,
+      membership,
+      membership.collect({ case (_member, _index) if _member.CharId > 0 => (_member.CharId, _index) }).toList
+    )
+  }
+
+  def CloseOutSquad(squad : Squad, membership : Iterable[(Member, Int)], updateList : List[(Long, Int)]) : Unit = {
+    val leaderCharId = squad.Leader.CharId
+    membership.foreach {
+      case (member, _) =>
+        val charId = member.CharId
+        member.Name = ""
+        member.CharId = 0L
+        memberToSquad.remove(charId)
+        SquadEvents.publish( SquadServiceResponse(s"/$charId/Squad", SquadResponse.Leave(squad, updateList)) )
+    }
+    idToSquad.remove(squad.GUID)
+  }
+
+  def SwapMemberPosition(squad : Squad, 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
+  }
+
   def UpdateSquadList(squad : Squad, changes : SquadInfo) : Unit = {
     UpdateSquadList(squad, Some(changes))
   }
@@ -650,7 +753,7 @@ class SquadService extends Actor {
   }
 
   def UpdateSquadListWhenListed(squad : Squad, changes : Option[SquadInfo]) : Unit = {
-    if(squad.Listed) {
+    if(squad.Listed || squad.Size > 1) {
       UpdateSquadList(squad, changes)
     }
   }
@@ -673,7 +776,7 @@ class SquadService extends Actor {
             log.info(s"Squad will be updated")
             factionListings(index) = entry
             SquadEvents.publish(
-              SquadServiceResponse(s"$faction/Squad", SquadResponse.UpdateList(Seq((index, changedFields))))
+              SquadServiceResponse(s"/$faction/Squad", SquadResponse.UpdateList(Seq((index, changedFields))))
             )
           case None =>
             //remove squad from listing
@@ -681,7 +784,7 @@ class SquadService extends Actor {
             factionListings.remove(index)
             SquadEvents.publish(
               //SquadServiceResponse(s"$faction/Squad", SquadResponse.RemoveFromList(Seq(index)))
-              SquadServiceResponse(s"$faction/Squad", SquadResponse.InitList(factionListings.toVector))
+              SquadServiceResponse(s"/$faction/Squad", SquadResponse.InitList(factionListings.toVector))
             )
         }
       case None =>
@@ -689,40 +792,33 @@ class SquadService extends Actor {
         log.info(s"Squad will be introduced")
         factionListings += entry
         SquadEvents.publish(
-          SquadServiceResponse(s"$faction/Squad", SquadResponse.InitList(factionListings.toVector))
+          SquadServiceResponse(s"/$faction/Squad", SquadResponse.InitList(factionListings.toVector))
         )
     }
   }
 
-  def GenSquadDetail(guid : PlanetSideGUID, squad : Squad) : SquadResponse.Detail = {
-    SquadResponse.Detail(
-      squad.GUID,
-      squad.Leader.Name,
-      squad.Task,
-      PlanetSideZoneID(squad.ZoneId),
-      squad.Membership.zipWithIndex.map({ case (p, index) =>
-        if(squad.Availability(index)) {
-          SquadPositionDetail(p.Role, p.Orders, p.Requirements, p.CharId, p.Name)
-        }
-        else {
-          SquadPositionDetail.Closed
-        }
-      }).toList
-    )
+  def UpdateSquadDetail(guid : PlanetSideGUID, squad : Squad) : Unit = {
+    UpdateSquadDetailWhenListed(guid, squad, SquadService.Detail.Publish(squad))
   }
 
-  def UpdateListedSquadDetail(guid : PlanetSideGUID, squad : Squad) : Unit = {
-    if(squad.Listed) {
-      UpdateSquadDetail(guid, squad)
+  def UpdateSquadDetailWhenListed(guid : PlanetSideGUID, squad : Squad) : Unit = {
+    if(squad.Listed || squad.Size > 1) {
+      UpdateSquadDetail(guid, squad, SquadService.Detail.Publish(squad))
     }
   }
 
-  def UpdateSquadDetail(guid : PlanetSideGUID, squad : Squad) : Unit = {
-    val detail = GenSquadDetail(guid, squad)
+  def UpdateSquadDetailWhenListed(guid : PlanetSideGUID, squad : Squad, detail : SquadDetail) : Unit = {
+    if(squad.Listed || squad.Size > 1) {
+      UpdateSquadDetail(guid, squad, detail)
+    }
+  }
+
+  def UpdateSquadDetail(guid : PlanetSideGUID, squad : Squad, detail : SquadDetail) : Unit = {
+    val output = SquadResponse.Detail(guid, detail)
     squad.Membership
-      .collect { case member if member.CharId > 0L => member.CharId }
-      .foreach { charId =>
-        SquadEvents.publish(SquadServiceResponse(s"$charId/Squad", detail))
+      .filter { _.CharId > 0L }
+      .foreach { member =>
+        SquadEvents.publish(SquadServiceResponse(s"/${member.CharId}/Squad", output))
       }
   }
 }
@@ -734,7 +830,7 @@ object SquadService {
 
   final case class VacancyBid(char_id : Long, squad_guid : PlanetSideGUID) extends PositionBid
 
-  final case class CamraderieBid(char_id : Long, placeholder : Squad) extends PositionBid
+  final case class SpontaneousBid(char_id : Long, placeholder : Squad) extends PositionBid
 
   object SquadList {
     def Publish(squad : Squad) : SquadInfo = {
@@ -768,86 +864,6 @@ object SquadService {
         )
         .Complete
     }
-
-    def Differences(updates : List[Int], info : SquadDetail) : SquadDetail = {
-      if(updates.nonEmpty) {
-        val list = Seq(
-          SquadDetail.Blank, //must be index-0
-          SquadDetail(info.unk1, None, None, None, None, None, None, None, None),
-          SquadDetail(None, None, info.leader_char_id, None, None, None, None, None, None),
-          SquadDetail(None, None, None, info.unk3, None, None, None, None, None),
-          SquadDetail(None, None, None, None, info.leader_name, None, None, None, None),
-          SquadDetail(None, None, None, None, None, info.task, None, None, None),
-          SquadDetail(None, None, None, None, None, None, info.zone_id, None, None),
-          SquadDetail(None, None, None, None, None, None, None, info.unk7, None),
-          SquadDetail(None, None, None, None, None, None, None, None, info.member_info)
-        )
-        var out = SquadDetail.Blank
-        updates
-          .map(i => list(i))
-          .filterNot { _ == SquadDetail.Blank }
-          .foreach(sinfo => out = out And sinfo )
-        out
-      }
-      else {
-        SquadDetail.Blank
-      }
-    }
-
-    def Differences(before : SquadDetail, after : SquadDetail) : SquadDetail = {
-      SquadDetail(
-        if(!before.unk1.equals(after.unk1)) after.unk1 else None,
-        None,
-        if(!before.leader_char_id.equals(after.leader_char_id)) after.leader_char_id else None,
-        if(!before.unk3.equals(after.unk3)) after.unk3 else None,
-        if(!before.leader_name.equals(after.leader_name)) after.leader_name else None,
-        if(!before.task.equals(after.task)) after.task else None,
-        if(!before.zone_id.equals(after.zone_id)) after.zone_id else None,
-        if(!before.unk7.equals(after.unk7)) after.unk7 else None,
-        {
-          (before.member_info, after.member_info) match {
-            case (Some(beforeEntry), Some(afterEntry)) =>
-              val out = beforeEntry.zip(afterEntry)
-                .map { case (b, a) => PositionEquality(b, a) }
-                .collect { case Some(entry) => entry }
-              if(out.nonEmpty) {
-                Some(out)
-              }
-              else {
-                None
-              }
-            case _ =>
-              None
-          }
-        }
-      )
-    }
-
-    private def PositionEquality(before : SquadPositionEntry, after : SquadPositionEntry) : Option[SquadPositionEntry] = {
-      (before.info, after.info) match {
-        case (Some(binfo), Some(ainfo)) =>
-          val out = MemberEquality(binfo, ainfo)
-          if(out == SquadPositionDetail.Blank) {
-            None
-          }
-          else {
-            Some(SquadPositionEntry(before.index, out))
-          }
-        case _ =>
-          None
-      }
-    }
-
-    private def MemberEquality(before : SquadPositionDetail, after : SquadPositionDetail) : SquadPositionDetail = {
-      SquadPositionDetail(
-        if(!before.is_closed.equals(after.is_closed)) after.is_closed else None,
-        if(!before.role.equals(after.role)) after.role else None,
-        if(!before.detailed_orders.equals(after.detailed_orders)) after.detailed_orders else None,
-        if(!before.requirements.equals(after.requirements)) after.requirements else None,
-        if(!before.char_id.equals(after.char_id)) after.char_id else None,
-        if(!before.name.equals(after.name)) after.name else None
-      )
-    }
   }
 
   def LoadSquadDefinition(squad : Squad, favorite : SquadLoadout) : Unit = {
@@ -869,6 +885,10 @@ object SquadService {
   }
 
   def ValidOpenSquadPosition(squad : Squad, position : Int, member : Member, reqs : Set[CertificationType.Value]) : Boolean = {
-    squad.Availability(position) && member.CharId == 0 && reqs.intersect(member.Requirements) == member.Requirements
+    ValidSquadPosition(squad, position, member, reqs) && member.CharId == 0
+  }
+
+  def ValidSquadPosition(squad : Squad, position : Int, member : Member, reqs : Set[CertificationType.Value]) : Boolean = {
+    squad.Availability(position) && reqs.intersect(member.Requirements) == member.Requirements
   }
 }
diff --git a/common/src/main/scala/services/teamwork/SquadSwitchboard.scala b/common/src/main/scala/services/teamwork/SquadSwitchboard.scala
new file mode 100644
index 00000000..165d757c
--- /dev/null
+++ b/common/src/main/scala/services/teamwork/SquadSwitchboard.scala
@@ -0,0 +1,142 @@
+// Copyright (c) 2019 PSForever
+package services.teamwork
+
+import akka.actor.{Actor, ActorRef, Terminated}
+
+import scala.collection.mutable
+
+/**
+  * 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`.
+  * 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.
+  */
+class SquadSwitchboard extends Actor {
+  /**
+    * 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
+    */
+  val UserActorMap : mutable.LongMap[ActorRef] = mutable.LongMap[ActorRef]()
+  /**
+    * 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()
+  }
+
+  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(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)
+
+    case SquadSwitchboard.To(member, msg) =>
+      UserActorMap.find { case (char_id, _) => char_id == member } match {
+        case Some((_, actor)) =>
+          actor ! msg
+        case None => ;
+      }
+
+    case SquadSwitchboard.ToAll(msg) =>
+      UserActorMap
+        .foreach { case (_, actor) =>
+          actor ! msg
+        }
+
+    case SquadSwitchboard.Except(excluded, msg) =>
+      UserActorMap
+        .filterNot { case (char_id, _) => char_id == excluded }
+        .foreach { case (_, 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 _ => ;
+  }
+}
+
+object SquadSwitchboard {
+  final case class Join(char_id : Long, actor : Option[ActorRef])
+
+  final case class DelayJoin(char_id : Long, actor : ActorRef)
+
+  final case class Leave(char_id : Long)
+
+  final case class Watch(char_id : Long, actor : ActorRef)
+
+  final case class Unwatch(char_id : Long)
+
+  final case class To(member : Long, msg : SquadServiceResponse)
+
+  final case class ToAll(msg : SquadServiceResponse)
+
+  final case class Except(excluded_member : Long, msg : SquadServiceResponse)
+}
diff --git a/common/src/test/scala/game/CharacterKnowledgeMessageTest.scala b/common/src/test/scala/game/CharacterKnowledgeMessageTest.scala
new file mode 100644
index 00000000..37ba5ad0
--- /dev/null
+++ b/common/src/test/scala/game/CharacterKnowledgeMessageTest.scala
@@ -0,0 +1,70 @@
+// Copyright (c) 2017 PSForever
+package game
+
+import org.specs2.mutable._
+import net.psforever.packet._
+import net.psforever.packet.game._
+import net.psforever.types.CertificationType
+import scodec.bits._
+
+class CharacterKnowledgeMessageTest extends Specification {
+  val string = hex"ec cc637a02 45804600720061006e006b0065006e00740061006e006b0003c022dc0008f01800"
+
+  "decode" in {
+    PacketCoding.DecodePacket(string).require match {
+      case CharacterKnowledgeMessage(char_id, Some(info)) =>
+        char_id mustEqual 41575372L
+        info mustEqual CharacterKnowledgeInfo(
+          "Frankentank",
+          Set(
+            CertificationType.StandardAssault,
+            CertificationType.ArmoredAssault1,
+            CertificationType.MediumAssault,
+            CertificationType.ReinforcedExoSuit,
+            CertificationType.Harasser,
+            CertificationType.Engineering,
+            CertificationType.GroundSupport,
+            CertificationType.AgileExoSuit,
+            CertificationType.AIMAX,
+            CertificationType.StandardExoSuit,
+            CertificationType.AAMAX,
+            CertificationType.ArmoredAssault2
+          ),
+          15,
+          0,
+          PlanetSideGUID(12)
+        )
+      case _ =>
+        ko
+    }
+  }
+
+  "encode" in {
+    val msg = CharacterKnowledgeMessage(
+      41575372L,
+      CharacterKnowledgeInfo(
+        "Frankentank",
+        Set(
+          CertificationType.StandardAssault,
+          CertificationType.ArmoredAssault1,
+          CertificationType.MediumAssault,
+          CertificationType.ReinforcedExoSuit,
+          CertificationType.Harasser,
+          CertificationType.Engineering,
+          CertificationType.GroundSupport,
+          CertificationType.AgileExoSuit,
+          CertificationType.AIMAX,
+          CertificationType.StandardExoSuit,
+          CertificationType.AAMAX,
+          CertificationType.ArmoredAssault2
+        ),
+        15,
+        0,
+        PlanetSideGUID(12)
+      )
+    )
+    val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+    pkt mustEqual string
+  }
+}
diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala
index 37619aec..49fdca7e 100644
--- a/pslogin/src/main/scala/WorldSessionActor.scala
+++ b/pslogin/src/main/scala/WorldSessionActor.scala
@@ -376,19 +376,8 @@ class WorldSessionActor extends Actor with MDCContextAware {
             )
           )
 
-        case SquadResponse.Detail(guid, leader, task, zone, member_info) =>
-          sendResponse(
-            SquadDetailDefinitionUpdateMessage(
-              guid,
-              SquadDetail()
-                .LeaderCharId(member_info.find(_.name.contains(leader)).get.char_id.get)
-                .LeaderName(leader)
-                .Task(task)
-                .ZoneId(zone)
-                .Members(member_info.zipWithIndex.map { case (a, b) => SquadPositionEntry(b, a) })
-                .Complete
-            )
-          )
+        case SquadResponse.Detail(guid, detail) =>
+          sendResponse(SquadDetailDefinitionUpdateMessage(guid, detail))
 
         case SquadResponse.InitSquad(squad_guid) =>
           sendResponse(SquadDefinitionActionMessage(squad_guid, 0, SquadAction.Unknown(16)))
@@ -414,7 +403,6 @@ class WorldSessionActor extends Actor with MDCContextAware {
           membershipPositions.find({ case(member, _) => member.CharId == avatar.CharId }) match {
             case Some((ourMember, ourIndex)) =>
               //we are joining the squad
-              sendResponse(SquadMembershipResponse(SquadResponseType.Accept, 0, 0, player.CharId, Some(leader.CharId), player.Name, true, Some(None)))
               //load each member's entry (our own too)
               membershipPositions.foreach { case(member, index) =>
                 sendResponse(SquadMemberEvent(0, id, member.CharId, index, Some(member.Name), Some(member.ZoneId), Some(0)))
@@ -447,7 +435,6 @@ class WorldSessionActor extends Actor with MDCContextAware {
           positionsToUpdate.find({ case(member, _) => member == avatar.CharId }) match {
             case Some((ourMember, ourIndex)) =>
               //we are leaving the squad
-              sendResponse(SquadMembershipResponse(SquadResponseType.Leave, 0,1, avatar.CharId, Some(avatar.CharId), avatar.name, true, Some(None)))
               //remove each member's entry (our own too)
               positionsToUpdate.foreach { case(member, index) =>
                 sendResponse(SquadMemberEvent(1, id, member, index, None, None, None))
@@ -498,23 +485,53 @@ class WorldSessionActor extends Actor with MDCContextAware {
             )
           }
 
-        case SquadResponse.SwapMember(squad, to_index, from_index) =>
-          //this failsafe is not supported by normal squad member operations
-          val member = squad.Membership(to_index)
-          val charId = member.CharId
-          val elem = squadUI(charId)
-          val id = 11
-          squadUI(charId) = SquadUIElement(elem.name, to_index, elem.zone, elem.health, elem.armor, elem.position)
-          sendResponse(SquadMemberEvent(1, id, charId, from_index, None, None, None))
-          sendResponse(SquadMemberEvent(0, id, charId, to_index, Some(elem.name), Some(elem.zone), Some(0)))
-          sendResponse(
-            SquadState(
-              PlanetSideGUID(id),
-              List(SquadStateInfo(charId, elem.health, elem.armor, elem.position, 2,2, false, 429, None,None))
-            )
-          )
-          if(charId == avatar.CharId) {
-            sendResponse(PlanetsideAttributeMessage(player.GUID, 32, to_index))
+        case SquadResponse.AssignMember(squad, from_index, to_index) =>
+          if(squadUI.nonEmpty) {
+            val toMember = squad.Membership(to_index)
+            val toCharId = toMember.CharId
+            val fromMember = squad.Membership(from_index)
+            val fromCharId = fromMember.CharId
+            val id = 11
+            if(fromCharId > 0) {
+              //toMember and fromMember have swapped places
+              val fromElem = squadUI(fromCharId)
+              val toElem = squadUI(toCharId)
+              sendResponse(SquadMemberEvent(1, id, toCharId, from_index, None, None, None))
+              sendResponse(SquadMemberEvent(1, id, fromCharId, to_index, None, None, None))
+              squadUI(toCharId) = SquadUIElement(fromElem.name, to_index, fromElem.zone, fromElem.health, fromElem.armor, fromElem.position)
+              squadUI(fromCharId) = SquadUIElement(toElem.name, from_index, toElem.zone, toElem.health, toElem.armor, toElem.position)
+              sendResponse(SquadMemberEvent(0, id, toCharId, to_index, Some(fromElem.name), Some(fromElem.zone), Some(0)))
+              sendResponse(SquadMemberEvent(0, id, fromCharId, from_index, Some(toElem.name), Some(toElem.zone), Some(0)))
+              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)
+                  )
+                )
+              )
+            }
+            else {
+              //previous fromMember has moved toMember
+              val elem = squadUI(fromCharId)
+              sendResponse(SquadMemberEvent(1, id, toCharId, from_index, None, None, None))
+              squadUI(toCharId) = SquadUIElement(elem.name, to_index, elem.zone, elem.health, elem.armor, elem.position)
+              sendResponse(SquadMemberEvent(0, id, toCharId, to_index, Some(elem.name), Some(elem.zone), Some(0)))
+              sendResponse(
+                SquadState(
+                  PlanetSideGUID(id),
+                  List(SquadStateInfo(toCharId, elem.health, elem.armor, elem.position, 2, 2, false, 429, None, None))
+                )
+              )
+            }
+            val charId = avatar.CharId
+            if(toCharId == charId) {
+              sendResponse(PlanetsideAttributeMessage(player.GUID, 32, to_index))
+            }
+            else if(fromCharId == charId) {
+              sendResponse(PlanetsideAttributeMessage(player.GUID, 32, from_index))
+            }
           }
 
         case _ => ;
@@ -3454,7 +3471,32 @@ class WorldSessionActor extends Actor with MDCContextAware {
     case msg @ PlayerStateMessageUpstream(avatar_guid, pos, vel, yaw, pitch, yaw_upper, seq_time, unk3, is_crouching, is_jumping, unk4, is_cloaking, unk5, unk6) =>
       if(deadState == DeadState.Alive) {
         if(!player.Crouching && is_crouching) { //SQUAD TESTING CODE
-          sendResponse(SquadMembershipResponse(SquadResponseType.Unk01, 0, 0, player.CharId, None, "Dummy", false, Some(None)))
+          sendResponse(
+            CharacterKnowledgeMessage(
+              41577140L,
+              CharacterKnowledgeInfo(
+                "Degrado",
+                Set(
+                  CertificationType.StandardAssault,
+                  CertificationType.ArmoredAssault1,
+                  CertificationType.MediumAssault,
+                  CertificationType.ReinforcedExoSuit,
+                  CertificationType.Harasser,
+                  CertificationType.Engineering,
+                  CertificationType.GroundSupport,
+                  CertificationType.AgileExoSuit,
+                  CertificationType.AIMAX,
+                  CertificationType.StandardExoSuit,
+                  CertificationType.AAMAX,
+                  CertificationType.ArmoredAssault2
+                ),
+                9,
+                0,
+                PlanetSideGUID(1)
+              )
+            )
+          )
+          sendResponse(SquadInvitationRequestMessage(PlanetSideGUID(1), 9, 41577140L, "Degrado"))
         }
         player.Position = pos
         player.Velocity = vel

From e5b2efd38107a2570d29d4bcc3609a64a82494e4 Mon Sep 17 00:00:00 2001
From: FateJH <Jason_DiDonato@yahoo.com>
Date: Wed, 7 Aug 2019 10:56:13 -0400
Subject: [PATCH 16/31] better support for the various ways to join a squad,
 and the various ways to leave a squad; squad-based messages; jury-rigged
 'player wants to join' operations

---
 .../game/SquadDefinitionActionMessage.scala   |  48 +-
 .../packet/game/SquadMemberEvent.scala        |  60 +-
 .../services/teamwork/SquadResponse.scala     |   4 +
 .../services/teamwork/SquadService.scala      | 702 ++++++++++++------
 .../SquadDefinitionActionMessageTest.scala    |  20 +-
 .../scala/game/SquadMemberEventTest.scala     |   4 +-
 .../src/main/scala/WorldSessionActor.scala    | 174 +++--
 7 files changed, 681 insertions(+), 331 deletions(-)

diff --git a/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala b/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala
index 32bbc68e..11a56ee2 100644
--- a/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala
+++ b/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala
@@ -16,9 +16,26 @@ import shapeless.{::, HNil}
 abstract class SquadAction(val code : Int)
 
 object SquadAction{
+  object SearchMode extends Enumeration {
+    type Type = Value
+
+    val
+    AnyPositions,
+    AvailablePositions,
+    SomeCertifications,
+    AllCertifications
+    = Value
+
+    implicit val codec : Codec[SearchMode.Value] = PacketHelpers.createEnumerationCodec(enum = this, uint(bits = 3))
+  }
+
   final case class DisplaySquad() extends SquadAction(0)
 
-  final case class AnswerSquadJoinRequest() extends SquadAction(1)
+  /**
+    * Dispatched from client to server to indicate a squad detail update that has no foundation entry to update?
+    * Not dissimilar from `DisplaySquad`.
+    */
+  final case class DisplayFullSquad() extends SquadAction(1)
 
   final case class SaveSquadFavorite() extends SquadAction(3)
 
@@ -58,12 +75,14 @@ object SquadAction{
 
   final case class LocationFollowsSquadLead(state : Boolean) extends SquadAction(31)
 
-  final case class SearchForSquadsWithParticularRole(u1: String, u2 : Long, u3: Int, u4 : Int) extends SquadAction(34)
+  final case class SearchForSquadsWithParticularRole(role: String, requirements : Set[CertificationType.Value], zone_id: Int, mode : SearchMode.Value) extends SquadAction(34)
 
   final case class CancelSquadSearch() extends SquadAction(35)
 
   final case class AssignSquadMemberToRole(position : Int, char_id : Long) extends SquadAction(38)
 
+  final case class NoSquadSearchResults() extends SquadAction(39)
+
   final case class FindLfsSoldiersForRole(state : Int) extends SquadAction(40)
 
   final case class CancelFind() extends SquadAction(41)
@@ -91,10 +110,10 @@ object SquadAction{
       }
     )
 
-    val answerSquadJoinRequestCodec = everFailCondition.xmap[AnswerSquadJoinRequest] (
-      _ => AnswerSquadJoinRequest(),
+    val displayFullSquadCodec = everFailCondition.xmap[DisplayFullSquad] (
+      _ => DisplayFullSquad(),
       {
-        case AnswerSquadJoinRequest() => None
+        case DisplayFullSquad() => None
       }
     )
 
@@ -243,12 +262,12 @@ object SquadAction{
       PacketHelpers.encodedWideStringAligned(6) ::
         ulongL(46) ::
         uint16L ::
-        uintL(3)).xmap[SearchForSquadsWithParticularRole] (
+        SearchMode.codec).xmap[SearchForSquadsWithParticularRole] (
       {
-        case u1 :: u2 :: u3 :: u4 :: HNil => SearchForSquadsWithParticularRole(u1, u2, u3, u4)
+        case u1 :: u2 :: u3 :: u4 :: HNil => SearchForSquadsWithParticularRole(u1, CertificationType.fromEncodedLong(u2), u3, u4)
       },
       {
-        case SearchForSquadsWithParticularRole(u1, u2, u3, u4) => u1 :: u2 :: u3 :: u4 :: HNil
+        case SearchForSquadsWithParticularRole(u1, u2, u3, u4) => u1 :: CertificationType.toEncodedLong(u2) :: u3 :: u4 :: HNil
       }
     )
 
@@ -268,6 +287,13 @@ object SquadAction{
       }
     )
 
+    val noSquadSearchResultsCodec = everFailCondition.xmap[NoSquadSearchResults] (
+      _ => NoSquadSearchResults(),
+      {
+        case NoSquadSearchResults() => None
+      }
+    )
+
     val findLfsSoldiersForRoleCodec = uint4.xmap[FindLfsSoldiersForRole] (
       state => FindLfsSoldiersForRole(state),
       {
@@ -326,6 +352,7 @@ object SquadAction{
   * &nbsp;&nbsp;&nbsp;&nbsp;`18` - UNKNOWN<br>
   * &nbsp;&nbsp;&nbsp;&nbsp;`26` - Reset All<br>
   * &nbsp;&nbsp;&nbsp;&nbsp;`35` - Cancel Squad Search<br>
+  * &nbsp;&nbsp;&nbsp;&nbsp;`39` - No Squad Search Results<br>
   * &nbsp;&nbsp;&nbsp;&nbsp;`41` - Cancel Find<br>
   * &nbsp;&nbsp;&nbsp;&nbsp;`42` - UNKNOWN<br>
   * &nbsp;&nbsp;&nbsp;&nbsp;`43` - UNKNOWN<br>
@@ -388,7 +415,7 @@ object SquadDefinitionActionMessage extends Marshallable[SquadDefinitionActionMe
     import scala.annotation.switch
     ((code : @switch) match {
       case 0 => displaySquadCodec
-      case 1 => answerSquadJoinRequestCodec
+      case 1 => displayFullSquadCodec
       case 3 => saveSquadFavoriteCodec
       case 4 => loadSquadFavoriteCodec
       case 5 => deleteSquadFavoriteCodec
@@ -411,12 +438,13 @@ object SquadDefinitionActionMessage extends Marshallable[SquadDefinitionActionMe
       case 34 => searchForSquadsWithParticularRoleCodec
       case 35 => cancelSquadSearchCodec
       case 38 => assignSquadMemberToRoleCodec
+      case 39 => noSquadSearchResultsCodec
       case 40 => findLfsSoldiersForRoleCodec
       case 41 => cancelFindCodec
       case 2 | 6 | 11 |
            12 | 13 | 14 | 16 |
            18 | 29 | 30 | 32 | 33 |
-           36 | 37 | 39 | 42 | 43 => unknownCodec(code)
+           36 | 37 | 42 | 43 => unknownCodec(code)
       case _ => failureCodec(code)
     }).asInstanceOf[Codec[SquadAction]]
   }
diff --git a/common/src/main/scala/net/psforever/packet/game/SquadMemberEvent.scala b/common/src/main/scala/net/psforever/packet/game/SquadMemberEvent.scala
index 503254a5..51b74955 100644
--- a/common/src/main/scala/net/psforever/packet/game/SquadMemberEvent.scala
+++ b/common/src/main/scala/net/psforever/packet/game/SquadMemberEvent.scala
@@ -12,18 +12,18 @@ object MemberEvent extends Enumeration {
   val
   Add,
   Remove,
-  Unknown2,
+  Promote,
   UpdateZone,
   Unknown4
     = Value
 
-  implicit val codec = PacketHelpers.createEnumerationCodec(this, uint(3))
+  implicit val codec = PacketHelpers.createEnumerationCodec(enum = this, uint(bits = 3))
 }
 
-final case class SquadMemberEvent(unk1 : Int,
+final case class SquadMemberEvent(action : MemberEvent.Value,
                                   unk2 : Int,
                                   char_id : Long,
-                                  member_position : Int,
+                                  position : Int,
                                   player_name : Option[String],
                                   zone_number : Option[Int],
                                   unk7 : Option[Long])
@@ -34,40 +34,46 @@ final case class SquadMemberEvent(unk1 : Int,
 }
 
 object SquadMemberEvent extends Marshallable[SquadMemberEvent] {
-  def apply(unk1 : Int, unk2 : Int, char_id : Long, member_position : Int) : SquadMemberEvent =
-    SquadMemberEvent(unk1, unk2, char_id, member_position, None, None, None)
+  def apply(action : MemberEvent.Value, unk2 : Int, char_id : Long, position : Int) : SquadMemberEvent =
+    SquadMemberEvent(action, unk2, char_id, position, None, None, None)
 
-  def apply(unk2 : Int, char_id : Long, member_position : Int, player_name : String, zone_number : Int, unk7 : Long) : SquadMemberEvent =
-    SquadMemberEvent(0, unk2, char_id, member_position, Some(player_name), Some(zone_number), Some(unk7))
+  def Add(unk2 : Int, char_id : Long, position : Int, player_name : String, zone_number : Int, unk7 : Long) : SquadMemberEvent =
+    SquadMemberEvent(MemberEvent.Add, unk2, char_id, position, Some(player_name), Some(zone_number), Some(unk7))
 
-  def apply(unk2 : Int, char_id : Long, member_position : Int, zone_number : Int) : SquadMemberEvent =
-    SquadMemberEvent(3, unk2, char_id, member_position, None, Some(zone_number), None)
+  def Remove(unk2 : Int, char_id : Long, position : Int) : SquadMemberEvent =
+    SquadMemberEvent(MemberEvent.Remove, unk2, char_id, position, None, None, None)
 
-  def apply(unk2 : Int, char_id : Long, member_position : Int, unk7 : Long) : SquadMemberEvent =
-    SquadMemberEvent(4, unk2, char_id, member_position, None, None, Some(unk7))
+  def Promote(unk2 : Int, char_id : Long) : SquadMemberEvent =
+    SquadMemberEvent(MemberEvent.Promote, unk2, char_id, 0, None, None, None)
+
+  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))
 
   implicit val codec : Codec[SquadMemberEvent] = (
-    ("unk1" | uint(3)) >>:~ { unk1 =>
+    ("action" | MemberEvent.codec) >>:~ { action =>
       ("unk2" | uint16L) ::
         ("char_id" | uint32L) ::
-        ("member_position" | uint4) ::
-        conditional(unk1 == 0, "player_name" | PacketHelpers.encodedWideStringAligned(1)) ::
-        conditional(unk1 == 0 || unk1 == 3, "zone_number" | uint16L) ::
-        conditional(unk1 == 0 || unk1 == 4, "unk7" | 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)
     }).exmap[SquadMemberEvent] (
     {
-      case unk1 :: unk2 :: char_id :: member_position :: player_name :: zone_number :: unk7 :: HNil =>
-        Attempt.Successful(SquadMemberEvent(unk1, unk2, char_id, member_position, player_name, zone_number, unk7))
+      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 data @ SquadMemberEvent(0, unk2, char_id, member_position, Some(player_name), Some(zone_number), Some(unk7)) =>
-        Attempt.Successful(0 :: unk2 :: char_id :: member_position :: Some(player_name) :: Some(zone_number) :: Some(unk7) :: HNil)
-      case data @ SquadMemberEvent(3, unk2, char_id, member_position, None, Some(zone_number), None) =>
-        Attempt.Successful(3 :: unk2 :: char_id :: member_position :: None :: Some(zone_number) :: None :: HNil)
-      case data @ SquadMemberEvent(4, unk2, char_id, member_position, None, None, Some(unk7)) =>
-        Attempt.Successful(4 :: unk2 :: char_id :: member_position :: None :: None :: Some(unk7) :: HNil)
-      case data @ SquadMemberEvent(unk1, unk2, char_id, member_position, None, None, None) =>
-        Attempt.Successful(unk1 :: unk2 :: char_id :: member_position :: None :: None :: None :: HNil)
+      case SquadMemberEvent(MemberEvent.Add, unk2, char_id, member_position, Some(player_name), Some(zone_number), Some(unk7)) =>
+        Attempt.Successful(MemberEvent.Add :: unk2 :: char_id :: member_position :: Some(player_name) :: Some(zone_number) :: Some(unk7) :: 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)) =>
+        Attempt.Successful(MemberEvent.Unknown4 :: unk2 :: char_id :: member_position :: None :: None :: Some(unk7) :: HNil)
+      case SquadMemberEvent(action, unk2, char_id, member_position, None, None, None) =>
+        Attempt.Successful(action :: unk2 :: char_id :: member_position :: None :: None :: None :: HNil)
       case data =>
         Attempt.Failure(Err(s"SquadMemberEvent can not encode with this pattern - $data"))
     }
diff --git a/common/src/main/scala/services/teamwork/SquadResponse.scala b/common/src/main/scala/services/teamwork/SquadResponse.scala
index 26215f02..562082ae 100644
--- a/common/src/main/scala/services/teamwork/SquadResponse.scala
+++ b/common/src/main/scala/services/teamwork/SquadResponse.scala
@@ -18,10 +18,14 @@ object SquadResponse {
   final case class Unknown17(squad : Squad, char_id : Long) extends Response
   final case class Membership(request_type : SquadResponseType.Value, unk1 : Int, unk2 : Int, unk3 : Long, unk4 : Option[Long], player_name : String, unk5 : Boolean, unk6 : Option[Option[String]]) extends Response //see SquadMembershipResponse
   final case class Invite(from_char_id : Long, to_char_id : Long, name : String) extends Response
+  final case class WantsSquadPosition(bid_name : String) extends Response
   final case class Join(squad : Squad, positionsToUpdate : List[Int]) 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 Detail(guid : PlanetSideGUID, squad_detail : SquadDetail) extends Response
+
+  final case class SquadSearchResults() extends Response
 }
diff --git a/common/src/main/scala/services/teamwork/SquadService.scala b/common/src/main/scala/services/teamwork/SquadService.scala
index e3dbe592..8f3b0de2 100644
--- a/common/src/main/scala/services/teamwork/SquadService.scala
+++ b/common/src/main/scala/services/teamwork/SquadService.scala
@@ -22,13 +22,14 @@ class SquadService extends Actor {
   private var memberToSquad : mutable.LongMap[Squad] = mutable.LongMap[Squad]()
   private var idToSquad : TrieMap[PlanetSideGUID, Squad] = new TrieMap[PlanetSideGUID, Squad]()
   private var idToSwitchboard : TrieMap[PlanetSideGUID, ActorRef] = new TrieMap[PlanetSideGUID, ActorRef]()
-  private var i : Int = 1
+  private var sid : Int = 1
   private val publishedLists : TrieMap[PlanetSideEmpire.Value, ListBuffer[SquadInfo]] = TrieMap[PlanetSideEmpire.Value, ListBuffer[SquadInfo]](
     PlanetSideEmpire.TR -> ListBuffer.empty,
     PlanetSideEmpire.NC -> ListBuffer.empty,
     PlanetSideEmpire.VS -> ListBuffer.empty
   )
-  private val bids : mutable.LongMap[PositionBid] = mutable.LongMap[PositionBid]()
+  private val invites : mutable.LongMap[Invitation] = mutable.LongMap[Invitation]()
+  private val queuedInvites : mutable.LongMap[List[Invitation]] = mutable.LongMap[List[Invitation]]()
   private val viewDetails : mutable.LongMap[PlanetSideGUID] = mutable.LongMap[PlanetSideGUID]()
 
   private [this] val log = org.log4s.getLogger
@@ -36,56 +37,70 @@ class SquadService extends Actor {
   override def preStart : Unit = {
     log.info("Starting...")
 
-    val testSquad = new Squad(PlanetSideGUID(3), PlanetSideEmpire.VS)
-    testSquad.Task = "\\#66CCFF Sentinels of Auraxis\\#FFFFFF ... \\#40FF40 Squad Up!!"
-    testSquad.ZoneId = 5
-    testSquad.Membership(0).Name = "Wizkid45"
-    testSquad.Membership(0).Role = "Fearless Leader"
-    testSquad.Membership(0).CharId = 30910985L
-    testSquad.Membership(0).ZoneId = 5
-    testSquad.Membership(0).Health = 64
-    testSquad.Membership(0).Armor = 34
-    testSquad.Membership(0).Position = Vector3(5526.5234f, 3818.7344f, 54.59375f)
-    testSquad.Membership(1).Name = "xoBLADEox"
-    testSquad.Membership(1).Role = "Right Hand"
-    testSquad.Membership(1).CharId = 42781919L
-    testSquad.Membership(1).ZoneId = 5
-    testSquad.Membership(1).Health = 54
-    testSquad.Membership(1).Armor = 44
-    testSquad.Membership(1).Position = Vector3(4673.5312f, 2604.8047f, 40.015625f)
-    testSquad.Membership(3).Name = "cabal0428"
-    testSquad.Membership(3).Role = "Left Hand"
-    testSquad.Membership(3).CharId = 353380L
-    testSquad.Membership(3).ZoneId = 5
-    testSquad.Membership(3).Health = 44
-    testSquad.Membership(3).Armor = 54
-    testSquad.Membership(3).Position = Vector3(4727.492f, 2613.5312f, 51.390625f)
-    testSquad.Membership(4).Name = "xSkiku"
-    testSquad.Membership(4).Role = "Right Foot's Middle Toe's Nail"
-    testSquad.Membership(4).CharId = 41588340L
-    testSquad.Membership(4).ZoneId = 5
-    testSquad.Membership(4).Health = 34
-    testSquad.Membership(4).Armor = 64
-    testSquad.Membership(4).Position = Vector3(3675.0f, 4789.8047f, 63.21875f)
-    idToSquad(PlanetSideGUID(3)) = testSquad
-    testSquad.Listed = true
-    UpdateSquadList(testSquad, None)
+//    val testSquad = new Squad(PlanetSideGUID(3), PlanetSideEmpire.VS)
+//    testSquad.Task = "\\#66CCFF Sentinels of Auraxis\\#FFFFFF ... \\#40FF40 Squad Up!!"
+//    testSquad.ZoneId = 5
+//    testSquad.Membership(0).Name = "Wizkid45"
+//    testSquad.Membership(0).Role = "Fearless Leader"
+//    testSquad.Membership(0).CharId = 30910985L
+//    testSquad.Membership(0).ZoneId = 5
+//    testSquad.Membership(0).Health = 64
+//    testSquad.Membership(0).Armor = 34
+//    testSquad.Membership(0).Position = Vector3(5526.5234f, 3818.7344f, 54.59375f)
+//    testSquad.Membership(1).Name = "xoBLADEox"
+//    testSquad.Membership(1).Role = "Right Hand"
+//    testSquad.Membership(1).CharId = 42781919L
+//    testSquad.Membership(1).ZoneId = 5
+//    testSquad.Membership(1).Health = 54
+//    testSquad.Membership(1).Armor = 44
+//    testSquad.Membership(1).Position = Vector3(4673.5312f, 2604.8047f, 40.015625f)
+//    testSquad.Membership(3).Name = "cabal0428"
+//    testSquad.Membership(3).Role = "Left Hand"
+//    testSquad.Membership(3).CharId = 353380L
+//    testSquad.Membership(3).ZoneId = 5
+//    testSquad.Membership(3).Health = 44
+//    testSquad.Membership(3).Armor = 54
+//    testSquad.Membership(3).Position = Vector3(4727.492f, 2613.5312f, 51.390625f)
+//    testSquad.Membership(4).Name = "xSkiku"
+//    testSquad.Membership(4).Role = "Right Foot's Middle Toe's Nail"
+//    testSquad.Membership(4).CharId = 41588340L
+//    testSquad.Membership(4).ZoneId = 5
+//    testSquad.Membership(4).Health = 34
+//    testSquad.Membership(4).Armor = 64
+//    testSquad.Membership(4).Position = Vector3(3675.0f, 4789.8047f, 63.21875f)
+//    idToSquad(PlanetSideGUID(3)) = testSquad
+//    testSquad.Listed = true
+//    UpdateSquadList(testSquad, None)
   }
 
   def GetNextSquadId() : PlanetSideGUID = {
-    val out = i
-    val j = i + 1
+    val out = sid
+    val j = sid + 1
     if(j == 65536) {
-      i = 1
+      sid = 1
     }
     else {
-      i = j
+      sid = j
     }
     PlanetSideGUID(out)
   }
 
+  def TryResetSquadId() : Boolean = {
+    if(idToSquad.isEmpty) {
+      sid = 1
+      true
+    }
+    else {
+      false
+    }
+  }
+
   def GetParticipatingSquad(player : Player) : Option[Squad] = {
-    memberToSquad.get(player.CharId) match {
+    GetParticipatingSquad(player.CharId)
+  }
+
+  def GetParticipatingSquad(charId : Long) : Option[Squad] = {
+    memberToSquad.get(charId) match {
       case opt @ Some(_) =>
         opt
       case None =>
@@ -114,24 +129,49 @@ class SquadService extends Actor {
     }
   }
 
-  def StartSquad(player : Player) : Squad = {
+  def GetLeadingSquad(charId : Long, opt : Option[Squad]) : Option[Squad] = {
+    opt.orElse(memberToSquad.get(charId)) match {
+      case Some(squad) =>
+        if(squad.Leader.CharId == charId) {
+          Some(squad)
+        }
+        else {
+          None
+        }
+      case _ =>
+        None
+    }
+  }
+
+  def CreateSquad(player : Player) : Squad = {
     val charId = player.CharId
     val faction = player.Faction
-    val id = GetNextSquadId()
     val name = player.Name
-    val squad = new Squad(id, faction)
+    val squad = new Squad(GetNextSquadId(), faction)
     val leadPosition = squad.Membership(squad.LeaderPositionIndex)
     leadPosition.Name = name
-    leadPosition.CharId = charId
+    leadPosition.CharId = player.CharId
     leadPosition.Health = player.Health
     leadPosition.Armor = player.Armor
     leadPosition.Position = player.Position
     leadPosition.ZoneId = 1
+    log.info(s"$name-$faction has started a new squad")
+    squad
+  }
+
+  def StartSquad(squad : Squad) : Squad = {
+    val charId = squad.Leader.CharId
+    val id = squad.GUID
     val switchboard = context.actorOf(Props[SquadSwitchboard], s"squad${id.guid}")
     memberToSquad += charId -> squad
     idToSquad += id -> squad
     idToSwitchboard += id -> switchboard
-    log.info(s"$name-$faction has started a new squad")
+    squad
+  }
+
+  def StartSquad(player : Player) : Squad = {
+    val squad = CreateSquad(player)
+    StartSquad(squad)
     squad
   }
 
@@ -199,137 +239,253 @@ class SquadService extends Actor {
     case Service.Leave(None) | Service.LeaveAll() => ;
 
     case SquadServiceMessage(tplayer, squad_action) => squad_action match {
-      case SquadAction.Membership(request_type, char_id, optional_char_id, _, _) => request_type match {
-        case SquadRequestType.Invite =>
-          //char_id is the inviter, e.g., the (prospective) squad leader
-          //this is just busy work; for actual joining operations, see SquadRequestType.Accept
-          (optional_char_id, memberToSquad.get(char_id)) match {
-            case (Some(invitee), Some(squad)) =>
-              bids(invitee) = VacancyBid(char_id, squad.GUID)
-              log.info(s"$invitee has been invited to squad ${squad.Task} by $char_id")
-              SquadEvents.publish( SquadServiceResponse(s"/$invitee/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, char_id, Some(invitee), tplayer.Name, false, Some(None))) )
-              SquadEvents.publish( SquadServiceResponse(s"/$char_id/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, invitee, Some(char_id), tplayer.Name, true, Some(None))) )
-            case (Some(invitee), None) =>
-              //the inviter does not currently belong to a squad; check for an existing placeholder, or create a new one
-              val ourSquad = {
-                bids.find { case (inviter, _) => inviter == char_id } match {
-                  case Some((_, SpontaneousBid(_, _squad))) =>
-                    _squad //borrow
-                  case _ =>
-                    val _squad = StartSquad(tplayer)
-                    memberToSquad.remove(char_id) //completely unlist until assured the squad is necessary
-                    _squad
-                }
-              }
-              bids(invitee) = SpontaneousBid(char_id, ourSquad)
-              SquadEvents.publish( SquadServiceResponse(s"/$invitee/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, char_id, Some(invitee), tplayer.Name, false, Some(None))) )
-              SquadEvents.publish( SquadServiceResponse(s"/$char_id/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, invitee, Some(char_id), tplayer.Name, true, Some(None))) )
-            case _ => ;
-          }
+      case SquadAction.Membership(SquadRequestType.Invite, invitingPlayer, Some(invitedPlayer), _, _) =>
+        //this is just busy work; for actual joining operations, see SquadRequestType.Accept
+//        FindBid(invitingPlayer, invitedPlayer) match {
+//          case Some(bid) =>
+//            //invitingPlayer and invitedPlayer have both tried to join each others's squads
+//            //treat this junction as consent
+//            self ! SquadServiceMessage(tplayer, SquadAction.Membership(SquadRequestType.Accept, invitingPlayer, Some(invitedPlayer), "", None))
+//          case _ => ;
+//        }
+        (memberToSquad.get(invitingPlayer), memberToSquad.get(invitedPlayer)) match {
+          case (Some(squad1), Some(squad2)) =>
+            //both players are in squads
+            if(squad1.GUID == squad2.GUID) {
+              //both players are in the same squad; no need to do anything
+            }
+            else {
+              //we might do some platoon chicanery with this case later
+              //TODO platoons
+            }
 
-        case SquadRequestType.Accept =>
-          //char_id is the invitee, e.g., the person joining the squad
-          bids.remove(char_id) match {
-            case Some(NormalBid(_/*inviterCharId*/, squadGUID, line)) if idToSquad.get(squadGUID).nonEmpty =>
-              //player requested to join a squad's specific position
-              JoinSquad(tplayer, idToSquad(squadGUID), line)
+          case (Some(squad), None) =>
+            //the classic situation
+            log.info(s"$invitedPlayer has been invited to squad ${squad.Task} by $invitingPlayer")
+            val leader = squad.Leader
+            val leaderCharId = leader.CharId
+            val bid = VacancyInvite(leaderCharId, leader.Name, squad.GUID)
+            AddInvite(invitedPlayer, bid) match {
+              case out @ Some(_) if out.contains(bid) =>
+                SquadEvents.publish(SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, leaderCharId, Some(invitedPlayer), tplayer.Name, false, Some(None))))
+                SquadEvents.publish(SquadServiceResponse(s"/$leaderCharId/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, invitedPlayer, Some(leaderCharId), tplayer.Name, true, Some(None))))
+              case Some(_) =>
+                SquadEvents.publish(SquadServiceResponse(s"/$leaderCharId/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, invitedPlayer, Some(leaderCharId), tplayer.Name, true, Some(None))))
+              case _ => ;
+            }
 
-            case Some(VacancyBid(inviterCharId, squadGUID)) if idToSquad.get(squadGUID).nonEmpty =>
-              //we were invited by the squad leader into an existing squad
-              val squad = idToSquad(squadGUID)
+          case (None, Some(squad)) =>
+            //flip around the roles - the inviting becomes the invited
+            //TODO needs work
+            log.info(s"$invitedPlayer has asked $invitingPlayer for an invition to squad ${squad.Task}")
+            val bid = VacancyInvite(invitedPlayer, "", squad.GUID)
+            AddInvite(invitingPlayer, bid) match {
+              case out @ Some(_) if out.contains(bid) =>
+                SquadEvents.publish(SquadServiceResponse(s"/$invitingPlayer/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, invitedPlayer, Some(invitingPlayer), tplayer.Name, false, Some(None))))
+                SquadEvents.publish(SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, invitingPlayer, Some(invitedPlayer), tplayer.Name, true, Some(None))))
+              case Some(_) =>
+                SquadEvents.publish(SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, invitingPlayer, Some(invitedPlayer), tplayer.Name, true, Some(None))))
+              case _ => ;
+            }
+
+          case (None, None) =>
+            //neither the invited player nor the inviting player belong to any squad
+            log.info(s"$invitedPlayer has been invited to join $invitingPlayer's spontaneous squad")
+            val bid = SpontaneousInvite(tplayer)
+            AddInvite(invitedPlayer, bid) match {
+              case out @ Some(_) if out.contains(bid) =>
+                SquadEvents.publish(SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, invitingPlayer, Some(invitedPlayer), tplayer.Name, false, Some(None))))
+                SquadEvents.publish(SquadServiceResponse(s"/$invitingPlayer/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, invitedPlayer, Some(invitingPlayer), tplayer.Name, true, Some(None))))
+              case Some(_) =>
+                SquadEvents.publish(SquadServiceResponse(s"/$invitingPlayer/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, invitedPlayer, Some(invitingPlayer), tplayer.Name, true, Some(None))))
+              case _ => ;
+            }
+
+          case _ => ;
+        }
+
+      case SquadAction.Membership(SquadRequestType.Accept, invitedPlayer, _, _, _) =>
+        val acceptedInvite = RemoveInvite(invitedPlayer)
+        acceptedInvite match {
+          case Some(BidForPosition(petitioner, guid, position)) if idToSquad.get(guid).nonEmpty =>
+            //player requested to join a squad's specific position
+            //invitedPlayer is actually the squad leader; petitioner is the actual "invitedPlayer"
+            if(memberToSquad.get(petitioner.CharId).isEmpty) {
+              JoinSquad(petitioner, idToSquad(guid), position)
+            }
+            else {
+              log.warn("Accept -> Bid: the invited player is already a member of a squad and can not join a second one")
+            }
+
+          case Some(VacancyInvite(invitingPlayer, _, guid))
+            if idToSquad.get(guid).nonEmpty =>
+            //we were invited by the squad leader into an existing squad
+            if(memberToSquad.get(invitedPlayer).isEmpty) {
+              val squad = idToSquad(guid)
               squad.Membership.zipWithIndex.find({ case (member, index) =>
                 ValidOpenSquadPosition(squad, index, member, tplayer.Certifications)
               }) match {
                 case Some((_, line)) =>
-                  SquadEvents.publish( SquadServiceResponse(s"/$inviterCharId/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, inviterCharId, Some(char_id), tplayer.Name, false, Some(None))) )
-                  SquadEvents.publish( SquadServiceResponse(s"/$char_id/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, char_id, Some(inviterCharId), "", true, Some(None))) )
+                  SquadEvents.publish(SquadServiceResponse(s"/$invitingPlayer/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitingPlayer, Some(invitedPlayer), tplayer.Name, false, Some(None))))
+                  SquadEvents.publish(SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitedPlayer, Some(invitingPlayer), "", true, Some(None))))
                   JoinSquad(tplayer, squad, line)
+                  RemoveQueuedInvites(invitedPlayer) //TODO deal with these somehow
                 case _ => ;
               }
+            }
+            else {
+              log.warn("Accept -> Invite: the invited player is already a member of a squad and can not join a second one")
+            }
 
-            case Some(SpontaneousBid(inviterCharId, placeholderSquad)) =>
-              //we were invited by someone into a new squad they would form
-              (GetParticipatingSquad(tplayer) match {
-                case Some(participating) =>
-                  if(participating.Leader.CharId == inviterCharId) {
-                    Some(participating)
+          case Some(SpontaneousInvite(invitingPlayer)) =>
+            //we were invited by someone into a new squad they would form
+            val invitingPlayerCharId = invitingPlayer.CharId
+            (GetParticipatingSquad(invitingPlayer) match {
+              case Some(participating) =>
+                if(participating.Leader.CharId == invitingPlayerCharId) {
+                  Some(participating)
+                }
+                else {
+                  //inviter joined a squad and is not its leader; bounce this request off of the squad leader
+                  participating.Membership.zipWithIndex.find({ case (member, index) =>
+                    ValidOpenSquadPosition(participating, index, member, tplayer.Certifications)
+                  }) match {
+                    case Some((_, line)) =>
+                      val bid = BidForPosition(tplayer, participating.GUID, line)
+                      AddInvite(participating.Leader.CharId, bid) match {
+                        case out @ Some(_) if out.contains(bid) =>
+                          HandleBidForPosition(bid, tplayer)
+                        case _ => ;
+                      }
+                    case _ => ;
                   }
-                  else {
-                    //inviter joined a squad and is not its leader; bounce this request off of the squad leader
-                    //TODO squad leader receives "<player> wants to join squad" prompt
-                    val leaderCharId = participating.Leader.CharId
-                    bids(char_id) = VacancyBid(leaderCharId, participating.GUID) //reframed request
-                    SquadEvents.publish(SquadServiceResponse(s"/$leaderCharId/Squad", SquadResponse.Invite(char_id, leaderCharId, tplayer.Name)))
-                    None
-                  }
-                case None =>
-                  placeholderSquad.Task = s"${tplayer.Name}'s Squad"
-                  memberToSquad(inviterCharId) = placeholderSquad
-                  SquadEvents.publish(SquadServiceResponse(s"/$inviterCharId/Squad", SquadResponse.InitSquad(placeholderSquad.GUID)))
-                  Some(placeholderSquad)
-              }) match {
+                  None
+                }
+              case None =>
+                val squad = StartSquad(invitingPlayer)
+                squad.Task = s"${tplayer.Name}'s Squad"
+                SquadEvents.publish(SquadServiceResponse(s"/$invitingPlayer/Squad", SquadResponse.InitSquad(squad.GUID)))
+                Some(squad)
+            }) match {
               case Some(squad) =>
                 squad.Membership.zipWithIndex.find({ case (member, index) =>
                   ValidOpenSquadPosition(squad, index, member, tplayer.Certifications)
                 }) match {
                   case Some((_, line)) =>
-                    SquadEvents.publish( SquadServiceResponse(s"/$inviterCharId/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, inviterCharId, Some(char_id), tplayer.Name, false, Some(None))) )
-                    SquadEvents.publish( SquadServiceResponse(s"/$char_id/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, char_id, Some(inviterCharId), "", true, Some(None))) )
+                    SquadEvents.publish( SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitedPlayer, Some(invitingPlayerCharId), "", true, Some(None))) )
                     JoinSquad(tplayer, squad, line)
+                    SquadEvents.publish( SquadServiceResponse(s"/$invitingPlayerCharId/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitingPlayerCharId, Some(invitedPlayer), tplayer.Name, false, Some(None))) )
+                    RemoveQueuedInvites(invitedPlayer) //TODO deal with these somehow
                   case _ => ;
                 }
               case None => ;
-              }
-
-            case _ => ;
-          }
-
-        case SquadRequestType.Leave =>
-          //char_id is the player leaving
-          val squad = memberToSquad(char_id)
-          val leader = squad.Leader.CharId
-          if(char_id == leader) {
-            //squad leader is leaving his own squad, so it will be disbanded
-            squad.Membership
-              .filterNot { _.CharId == leader }
-              .foreach { member =>
-                val charId = member.CharId
-                SquadEvents.publish(SquadServiceResponse(s"/$charId/Squad", SquadResponse.Membership(SquadResponseType.Disband, 0, 0, charId, None, "", false, Some(None))))
-              }
-            CloseOutSquad(squad)
-            SquadEvents.publish(SquadServiceResponse(s"/$leader/Squad", SquadResponse.Membership(SquadResponseType.Disband, 0, 0, leader, None, "", true, Some(None))))
-          }
-          else {
-            if(optional_char_id.contains(char_id)) {
-              //leaving the squad of our own accord
-              LeaveSquad(tplayer, squad)
             }
-            else if(optional_char_id.contains(leader)) {
-              //kicked by the squad leader
-              SquadEvents.publish( SquadServiceResponse(s"/$char_id/Squad", SquadResponse.Membership(SquadResponseType.Leave, 0, 0, char_id, Some(leader), tplayer.Name, false, Some(None))) )
-              SquadEvents.publish( SquadServiceResponse(s"/$leader/Squad", SquadResponse.Membership(SquadResponseType.Leave, 0, 0, leader, Some(char_id), "", true, Some(None))) )
-              LeaveSquad(tplayer, squad)
+
+          case None =>
+            //the invite either timed-out or was withdrawn; select a new one?
+            NextInvite(invitedPlayer) match {
+              case Some(bid : BidForPosition) if !acceptedInvite.contains(bid) =>
+                HandleBidForPosition(bid, tplayer)
+              case Some(bid) if !acceptedInvite.contains(bid) =>
+                SquadEvents.publish(SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, bid.InviterCharId, Some(invitedPlayer), bid.InviterName, false, Some(None))))
+              case None => ;
             }
-          }
+        }
 
-        case SquadRequestType.Reject =>
-          //a player has opted out of joining a squad
-          (bids.remove(char_id) match {
-            case Some(SpontaneousBid(inviterCharId, squad)) if squad.Leader.CharId != char_id =>
-              (inviterCharId, squad)
-            case Some(VacancyBid(inviterCharId, guid)) if idToSquad(guid).Leader.CharId != char_id =>
-              (inviterCharId, idToSquad.get(guid))
-            case _ => ;
-              (0L, None)
-          }) match {
-            case (inviterCharId, Some(squad)) =>
-              SquadEvents.publish( SquadServiceResponse(s"/$inviterCharId/Squad", SquadResponse.Membership(SquadResponseType.Reject, 0, 0, inviterCharId, Some(char_id), tplayer.Name, false, Some(None))) )
-              SquadEvents.publish( SquadServiceResponse(s"/$char_id/Squad", SquadResponse.Membership(SquadResponseType.Reject, 0, 0, char_id, Some(inviterCharId), "", true, Some(None))) )
-            case _ => ;
+      case SquadAction.Membership(SquadRequestType.Leave, leavingPlayer, optionalPlayer, _, _) =>
+        val squad = memberToSquad(leavingPlayer)
+        val leader = squad.Leader.CharId
+        if(leavingPlayer == leader || squad.Size == 2) {
+          //squad leader is leaving his own squad, so it will be disbanded
+          //alternately, squad is only composed of two people, so it will be closed-out when one of them leaves
+          val membership = squad.Membership.collect { case member if member.CharId > 0 => member.CharId }
+          membership.foreach { charId =>
+            SquadEvents.publish(SquadServiceResponse(s"/$charId/Squad", SquadResponse.Membership(SquadResponseType.Leave, 0, 0, charId, None, "", false, Some(None))))
           }
+          CloseOutSquad(squad)
+          SquadEvents.publish(SquadServiceResponse(s"/$leader/Squad", SquadResponse.Membership(SquadResponseType.Disband, 0, 0, leader, None, "", true, Some(None))))
+          membership
+            .filterNot(_ == leader)
+            .foreach { charId =>
+            SquadEvents.publish(SquadServiceResponse(s"/$charId/Squad", SquadResponse.Membership(SquadResponseType.Disband, 0, 0, charId, None, "", false, Some(None))))
+          }
+          SquadEvents.publish( SquadServiceResponse(s"/$leader/Squad", SquadResponse.InitSquad(PlanetSideGUID(0))) )
+          SquadEvents.publish( SquadServiceResponse(s"/$leader/Squad", SquadResponse.Detail(PlanetSideGUID(0), SquadDetail().Complete)) )
+        }
+        else {
+          if(optionalPlayer.contains(leavingPlayer)) {
+            //leaving the squad of own accord
+            LeaveSquad(tplayer, squad)
+          }
+          else if(optionalPlayer.contains(leader)) {
+            //kicked by the squad leader
+            SquadEvents.publish( SquadServiceResponse(s"/$leavingPlayer/Squad", SquadResponse.Membership(SquadResponseType.Leave, 0, 0, leavingPlayer, Some(leader), tplayer.Name, false, Some(None))) )
+            SquadEvents.publish( SquadServiceResponse(s"/$leader/Squad", SquadResponse.Membership(SquadResponseType.Leave, 0, 0, leader, Some(leavingPlayer), "", true, Some(None))) )
+            LeaveSquad(tplayer, squad)
+          }
+        }
 
-        case _ => ;
-      }
+      case SquadAction.Membership(SquadRequestType.Reject, rejectingPlayer, optionalPlayer, _, _) =>
+        val rejectedBid = RemoveInvite(rejectingPlayer)
+        //(A, B) -> person who made the rejection, person who was rejected
+        (rejectedBid match {
+          case Some(SpontaneousInvite(invitingPlayer)) =>
+            //rejectingPlayer is the would-be squad member
+            (Some(rejectingPlayer), Some(invitingPlayer.CharId))
+          case Some(VacancyInvite(invitingPlayer, _, guid)) if idToSquad(guid).Leader.CharId != rejectingPlayer =>
+            //rejectingPlayer is the would-be squad member
+            (Some(rejectingPlayer), Some(invitingPlayer))
+          case Some(BidForPosition(_, guid, _)) if idToSquad(guid).Leader.CharId != rejectingPlayer =>
+            //rejectingPlayer is the squad leader
+            (Some(rejectingPlayer), None)
+          case _ => ;
+            (None, None)
+        }) match {
+          case (Some(rejected), Some(invited)) =>
+            SquadEvents.publish( SquadServiceResponse(s"/$rejected/Squad", SquadResponse.Membership(SquadResponseType.Reject, 0, 0, rejected, Some(invited), "", true, Some(None))) )
+            SquadEvents.publish( SquadServiceResponse(s"/$invited/Squad", SquadResponse.Membership(SquadResponseType.Reject, 0, 0, invited, Some(rejected), tplayer.Name, false, Some(None))) )
+          case (Some(rejected), None) =>
+            SquadEvents.publish( SquadServiceResponse(s"/$rejected/Squad", SquadResponse.Membership(SquadResponseType.Reject, 0, 0, rejected, Some(rejected), "", true, Some(None))) )
+          case _ => ;
+        }
+        NextInvite(rejectingPlayer) match {
+          case Some(bid : BidForPosition) if rejectedBid.isEmpty || !rejectedBid.contains(bid) =>
+            HandleBidForPosition(bid, tplayer)
+          case Some(bid) if rejectedBid.isEmpty || !rejectedBid.contains(bid) =>
+            SquadEvents.publish(SquadServiceResponse(s"/$rejectingPlayer/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, bid.InviterCharId, Some(rejectingPlayer), bid.InviterName, false, Some(None))))
+          case None => ;
+        }
+
+      case SquadAction.Membership(SquadRequestType.Promote, promotingPlayer, Some(promotedPlayer), _, _) =>
+        (memberToSquad.get(promotingPlayer), memberToSquad.get(promotedPlayer)) match {
+          case (Some(squad), Some(squad2)) if squad.GUID == squad2.GUID && squad.Leader.CharId == promotingPlayer =>
+            val membership = squad.Membership.filter { _member => _member.CharId > 0 }
+            val (leader, position) = (squad.Leader, 0)
+            val (member, index) = membership.zipWithIndex.find { case (_member, _) => _member.CharId == promotedPlayer }.get
+            SwapMemberPosition(squad, leader, member)
+            log.info(s"Promoting player ${leader.Name} to be the leader of ${squad.Task}")
+            membership.foreach { _member =>
+              SquadEvents.publish(SquadServiceResponse(s"/${_member.CharId}/Squad", SquadResponse.PromoteMember(squad, promotedPlayer, index, position)))
+            }
+            SquadEvents.publish(SquadServiceResponse(s"/$promotingPlayer/Squad", SquadResponse.InitSquad(PlanetSideGUID(0))))
+            SquadEvents.publish(SquadServiceResponse(s"/$promotedPlayer/Squad", SquadResponse.InitSquad(squad.GUID)))
+            UpdateSquadListWhenListed(
+              squad,
+              SquadInfo().Leader(leader.Name)
+            )
+            UpdateSquadDetail(squad.GUID, squad,
+              SquadDetail()
+                .LeaderCharId(leader.CharId)
+                .Field3(value = 0L)
+                .LeaderName(leader.Name)
+                .Members(List(
+                  SquadPositionEntry(position, SquadPositionDetail().CharId(member.CharId).Name(member.Name)),
+                  SquadPositionEntry(index, SquadPositionDetail().CharId(leader.CharId).Name(leader.Name))
+                ))
+            )
+
+          case _ => ;
+        }
 
       case SquadAction.Update(char_id, health, max_health, armor, max_armor, pos, zone_number) =>
         memberToSquad.get(char_id) match {
@@ -387,14 +543,14 @@ class SquadService extends Actor {
             val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
             squad.Task = purpose
             UpdateSquadListWhenListed(squad, SquadInfo().Task(purpose))
-            UpdateSquadDetailWhenListed(squad.GUID, squad, SquadDetail().Task(purpose))
+            UpdateSquadDetail(squad.GUID, squad, SquadDetail().Task(purpose))
 
           case ChangeSquadZone(zone) =>
             log.info(s"${tplayer.Name}-${tplayer.Faction} has changed squad's ops zone to $zone")
             val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
             squad.ZoneId = zone.zoneId.toInt
             UpdateSquadListWhenListed(squad, SquadInfo().ZoneId(zone))
-            UpdateSquadDetailWhenListed(squad.GUID, squad, SquadDetail().ZoneId(zone))
+            UpdateSquadDetail(squad.GUID, squad, SquadDetail().ZoneId(zone))
 
           case CloseSquadMemberPosition(position) =>
             val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
@@ -411,7 +567,7 @@ class SquadService extends Actor {
                 }
                 memberPosition.Close()
                 UpdateSquadListWhenListed(squad, listingChanged)
-                UpdateSquadDetailWhenListed(squad.GUID, squad,
+                UpdateSquadDetail(squad.GUID, squad,
                   SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail.Closed)))
                 )
               case Some(false) | None => ;
@@ -424,7 +580,7 @@ class SquadService extends Actor {
                 log.info(s"${tplayer.Name}-${tplayer.Faction} has opened the #$position position in squad")
                 squad.Availability.update(position, true)
                 UpdateSquadListWhenListed(squad, SquadInfo().Capacity(squad.Capacity))
-                UpdateSquadDetailWhenListed(squad.GUID, squad,
+                UpdateSquadDetail(squad.GUID, squad,
                   SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail.Open)))
                 )
               case Some(true) | None => ;
@@ -436,7 +592,7 @@ class SquadService extends Actor {
               case Some(true) =>
                 log.info(s"${tplayer.Name}-${tplayer.Faction} has changed the role of squad position #$position")
                 squad.Membership(position).Role = role
-                UpdateSquadDetailWhenListed(squad.GUID, squad,
+                UpdateSquadDetail(squad.GUID, squad,
                   SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail().Role(role))))
                 )
               case Some(false) | None => ;
@@ -448,7 +604,7 @@ class SquadService extends Actor {
               case Some(true) =>
                 log.info(s"${tplayer.Name}-${tplayer.Faction} has changed the orders for squad position #$position")
                 squad.Membership(position).Orders = orders
-                UpdateSquadDetailWhenListed(squad.GUID, squad,
+                UpdateSquadDetail(squad.GUID, squad,
                   SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail().DetailedOrders(orders))))
                 )
               case Some(false) | None => ;
@@ -460,7 +616,7 @@ class SquadService extends Actor {
               case Some(true) =>
                 log.info(s"${tplayer.Name}-${tplayer.Faction} has changed the requirements for squad position #$position")
                 squad.Membership(position).Requirements = certs
-                UpdateSquadDetailWhenListed(squad.GUID, squad,
+                UpdateSquadDetail(squad.GUID, squad,
                   SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail().Requirements(certs))))
                 )
               case Some(false) | None => ;
@@ -519,7 +675,7 @@ class SquadService extends Actor {
             squad.LocationFollowsSquadLead = false
             squad.AutoApproveInvitationRequests = false
             UpdateSquadListWhenListed(squad, SquadInfo().Task("").ZoneId(None).Capacity(squad.Capacity))
-            UpdateSquadDetailWhenListed(squad.GUID, squad)
+            UpdateSquadDetail(squad.GUID, squad)
 
           case _ =>
         }
@@ -530,6 +686,9 @@ class SquadService extends Actor {
             log.info(s"${tplayer.Name} would like the #${position+1} spot in this squad")
             val membership = squad.Membership.zipWithIndex
             val toMember = squad.Membership(position)
+            if(squad.Leader.CharId == tplayer.CharId) {
+              //TODO 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 && toMember.CharId == 0) || ValidOpenSquadPosition(squad, position, toMember, tplayer.Certifications)) {
               membership.find { case (member, _) => member.CharId == tplayer.CharId } match {
@@ -538,12 +697,15 @@ class SquadService extends Actor {
                   if(fromIndex == squad.LeaderPositionIndex) {
                     squad.LeaderPositionIndex = position
                   }
+                  //RemoveInvite(tplayer.CharId).foreach { _ =>
+                    //close the old bids out
+                  //}
                   membership
                     .filter { case (_member, _) => _member.CharId > 0 }
                     .foreach { case (_member, _) =>
                       SquadEvents.publish(SquadServiceResponse(s"/${_member.CharId}/Squad", SquadResponse.AssignMember(squad, fromIndex, position)))
                     }
-                  UpdateSquadDetailWhenListed(squad.GUID, squad)
+                  UpdateSquadDetail(squad.GUID, squad)
                 case _ => ;
                 //somehow, this is not our squad; do nothing, for now
               }
@@ -561,10 +723,14 @@ class SquadService extends Actor {
                 if(ValidOpenSquadPosition(squad, position, toMember, tplayer.Certifications)) {
                   //we could join but we may need permission from the squad leader first
                   log.info(s"Player ${tplayer.Name} would like to join the squad ${squad.Task}.")
-                  bids(tplayer.CharId) = NormalBid(squad.Leader.CharId, guid, position)
-                  //val leader = squad.Leader
-                  //TODO ask permission from the squad leader, unless auto-approve is in effect
-                  self ! SquadServiceMessage(tplayer, SquadAction.Membership(SquadRequestType.Accept, tplayer.CharId, None, "", None))
+                  val leader = squad.Leader
+                  val bid = BidForPosition(tplayer, guid, position)
+                  val leaderCharId = leader.CharId
+                  AddInvite(leaderCharId, bid) match {
+                    case out @ Some(_) if out.contains(bid) =>
+                      HandleBidForPosition(bid, tplayer)
+                    case _ => ;
+                  }
                 }
               case None => ;
               //squad does not exist? assume old local data; force update to correct discrepancy
@@ -574,16 +740,19 @@ class SquadService extends Actor {
           case (Some(squad), AssignSquadMemberToRole(position, char_id)) =>
             val membership = squad.Membership.zipWithIndex
             (membership.find({ case (member, _) => member.CharId == char_id}), membership(position)) match {
-              case (Some((fromMember, fromPosition)), (toMember, _)) =>
+              //TODO squad leader currently disallowed
+              case (Some((fromMember, fromPosition)), (toMember, _)) if fromPosition != 0 =>
                 val name = fromMember.Name
                 SwapMemberPosition(squad, toMember, fromMember)
-                //
+                if(fromPosition == squad.LeaderPositionIndex) {
+                  squad.LeaderPositionIndex = position
+                }
                 membership
                   .filter({ case (_member, _) => _member.CharId > 0 })
                   .foreach { case (_member, _) =>
                     SquadEvents.publish(SquadServiceResponse(s"/${_member.CharId}/Squad", SquadResponse.AssignMember(squad, fromPosition, position)))
                   }
-                UpdateSquadDetailWhenListed(squad.GUID, squad,
+                UpdateSquadDetail(squad.GUID, squad,
                   SquadDetail().Members(List(
                     SquadPositionEntry(position, SquadPositionDetail().CharId(fromMember.CharId).Name(fromMember.Name)),
                     SquadPositionEntry(fromPosition, SquadPositionDetail().CharId(char_id).Name(name))
@@ -597,7 +766,15 @@ class SquadService extends Actor {
             idToSquad.get(guid) match {
               case Some(squad) =>
                 viewDetails(tplayer.CharId) = guid
-                sender ! SquadServiceResponse(s"/${tplayer.CharId}/Squad", SquadResponse.Detail(squad.GUID, SquadService.Detail.Publish(squad)))
+                sender ! SquadServiceResponse("", SquadResponse.Detail(squad.GUID, SquadService.Detail.Publish(squad)))
+              case None => ;
+            }
+
+          //the following message is feedback from a specific client, awaiting proper initialization
+          case (_, DisplayFullSquad()) =>
+            idToSquad.get(guid) match {
+              case Some(squad) =>
+                sender ! SquadServiceResponse("", SquadResponse.InitSquad(squad.GUID))
               case None => ;
             }
 
@@ -609,6 +786,122 @@ class SquadService extends Actor {
     }
   }
 
+  /**
+    *
+    * @param invitedPlayer
+    * @param bid
+    * @return an optional invite;
+    *         if added to the active invite position, return the parameter bid;
+    *         if added to the queued invite, return the invite in the active position;
+    *         if not added, return `None`
+    */
+  def AddInvite(invitedPlayer : Long, bid : Invitation) : Option[Invitation] = {
+    invites.get(invitedPlayer) match {
+      case Some(_bid) =>
+        queuedInvites.get(invitedPlayer) match {
+          case Some(bidList) =>
+            if(_bid.InviterCharId != bid.InviterCharId && !bidList.exists { eachBid => eachBid.InviterCharId == bid.InviterCharId }) {
+              log.debug(s"Invite from ${bid.InviterCharId} to $invitedPlayer stored in queue while active invite request pending")
+              queuedInvites(invitedPlayer) = bid match {
+                case _: BidForPosition =>
+                  val (normals, others) = bidList.partition(_.isInstanceOf[BidForPosition])
+                  (normals :+ bid) ++ others
+                case _ =>
+                  bidList :+ bid
+              }
+              None
+            }
+            else {
+              Some(_bid)
+            }
+          case None =>
+            if(_bid.InviterCharId != bid.InviterCharId) {
+              log.debug(s"Invite from ${bid.InviterCharId} to $invitedPlayer stored while active invite request pending")
+              queuedInvites(invitedPlayer) = List[Invitation](bid)
+            }
+            Some(_bid)
+        }
+
+      case None =>
+        invites(invitedPlayer) = bid
+        Some(bid)
+    }
+  }
+
+  def RemoveInvite(invitedPlayer : Long) : Option[Invitation] = {
+    invites.remove(invitedPlayer)
+  }
+
+  def RemoveQueuedInvites(invitedPlayer : Long) : List[Invitation] = {
+    queuedInvites.remove(invitedPlayer) match {
+      case Some(_bidList) => _bidList.toList
+      case None => Nil
+    }
+  }
+
+  def RemoveInvites(invitedPlayer : Long, invitingPlayer : Long) : Unit = {
+    queuedInvites.get(invitedPlayer) match {
+      case Some(bidList) =>
+        val list = bidList.filterNot { _.InviterCharId == invitingPlayer }
+        if(list.nonEmpty) {
+          queuedInvites(invitedPlayer) = list
+        }
+        else {
+          queuedInvites.remove(invitedPlayer)
+        }
+      case None => ;
+    }
+    invites.get(invitedPlayer) match {
+      case Some(_bid) =>
+        if(_bid.InviterCharId == invitingPlayer) {
+          //drop bid, try reload new bid
+        }
+      case None => ;
+    }
+  }
+
+  def NextInvite(invitedPlayer : Long) : Option[Invitation] = {
+    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
+    }
+  }
+
+  def HandleBidForPosition(bid : BidForPosition, player : Player) : Unit = {
+    idToSquad.get(bid.squad_guid) match {
+      case Some(squad) =>
+        val leaderCharId = squad.Leader.CharId
+        if(squad.AutoApproveInvitationRequests) {
+          self ! SquadServiceMessage(player, SquadAction.Membership(SquadRequestType.Accept, leaderCharId, None, "", None))
+        }
+        else {
+          SquadEvents.publish(SquadServiceResponse(s"/$leaderCharId/Squad", SquadResponse.WantsSquadPosition(bid.player.Name)))
+        }
+      case _ =>
+        //squad is missing; will this properly short-circuit?
+        log.error(s"Attempted to process ${bid.InviterName}'s bid for a position in a squad (id:${bid.squad_guid.guid}) that does not exist")
+    }
+  }
+
   def JoinSquad(player : Player, squad : Squad, line : Int) : Boolean = {
     val charId = player.CharId
     val position = squad.Membership(line)
@@ -652,7 +945,7 @@ class SquadService extends Actor {
           .foreach { member =>
             SquadEvents.publish(SquadServiceResponse(s"/${member.CharId}/Squad", SquadResponse.Join(squad, updatedIndex)))
           }
-        UpdateSquadDetailWhenListed(squad.GUID, squad,
+        UpdateSquadDetail(squad.GUID, squad,
           SquadDetail().Members(List(SquadPositionEntry(line, SquadPositionDetail().CharId(charId).Name(player.Name))))
         )
       }
@@ -670,30 +963,18 @@ class SquadService extends Actor {
     membership.find { case (_member, _) => _member.CharId == charId } match {
       case Some((member, index)) =>
         val updateList = membership.collect({ case (_member, _index) if _member.CharId > 0 => (_member.CharId, _index) }).toList
+        //member leaves the squad completely
         memberToSquad.remove(charId)
         member.Name = ""
         member.CharId = 0
-
-        val size = squad.Size
-        if(size < 2) {
-          //squad is rendered to just one person or less; collapse it
-          squad.Membership.foreach { _member =>
-              SquadEvents.publish(SquadServiceResponse(s"/${_member.CharId}/Squad", SquadResponse.Membership(SquadResponseType.Disband, 0, 0, charId, None, "", false, Some(None))))
+        sender ! SquadServiceResponse("", SquadResponse.Leave(squad, updateList))
+        //other squad members see the member leaving
+        val leavingMember = List((charId, index))
+        membership
+          .filter { case (_member, _) => _member.CharId > 0 }
+          .foreach { case (_member, _) =>
+            SquadEvents.publish( SquadServiceResponse(s"/${_member.CharId}/Squad", SquadResponse.Leave(squad, leavingMember)) )
           }
-          CloseOutSquad(squad, membership, updateList)
-        }
-        else {
-          //squad continues, despite player's parting
-          //member leaves the squad completely
-          sender ! SquadServiceResponse("", SquadResponse.Leave(squad, updateList))
-          //other squad members see the member leaving
-          val leavingMember = List((charId, index))
-          membership
-            .filter { case (_member, _) => _member.CharId > 0 }
-            .foreach { case (_member, _) =>
-              SquadEvents.publish( SquadServiceResponse(s"/${_member.CharId}/Squad", SquadResponse.Leave(squad, leavingMember)) )
-            }
-        }
         true
       case None =>
         false
@@ -720,6 +1001,7 @@ class SquadService extends Actor {
         SquadEvents.publish( SquadServiceResponse(s"/$charId/Squad", SquadResponse.Leave(squad, updateList)) )
     }
     idToSquad.remove(squad.GUID)
+    UpdateSquadList(squad, None)
   }
 
   def SwapMemberPosition(squad : Squad, toMember : Member, fromMember : Member) : Unit = {
@@ -744,6 +1026,13 @@ class SquadService extends Actor {
     toMember.Armor = armor
   }
 
+  def UpdateSquadList(faction : PlanetSideEmpire.Value): Unit = {
+    val factionListings = publishedLists(faction)
+    SquadEvents.publish(
+      SquadServiceResponse(s"/$faction/Squad", SquadResponse.InitList(factionListings.toVector))
+    )
+  }
+
   def UpdateSquadList(squad : Squad, changes : SquadInfo) : Unit = {
     UpdateSquadList(squad, Some(changes))
   }
@@ -759,7 +1048,6 @@ class SquadService extends Actor {
   }
 
   def UpdateSquadList(squad : Squad, changes : Option[SquadInfo]) : Unit = {
-    val entry = SquadService.SquadList.Publish(squad)
     val faction = squad.Faction
     val factionListings = publishedLists(faction)
     factionListings.find(info => {
@@ -774,7 +1062,7 @@ class SquadService extends Actor {
           case Some(changedFields) =>
             //squad information update
             log.info(s"Squad will be updated")
-            factionListings(index) = entry
+            factionListings(index) = SquadService.SquadList.Publish(squad)
             SquadEvents.publish(
               SquadServiceResponse(s"/$faction/Squad", SquadResponse.UpdateList(Seq((index, changedFields))))
             )
@@ -790,7 +1078,7 @@ class SquadService extends Actor {
       case None =>
         //first time being published
         log.info(s"Squad will be introduced")
-        factionListings += entry
+        factionListings += SquadService.SquadList.Publish(squad)
         SquadEvents.publish(
           SquadServiceResponse(s"/$faction/Squad", SquadResponse.InitList(factionListings.toVector))
         )
@@ -798,19 +1086,7 @@ class SquadService extends Actor {
   }
 
   def UpdateSquadDetail(guid : PlanetSideGUID, squad : Squad) : Unit = {
-    UpdateSquadDetailWhenListed(guid, squad, SquadService.Detail.Publish(squad))
-  }
-
-  def UpdateSquadDetailWhenListed(guid : PlanetSideGUID, squad : Squad) : Unit = {
-    if(squad.Listed || squad.Size > 1) {
-      UpdateSquadDetail(guid, squad, SquadService.Detail.Publish(squad))
-    }
-  }
-
-  def UpdateSquadDetailWhenListed(guid : PlanetSideGUID, squad : Squad, detail : SquadDetail) : Unit = {
-    if(squad.Listed || squad.Size > 1) {
-      UpdateSquadDetail(guid, squad, detail)
-    }
+    UpdateSquadDetail(guid, squad, SquadService.Detail.Publish(squad))
   }
 
   def UpdateSquadDetail(guid : PlanetSideGUID, squad : Squad, detail : SquadDetail) : Unit = {
@@ -824,13 +1100,19 @@ class SquadService extends Actor {
 }
 
 object SquadService {
-  trait PositionBid
+  abstract class Invitation(char_id : Long, name : String) {
+    def InviterCharId : Long = char_id
+    def InviterName : String = name
+  }
 
-  final case class NormalBid(char_id : Long, squad_guid : PlanetSideGUID, position : Int) extends PositionBid
+  final case class BidForPosition(player : Player, squad_guid : PlanetSideGUID, position : Int)
+    extends Invitation(player.CharId, player.Name)
 
-  final case class VacancyBid(char_id : Long, squad_guid : PlanetSideGUID) extends PositionBid
+  final case class VacancyInvite(char_id : Long, name : String, squad_guid : PlanetSideGUID)
+    extends Invitation(char_id, name)
 
-  final case class SpontaneousBid(char_id : Long, placeholder : Squad) extends PositionBid
+  final case class SpontaneousInvite(player : Player)
+    extends Invitation(player.CharId, player.Name)
 
   object SquadList {
     def Publish(squad : Squad) : SquadInfo = {
diff --git a/common/src/test/scala/game/SquadDefinitionActionMessageTest.scala b/common/src/test/scala/game/SquadDefinitionActionMessageTest.scala
index 09e2cd42..3ef29577 100644
--- a/common/src/test/scala/game/SquadDefinitionActionMessageTest.scala
+++ b/common/src/test/scala/game/SquadDefinitionActionMessageTest.scala
@@ -222,7 +222,7 @@ class SquadDefinitionActionMessageTest extends Specification {
       case SquadDefinitionActionMessage(unk1, unk2, action) =>
         unk1 mustEqual PlanetSideGUID(0)
         unk2 mustEqual 0
-        action mustEqual SearchForSquadsWithParticularRole("Badass", 0L, 1, 0)
+        action mustEqual SearchForSquadsWithParticularRole("Badass", Set(), 1, SearchMode.AnyPositions)
       case _ =>
         ko
     }
@@ -233,7 +233,7 @@ class SquadDefinitionActionMessageTest extends Specification {
       case SquadDefinitionActionMessage(unk1, unk2, action) =>
         unk1 mustEqual PlanetSideGUID(0)
         unk2 mustEqual 0
-        action mustEqual SearchForSquadsWithParticularRole("Badass", 0L, 2, 0)
+        action mustEqual SearchForSquadsWithParticularRole("Badass", Set(), 2, SearchMode.AnyPositions)
       case _ =>
         ko
     }
@@ -244,7 +244,7 @@ class SquadDefinitionActionMessageTest extends Specification {
       case SquadDefinitionActionMessage(unk1, unk2, action) =>
         unk1 mustEqual PlanetSideGUID(0)
         unk2 mustEqual 0
-        action mustEqual SearchForSquadsWithParticularRole("Badass", 0L, 2, 1)
+        action mustEqual SearchForSquadsWithParticularRole("Badass", Set(), 2, SearchMode.AvailablePositions)
       case _ =>
         ko
     }
@@ -255,7 +255,7 @@ class SquadDefinitionActionMessageTest extends Specification {
       case SquadDefinitionActionMessage(unk1, unk2, action) =>
         unk1 mustEqual PlanetSideGUID(0)
         unk2 mustEqual 0
-        action mustEqual SearchForSquadsWithParticularRole("Badass", 536870928L, 2, 2)
+        action mustEqual SearchForSquadsWithParticularRole("Badass", Set(CertificationType.InfiltrationSuit, CertificationType.AntiVehicular), 2, SearchMode.SomeCertifications)
       case _ =>
         ko
     }
@@ -266,7 +266,7 @@ class SquadDefinitionActionMessageTest extends Specification {
       case SquadDefinitionActionMessage(unk1, unk2, action) =>
         unk1 mustEqual PlanetSideGUID(0)
         unk2 mustEqual 0
-        action mustEqual SearchForSquadsWithParticularRole("Badass", 536870928L, 2, 3)
+        action mustEqual SearchForSquadsWithParticularRole("Badass", Set(CertificationType.InfiltrationSuit, CertificationType.AntiVehicular), 2, SearchMode.AllCertifications)
       case _ =>
         ko
     }
@@ -436,35 +436,35 @@ class SquadDefinitionActionMessageTest extends Specification {
   }
 
   "encode (34a)" in {
-    val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SearchForSquadsWithParticularRole("Badass", 0L, 1, 0))
+    val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SearchForSquadsWithParticularRole("Badass", Set(), 1, SearchMode.AnyPositions))
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_34a
   }
 
   "encode (34b)" in {
-    val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SearchForSquadsWithParticularRole("Badass", 0L, 2, 0))
+    val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SearchForSquadsWithParticularRole("Badass", Set(), 2, SearchMode.AnyPositions))
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_34b
   }
 
   "encode (34c)" in {
-    val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SearchForSquadsWithParticularRole("Badass", 0L, 2, 1))
+    val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SearchForSquadsWithParticularRole("Badass", Set(), 2, SearchMode.AvailablePositions))
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_34c
   }
 
   "encode (34d)" in {
-    val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SearchForSquadsWithParticularRole("Badass", 536870928L, 2, 2))
+    val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SearchForSquadsWithParticularRole("Badass", Set(CertificationType.InfiltrationSuit, CertificationType.AntiVehicular), 2, SearchMode.SomeCertifications))
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_34d
   }
 
   "encode (34e)" in {
-    val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SearchForSquadsWithParticularRole("Badass", 536870928L, 2, 3))
+    val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SearchForSquadsWithParticularRole("Badass", Set(CertificationType.InfiltrationSuit, CertificationType.AntiVehicular), 2, SearchMode.AllCertifications))
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_34e
diff --git a/common/src/test/scala/game/SquadMemberEventTest.scala b/common/src/test/scala/game/SquadMemberEventTest.scala
index 5ecc1cef..2f431991 100644
--- a/common/src/test/scala/game/SquadMemberEventTest.scala
+++ b/common/src/test/scala/game/SquadMemberEventTest.scala
@@ -12,7 +12,7 @@ class SquadMemberEventTest extends Specification {
   "decode" in {
     PacketCoding.DecodePacket(string).require match {
       case SquadMemberEvent(u1, u2, u3, u4, u5, u6, u7) =>
-        u1 mustEqual 0
+        u1 mustEqual MemberEvent.Add
         u2 mustEqual 7
         u3 mustEqual 42771010L
         u4 mustEqual 0
@@ -25,7 +25,7 @@ class SquadMemberEventTest extends Specification {
   }
 
   "encode" in {
-    val msg = SquadMemberEvent(0, 7, 42771010L, 0, Some("HofD"), Some(7), Some(529745L))
+    val msg = SquadMemberEvent(MemberEvent.Add, 7, 42771010L, 0, Some("HofD"), Some(7), Some(529745L))
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
     pkt mustEqual string
   }
diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala
index 49fdca7e..a7bbe9ef 100644
--- a/pslogin/src/main/scala/WorldSessionActor.scala
+++ b/pslogin/src/main/scala/WorldSessionActor.scala
@@ -40,6 +40,7 @@ import net.psforever.objects.serverobject.terminals._
 import net.psforever.objects.serverobject.terminals.Terminal.TerminalMessage
 import net.psforever.objects.serverobject.tube.SpawnTube
 import net.psforever.objects.serverobject.turret.{FacilityTurret, TurretUpgrade, WeaponTurret}
+import net.psforever.objects.teamwork.Squad
 import net.psforever.objects.vehicles.{AccessPermissionGroup, Cargo, Utility, VehicleLockState, _}
 import net.psforever.objects.vital._
 import net.psforever.objects.zones.{InterstellarCluster, Zone, ZoneHotSpotProjector}
@@ -358,7 +359,6 @@ class WorldSessionActor extends Actor with MDCContextAware {
               SquadListing(index, squadInfo)
             }.toVector
           )
-          log.info(s"updating squad with msg $o")
           sendResponse(
             ReplicationStreamMessage(6, None,
               infos.map { case (index, squadInfo) =>
@@ -388,12 +388,33 @@ class WorldSessionActor extends Actor with MDCContextAware {
             SquadDefinitionActionMessage(squad.GUID, 0, SquadAction.Unknown(33))
           )
 
-        case SquadResponse.Membership(request_type, unk1, unk2, unk3, unk4, player_name, unk5, unk6) =>
-          sendResponse(SquadMembershipResponse(request_type, unk1, unk2, unk3, unk4, player_name, unk5, unk6))
+        case SquadResponse.Membership(request_type, unk1, unk2, char_id, 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
+              LivePlayerList.WorldPopulation({ case (_, a : Avatar) => char_id == a.CharId }).headOption match {
+                case Some(player) =>
+                  player.name
+                case None =>
+                  player_name
+              }
+            case _ =>
+              player_name
+          }
+          sendResponse(SquadMembershipResponse(request_type, unk1, unk2, char_id, opt_char_id, name, unk5, unk6))
 
         case SquadResponse.Invite(from_char_id, to_char_id, name) =>
           sendResponse(SquadMembershipResponse(SquadResponseType.Invite, 0, 0, from_char_id, Some(to_char_id), s"$name", false, Some(None)))
 
+        case SquadResponse.WantsSquadPosition(name : String) =>
+          sendResponse(
+            ChatMsg(
+              ChatMessageType.CMT_TELL, true, "",
+              s"\\#6[SQUAD] \\#3$name\\#6 would like to join your squad. (respond with \\#3/accept\\#6 or \\#3/reject\\#6)",
+              None
+            )
+          )
+
         case SquadResponse.Join(squad, positionsToUpdate) =>
           val leader = squad.Leader
           val id = 11
@@ -405,20 +426,20 @@ class WorldSessionActor extends Actor with MDCContextAware {
               //we are joining the squad
               //load each member's entry (our own too)
               membershipPositions.foreach { case(member, index) =>
-                sendResponse(SquadMemberEvent(0, id, member.CharId, index, Some(member.Name), Some(member.ZoneId), Some(0)))
+                sendResponse(SquadMemberEvent.Add(id, member.CharId, index, member.Name, member.ZoneId, unk7 = 0))
                 squadUI(member.CharId) = SquadUIElement(member.Name, index, member.ZoneId, member.Health, member.Armor, member.Position)
               }
               //initialization
-              sendResponse(SquadMemberEvent(0, id, ourMember.CharId, ourIndex, Some(ourMember.Name), Some(ourMember.ZoneId), Some(0))) //repeat of our entry
+              sendResponse(SquadMemberEvent.Add(id, ourMember.CharId, ourIndex, ourMember.Name, ourMember.ZoneId, unk7 = 0)) //repeat of our entry
               sendResponse(PlanetsideAttributeMessage(player.GUID, 31, id)) //associate with squad?
               sendResponse(PlanetsideAttributeMessage(player.GUID, 32, ourIndex)) //associate with member position in squad?
               //a finalization? what does this do?
-              sendResponse(SquadDefinitionActionMessage(squad.GUID, 0, SquadAction.Unknown(18)))
+              sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(18)))
             case _ =>
               //other player is joining our squad
               //load each member's entry
               membershipPositions.foreach { case(member, index) =>
-                sendResponse(SquadMemberEvent(0, id, member.CharId, index, Some(member.Name), Some(member.ZoneId), Some(0)))
+                sendResponse(SquadMemberEvent.Add(id, member.CharId, index, member.Name, member.ZoneId, unk7 = 0))
                 squadUI(member.CharId) = SquadUIElement(member.Name, index, member.ZoneId, member.Health, member.Armor, member.Position)
               }
           }
@@ -429,6 +450,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
               .map{ case (member, _) => SquadStateInfo(member.CharId, member.Health, member.Armor, member.Position, 2,2, false, 429, None,None) }
               .toList
           ))
+          log.info(s"SquadCards: ${squadUI.map { case(id, card) => s"[${card.index}:${card.name}/$id]" }.mkString}")
 
         case SquadResponse.Leave(_, positionsToUpdate) =>
           val id = 11
@@ -437,11 +459,11 @@ class WorldSessionActor extends Actor with MDCContextAware {
               //we are leaving the squad
               //remove each member's entry (our own too)
               positionsToUpdate.foreach { case(member, index) =>
-                sendResponse(SquadMemberEvent(1, id, member, index, None, None, None))
+                sendResponse(SquadMemberEvent.Remove(id, member, index))
                 squadUI.remove(member)
               }
               //uninitialize
-              sendResponse(SquadMemberEvent(1, id, ourMember, ourIndex, None, None, None)) //repeat of our entry
+              sendResponse(SquadMemberEvent.Remove(id, ourMember, ourIndex)) //repeat of our entry
               sendResponse(PlanetsideAttributeMessage(player.GUID, 31, 0)) //disassociate with squad?
               sendResponse(PlanetsideAttributeMessage(player.GUID, 32, 0)) //disassociate with member position in squad?
               sendResponse(PlanetsideAttributeMessage(player.GUID, 34, 4294967295L)) //unknown, perhaps unrelated?
@@ -450,9 +472,10 @@ class WorldSessionActor extends Actor with MDCContextAware {
             case _ =>
               //remove each member's entry
               positionsToUpdate.foreach { case(member, index) =>
-                sendResponse(SquadMemberEvent(1, id, member, index, None, None, None))
+                sendResponse(SquadMemberEvent.Remove(id, member, index))
                 squadUI.remove(member)
               }
+              log.info(s"SquadCards: ${squadUI.map { case(id, card) => s"[${card.index}:${card.name}/$id]" }.mkString}")
           }
 
         case SquadResponse.UpdateMembers(squad, positions) =>
@@ -467,7 +490,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
             .collect({
               case (entry, element) if entry.zone_number != element.zone =>
                 //zone gets updated for these entries
-                sendResponse(SquadMemberEvent(3, 11, entry.char_id, element.index, None, Some(entry.zone_number), None))
+                sendResponse(SquadMemberEvent.UpdateZone(11, 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)
                 entry
               case (entry, element) if entry.health != element.health || entry.armor != element.armor || entry.pos != element.position =>
@@ -486,53 +509,19 @@ class WorldSessionActor extends Actor with MDCContextAware {
           }
 
         case SquadResponse.AssignMember(squad, from_index, to_index) =>
-          if(squadUI.nonEmpty) {
-            val toMember = squad.Membership(to_index)
-            val toCharId = toMember.CharId
-            val fromMember = squad.Membership(from_index)
-            val fromCharId = fromMember.CharId
-            val id = 11
-            if(fromCharId > 0) {
-              //toMember and fromMember have swapped places
-              val fromElem = squadUI(fromCharId)
-              val toElem = squadUI(toCharId)
-              sendResponse(SquadMemberEvent(1, id, toCharId, from_index, None, None, None))
-              sendResponse(SquadMemberEvent(1, id, fromCharId, to_index, None, None, None))
-              squadUI(toCharId) = SquadUIElement(fromElem.name, to_index, fromElem.zone, fromElem.health, fromElem.armor, fromElem.position)
-              squadUI(fromCharId) = SquadUIElement(toElem.name, from_index, toElem.zone, toElem.health, toElem.armor, toElem.position)
-              sendResponse(SquadMemberEvent(0, id, toCharId, to_index, Some(fromElem.name), Some(fromElem.zone), Some(0)))
-              sendResponse(SquadMemberEvent(0, id, fromCharId, from_index, Some(toElem.name), Some(toElem.zone), Some(0)))
-              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)
-                  )
-                )
-              )
-            }
-            else {
-              //previous fromMember has moved toMember
-              val elem = squadUI(fromCharId)
-              sendResponse(SquadMemberEvent(1, id, toCharId, from_index, None, None, None))
-              squadUI(toCharId) = SquadUIElement(elem.name, to_index, elem.zone, elem.health, elem.armor, elem.position)
-              sendResponse(SquadMemberEvent(0, id, toCharId, to_index, Some(elem.name), Some(elem.zone), Some(0)))
-              sendResponse(
-                SquadState(
-                  PlanetSideGUID(id),
-                  List(SquadStateInfo(toCharId, elem.health, elem.armor, elem.position, 2, 2, false, 429, None, None))
-                )
-              )
-            }
-            val charId = avatar.CharId
-            if(toCharId == charId) {
-              sendResponse(PlanetsideAttributeMessage(player.GUID, 32, to_index))
-            }
-            else if(fromCharId == charId) {
-              sendResponse(PlanetsideAttributeMessage(player.GUID, 32, from_index))
-            }
-          }
+          //we've already swapped position internally; now we swap the cards
+          SwapSquadUIElements(squad, from_index, to_index)
+          log.info(s"SquadCards: ${squadUI.map { case(id, card) => s"[${card.index}:${card.name}/$id]" }.mkString}")
+
+        case SquadResponse.PromoteMember(squad, char_id, from_index, to_index) =>
+          //promotion will swap visual cards, but we must fix the backend
+          val id = 11
+          sendResponse(SquadMemberEvent.Promote(id, char_id))
+          SwapSquadUIElements(squad, from_index, to_index)
+          log.info(s"SquadCards: ${squadUI.map { case(id, card) => s"[${card.index}:${card.name}/$id]" }.mkString}")
+
+        case SquadResponse.SquadSearchResults() =>
+          sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.NoSquadSearchResults()))
 
         case _ => ;
       }
@@ -3478,25 +3467,16 @@ class WorldSessionActor extends Actor with MDCContextAware {
                 "Degrado",
                 Set(
                   CertificationType.StandardAssault,
-                  CertificationType.ArmoredAssault1,
-                  CertificationType.MediumAssault,
-                  CertificationType.ReinforcedExoSuit,
-                  CertificationType.Harasser,
-                  CertificationType.Engineering,
-                  CertificationType.GroundSupport,
                   CertificationType.AgileExoSuit,
-                  CertificationType.AIMAX,
-                  CertificationType.StandardExoSuit,
-                  CertificationType.AAMAX,
-                  CertificationType.ArmoredAssault2
+                  CertificationType.StandardExoSuit
                 ),
-                9,
-                0,
-                PlanetSideGUID(1)
+                37,
+                5,
+                PlanetSideGUID(7)
               )
             )
           )
-          sendResponse(SquadInvitationRequestMessage(PlanetSideGUID(1), 9, 41577140L, "Degrado"))
+          sendResponse(SquadInvitationRequestMessage(PlanetSideGUID(1), 4, 41577140L, "Degrado"))
         }
         player.Position = pos
         player.Velocity = vel
@@ -9136,6 +9116,56 @@ class WorldSessionActor extends Actor with MDCContextAware {
     }
   }
 
+  def SwapSquadUIElements(squad : Squad, fromIndex : Int, toIndex : Int) : Unit = {
+    if(squadUI.nonEmpty) {
+      val fromMember = squad.Membership(fromIndex)
+      val fromCharId = fromMember.CharId
+      val toMember = squad.Membership(toIndex)
+      val toCharId = toMember.CharId
+      val id = 11
+      if(fromCharId > 0) {
+        //toMember and fromMember have swapped places
+        val fromElem = squadUI(fromCharId)
+        val toElem = squadUI(toCharId)
+        sendResponse(SquadMemberEvent.Remove(id, toCharId, fromIndex))
+        sendResponse(SquadMemberEvent.Remove(id, fromCharId, toIndex))
+        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))
+        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)
+            )
+          )
+        )
+      }
+      else {
+        //previous fromMember has moved toMember
+        val elem = squadUI(toCharId)
+        sendResponse(SquadMemberEvent.Remove(id, toCharId, fromIndex))
+        squadUI(toCharId) = SquadUIElement(elem.name, toIndex, elem.zone, elem.health, elem.armor, elem.position)
+        sendResponse(SquadMemberEvent.Add(id, toCharId, toIndex, elem.name, elem.zone, unk7 = 0))
+        sendResponse(
+          SquadState(
+            PlanetSideGUID(id),
+            List(SquadStateInfo(toCharId, elem.health, elem.armor, elem.position, 2, 2, false, 429, None, None))
+          )
+        )
+      }
+      val charId = avatar.CharId
+      if(toCharId == charId) {
+        sendResponse(PlanetsideAttributeMessage(player.GUID, 32, toIndex))
+      }
+      else if(fromCharId == charId) {
+        sendResponse(PlanetsideAttributeMessage(player.GUID, 32, fromIndex))
+      }
+    }
+  }
+
   def failWithError(error : String) = {
     log.error(error)
     sendResponse(ConnectionClose())

From 56d8748e994c0c768a4c3808ffab109997255ecb Mon Sep 17 00:00:00 2001
From: FateJH <Jason_DiDonato@yahoo.com>
Date: Tue, 13 Aug 2019 12:52:43 -0400
Subject: [PATCH 17/31] modified squad user initialization messages to better
 focus on specific steps of the process; dealt with different methods of
 communicating squad information to users, though not yet ready to implement
 the switchboard protocol

---
 .../game/SquadDefinitionActionMessage.scala   |  24 +-
 .../scala/services/teamwork/SquadAction.scala |   3 +-
 .../services/teamwork/SquadResponse.scala     |   4 +-
 .../services/teamwork/SquadService.scala      | 399 ++++++++++++++----
 .../src/main/scala/WorldSessionActor.scala    |  11 +-
 5 files changed, 356 insertions(+), 85 deletions(-)

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

From 60d65e22d3461b56aff09b6c4254d7161f2bc9ff Mon Sep 17 00:00:00 2001
From: FateJH <Jason_DiDonato@yahoo.com>
Date: Mon, 19 Aug 2019 19:20:42 -0400
Subject: [PATCH 18/31] added a refactored squad joining entrypoint; initial
 work and test on SquadWaypointEvent packet; initial work on
 SquadWaypointRequest packet; squad waypoints up to #5 (squad experience)
 should be working

---
 .../psforever/packet/GamePacketOpcode.scala   |   2 +-
 .../packet/game/SquadWaypointEvent.scala      |  74 +++---
 .../packet/game/SquadWaypointRequest.scala    | 115 +++++++++
 .../scala/services/teamwork/SquadAction.scala |   4 +-
 .../services/teamwork/SquadResponse.scala     |   3 +
 .../services/teamwork/SquadService.scala      | 233 +++++++++++-------
 .../scala/game/SquadWaypointEventTest.scala   |  34 +--
 .../src/main/scala/WorldSessionActor.scala    |  25 +-
 8 files changed, 343 insertions(+), 147 deletions(-)
 create mode 100644 common/src/main/scala/net/psforever/packet/game/SquadWaypointRequest.scala

diff --git a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
index 54452c55..2d10e152 100644
--- a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
+++ b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
@@ -472,7 +472,7 @@ object GamePacketOpcode extends Enumeration {
     case 0x80 => noDecoder(GenericObjectAction2Message)
     case 0x81 => game.DestroyDisplayMessage.decode
     case 0x82 => noDecoder(TriggerBotAction)
-    case 0x83 => noDecoder(SquadWaypointRequest)
+    case 0x83 => game.SquadWaypointRequest.decode
     case 0x84 => game.SquadWaypointEvent.decode
     case 0x85 => noDecoder(OffshoreVehicleMessage)
     case 0x86 => game.ObjectDeployedMessage.decode
diff --git a/common/src/main/scala/net/psforever/packet/game/SquadWaypointEvent.scala b/common/src/main/scala/net/psforever/packet/game/SquadWaypointEvent.scala
index 3874b625..4fe9f649 100644
--- a/common/src/main/scala/net/psforever/packet/game/SquadWaypointEvent.scala
+++ b/common/src/main/scala/net/psforever/packet/game/SquadWaypointEvent.scala
@@ -7,16 +7,16 @@ import scodec.{Attempt, Codec, Err}
 import scodec.codecs._
 import shapeless.{::, HNil}
 
-final case class WaypointEvent(unk1 : Int,
+final case class WaypointEvent(zone_number : Int,
                                pos : Vector3,
-                               unk2 : Int)
+                               unk : Int)
 
-final case class SquadWaypointEvent(unk1 : Int,
-                                    unk2 : Int,
-                                    unk3 : Long,
-                                    unk4 : Int,
+final case class SquadWaypointEvent(event_type : WaypointEventAction.Value,
+                                    unk : Int,
+                                    char_id : Long,
+                                    waypoint_type : Int,
                                     unk5 : Option[Long],
-                                    unk6 : Option[WaypointEvent])
+                                    waypoint_info : Option[WaypointEvent])
   extends PlanetSideGamePacket {
   type Packet = SquadWaypointEvent
   def opcode = GamePacketOpcode.SquadWaypointEvent
@@ -24,55 +24,55 @@ final case class SquadWaypointEvent(unk1 : Int,
 }
 
 object SquadWaypointEvent extends Marshallable[SquadWaypointEvent] {
-  def apply(unk1 : Int, unk2 : Int, unk3 : Long, unk4 : Int, unk_a : Long) : SquadWaypointEvent =
-    SquadWaypointEvent(unk1, unk2, unk3, unk4, Some(unk_a), None)
+  def Add(unk : Int, char_id : Long, waypoint_type : Int, waypoint : WaypointEvent) : SquadWaypointEvent =
+    SquadWaypointEvent(WaypointEventAction.Add, unk, char_id, waypoint_type, None, Some(waypoint))
 
-  def apply(unk1 : Int, unk2 : Int, unk3 : Long, unk4 : Int, unk_a : Int, pos : Vector3, unk_b : Int) : SquadWaypointEvent =
-    SquadWaypointEvent(unk1, unk2, unk3, unk4, None, Some(WaypointEvent(unk_a, pos, unk_b)))
+  def Unknown1(unk : Int, char_id : Long, waypoint_type : Int, unk_a : Long) : SquadWaypointEvent =
+    SquadWaypointEvent(WaypointEventAction.Unknown1, unk, char_id, waypoint_type, Some(unk_a), None)
 
-  def apply(unk1 : Int, unk2 : Int, unk3 : Long, unk4 : Int) : SquadWaypointEvent =
-    SquadWaypointEvent(unk1, unk2, unk3, unk4, None, None)
+  def Remove(unk : Int, char_id : Long, waypoint_type : Int) : SquadWaypointEvent =
+    SquadWaypointEvent(WaypointEventAction.Remove, unk, char_id, waypoint_type, None, None)
 
   private val waypoint_codec : Codec[WaypointEvent] = (
-    ("unk1" | uint16L) ::
+    ("zone_number" | uint16L) ::
       ("pos" | Vector3.codec_pos) ::
-      ("unk2" | uint(3))
+      ("unk" | uint(3))
     ).as[WaypointEvent]
 
   implicit val codec : Codec[SquadWaypointEvent] = (
-    ("unk1" | uint2) >>:~ { unk1 =>
-      ("unk2" | uint16L) ::
-      ("unk3" | uint32L) ::
-        ("unk4" | uint8L) ::
-        ("unk5" | conditional(unk1 == 1, uint32L)) ::
-        ("unk6" | conditional(unk1 == 0, waypoint_codec))
+    ("event_type" | WaypointEventAction.codec) >>:~ { event_type =>
+      ("unk" | uint16L) ::
+      ("char_id" | uint32L) ::
+        ("waypoint_type" | uint8L) ::
+        ("unk5" | conditional(event_type == WaypointEventAction.Unknown1, uint32L)) ::
+        ("waypoint_info" | conditional(event_type == WaypointEventAction.Add, waypoint_codec))
     }
   ).exmap[SquadWaypointEvent] (
     {
-      case 0 :: a :: b :: c :: None :: Some(d) :: HNil =>
-        Attempt.Successful(SquadWaypointEvent(0, a, b, c, None, Some(d)))
+      case WaypointEventAction.Add :: a :: char_id :: waypoint_type :: None :: Some(waypoint) :: HNil =>
+        Attempt.Successful(SquadWaypointEvent(WaypointEventAction.Add, a, char_id, waypoint_type, None, Some(waypoint)))
 
-      case 1 :: a :: b :: c :: Some(d) :: None :: HNil =>
-        Attempt.Successful(SquadWaypointEvent(1, a, b, c, Some(d), None))
+      case WaypointEventAction.Unknown1 :: a :: char_id :: waypoint_type :: Some(d) :: None :: HNil =>
+        Attempt.Successful(SquadWaypointEvent(WaypointEventAction.Unknown1, a, char_id, waypoint_type, Some(d), None))
 
-      case a :: b :: c :: d :: None :: None :: HNil =>
-        Attempt.Successful(SquadWaypointEvent(a, b, c, d, None, None))
+      case event_type :: b :: char_id :: waypoint_type :: None :: None :: HNil =>
+        Attempt.Successful(SquadWaypointEvent(event_type, b, char_id, waypoint_type, None, None))
 
-      case n :: _ :: _ :: _ :: _ :: _ :: HNil =>
-        Attempt.Failure(Err(s"unexpected format for unk1 - $n"))
+      case data =>
+        Attempt.Failure(Err(s"unexpected format for $data"))
     },
     {
-      case SquadWaypointEvent(0, a, b, c, None, Some(d)) =>
-        Attempt.Successful(0 :: a :: b :: c :: None :: Some(d) :: HNil)
+      case SquadWaypointEvent(WaypointEventAction.Add, a, char_id, waypoint_type, None, Some(waypoint)) =>
+        Attempt.Successful(WaypointEventAction.Add :: a :: char_id :: waypoint_type :: None :: Some(waypoint) :: HNil)
 
-      case SquadWaypointEvent(1, a, b, c, Some(d), None) =>
-        Attempt.Successful(1 :: a :: b :: c :: Some(d) :: None :: HNil)
+      case SquadWaypointEvent(WaypointEventAction.Unknown1, a, char_id, waypoint_type, Some(d), None) =>
+        Attempt.Successful(WaypointEventAction.Unknown1 :: a :: char_id :: waypoint_type :: Some(d) :: None :: HNil)
 
-      case SquadWaypointEvent(a, b, c, d, None, None) =>
-        Attempt.Successful(a :: b :: c :: d :: None :: None :: HNil)
+      case SquadWaypointEvent(event_type, b, char_id, waypoint_type, None, None) =>
+        Attempt.Successful(event_type :: b :: char_id :: waypoint_type :: None :: None :: HNil)
 
-      case SquadWaypointEvent(n, _, _, _, _, _) =>
-        Attempt.Failure(Err(s"unexpected format for unk1 - $n"))
+      case data =>
+        Attempt.Failure(Err(s"unexpected format for $data"))
     }
   )
 }
diff --git a/common/src/main/scala/net/psforever/packet/game/SquadWaypointRequest.scala b/common/src/main/scala/net/psforever/packet/game/SquadWaypointRequest.scala
new file mode 100644
index 00000000..5b7ebd57
--- /dev/null
+++ b/common/src/main/scala/net/psforever/packet/game/SquadWaypointRequest.scala
@@ -0,0 +1,115 @@
+// Copyright (c) 2019 PSForever
+package net.psforever.packet.game
+
+import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
+import net.psforever.types.Vector3
+import scodec.{Attempt, Codec, Err}
+import scodec.codecs._
+import shapeless.{::, HNil}
+
+/**
+  * Actions that can be requested of the specific waypoint.
+  */
+object WaypointEventAction extends Enumeration {
+  type Type = Value
+
+  val
+  Add,
+  Unknown1,
+  Remove,
+  Unknown3 //unconfirmed
+  = Value
+
+  implicit val codec : Codec[WaypointEventAction.Value] = PacketHelpers.createEnumerationCodec(enum = this, uint2)
+}
+
+/**
+  * na
+  * @param zone_number the zone
+  * @param pos the continental map coordinate location of the waypoint;
+  *            the z-coordinate is almost always 0.0
+  */
+final case class WaypointInfo(zone_number : Int,
+                              pos : Vector3)
+
+/**
+  * na
+  * @param request_type the action to be performed
+  * @param char_id the unique id of player setting the waypoint
+  * @param waypoint_type the waypoint being updated;
+  *                      0-3 for the standard squad waypoints numbered "1-4";
+  *                      4 for the squad leader experience waypoint;
+  *                      cycles through 0-3 continuously
+  *
+  * @param unk4 na
+  * @param waypoint_info essential data about the waypoint
+  */
+final case class SquadWaypointRequest(request_type : WaypointEventAction.Value,
+                                      char_id : Long,
+                                      waypoint_type : Int,
+                                      unk4 : Option[Long],
+                                      waypoint_info : Option[WaypointInfo])
+  extends PlanetSideGamePacket {
+  type Packet = SquadWaypointRequest
+  def opcode = GamePacketOpcode.SquadWaypointRequest
+  def encode = SquadWaypointRequest.encode(this)
+}
+
+object SquadWaypointRequest extends Marshallable[SquadWaypointRequest] {
+  def Add(char_id : Long, waypoint_type : Int, waypoint : WaypointInfo) : SquadWaypointRequest =
+    SquadWaypointRequest(WaypointEventAction.Add, char_id, waypoint_type, None, Some(waypoint))
+
+  def Unknown1(char_id : Long, waypoint_type : Int, unk_a : Long) : SquadWaypointRequest =
+    SquadWaypointRequest(WaypointEventAction.Unknown1, char_id, waypoint_type, Some(unk_a), None)
+
+  def Remove(char_id : Long, waypoint_type : Int) : SquadWaypointRequest =
+    SquadWaypointRequest(WaypointEventAction.Remove, char_id, waypoint_type, None, None)
+
+  private val waypoint_codec : Codec[WaypointInfo] = (
+    ("zone_number" | uint16L) ::
+      ("pos" | Vector3.codec_pos)
+    ).xmap[WaypointInfo] (
+    {
+      case zone_number :: pos :: HNil => WaypointInfo(zone_number, pos)
+    },
+    {
+      case WaypointInfo(zone_number, pos) => zone_number :: pos.xy :: HNil
+    }
+  )
+
+  implicit val codec : Codec[SquadWaypointRequest] = (
+    ("request_type" | WaypointEventAction.codec) >>:~ { request_type =>
+      ("char_id" | uint32L) ::
+        ("waypoint_type" | uint8L) ::
+        ("unk4" | conditional(request_type == WaypointEventAction.Unknown1, uint32L)) ::
+        ("waypoint" | conditional(request_type == WaypointEventAction.Add, waypoint_codec))
+    }
+    ).exmap[SquadWaypointRequest] (
+    {
+      case WaypointEventAction.Add :: char_id :: waypoint_type :: None :: Some(waypoint) :: HNil =>
+        Attempt.Successful(SquadWaypointRequest(WaypointEventAction.Add, char_id, waypoint_type, None, Some(waypoint)))
+
+      case WaypointEventAction.Unknown1 :: char_id :: waypoint_type :: Some(d) :: None :: HNil =>
+        Attempt.Successful(SquadWaypointRequest(WaypointEventAction.Unknown1, char_id, waypoint_type, Some(d), None))
+
+      case request_type :: char_id :: waypoint_type :: None :: None :: HNil =>
+        Attempt.Successful(SquadWaypointRequest(request_type, char_id, waypoint_type, None, None))
+
+      case data =>
+        Attempt.Failure(Err(s"unexpected format while decoding - $data"))
+    },
+    {
+      case SquadWaypointRequest(WaypointEventAction.Add, char_id, waypoint_type, None, Some(waypoint)) =>
+        Attempt.Successful(WaypointEventAction.Add :: char_id :: waypoint_type :: None :: Some(waypoint) :: HNil)
+
+      case SquadWaypointRequest(WaypointEventAction.Unknown1, char_id, waypoint_type, Some(d), None) =>
+        Attempt.Successful(WaypointEventAction.Unknown1 :: char_id :: waypoint_type :: Some(d) :: None :: HNil)
+
+      case SquadWaypointRequest(request_type, char_id, waypoint_type, None, None) =>
+        Attempt.Successful(request_type :: char_id :: waypoint_type :: None :: None :: HNil)
+
+      case data : SquadWaypointRequest =>
+        Attempt.Failure(Err(s"unexpected format while encoding - $data"))
+    }
+  )
+}
diff --git a/common/src/main/scala/services/teamwork/SquadAction.scala b/common/src/main/scala/services/teamwork/SquadAction.scala
index 62e3f2fe..d5da028e 100644
--- a/common/src/main/scala/services/teamwork/SquadAction.scala
+++ b/common/src/main/scala/services/teamwork/SquadAction.scala
@@ -1,7 +1,6 @@
 // Copyright (c) 2019 PSForever
 package services.teamwork
 
-import net.psforever.objects.Player
 import net.psforever.objects.zones.Zone
 import net.psforever.packet.game._
 import net.psforever.types.{SquadRequestType, Vector3}
@@ -9,7 +8,8 @@ import net.psforever.types.{SquadRequestType, Vector3}
 object SquadAction {
   trait Action
 
-  final case class Definition(player : Player, zone : Zone, guid : PlanetSideGUID, line : Int, action : SquadAction) extends Action
+  final case class Definition(zone : Zone, guid : PlanetSideGUID, line : Int, action : SquadAction) extends Action
   final case class Membership(request_type : SquadRequestType.Value, unk2 : Long, unk3 : Option[Long], player_name : String, unk5 : Option[Option[String]]) extends Action
+  final case class Waypoint(event_type : WaypointEventAction.Value, waypoint_type : Int, unk : Option[Long], waypoint_info : Option[WaypointInfo]) extends Action
   final case class Update(char_id : Long, health : Int, max_health : Int, armor : Int, max_armor : Int, pos : Vector3, zone_number : Int) extends Action
 }
diff --git a/common/src/main/scala/services/teamwork/SquadResponse.scala b/common/src/main/scala/services/teamwork/SquadResponse.scala
index fa06d866..817d27e5 100644
--- a/common/src/main/scala/services/teamwork/SquadResponse.scala
+++ b/common/src/main/scala/services/teamwork/SquadResponse.scala
@@ -29,5 +29,8 @@ object SquadResponse {
 
   final case class Detail(guid : PlanetSideGUID, squad_detail : SquadDetail) extends Response
 
+  final case class InitWaypoints(char_id : Long, waypoints : Iterable[(Int, WaypointInfo, Int)]) extends Response
+  final case class WaypointEvent(event_type : WaypointEventAction.Value, char_id : Long, waypoint_type : Int, unk5 : Option[Long], waypoint_info : Option[WaypointInfo], unk : Int) extends Response
+
   final case class SquadSearchResults() extends Response
 }
diff --git a/common/src/main/scala/services/teamwork/SquadService.scala b/common/src/main/scala/services/teamwork/SquadService.scala
index 17569777..5a439432 100644
--- a/common/src/main/scala/services/teamwork/SquadService.scala
+++ b/common/src/main/scala/services/teamwork/SquadService.scala
@@ -6,7 +6,6 @@ import net.psforever.objects.Player
 import net.psforever.objects.definition.converter.StatConverter
 import net.psforever.objects.loadouts.SquadLoadout
 import net.psforever.objects.teamwork.{Member, Squad}
-import net.psforever.objects.zones.Zone
 import net.psforever.packet.game._
 import net.psforever.types._
 import services.{GenericEventBus, Service}
@@ -44,6 +43,8 @@ class SquadService extends Actor {
     */
   private val initialAssociation : ListBuffer[PlanetSideGUID] = new ListBuffer[PlanetSideGUID]()
   private val queuedInvites : mutable.LongMap[List[Invitation]] = mutable.LongMap[List[Invitation]]()
+  private val waypoints : TrieMap[PlanetSideGUID, Array[WaypointData]] =
+    new TrieMap[PlanetSideGUID, Array[WaypointData]]()
   private val viewDetails : mutable.LongMap[PlanetSideGUID] = mutable.LongMap[PlanetSideGUID]()
 
   private [this] val log = org.log4s.getLogger
@@ -122,7 +123,7 @@ class SquadService extends Actor {
     }
   }
 
-  def GetLeadingSquad(player : Player, zone : Int, opt : Option[Squad]) : Option[Squad] = {
+  def GetLeadingSquad(player : Player, opt : Option[Squad]) : Option[Squad] = {
     val charId = player.CharId
     opt match {
       case Some(squad) =>
@@ -350,80 +351,31 @@ class SquadService extends Actor {
                 RemoveQueuedInvites(invitedPlayer) //TODO deal with these somehow
               case _ => ;
             }
-//            if(idToSquad.get(guid).isEmpty) {
-//              log.warn("Accept->Invite: the squad no longer exists")
-//            }
-//            else if(memberToSquad.get(invitedPlayer).nonEmpty) {
-//              log.warn("Accept->Invite: player is already a member of a squad and can not join a second one")
-//            }
-//            else {
-//              val squad = idToSquad(guid)
-//              if(!squad.AutoApproveInvitationRequests && squad.Leader.CharId != invitingPlayer) {
-//                //the inviting player was not the squad leader and this decision should be bounced off the squad leader
-//                val bid = IndirectVacancy(tplayer, guid)
-//                AddInvite(squad.Leader.CharId, bid) match {
-//                  case out @ Some(_) if out.contains(bid) =>
-//                    HandleBidForPosition(bid, tplayer)
-//                  case _ => ;
-//                }
-//              }
-//              else {
-//                //if a suitable position in the squad can be found, player may occupy it
-//                squad.Membership.zipWithIndex.find({ case (member, index) =>
-//                  ValidOpenSquadPosition(squad, index, member, tplayer.Certifications)
-//                }) match {
-//                  case Some((_, line)) =>
-//                    SquadEvents.publish(SquadServiceResponse(s"/$invitingPlayer/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitingPlayer, Some(invitedPlayer), tplayer.Name, false, Some(None))))
-//                    SquadEvents.publish(SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitedPlayer, Some(invitingPlayer), "", true, Some(None))))
-//                    JoinSquad(tplayer, squad, line)
-//                    RemoveQueuedInvites(invitedPlayer) //TODO deal with these somehow
-//                  case _ => ;
-//                }
-//              }
-//            }
 
           case Some(SpontaneousInvite(invitingPlayer)) =>
-            //we were invited by someone into a new squad they would form
+            //originally, we were invited by someone into a new squad they would form
             val invitingPlayerCharId = invitingPlayer.CharId
             (GetParticipatingSquad(invitingPlayer) match {
               case Some(participating) =>
-                if(participating.Leader.CharId == invitingPlayerCharId) {
-                  Some(participating)
-                }
-                else {
-                  //inviter joined a squad and is not its leader; bounce this request off of the squad leader
-                  participating.Membership.zipWithIndex.find({ case (member, index) =>
-                    ValidOpenSquadPosition(participating, index, member, tplayer.Certifications)
-                  }) match {
-                    case Some((_, line)) =>
-                      val bid = BidForPosition(tplayer, participating.GUID, line)
-                      AddInvite(participating.Leader.CharId, bid) match {
-                        case out @ Some(_) if out.contains(bid) =>
-                          HandleBidForPosition(bid, tplayer)
-                        case _ => ;
-                      }
-                    case _ => ;
-                  }
-                  None
-                }
-              case None =>
+                //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"${tplayer.Name}'s Squad"
-                SquadEvents.publish(SquadServiceResponse(s"/$invitingPlayer/Squad", SquadResponse.AssociateWithSquad(squad.GUID)))
+                squad.Task = s"${invitingPlayer.Name}'s Squad"
+                SquadEvents.publish( SquadServiceResponse(s"/$invitingPlayerCharId/Squad", SquadResponse.AssociateWithSquad(squad.GUID)) )
                 Some(squad)
             }) match {
               case Some(squad) =>
-                squad.Membership.zipWithIndex.find({ case (member, index) =>
-                  ValidOpenSquadPosition(squad, index, member, tplayer.Certifications)
-                }) match {
+                HandleVacancyInvite(squad.GUID, tplayer.CharId, invitingPlayerCharId, tplayer) match {
                   case Some((_, line)) =>
                     SquadEvents.publish( SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitedPlayer, Some(invitingPlayerCharId), "", true, Some(None))) )
                     JoinSquad(tplayer, squad, line)
                     SquadEvents.publish( SquadServiceResponse(s"/$invitingPlayerCharId/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitingPlayerCharId, Some(invitedPlayer), tplayer.Name, false, Some(None))) )
-                    RemoveQueuedInvites(invitedPlayer) //TODO deal with these somehow
+                    RemoveQueuedInvites(tplayer.CharId) //TODO deal with these somehow
                   case _ => ;
                 }
-              case None => ;
+              case _ => ;
             }
 
           case None =>
@@ -566,32 +518,46 @@ class SquadService extends Actor {
           case _ => ;
         }
 
-      case SquadAction.Update(char_id, health, max_health, armor, max_armor, pos, zone_number) =>
-        memberToSquad.get(char_id) match {
+      case SquadAction.Waypoint(_, wtype, _, info) =>
+        val playerCharId = tplayer.CharId
+        (GetLeadingSquad(tplayer, None) match {
           case Some(squad) =>
-            squad.Membership.find(_.CharId == char_id) match {
-              case Some(member) =>
-                member.Health = StatConverter.Health(health, max_health, min=1, max=64)
-                member.Armor = StatConverter.Health(armor, max_armor, min=1, max=64)
-                member.Position = pos
-                member.ZoneId = zone_number
-                sender ! SquadServiceResponse("", 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 _ => ;
+            info match {
+              case Some(winfo) =>
+                (Some(squad), AddWaypoint(squad.GUID, wtype, winfo))
+              case _ =>
+                RemoveWaypoint(squad.GUID, wtype)
+                (Some(squad), None)
             }
-
-          case None => ;
+          case _ => (None, None)
+        }) match {
+          case (Some(squad), Some(waypoint)) =>
+            //waypoint added or updated
+            squad.Membership
+              .filterNot { member => member.CharId == tplayer.CharId }
+              .foreach { member =>
+                val charId = member.CharId
+                SquadEvents.publish(
+                  SquadServiceResponse(s"/$charId/Squad", SquadResponse.WaypointEvent(WaypointEventAction.Add, playerCharId, wtype, None, info, 1))
+                )
+              }
+          case (Some(squad), None) =>
+            //waypoint removed?
+            squad.Membership
+              .filterNot { member => member.CharId == tplayer.CharId }
+              .foreach { member =>
+                val charId = member.CharId
+                SquadEvents.publish(
+                  SquadServiceResponse(s"/$charId/Squad", SquadResponse.WaypointEvent(WaypointEventAction.Remove, playerCharId, wtype, None, None, 0))
+                )
+              }
+          case _ => ;
         }
 
-      case SquadAction.Definition(tplayer : Player, zone : Zone, guid : PlanetSideGUID, line : Int, action : SquadAction) =>
+      case SquadAction.Definition(zone, guid, line, action) =>
         import net.psforever.packet.game.SquadAction._
         val pSquadOpt = GetParticipatingSquad(tplayer)
-        val lSquadOpt = GetLeadingSquad(tplayer, zone.Number, pSquadOpt)
+        val lSquadOpt = GetLeadingSquad(tplayer, pSquadOpt)
         //the following actions can only be performed by a squad's leader
         action match {
           case SaveSquadFavorite() =>
@@ -624,17 +590,17 @@ class SquadService extends Actor {
             UpdateSquadListWhenListed(squad, SquadInfo().Task(purpose))
             UpdateSquadDetail(squad.GUID, squad, SquadDetail().Task(purpose))
 
-          case ChangeSquadZone(zone) =>
-            log.info(s"${tplayer.Name}-${tplayer.Faction} has changed squad's ops zone to $zone")
+          case ChangeSquadZone(zone_id) =>
+            log.info(s"${tplayer.Name}-${tplayer.Faction} has changed squad's ops zone to $zone_id")
             val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
-            squad.ZoneId = zone.zoneId.toInt
-            UpdateSquadListWhenListed(squad, SquadInfo().ZoneId(zone))
+            squad.ZoneId = zone_id.zoneId.toInt
+            UpdateSquadListWhenListed(squad, SquadInfo().ZoneId(zone_id))
             InitialAssociation(squad)
             sender ! SquadServiceResponse("", SquadResponse.Detail(
               squad.GUID,
               SquadService.Detail.Publish(squad))
             )
-            UpdateSquadDetail(squad.GUID, squad.Membership.map { _m => _m.CharId }.filterNot { _ == squad.Leader.CharId }, SquadDetail().ZoneId(zone))
+            UpdateSquadDetail(squad.GUID, squad.Membership.map { _m => _m.CharId }.filterNot { _ == squad.Leader.CharId }, SquadDetail().ZoneId(zone_id))
 
           case CloseSquadMemberPosition(position) =>
             val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
@@ -906,6 +872,12 @@ class SquadService extends Actor {
               case _ => ;
             }
 
+          //the following action can be peprformed by anyone
+          case (_, SearchForSquadsWithParticularRole(role, requirements, zone_id, search_mode)) =>
+            //though we should be able correctly search squads as is intended
+            //I don't know how search results should be prioritized or even how to return search results to the user
+            sender ! SquadServiceResponse("", SquadResponse.SquadSearchResults())
+
           //the following action can be performed by anyone
           case (_, DisplaySquad()) =>
             idToSquad.get(guid) match {
@@ -926,6 +898,28 @@ class SquadService extends Actor {
           case _ => ;
         }
 
+      case SquadAction.Update(char_id, health, max_health, armor, max_armor, pos, zone_number) =>
+        memberToSquad.get(char_id) match {
+          case Some(squad) =>
+            squad.Membership.find(_.CharId == char_id) match {
+              case Some(member) =>
+                member.Health = StatConverter.Health(health, max_health, min=1, max=64)
+                member.Armor = StatConverter.Health(armor, max_armor, min=1, max=64)
+                member.Position = pos
+                member.ZoneId = zone_number
+                sender ! SquadServiceResponse("", SquadResponse.UpdateMembers(
+                  squad,
+                  squad.Membership
+                    .filterNot { _.CharId == 0 }
+                    .map { member => SquadAction.Update(member.CharId, member.Health, 0, member.Armor, 0, member.Position, member.ZoneId) }
+                    .toList
+                ))
+              case _ => ;
+            }
+
+          case None => ;
+        }
+
       case msg =>
         log.info(s"Unhandled message $msg from $sender")
     }
@@ -1146,6 +1140,7 @@ class SquadService extends Actor {
           .filterNot { _.CharId == 0 }
           .foreach { member =>
             SquadEvents.publish(SquadServiceResponse(s"/${member.CharId}/Squad", SquadResponse.Join(squad, indices)))
+            InitWaypoints(member.CharId, squad.GUID)
           }
         //fully update for all users
         UpdateSquadDetail(squad.GUID, squad)
@@ -1156,6 +1151,7 @@ class SquadService extends Actor {
         val indices = squad.Membership.zipWithIndex.collect({ case (member, index) if member.CharId != 0 => index }).toList
         SquadEvents.publish(SquadServiceResponse(s"/$charId/Squad", SquadResponse.Join(squad, indices)))
         InitSquadDetail(squad.GUID, Seq(charId), squad)
+        InitWaypoints(charId, squad.GUID)
         //other squad members see new member joining the squad
         val updatedIndex = List(line)
         val otherMembers = squad.Membership.filterNot { member => member.CharId == 0 || member.CharId == charId }.map{ _.CharId }
@@ -1163,7 +1159,7 @@ class SquadService extends Actor {
           SquadEvents.publish(SquadServiceResponse(s"/$member/Squad", SquadResponse.Join(squad, updatedIndex)))
         }
         val details = SquadDetail().Members(List(SquadPositionEntry(line, SquadPositionDetail().CharId(charId).Name(player.Name))))
-        UpdateSquadDetail(squad.GUID, Seq(charId), details)
+        UpdateSquadDetail(squad.GUID, otherMembers, details)
       }
       UpdateSquadListWhenListed(squad, SquadInfo().Size(size))
       true
@@ -1207,7 +1203,6 @@ class SquadService extends Actor {
   }
 
   def CloseOutSquad(squad : Squad, membership : Iterable[(Member, Int)], updateList : List[(Long, Int)]) : Unit = {
-    val leaderCharId = squad.Leader.CharId
     membership.foreach {
       case (member, _) =>
         val charId = member.CharId
@@ -1217,6 +1212,7 @@ class SquadService extends Actor {
         SquadEvents.publish( SquadServiceResponse(s"/$charId/Squad", SquadResponse.Leave(squad, updateList)) )
     }
     idToSquad.remove(squad.GUID)
+    waypoints.remove(squad.GUID)
     UpdateSquadList(squad, None)
   }
 
@@ -1332,9 +1328,70 @@ class SquadService extends Actor {
       toMembers.foreach { charId => SquadEvents.publish(SquadServiceResponse(s"/$charId/Squad", output)) }
     }
   }
+
+  def AddWaypoint(guid : PlanetSideGUID, waypointType : Int, info : WaypointInfo) : Option[WaypointData] = {
+    (waypoints.get(guid) match {
+      case Some(array) =>
+        array
+      case None if idToSquad.get(guid).nonEmpty =>
+        log.debug(s"initializing squad waypoint system for squad #${guid.guid}")
+        val array = Array.fill[WaypointData](5)(new WaypointData())
+        waypoints(guid) = array
+        array
+      case _ =>
+        log.warn(s"squad #${guid.guid} does not currently exist so it can not render waypoints")
+        Array.empty[WaypointData]
+    }).lift(waypointType) match {
+      case Some(point) =>
+        //update the waypoint
+        log.debug(s"rendering squad waypoint $waypointType for squad #${guid.guid}")
+        point.zone_number = info.zone_number
+        point.pos = info.pos
+        Some(point)
+      case _ =>
+        log.warn(s"no squad waypoint $waypointType found")
+        None
+    }
+  }
+
+  def RemoveWaypoint(guid : PlanetSideGUID, waypointType : Int) : Unit = {
+    (waypoints.get(guid) match {
+      case Some(array) =>
+        array
+      case None =>
+        Array.empty[WaypointData]
+    }).lift(waypointType) match {
+      case Some(point) =>
+        //update the waypoint
+        log.debug(s"removing squad waypoint $waypointType for squad #${guid.guid}")
+        point.zone_number = 1
+        point.pos = Vector3.z(1)
+      case _ => ;
+    }
+  }
+
+  def InitWaypoints(toCharId : Long, guid : PlanetSideGUID) : Unit = {
+    (idToSquad.get(guid), waypoints.get(guid)) match {
+      case (Some(squad), Some(list)) =>
+        val vz1 = Vector3.z(1)
+        SquadEvents.publish(
+          SquadServiceResponse(s"/$toCharId/Squad", SquadResponse.InitWaypoints(squad.Leader.CharId,
+            list.zipWithIndex.collect { case (point, index) if point.pos != vz1 =>
+              (index, WaypointInfo(point.zone_number, point.pos), 1)
+            }
+          ))
+        )
+      case _ => ;
+    }
+  }
 }
 
 object SquadService {
+  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
+  }
+
   abstract class Invitation(char_id : Long, name : String) {
     def InviterCharId : Long = char_id
     def InviterName : String = name
diff --git a/common/src/test/scala/game/SquadWaypointEventTest.scala b/common/src/test/scala/game/SquadWaypointEventTest.scala
index 3c2b8bb8..5903a997 100644
--- a/common/src/test/scala/game/SquadWaypointEventTest.scala
+++ b/common/src/test/scala/game/SquadWaypointEventTest.scala
@@ -3,7 +3,7 @@ package game
 
 import org.specs2.mutable._
 import net.psforever.packet._
-import net.psforever.packet.game.{SquadWaypointEvent, WaypointEvent}
+import net.psforever.packet.game.{SquadWaypointEvent, WaypointEventAction, WaypointEvent}
 import net.psforever.types.Vector3
 import scodec.bits._
 
@@ -16,12 +16,12 @@ class SquadWaypointEventTest extends Specification {
   "decode (1)" in {
     PacketCoding.DecodePacket(string_1).require match {
       case SquadWaypointEvent(unk1, unk2, unk3, unk4, unk5, unk6) =>
-        unk1 mustEqual 2
+        unk1 mustEqual WaypointEventAction.Remove
         unk2 mustEqual 11
         unk3 mustEqual 31155863L
         unk4 mustEqual 0
-        unk5 mustEqual None
-        unk6 mustEqual None
+        unk5.isEmpty mustEqual true
+        unk6.isEmpty mustEqual true
       case _ =>
         ko
     }
@@ -30,12 +30,12 @@ class SquadWaypointEventTest extends Specification {
   "decode (2)" in {
     PacketCoding.DecodePacket(string_2).require match {
       case SquadWaypointEvent(unk1, unk2, unk3, unk4, unk5, unk6) =>
-        unk1 mustEqual 2
+        unk1 mustEqual WaypointEventAction.Remove
         unk2 mustEqual 10
         unk3 mustEqual 0L
         unk4 mustEqual 4
-        unk5 mustEqual None
-        unk6 mustEqual None
+        unk5.isEmpty mustEqual true
+        unk6.isEmpty mustEqual true
       case _ =>
         ko
     }
@@ -44,12 +44,12 @@ class SquadWaypointEventTest extends Specification {
   "decode (3)" in {
     PacketCoding.DecodePacket(string_3).require match {
       case SquadWaypointEvent(unk1, unk2, unk3, unk4, unk5, unk6) =>
-        unk1 mustEqual 0
+        unk1 mustEqual WaypointEventAction.Add
         unk2 mustEqual 3
         unk3 mustEqual 41581052L
         unk4 mustEqual 1
-        unk5 mustEqual None
-        unk6 mustEqual Some(WaypointEvent(10, Vector3(3457.9688f, 5514.4688f, 0.0f), 1))
+        unk5.isEmpty mustEqual true
+        unk6.contains( WaypointEvent(10, Vector3(3457.9688f, 5514.4688f, 0.0f), 1) ) mustEqual true
       case _ =>
         ko
     }
@@ -58,40 +58,40 @@ class SquadWaypointEventTest extends Specification {
   "decode (4)" in {
     PacketCoding.DecodePacket(string_4).require match {
       case SquadWaypointEvent(unk1, unk2, unk3, unk4, unk5, unk6) =>
-        unk1 mustEqual 1
+        unk1 mustEqual WaypointEventAction.Unknown1
         unk2 mustEqual 3
         unk3 mustEqual 41581052L
         unk4 mustEqual 1
-        unk5 mustEqual Some(4L)
-        unk6 mustEqual None
+        unk5.contains( 4L ) mustEqual true
+        unk6.isEmpty mustEqual true
       case _ =>
         ko
     }
   }
 
   "encode (1)" in {
-    val msg = SquadWaypointEvent(2, 11, 31155863L, 0)
+    val msg = SquadWaypointEvent.Remove(11, 31155863L, 0)
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_1
   }
 
   "encode (2)" in {
-    val msg = SquadWaypointEvent(2, 10, 0L, 4)
+    val msg = SquadWaypointEvent.Remove(10, 0L, 4)
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_2
   }
 
   "encode (3)" in {
-    val msg = SquadWaypointEvent(0, 3, 41581052L, 1, 10, Vector3(3457.9688f, 5514.4688f, 0.0f), 1)
+    val msg = SquadWaypointEvent.Add(3, 41581052L, 1, WaypointEvent(10, Vector3(3457.9688f, 5514.4688f, 0.0f), 1))
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_3
   }
 
   "encode (4)" in {
-    val msg = SquadWaypointEvent(1, 3, 41581052L, 1, 4L)
+    val msg = SquadWaypointEvent.Unknown1(3, 41581052L, 1, 4L)
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_4
diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala
index 8eadcead..bde61079 100644
--- a/pslogin/src/main/scala/WorldSessionActor.scala
+++ b/pslogin/src/main/scala/WorldSessionActor.scala
@@ -516,15 +516,32 @@ class WorldSessionActor extends Actor with MDCContextAware {
           log.info(s"SquadCards: ${squadUI.map { case(id, card) => s"[${card.index}:${card.name}/$id]" }.mkString}")
 
         case SquadResponse.PromoteMember(squad, char_id, from_index, to_index) =>
-          //promotion will swap visual cards, but we must fix the backend
+          //promotion will swap cards visually, but we must fix the backend
           val id = 11
           sendResponse(SquadMemberEvent.Promote(id, char_id))
           SwapSquadUIElements(squad, from_index, to_index)
           log.info(s"SquadCards: ${squadUI.map { case(id, card) => s"[${card.index}:${card.name}/$id]" }.mkString}")
 
         case SquadResponse.SquadSearchResults() =>
+          //I don't actually know how to return search results
           sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.NoSquadSearchResults()))
 
+        case SquadResponse.InitWaypoints(char_id, waypoints) =>
+          val id = 11
+          StartBundlingPackets()
+          waypoints.foreach { case (waypoint_type, info, unk) =>
+            sendResponse(SquadWaypointEvent.Add(id, char_id, waypoint_type, WaypointEvent(info.zone_number, info.pos, unk)))
+          }
+          StopBundlingPackets()
+
+        case SquadResponse.WaypointEvent(WaypointEventAction.Add, char_id, waypoint_type, _, Some(info), unk) =>
+          val id = 11
+          sendResponse(SquadWaypointEvent.Add(id, char_id, waypoint_type, WaypointEvent(info.zone_number, info.pos, unk)))
+
+        case SquadResponse.WaypointEvent(WaypointEventAction.Remove, char_id, waypoint_type, _, _, _) =>
+          val id = 11
+          sendResponse(SquadWaypointEvent.Remove(id, char_id, waypoint_type))
+
         case _ => ;
       }
 
@@ -4960,12 +4977,16 @@ class WorldSessionActor extends Actor with MDCContextAware {
 
     case msg @ SquadDefinitionActionMessage(u1, u2, action) =>
       log.info(s"SquadDefinitionAction: $msg")
-        squadService ! SquadServiceMessage(player, SquadServiceAction.Definition(player, continent, u1, u2, action))
+        squadService ! SquadServiceMessage(player, SquadServiceAction.Definition(continent, u1, u2, action))
 
     case msg @ SquadMembershipRequest(request_type, unk2, unk3, player_name, unk5) =>
       log.info(s"$msg")
       squadService ! SquadServiceMessage(player, SquadServiceAction.Membership(request_type, unk2, unk3, player_name, unk5))
 
+    case msg @ SquadWaypointRequest(request, _, wtype, unk, info) =>
+      log.info(s"Waypoint Request: $msg")
+      squadService ! SquadServiceMessage(player, SquadServiceAction.Waypoint(request, wtype, unk, info))
+
     case msg @ GenericCollisionMsg(u1, p, t, php, thp, pv, tv, ppos, tpos, u2, u3, u4) =>
       log.info("Ouch! " + msg)
 

From ad4b2590147a48a480105d8c21968b0275ed50b7 Mon Sep 17 00:00:00 2001
From: FateJH <Jason_DiDonato@yahoo.com>
Date: Tue, 20 Aug 2019 23:03:04 -0400
Subject: [PATCH 19/31] Looking For Squad player operations and interaction
 with squad services; evene more conditions to resolve joining/forming a squad

---
 .../scala/net/psforever/objects/Avatar.scala  |  18 ++
 .../scala/net/psforever/objects/Player.scala  |   2 +
 .../converter/AvatarConverter.scala           |   2 +-
 .../psforever/types/SquadRequestType.scala    |   2 +-
 .../services/teamwork/SquadService.scala      | 219 +++++++++++-------
 .../src/main/scala/WorldSessionActor.scala    |  46 +++-
 6 files changed, 198 insertions(+), 91 deletions(-)

diff --git a/common/src/main/scala/net/psforever/objects/Avatar.scala b/common/src/main/scala/net/psforever/objects/Avatar.scala
index ceae122b..0416b6a9 100644
--- a/common/src/main/scala/net/psforever/objects/Avatar.scala
+++ b/common/src/main/scala/net/psforever/objects/Avatar.scala
@@ -46,6 +46,17 @@ class Avatar(private val char_id : Long, val name : String, val faction : Planet
   }
 
   private val deployables : DeployableToolbox = new DeployableToolbox
+  /**
+    * Looking For Squad:<br>
+    * Used to indicate both the marque that appears underneath a player's nameplate and an actual player state<br>
+    * This `Avatar`-specific "Looking for Squad" variable
+    * is used to indicate the active state of the LFS marque in the game
+    * and will change to `false` if the player is the member of a squad.
+    * A client-local version of "Looking for Squad" will maintain the real state of LFS
+    * once the player has joined a squad.
+    * The client-local version will restore the `Avatar`-local variable upon leaving the squad.
+    */
+  private var lfs : Boolean = false
 
   def CharId : Long = char_id
 
@@ -184,6 +195,13 @@ class Avatar(private val char_id : Long, val name : String, val faction : Planet
 
   def Deployables : DeployableToolbox = deployables
 
+  def LFS : Boolean = lfs
+
+  def LFS_=(looking : Boolean) : Boolean = {
+    lfs = looking
+    LFS
+  }
+
   def Definition : AvatarDefinition = GlobalDefinitions.avatar
 
   /*
diff --git a/common/src/main/scala/net/psforever/objects/Player.scala b/common/src/main/scala/net/psforever/objects/Player.scala
index 7577b545..5ab70f6a 100644
--- a/common/src/main/scala/net/psforever/objects/Player.scala
+++ b/common/src/main/scala/net/psforever/objects/Player.scala
@@ -70,6 +70,8 @@ class Player(private val core : Avatar) extends PlanetSideGameObject
 
   def Voice : CharacterVoice.Value = core.voice
 
+  def LFS : Boolean = core.LFS
+
   def isAlive : Boolean = alive
 
   def isBackpack : Boolean = backpack
diff --git a/common/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala
index bbd9f1f5..828a434c 100644
--- a/common/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala
+++ b/common/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala
@@ -98,7 +98,7 @@ object AvatarConverter {
       false,
       facingPitch = obj.Orientation.y,
       facingYawUpper = obj.FacingYawUpper,
-      lfs = true,
+      obj.LFS,
       GrenadeState.None,
       obj.Cloaked,
       false,
diff --git a/common/src/main/scala/net/psforever/types/SquadRequestType.scala b/common/src/main/scala/net/psforever/types/SquadRequestType.scala
index 1b22bc38..a9c35f7a 100644
--- a/common/src/main/scala/net/psforever/types/SquadRequestType.scala
+++ b/common/src/main/scala/net/psforever/types/SquadRequestType.scala
@@ -8,7 +8,7 @@ object SquadRequestType extends Enumeration {
   type Type = Value
   val
   Invite,
-  Unk01,
+  ProximityInvite,
   Accept,
   Reject,
   Cancel,
diff --git a/common/src/main/scala/services/teamwork/SquadService.scala b/common/src/main/scala/services/teamwork/SquadService.scala
index 5a439432..5bbd895c 100644
--- a/common/src/main/scala/services/teamwork/SquadService.scala
+++ b/common/src/main/scala/services/teamwork/SquadService.scala
@@ -266,14 +266,41 @@ class SquadService extends Actor {
 //          case _ => ;
 //        }
         (memberToSquad.get(invitingPlayer), memberToSquad.get(invitedPlayer)) match {
-          case (Some(squad1), Some(squad2)) =>
-            //both players are in squads
-            if(squad1.GUID == squad2.GUID) {
-              //both players are in the same squad; no need to do anything
+          case (Some(squad1), Some(squad2))
+            if squad1.GUID == squad2.GUID =>
+          //both players are in the same squad; no need to do anything
+
+          case (Some(squad1), Some(squad2))
+            if squad1.Leader.CharId == invitingPlayer && squad2.Leader.CharId == invitedPlayer &&
+              squad1.Size > 1 && squad2.Size > 1 =>
+          //we might do some platoon chicanery with this case later
+          // TODO platoons
+
+          case (Some(squad1), Some(squad2)) if squad2.Size == 1 =>
+          //both players belong to squads, but the invitedplayer's squad is underutilized by comparison
+          //treat the same as "the classic situation" using squad1
+            log.info(s"$invitedPlayer has been invited to squad ${squad1.Task} by $invitingPlayer")
+            val charId = tplayer.CharId
+            val bid = VacancyInvite(charId, tplayer.Name, squad1.GUID)
+            AddInvite(invitedPlayer, bid) match {
+              case out @ Some(_) if out.contains(bid) =>
+                SquadEvents.publish(SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, charId, Some(invitedPlayer), tplayer.Name, false, Some(None))))
+                SquadEvents.publish(SquadServiceResponse(s"/$charId/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, invitedPlayer, Some(charId), tplayer.Name, true, Some(None))))
+              case Some(_) =>
+                SquadEvents.publish(SquadServiceResponse(s"/$charId/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, invitedPlayer, Some(charId), tplayer.Name, true, Some(None))))
+              case _ => ;
             }
-            else {
-              //we might do some platoon chicanery with this case later
-              //TODO platoons
+
+          case (Some(squad1), Some(squad2)) if squad1.Size == 1 =>
+          //both players belong to squads, but the invitingplayer's squad is underutilized by comparison
+          //treat the same as "indirection ..." using squad2
+            val leaderCharId = squad2.Leader.CharId
+            val bid = IndirectVacancy(tplayer, squad2.GUID)
+            log.warn(s"$invitedPlayer has asked $invitingPlayer for an invitation to squad ${squad2.Task}, but the squad leader needs to approve")
+            AddInvite(leaderCharId, bid) match {
+              case out @ Some(_) if out.contains(bid) =>
+                HandleBidForPosition(bid, tplayer)
+              case _ => ;
             }
 
           case (Some(squad), None) =>
@@ -314,79 +341,76 @@ class SquadService extends Actor {
               case _ => ;
             }
 
-          case _ => ;
+          case _ => //
         }
 
       case SquadAction.Membership(SquadRequestType.Accept, invitedPlayer, _, _, _) =>
         val acceptedInvite = RemoveInvite(invitedPlayer)
-        acceptedInvite match {
-          case Some(BidForPosition(petitioner, guid, position)) if idToSquad.get(guid).nonEmpty =>
-            //player requested to join a squad's specific position
-            //invitedPlayer is actually the squad leader; petitioner is the actual "invitedPlayer"
-            if(memberToSquad.get(petitioner.CharId).isEmpty) {
+        if(EnsureEmptySquad(invitedPlayer, "Accept: the invited player is already a member of a squad and can not join a second one")) {
+          acceptedInvite match {
+            case Some(BidForPosition(petitioner, guid, position)) if idToSquad.get(guid).nonEmpty =>
+              //player requested to join a squad's specific position
+              //invitedPlayer is actually the squad leader; petitioner is the actual "invitedPlayer"
               JoinSquad(petitioner, idToSquad(guid), position)
-            }
-            else {
-              log.warn("Accept->Bid: the invited player is already a member of a squad and can not join a second one")
-            }
 
-          case Some(IndirectVacancy(recruit, guid)) =>
-            //tplayer / invitedPlayer is actually the squad leader
-            val recuitCharId = recruit.CharId
-            HandleVacancyInvite(guid, recuitCharId, invitedPlayer, recruit) match {
-              case Some((squad, line)) =>
-                SquadEvents.publish(SquadServiceResponse(s"/$recuitCharId/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitedPlayer, Some(recuitCharId), "", true, Some(None))))
-                JoinSquad(recruit, squad, line)
+            case Some(IndirectVacancy(recruit, guid)) =>
+              //tplayer / invitedPlayer is actually the squad leader
+              val recruitCharId = recruit.CharId
+              HandleVacancyInvite(guid, recruitCharId, invitedPlayer, recruit) match {
+                case Some((squad, line)) =>
+                  SquadEvents.publish(SquadServiceResponse(s"/$recruitCharId/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitedPlayer, Some(recruitCharId), "", true, Some(None))))
+                  JoinSquad(recruit, squad, line)
                 //since we are the squad leader, we do not want to brush off our queued squad invite tasks
-              case _ => ;
-            }
+                case _ => ;
+              }
 
-          case Some(VacancyInvite(invitingPlayer, _, guid)) =>
-            //accepted an invitation to join an existing squad
-            HandleVacancyInvite(guid, invitedPlayer, invitingPlayer, tplayer) match {
-              case Some((squad, line)) =>
-                SquadEvents.publish(SquadServiceResponse(s"/$invitingPlayer/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitingPlayer, Some(invitedPlayer), tplayer.Name, false, Some(None))))
-                SquadEvents.publish(SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitedPlayer, Some(invitingPlayer), "", true, Some(None))))
-                JoinSquad(tplayer, squad, line)
-                RemoveQueuedInvites(invitedPlayer) //TODO deal with these somehow
-              case _ => ;
-            }
+            case Some(VacancyInvite(invitingPlayer, _, guid)) =>
+              //accepted an invitation to join an existing squad
+              HandleVacancyInvite(guid, invitedPlayer, invitingPlayer, tplayer) match {
+                case Some((squad, line)) =>
+                  SquadEvents.publish(SquadServiceResponse(s"/$invitingPlayer/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitingPlayer, Some(invitedPlayer), tplayer.Name, false, Some(None))))
+                  SquadEvents.publish(SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitedPlayer, Some(invitingPlayer), "", true, Some(None))))
+                  JoinSquad(tplayer, squad, line)
+                  RemoveQueuedInvites(invitedPlayer) //TODO deal with these somehow
+                case _ => ;
+              }
 
-          case Some(SpontaneousInvite(invitingPlayer)) =>
-            //originally, we were invited by someone into a new squad they would form
-            val invitingPlayerCharId = invitingPlayer.CharId
-            (GetParticipatingSquad(invitingPlayer) match {
-              case Some(participating) =>
-                //invitingPlayer became part of a squad while invited player was answering the original summons
-                Some(participating)
-              case _ =>
-                //generate a new squad, with invitingPlayer as the leader
-                val squad = StartSquad(invitingPlayer)
-                squad.Task = s"${invitingPlayer.Name}'s Squad"
-                SquadEvents.publish( SquadServiceResponse(s"/$invitingPlayerCharId/Squad", SquadResponse.AssociateWithSquad(squad.GUID)) )
-                Some(squad)
-            }) match {
-              case Some(squad) =>
-                HandleVacancyInvite(squad.GUID, tplayer.CharId, invitingPlayerCharId, tplayer) match {
-                  case Some((_, line)) =>
-                    SquadEvents.publish( SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitedPlayer, Some(invitingPlayerCharId), "", true, Some(None))) )
-                    JoinSquad(tplayer, squad, line)
-                    SquadEvents.publish( SquadServiceResponse(s"/$invitingPlayerCharId/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitingPlayerCharId, Some(invitedPlayer), tplayer.Name, false, Some(None))) )
-                    RemoveQueuedInvites(tplayer.CharId) //TODO deal with these somehow
-                  case _ => ;
-                }
-              case _ => ;
-            }
+            case Some(SpontaneousInvite(invitingPlayer)) =>
+              //originally, we were invited by someone into a new squad they would form
+              val invitingPlayerCharId = invitingPlayer.CharId
+              (GetParticipatingSquad(invitingPlayer) match {
+                case Some(participating) =>
+                  //invitingPlayer became part of a squad while invited player was answering the original summons
+                  Some(participating)
+                case _ =>
+                  //generate a new squad, with invitingPlayer as the leader
+                  val squad = StartSquad(invitingPlayer)
+                  squad.Task = s"${invitingPlayer.Name}'s Squad"
+                  SquadEvents.publish( SquadServiceResponse(s"/$invitingPlayerCharId/Squad", SquadResponse.AssociateWithSquad(squad.GUID)) )
+                  Some(squad)
+              }) match {
+                case Some(squad) =>
+                  HandleVacancyInvite(squad.GUID, tplayer.CharId, invitingPlayerCharId, tplayer) match {
+                    case Some((_, line)) =>
+                      SquadEvents.publish( SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitedPlayer, Some(invitingPlayerCharId), "", true, Some(None))) )
+                      JoinSquad(tplayer, squad, line)
+                      SquadEvents.publish( SquadServiceResponse(s"/$invitingPlayerCharId/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitingPlayerCharId, Some(invitedPlayer), tplayer.Name, false, Some(None))) )
+                      RemoveQueuedInvites(tplayer.CharId) //TODO deal with these somehow
+                    case _ => ;
+                  }
+                case _ => ;
+              }
 
-          case None =>
-            //the invite either timed-out or was withdrawn; select a new one?
-            NextInvite(invitedPlayer) match {
-              case Some(bid : BidForPosition) if !acceptedInvite.contains(bid) =>
-                HandleBidForPosition(bid, tplayer)
-              case Some(bid) if !acceptedInvite.contains(bid) =>
-                SquadEvents.publish(SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, bid.InviterCharId, Some(invitedPlayer), bid.InviterName, false, Some(None))))
-              case None => ;
-            }
+            case None =>
+              //the invite either timed-out or was withdrawn; select a new one?
+              NextInvite(invitedPlayer) match {
+                case Some(bid : BidForPosition) if !acceptedInvite.contains(bid) =>
+                  HandleBidForPosition(bid, tplayer)
+                case Some(bid) if !acceptedInvite.contains(bid) =>
+                  SquadEvents.publish(SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, bid.InviterCharId, Some(invitedPlayer), bid.InviterName, false, Some(None))))
+                case None => ;
+              }
+          }
         }
 
       case SquadAction.Membership(SquadRequestType.Leave, leavingPlayer, optionalPlayer, _, _) =>
@@ -712,24 +736,32 @@ class SquadService extends Actor {
             }
 
           case ResetAll() =>
-            val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
-            squad.Task = ""
-            squad.ZoneId = None
-            squad.Availability.indices.foreach { i =>
-              squad.Availability.update(i, true)
+            lSquadOpt match {
+              case Some(squad) if squad.Size > 1 =>
+                squad.Task = ""
+                squad.ZoneId = None
+                squad.Availability.indices.foreach { i =>
+                  squad.Availability.update(i, true)
+                }
+                squad.Membership.foreach(position => {
+                  position.Role = ""
+                  position.Orders = ""
+                  position.Requirements = Set()
+                })
+                squad.LocationFollowsSquadLead = false
+                squad.AutoApproveInvitationRequests = false
+                UpdateSquadListWhenListed(squad, SquadInfo().Task("").ZoneId(None).Capacity(squad.Capacity))
+                UpdateSquadDetail(squad.GUID, squad)
+                sender ! SquadServiceResponse("", SquadResponse.AssociateWithSquad(PlanetSideGUID(0)))
+                if(!initialAssociation.contains(squad.GUID)) {
+                  initialAssociation += squad.GUID
+                }
+                //do not unlist an already listed squad
+              case Some(squad) =>
+                //underutilized squad; just close it out
+                CloseOutSquad(squad)
+              case _ => ;
             }
-            squad.Membership.foreach(position => {
-              position.Role = ""
-              position.Orders = ""
-              position.Requirements = Set()
-            })
-            squad.LocationFollowsSquadLead = false
-            squad.AutoApproveInvitationRequests = false
-            UpdateSquadListWhenListed(squad, SquadInfo().Task("").ZoneId(None).Capacity(squad.Capacity))
-            UpdateSquadDetail(squad.GUID, squad)
-            sender ! SquadServiceResponse("", SquadResponse.AssociateWithSquad(PlanetSideGUID(0)))
-            initialAssociation += squad.GUID
-            //do not unlist an already listed squad
 
           case _ =>
         }
@@ -1193,6 +1225,19 @@ class SquadService extends Actor {
     }
   }
 
+  def EnsureEmptySquad(char_id : Long, msg : String = "default warning message") : Boolean = {
+    memberToSquad.get(char_id) match {
+      case None =>
+        true
+      case Some(squad) if squad.Size == 1 =>
+        CloseOutSquad(squad)
+        true
+      case _ =>
+        log.warn(msg)
+        false
+    }
+  }
+
   def CloseOutSquad(squad : Squad) : Unit = {
     val membership = squad.Membership.zipWithIndex
     CloseOutSquad(
diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala
index bde61079..3fe21933 100644
--- a/pslogin/src/main/scala/WorldSessionActor.scala
+++ b/pslogin/src/main/scala/WorldSessionActor.scala
@@ -118,6 +118,12 @@ class WorldSessionActor extends Actor with MDCContextAware {
     */
   var interstellarFerryTopLevelGUID : Option[PlanetSideGUID] = None
   val squadUI : LongMap[SquadUIElement] = new LongMap[SquadUIElement]()
+  /**
+    * `AvatarConverter` can only rely on the `Avatar`-local Looking For Squad variable.
+    * When joining or creating a squad, the original state of the avatar's LFS variable is stored here.
+    * Upon leaving or disbanding a squad, this value is restored to the avatar's LFS variable.
+    */
+  var lfs : Boolean = false
 
   var amsSpawnPoints : List[SpawnPoint] = Nil
   var clientKeepAlive : Cancellable = DefaultCancellable.obj
@@ -160,6 +166,10 @@ class WorldSessionActor extends Actor with MDCContextAware {
           .collect { case ((index, Some(obj))) => InventoryItem(obj, index) }
           ) ++ player.Inventory.Items)
         .filterNot({ case InventoryItem(obj, _) => obj.isInstanceOf[BoomerTrigger] || obj.isInstanceOf[Telepad] })
+      //put any temporary value back into the avatar
+      if(squadUI.nonEmpty) {
+        avatar.LFS = lfs
+      }
       //TODO final character save before doing any of this (use equipment)
       continent.Population ! Zone.Population.Release(avatar)
       if(player.isAlive) {
@@ -437,6 +447,12 @@ class WorldSessionActor extends Actor with MDCContextAware {
               sendResponse(PlanetsideAttributeMessage(player.GUID, 32, ourIndex)) //associate with member position in squad?
               //a finalization? what does this do?
               sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(18)))
+              //lfs state management (always OFF)
+              if(avatar.LFS) {
+                lfs = avatar.LFS
+                avatar.LFS = false
+                sendResponse(PlanetsideAttributeMessage(player.GUID, 53, 0))
+              }
             case _ =>
               //other player is joining our squad
               //load each member's entry
@@ -471,6 +487,12 @@ class WorldSessionActor extends Actor with MDCContextAware {
               sendResponse(PlanetsideAttributeMessage(player.GUID, 34, 4294967295L)) //unknown, perhaps unrelated?
               //a finalization? what does this do?
               sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(18)))
+              //lfs state management (maybe ON)
+              if(lfs) {
+                avatar.LFS = lfs
+                sendResponse(PlanetsideAttributeMessage(player.GUID, 53, 1))
+                lfs = false
+              }
             case _ =>
               //remove each member's entry
               positionsToUpdate.foreach { case(member, index) =>
@@ -3011,7 +3033,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
     sendResponse(SetChatFilterMessage(ChatChannel.Local, false, ChatChannel.values.toList)) //TODO will not always be "on" like this
     deadState = DeadState.Alive
     sendResponse(AvatarDeadStateMessage(DeadState.Alive, 0, 0, tplayer.Position, player.Faction, true))
-    sendResponse(PlanetsideAttributeMessage(guid, 53, 1))
+    sendResponse(PlanetsideAttributeMessage(guid, 53, tplayer.LFS))
     sendResponse(AvatarSearchCriteriaMessage(guid, List(0, 0, 0, 0, 0, 0)))
     (1 to 73).foreach(i => {
       // not all GUID's are set, and not all of the set ones will always be zero; what does this section do?
@@ -4663,7 +4685,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
             log.info(s"GenericObject: $player is MAX with an unexpected weapon - ${definition.Name}")
         }
       }
-      else if(action == 16) {
+      else if(action == 16) { //max deployment
         log.info(s"GenericObject: $player has released the anchors")
         player.UsingSpecial = SpecialExoSuitDefinition.Mode.Normal
         avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(player.GUID, 19, 0))
@@ -4681,6 +4703,26 @@ class WorldSessionActor extends Actor with MDCContextAware {
             log.info(s"GenericObject: $player is MAX with an unexpected weapon - ${definition.Name}")
         }
       }
+      else if(action == 37) { //Looking For Squad OFF
+        if(squadUI.nonEmpty) {
+          lfs = false
+        }
+        else if(avatar.LFS) {
+          avatar.LFS = false
+          avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(player.GUID, 53, 0))
+          log.info(s"GenericObject: ${player.Name} is no longer looking for a squad to join")
+        }
+      }
+      else if(action == 36) { //Looking For Squad ON
+        if(squadUI.nonEmpty) {
+          lfs = true
+        }
+        else if(!avatar.LFS) {
+          avatar.LFS = true
+          avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(player.GUID, 53, 1))
+          log.info(s"GenericObject: ${player.Name} has made himself available to join a squad")
+        }
+      }
 
     case msg @ ItemTransactionMessage(terminal_guid, transaction_type, _, _, _, _) =>
       log.info("ItemTransaction: " + msg)

From 3e631657b8a6ee308eaeef97ffc85640e646f7f0 Mon Sep 17 00:00:00 2001
From: FateJH <Jason_DiDonato@yahoo.com>
Date: Sun, 8 Sep 2019 15:00:12 -0400
Subject: [PATCH 20/31] significant refactoring in SquadService operations;
 properly force the player to leave any participating squad when WSA stops

---
 .../scala/services/teamwork/SquadAction.scala |    2 +-
 .../services/teamwork/SquadService.scala      | 1604 ++++++++++++-----
 .../teamwork/SquadServiceMessage.scala        |    3 +-
 .../game/SquadMembershipResponseTest.scala    |   70 +-
 .../src/main/scala/WorldSessionActor.scala    |    9 +-
 5 files changed, 1189 insertions(+), 499 deletions(-)

diff --git a/common/src/main/scala/services/teamwork/SquadAction.scala b/common/src/main/scala/services/teamwork/SquadAction.scala
index d5da028e..5b3ed90e 100644
--- a/common/src/main/scala/services/teamwork/SquadAction.scala
+++ b/common/src/main/scala/services/teamwork/SquadAction.scala
@@ -8,7 +8,7 @@ import net.psforever.types.{SquadRequestType, Vector3}
 object SquadAction {
   trait Action
 
-  final case class Definition(zone : Zone, guid : PlanetSideGUID, line : Int, action : SquadAction) extends Action
+  final case class Definition(guid : PlanetSideGUID, line : Int, action : SquadAction) extends Action
   final case class Membership(request_type : SquadRequestType.Value, unk2 : Long, unk3 : Option[Long], player_name : String, unk5 : Option[Option[String]]) extends Action
   final case class Waypoint(event_type : WaypointEventAction.Value, waypoint_type : Int, unk : Option[Long], waypoint_info : Option[WaypointInfo]) extends Action
   final case class Update(char_id : Long, health : Int, max_health : Int, armor : Int, max_armor : Int, pos : Vector3, zone_number : Int) extends Action
diff --git a/common/src/main/scala/services/teamwork/SquadService.scala b/common/src/main/scala/services/teamwork/SquadService.scala
index 5bbd895c..2c22886c 100644
--- a/common/src/main/scala/services/teamwork/SquadService.scala
+++ b/common/src/main/scala/services/teamwork/SquadService.scala
@@ -1,11 +1,12 @@
 // Copyright (c) 2019 PSForever
 package services.teamwork
 
-import akka.actor.{Actor, ActorRef, Props}
+import akka.actor.{Actor, ActorContext, ActorRef, Props}
 import net.psforever.objects.Player
 import net.psforever.objects.definition.converter.StatConverter
 import net.psforever.objects.loadouts.SquadLoadout
 import net.psforever.objects.teamwork.{Member, Squad}
+import net.psforever.objects.zones.Zone
 import net.psforever.packet.game._
 import net.psforever.types._
 import services.{GenericEventBus, Service}
@@ -19,73 +20,35 @@ import scala.collection.mutable.ListBuffer
 class SquadService extends Actor {
   import SquadService._
 
-  private var memberToSquad : mutable.LongMap[Squad] = mutable.LongMap[Squad]()
-  private var idToSquad : TrieMap[PlanetSideGUID, Squad] = new TrieMap[PlanetSideGUID, Squad]()
-  private var idToSwitchboard : TrieMap[PlanetSideGUID, ActorRef] = new TrieMap[PlanetSideGUID, ActorRef]()
+  /**
+    * 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.
+    */
   private var sid : Int = 1
+
+  private var memberToSquad : mutable.LongMap[Squad] = mutable.LongMap[Squad]()
+  private val invites : mutable.LongMap[Invitation] = mutable.LongMap[Invitation]()
+  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 invite is queued).
+    * Cleared when the next queued invite becomes active.
+    */
+  private val previousInvites : mutable.LongMap[Invitation] = mutable.LongMap[Invitation]()
+
+  private var squadFeatures : TrieMap[PlanetSideGUID, SquadService.SquadFeatures] = new TrieMap[PlanetSideGUID, SquadService.SquadFeatures]()
   private val publishedLists : TrieMap[PlanetSideEmpire.Value, ListBuffer[SquadInfo]] = TrieMap[PlanetSideEmpire.Value, ListBuffer[SquadInfo]](
     PlanetSideEmpire.TR -> ListBuffer.empty,
     PlanetSideEmpire.NC -> ListBuffer.empty,
     PlanetSideEmpire.VS -> ListBuffer.empty
   )
-  private val invites : mutable.LongMap[Invitation] = mutable.LongMap[Invitation]()
-  /**
-    * `initialAssociation` per squad is similar to "Does this squad want to recruit members?"
-    * The squad does not have to be listed.
-    * Dispatches an `AssociateWithSquad` `SDAM` to the squad leader and ???
-    * and then a `SDDUM` that includes at least the squad owner name and char id
-    * when a squad entry is removed from the list.
-    * Dispatched only once when a squad is first listed
-    * or when the squad leader searches for recruits by proximity or for certain roles or by invite
-    * or when a spontaneous squad forms,
-    * whichever happens first.
-    * Additionally, the packets are also sent when the check is made when the continent is changed (or set).
-    */
-  private val initialAssociation : ListBuffer[PlanetSideGUID] = new ListBuffer[PlanetSideGUID]()
-  private val queuedInvites : mutable.LongMap[List[Invitation]] = mutable.LongMap[List[Invitation]]()
-  private val waypoints : TrieMap[PlanetSideGUID, Array[WaypointData]] =
-    new TrieMap[PlanetSideGUID, Array[WaypointData]]()
-  private val viewDetails : mutable.LongMap[PlanetSideGUID] = mutable.LongMap[PlanetSideGUID]()
 
   private [this] val log = org.log4s.getLogger
 
   override def preStart : Unit = {
     log.info("Starting...")
-
-//    val testSquad = new Squad(PlanetSideGUID(3), PlanetSideEmpire.VS)
-//    testSquad.Task = "\\#66CCFF Sentinels of Auraxis\\#FFFFFF ... \\#40FF40 Squad Up!!"
-//    testSquad.ZoneId = 5
-//    testSquad.Membership(0).Name = "Wizkid45"
-//    testSquad.Membership(0).Role = "Fearless Leader"
-//    testSquad.Membership(0).CharId = 30910985L
-//    testSquad.Membership(0).ZoneId = 5
-//    testSquad.Membership(0).Health = 64
-//    testSquad.Membership(0).Armor = 34
-//    testSquad.Membership(0).Position = Vector3(5526.5234f, 3818.7344f, 54.59375f)
-//    testSquad.Membership(1).Name = "xoBLADEox"
-//    testSquad.Membership(1).Role = "Right Hand"
-//    testSquad.Membership(1).CharId = 42781919L
-//    testSquad.Membership(1).ZoneId = 5
-//    testSquad.Membership(1).Health = 54
-//    testSquad.Membership(1).Armor = 44
-//    testSquad.Membership(1).Position = Vector3(4673.5312f, 2604.8047f, 40.015625f)
-//    testSquad.Membership(3).Name = "cabal0428"
-//    testSquad.Membership(3).Role = "Left Hand"
-//    testSquad.Membership(3).CharId = 353380L
-//    testSquad.Membership(3).ZoneId = 5
-//    testSquad.Membership(3).Health = 44
-//    testSquad.Membership(3).Armor = 54
-//    testSquad.Membership(3).Position = Vector3(4727.492f, 2613.5312f, 51.390625f)
-//    testSquad.Membership(4).Name = "xSkiku"
-//    testSquad.Membership(4).Role = "Right Foot's Middle Toe's Nail"
-//    testSquad.Membership(4).CharId = 41588340L
-//    testSquad.Membership(4).ZoneId = 5
-//    testSquad.Membership(4).Health = 34
-//    testSquad.Membership(4).Armor = 64
-//    testSquad.Membership(4).Position = Vector3(3675.0f, 4789.8047f, 63.21875f)
-//    idToSquad(PlanetSideGUID(3)) = testSquad
-//    testSquad.Listed = true
-//    UpdateSquadList(testSquad, None)
   }
 
   def GetNextSquadId() : PlanetSideGUID = {
@@ -101,7 +64,7 @@ class SquadService extends Actor {
   }
 
   def TryResetSquadId() : Boolean = {
-    if(idToSquad.isEmpty) {
+    if(squadFeatures.isEmpty) {
       sid = 1
       true
     }
@@ -110,6 +73,13 @@ class SquadService extends Actor {
     }
   }
 
+  def GetSquad(id : PlanetSideGUID) : Option[Squad] = {
+    squadFeatures.get(id) match {
+      case Some(features) => Some(features.Squad)
+      case None => None
+    }
+  }
+
   def GetParticipatingSquad(player : Player) : Option[Squad] = {
     GetParticipatingSquad(player.CharId)
   }
@@ -159,7 +129,6 @@ class SquadService extends Actor {
   }
 
   def CreateSquad(player : Player) : Squad = {
-    val charId = player.CharId
     val faction = player.Faction
     val name = player.Name
     val squad = new Squad(GetNextSquadId(), faction)
@@ -170,18 +139,13 @@ class SquadService extends Actor {
     leadPosition.Armor = player.Armor
     leadPosition.Position = player.Position
     leadPosition.ZoneId = 1
-    log.info(s"$name-$faction has started a new squad")
+    log.info(s"$name-$faction has created a new squad")
     squad
   }
 
   def StartSquad(squad : Squad) : Squad = {
-    val charId = squad.Leader.CharId
-    val id = squad.GUID
-    val switchboard = context.actorOf(Props[SquadSwitchboard], s"squad${id.guid}")
-    memberToSquad += charId -> squad
-    idToSquad += id -> squad
-    idToSwitchboard += id -> switchboard
-    initialAssociation += squad.GUID
+    squadFeatures += squad.GUID -> new SquadService.SquadFeatures(squad).Start
+    memberToSquad += squad.Leader.CharId -> squad
     squad
   }
 
@@ -203,214 +167,334 @@ class SquadService extends Actor {
       //send initial squad catalog
       sender ! SquadServiceResponse(s"/$faction/Squad", SquadResponse.InitList(publishedLists(PlanetSideEmpire(faction)).toVector))
 
-    //subscribe to the player's personal channel - necessary only to inform about any previous squad association
+    //subscribe to the player's personal channel - necessary for future and previous squad information
     case Service.Join(char_id) =>
       val path = s"/$char_id/Squad"
       val who = sender()
       log.info(s"$who has joined $path")
-      SquadEvents.subscribe(who, path) //TODO squad-specific switchboard
+      SquadEvents.subscribe(who, path)
       //check for renewable squad information
-      memberToSquad.get(char_id.toLong) match {
-        case None => ;
-        case Some(_) =>
-          sender ! SquadServiceMessage.RecoverSquadMembership() //TODO?
-      }
-
-    case Service.Leave(Some(char_id)) => ;
-      SquadEvents.unsubscribe(sender())
       val longCharId = char_id.toLong
       memberToSquad.get(longCharId) match {
-        case Some(squad) =>
-          if(squad.Leader.Name.equals(char_id)) {
-            //we were the leader
-            if(squad.Membership.count(p => p.Name.equals("")) > 1) {
-              //other players were in the squad; publicly disband it
-              squad.Membership.foreach(position => {
-                position.Name = ""
-                position.CharId = 0L
-                position.ZoneId = 0
-                position.Position = Vector3.Zero
-                position.Health = 0
-                position.Armor = 0
-              })
-            }
-            memberToSquad.remove(longCharId)
-            idToSquad.remove(squad.GUID)
-            UpdateSquadList(squad, None)
-          }
-          else {
-            //we were just a grunt in the squad
-            val position = squad.Membership.find(_.CharId == longCharId).get
-            position.Name = ""
-            position.ZoneId = 0
-            position.Position = Vector3.Zero
-            position.Health = 0
-            position.Armor = 0
-            UpdateSquadDetail(squad.GUID, squad)
-          }
         case None => ;
+        case Some(squad) =>
+          val guid = squad.GUID
+          val indices = squad.Membership.zipWithIndex.collect({ case (member, index) if member.CharId != 0 => index }).toList
+          SquadEvents.publish(SquadServiceResponse(s"/$char_id/Squad", SquadResponse.AssociateWithSquad(guid)))
+          SquadEvents.publish(SquadServiceResponse(s"/$char_id/Squad", SquadResponse.Join(squad, indices)))
+          InitSquadDetail(guid, Seq(longCharId), squad)
+          InitWaypoints(longCharId, guid)
       }
-    //TODO leave squad, if joined to one, and perform clean-up
+
+    case Service.Leave(Some(char_id)) =>
+      SquadEvents.unsubscribe(sender())
+      val longCharId = char_id.toLong
+      val pSquadOpt = GetParticipatingSquad(longCharId)
+      (pSquadOpt, GetLeadingSquad(longCharId, pSquadOpt)) match {
+        case (Some(_), Some(squad)) =>
+          //leader of a squad; the squad will be disbanded
+          DisbandSquad(squad)
+        case (Some(squad), None) if squad.Size == 2 =>
+          //one of the last two members of a squad; the squad will be disbanded
+          DisbandSquad(squad)
+        case (Some(squad), None) =>
+          //member of the squad; leave the squad
+          LeaveSquad(longCharId, squad)
+        case _ =>
+          //not a member of any squad; nothing to do here
+      }
+      CleanupInvitesFromPlayer(longCharId)
 
     case Service.Leave(None) | Service.LeaveAll() => ;
 
-    case SquadServiceMessage(tplayer, squad_action) => squad_action match {
+    case SquadServiceMessage(tplayer, zone, squad_action) => squad_action match {
       case SquadAction.Membership(SquadRequestType.Invite, invitingPlayer, Some(invitedPlayer), _, _) =>
         //this is just busy work; for actual joining operations, see SquadRequestType.Accept
-        //for the purposes of this code, tplayer.CharId == invitingPlayer
-//        FindBid(invitingPlayer, invitedPlayer) match {
-//          case Some(bid) =>
-//            //invitingPlayer and invitedPlayer have both tried to join each others's squads
-//            //treat this junction as consent
-//            self ! SquadServiceMessage(tplayer, SquadAction.Membership(SquadRequestType.Accept, invitingPlayer, Some(invitedPlayer), "", None))
-//          case _ => ;
-//        }
         (memberToSquad.get(invitingPlayer), memberToSquad.get(invitedPlayer)) match {
           case (Some(squad1), Some(squad2))
             if squad1.GUID == squad2.GUID =>
-          //both players are in the same squad; no need to do anything
+            //both players are in the same squad; no need to do anything
 
           case (Some(squad1), Some(squad2))
             if squad1.Leader.CharId == invitingPlayer && squad2.Leader.CharId == invitedPlayer &&
               squad1.Size > 1 && squad2.Size > 1 =>
-          //we might do some platoon chicanery with this case later
-          // TODO platoons
+            //we might do some platoon chicanery with this case later
+            //TODO platoons
 
-          case (Some(squad1), Some(squad2)) if squad2.Size == 1 =>
-          //both players belong to squads, but the invitedplayer's squad is underutilized by comparison
-          //treat the same as "the classic situation" using squad1
+          case (Some(squad1), Some(squad2))
+            if squad2.Size == 1 && !squadFeatures(squad1.GUID).Refuse.contains(invitedPlayer) =>
+            //both players belong to squads, but the invitedplayer's squad (squad2) is underutilized by comparison
+            //treat the same as "the classic situation" using squad1
             log.info(s"$invitedPlayer has been invited to squad ${squad1.Task} by $invitingPlayer")
             val charId = tplayer.CharId
-            val bid = VacancyInvite(charId, tplayer.Name, squad1.GUID)
-            AddInvite(invitedPlayer, bid) match {
-              case out @ Some(_) if out.contains(bid) =>
-                SquadEvents.publish(SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, charId, Some(invitedPlayer), tplayer.Name, false, Some(None))))
-                SquadEvents.publish(SquadServiceResponse(s"/$charId/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, invitedPlayer, Some(charId), tplayer.Name, true, Some(None))))
-              case Some(_) =>
-                SquadEvents.publish(SquadServiceResponse(s"/$charId/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, invitedPlayer, Some(charId), tplayer.Name, true, Some(None))))
-              case _ => ;
-            }
+            AddInviteAndRespond(
+              invitedPlayer,
+              VacancyInvite(charId, tplayer.Name, squad1.GUID),
+              charId,
+              tplayer.Name
+            )
 
-          case (Some(squad1), Some(squad2)) if squad1.Size == 1 =>
-          //both players belong to squads, but the invitingplayer's squad is underutilized by comparison
-          //treat the same as "indirection ..." using squad2
-            val leaderCharId = squad2.Leader.CharId
-            val bid = IndirectVacancy(tplayer, squad2.GUID)
-            log.warn(s"$invitedPlayer has asked $invitingPlayer for an invitation to squad ${squad2.Task}, but the squad leader needs to approve")
-            AddInvite(leaderCharId, bid) match {
-              case out @ Some(_) if out.contains(bid) =>
-                HandleBidForPosition(bid, tplayer)
-              case _ => ;
-            }
+          case (Some(squad1), Some(squad2))
+            if squad1.Size == 1 && !squadFeatures(squad2.GUID).Refuse.contains(invitingPlayer) =>
+            //both players belong to squads, but the invitingPlayer's squad is underutilized by comparison
+            //treat the same as "indirection ..." using squad2
+            log.warn(s"$invitedPlayer has asked $invitingPlayer for an invitation to squad ${squad2.Task}, but the squad leader may need to approve")
+            AddInviteAndRespond(
+              squad2.Leader.CharId,
+              IndirectInvite(tplayer, squad2.GUID),
+              invitingPlayer,
+              tplayer.Name
+            )
 
-          case (Some(squad), None) =>
+          case (Some(squad), None)
+            if !squadFeatures(squad.GUID).Refuse.contains(invitedPlayer) =>
             //the classic situation
             log.info(s"$invitedPlayer has been invited to squad ${squad.Task} by $invitingPlayer")
-            val charId = tplayer.CharId
-            val bid = VacancyInvite(charId, tplayer.Name, squad.GUID)
-            AddInvite(invitedPlayer, bid) match {
-              case out @ Some(_) if out.contains(bid) =>
-                SquadEvents.publish(SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, charId, Some(invitedPlayer), tplayer.Name, false, Some(None))))
-                SquadEvents.publish(SquadServiceResponse(s"/$charId/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, invitedPlayer, Some(charId), tplayer.Name, true, Some(None))))
-              case Some(_) =>
-                SquadEvents.publish(SquadServiceResponse(s"/$charId/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, invitedPlayer, Some(charId), tplayer.Name, true, Some(None))))
-              case _ => ;
-            }
+            AddInviteAndRespond(
+              invitedPlayer,
+              VacancyInvite(tplayer.CharId, tplayer.Name, squad.GUID),
+              invitingPlayer,
+              tplayer.Name
+            )
 
-          case (None, Some(squad)) =>
+          case (None, Some(squad))
+            if !squadFeatures(squad.GUID).Refuse.contains(invitingPlayer) =>
             //indirection;  we're trying to invite ourselves to someone else's squad
-            val leaderCharId = squad.Leader.CharId
-            val bid = IndirectVacancy(tplayer, squad.GUID)
-            log.warn(s"$invitedPlayer has asked $invitingPlayer for an invitation to squad ${squad.Task}, but the squad leader needs to approve")
-            AddInvite(leaderCharId, bid) match {
-              case out @ Some(_) if out.contains(bid) =>
-                HandleBidForPosition(bid, tplayer)
-              case _ => ;
-            }
+            log.warn(s"$invitedPlayer has asked $invitingPlayer for an invitation to squad ${squad.Task}, but the squad leader may need to approve")
+            AddInviteAndRespond(
+              squad.Leader.CharId,
+              IndirectInvite(tplayer, squad.GUID),
+              invitingPlayer,
+              tplayer.Name
+            )
 
           case (None, None) =>
             //neither the invited player nor the inviting player belong to any squad
             log.info(s"$invitedPlayer has been invited to join $invitingPlayer's spontaneous squad")
-            val bid = SpontaneousInvite(tplayer)
-            AddInvite(invitedPlayer, bid) match {
-              case out @ Some(_) if out.contains(bid) =>
-                SquadEvents.publish(SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, invitingPlayer, Some(invitedPlayer), tplayer.Name, false, Some(None))))
-                SquadEvents.publish(SquadServiceResponse(s"/$invitingPlayer/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, invitedPlayer, Some(invitingPlayer), tplayer.Name, true, Some(None))))
-              case Some(_) =>
-                SquadEvents.publish(SquadServiceResponse(s"/$invitingPlayer/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, invitedPlayer, Some(invitingPlayer), tplayer.Name, true, Some(None))))
-              case _ => ;
+            AddInviteAndRespond(
+              invitedPlayer,
+              SpontaneousInvite(tplayer),
+              invitingPlayer,
+              tplayer.Name
+            )
+
+          case _ => ;
+        }
+
+      case SquadAction.Membership(SquadRequestType.ProximityInvite, invitingPlayer, _, _, _) =>
+        memberToSquad.get(invitingPlayer) 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
+                log.info("ProximityInvite: waiting for existing proximity invitations to clear")
+              case _ =>
+                log.info("ProximityInvite: looking for invitation targets ...")
+                val outstandingActiveInvites = features.SearchForRole match {
+                  case Some(pos) =>
+                    RemoveQueuedInvitesForSquadAndPosition(sguid, pos)
+                    invites.collect { case(charId, InviteForRole(_,_, squad_guid, role)) if squad_guid == sguid && role == pos => charId }
+                  case None =>
+                    List.empty[Long]
+                }
+                features.SearchForRole = Some(-1)
+                val faction = squad.Faction
+                val center = tplayer.Position
+                val excusedInvites = features.Refuse
+                //positions that can be recruited to
+                val positions = squad.Membership.zipWithIndex
+                  .collect { case(member, index) if member.CharId == 0 && squad.Availability(index) => member }
+                /*
+                players who are:
+                - the same faction as the squad
+                - have Looking For Squad enabled
+                - do not currently belong to a squad
+                - are denied the opportunity to be invited
+                - are a certain distance from the squad leader
+                 */
+                (zone.LivePlayers
+                  .collect { case player
+                    if player.Faction == faction && player.LFS && memberToSquad.get(player.CharId).isEmpty &&
+                      !excusedInvites.contains(player.CharId) &&
+                      Vector3.DistanceSquared(player.Position, center) < 100f &&
+                      {
+                        positions
+                          .map { role =>
+                            val requirementsToMeet = role.Requirements
+                            requirementsToMeet.intersect(player.Certifications) == requirementsToMeet
+                          }
+                          .foldLeft(false)(_ || _)
+                      } => player.CharId
+                  }
+                  .partition { charId => outstandingActiveInvites.exists(_ == charId) } match {
+                  case (Nil, Nil) =>
+                    //no one found
+                    log.info("ProximityInvite: no invitation targets 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
+                    log.info(s"ProximityInvite: found ${outstandingPlayerList.size} players already having been invited, and ${invitedPlayerList.size} players to invite")
+                    outstandingPlayerList.foreach { charId =>
+                      val bid = invites(charId).asInstanceOf[InviteForRole]
+                      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 _ => //
+          case None =>
         }
 
       case SquadAction.Membership(SquadRequestType.Accept, invitedPlayer, _, _, _) =>
         val acceptedInvite = RemoveInvite(invitedPlayer)
-        if(EnsureEmptySquad(invitedPlayer, "Accept: the invited player is already a member of a squad and can not join a second one")) {
-          acceptedInvite match {
-            case Some(BidForPosition(petitioner, guid, position)) if idToSquad.get(guid).nonEmpty =>
-              //player requested to join a squad's specific position
-              //invitedPlayer is actually the squad leader; petitioner is the actual "invitedPlayer"
-              JoinSquad(petitioner, idToSquad(guid), position)
+        val msg = "Accept: the invited player is already a member of a squad and can not join a second one"
+        acceptedInvite match {
+          case Some(BidForRole(petitioner, guid, position)) if EnsureEmptySquad(petitioner.CharId, msg) && 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(IndirectVacancy(recruit, guid)) =>
-              //tplayer / invitedPlayer is actually the squad leader
-              val recruitCharId = recruit.CharId
-              HandleVacancyInvite(guid, recruitCharId, invitedPlayer, recruit) match {
-                case Some((squad, line)) =>
-                  SquadEvents.publish(SquadServiceResponse(s"/$recruitCharId/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitedPlayer, Some(recruitCharId), "", true, Some(None))))
-                  JoinSquad(recruit, squad, line)
-                //since we are the squad leader, we do not want to brush off our queued squad invite tasks
-                case _ => ;
-              }
+          case Some(IndirectInvite(recruit, guid)) if EnsureEmptySquad(recruit.CharId, msg) =>
+            //tplayer / invitedPlayer is actually the squad leader
+            val recruitCharId = recruit.CharId
+            HandleVacancyInvite(guid, recruitCharId, invitedPlayer, recruit) match {
+              case Some((squad, line)) =>
+                SquadEvents.publish(SquadServiceResponse(s"/$invitedPlayer/Squad", 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)) =>
-              //accepted an invitation to join an existing squad
-              HandleVacancyInvite(guid, invitedPlayer, invitingPlayer, tplayer) match {
-                case Some((squad, line)) =>
-                  SquadEvents.publish(SquadServiceResponse(s"/$invitingPlayer/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitingPlayer, Some(invitedPlayer), tplayer.Name, false, Some(None))))
-                  SquadEvents.publish(SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitedPlayer, Some(invitingPlayer), "", true, Some(None))))
-                  JoinSquad(tplayer, squad, line)
-                  RemoveQueuedInvites(invitedPlayer) //TODO deal with these somehow
-                case _ => ;
-              }
+          case Some(VacancyInvite(invitingPlayer, _, guid)) if EnsureEmptySquad(invitedPlayer, msg) =>
+            //accepted an invitation to join an existing squad
+            HandleVacancyInvite(guid, invitedPlayer, invitingPlayer, tplayer) match {
+              case Some((squad, line)) =>
+                SquadEvents.publish(SquadServiceResponse(s"/$invitingPlayer/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitingPlayer, Some(invitedPlayer), tplayer.Name, false, Some(None))))
+                SquadEvents.publish(SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitedPlayer, Some(invitingPlayer), "", true, Some(None))))
+                JoinSquad(tplayer, squad, line)
+                RemoveQueuedInvites(invitedPlayer) //TODO deal with these somehow
+                RemoveInvitesForSquadAndPosition(squad.GUID, line)
+              case _ => ;
+            }
 
-            case Some(SpontaneousInvite(invitingPlayer)) =>
-              //originally, we were invited by someone into a new squad they would form
-              val invitingPlayerCharId = invitingPlayer.CharId
-              (GetParticipatingSquad(invitingPlayer) match {
-                case Some(participating) =>
-                  //invitingPlayer became part of a squad while invited player was answering the original summons
-                  Some(participating)
-                case _ =>
-                  //generate a new squad, with invitingPlayer as the leader
-                  val squad = StartSquad(invitingPlayer)
-                  squad.Task = s"${invitingPlayer.Name}'s Squad"
-                  SquadEvents.publish( SquadServiceResponse(s"/$invitingPlayerCharId/Squad", SquadResponse.AssociateWithSquad(squad.GUID)) )
-                  Some(squad)
-              }) match {
-                case Some(squad) =>
-                  HandleVacancyInvite(squad.GUID, tplayer.CharId, invitingPlayerCharId, tplayer) match {
-                    case Some((_, line)) =>
-                      SquadEvents.publish( SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitedPlayer, Some(invitingPlayerCharId), "", true, Some(None))) )
-                      JoinSquad(tplayer, squad, line)
-                      SquadEvents.publish( SquadServiceResponse(s"/$invitingPlayerCharId/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitingPlayerCharId, Some(invitedPlayer), tplayer.Name, false, Some(None))) )
-                      RemoveQueuedInvites(tplayer.CharId) //TODO deal with these somehow
-                    case _ => ;
+          case Some(SpontaneousInvite(invitingPlayer)) if EnsureEmptySquad(invitedPlayer, msg) =>
+            //originally, we were invited by someone into a new squad they would form
+            val invitingPlayerCharId = invitingPlayer.CharId
+            (GetParticipatingSquad(invitingPlayer) match {
+              case Some(participating) =>
+                //invitingPlayer became part of a squad while invited player was answering the original summons
+                Some(participating)
+              case _ =>
+                //generate a new squad, with invitingPlayer as the leader
+                val squad = StartSquad(invitingPlayer)
+                squad.Task = s"${invitingPlayer.Name}'s Squad"
+                SquadEvents.publish( SquadServiceResponse(s"/$invitingPlayerCharId/Squad", SquadResponse.AssociateWithSquad(squad.GUID)) )
+                Some(squad)
+            }) match {
+              case Some(squad) =>
+                HandleVacancyInvite(squad.GUID, tplayer.CharId, invitingPlayerCharId, tplayer) match {
+                  case Some((_, line)) =>
+                    SquadEvents.publish( SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitedPlayer, Some(invitingPlayerCharId), "", true, Some(None))) )
+                    SquadEvents.publish( SquadServiceResponse(s"/$invitingPlayerCharId/Squad", 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(InviteForRole(invitingPlayer, name, guid, position)) if EnsureEmptySquad(invitedPlayer, msg) =>
+            squadFeatures.get(guid) match {
+              case Some(features) if JoinSquad(tplayer, features.Squad, position) =>
+                //join this squad
+                SquadEvents.publish( SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitedPlayer, Some(invitingPlayer), "", true, Some(None))) )
+                SquadEvents.publish( SquadServiceResponse(s"/$invitingPlayer/Squad", 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)
+                NextInviteAndRespond(invitedPlayer)
+
+              case _ =>
+                //squad no longer exists?
+                NextInviteAndRespond(invitedPlayer)
+            }
+
+          case Some(ProximityInvite(invitingPlayer, _, guid)) if EnsureEmptySquad(invitedPlayer, msg) =>
+            squadFeatures.get(guid) match {
+              case Some(features) =>
+                val squad = features.Squad
+                if(squad.Size < squad.Capacity) {
+                  val positions = squad.Membership.zipWithIndex
+                    .collect { case (member, index) if member.CharId == 0 && squad.Availability(index) && {
+                      val requirementsToMeet = member.Requirements
+                      requirementsToMeet.intersect(tplayer.Certifications) == requirementsToMeet
+                    } =>
+                      (index, member.Requirements.size)
+                    }
+                    .sortBy({ case (_, requirements) => requirements })
+                  ((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 //(None, None)
+                  }) match {
+                    case Some(position) if JoinSquad(tplayer, squad, position) =>
+                      //join this squad
+                      SquadEvents.publish( SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitedPlayer, Some(invitingPlayer), "", true, Some(None))) )
+                      SquadEvents.publish( SquadServiceResponse(s"/$invitingPlayer/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitingPlayer, Some(invitedPlayer), tplayer.Name, false, Some(None))) )
+                      RemoveQueuedInvites(invitedPlayer)
+                      features.ProxyInvites = features.ProxyInvites.filterNot(_ == invitedPlayer)
+                    case _ =>
                   }
-                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
+                  features.SearchForRole = None
+                  features.ProxyInvites = Nil
+                  CleanupInvitesForSquad(guid)
+                  //CleanupInvitesFromPlayer(invitingPlayer)
+                }
 
-            case None =>
-              //the invite either timed-out or was withdrawn; select a new one?
-              NextInvite(invitedPlayer) match {
-                case Some(bid : BidForPosition) if !acceptedInvite.contains(bid) =>
-                  HandleBidForPosition(bid, tplayer)
-                case Some(bid) if !acceptedInvite.contains(bid) =>
-                  SquadEvents.publish(SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, bid.InviterCharId, Some(invitedPlayer), bid.InviterName, false, Some(None))))
-                case None => ;
-              }
-          }
+              case _ =>
+                //squad no longer exists?
+                NextInviteAndRespond(invitedPlayer)
+            }
+
+          case _ =>
+            //the invite either timed-out or was withdrawn or is now invalid; select a new one?
+            NextInviteAndRespond(invitedPlayer)
         }
 
       case SquadAction.Membership(SquadRequestType.Leave, leavingPlayer, optionalPlayer, _, _) =>
@@ -419,46 +503,58 @@ class SquadService extends Actor {
         if(leavingPlayer == leader || squad.Size == 2) {
           //squad leader is leaving his own squad, so it will be disbanded
           //alternately, squad is only composed of two people, so it will be closed-out when one of them leaves
-          val membership = squad.Membership.collect { case member if member.CharId > 0 => member.CharId }
-          membership.foreach { charId =>
-            SquadEvents.publish(SquadServiceResponse(s"/$charId/Squad", SquadResponse.Membership(SquadResponseType.Leave, 0, 0, charId, None, "", false, Some(None))))
-          }
-          CloseOutSquad(squad)
-          SquadEvents.publish(SquadServiceResponse(s"/$leader/Squad", SquadResponse.Membership(SquadResponseType.Disband, 0, 0, leader, None, "", true, Some(None))))
-          membership
-            .filterNot(_ == leader)
-            .foreach { charId =>
-            SquadEvents.publish(SquadServiceResponse(s"/$charId/Squad", SquadResponse.Membership(SquadResponseType.Disband, 0, 0, charId, None, "", false, Some(None))))
-          }
-          SquadEvents.publish( SquadServiceResponse(s"/$leader/Squad", SquadResponse.AssociateWithSquad(PlanetSideGUID(0))) )
-          SquadEvents.publish( SquadServiceResponse(s"/$leader/Squad", SquadResponse.Detail(PlanetSideGUID(0), SquadDetail().Complete)) )
+          DisbandSquad(squad)
         }
         else {
           if(optionalPlayer.contains(leavingPlayer)) {
             //leaving the squad of own accord
-            LeaveSquad(tplayer, squad)
+            LeaveSquad(tplayer.CharId, squad)
           }
           else if(optionalPlayer.contains(leader)) {
             //kicked by the squad leader
             SquadEvents.publish( SquadServiceResponse(s"/$leavingPlayer/Squad", SquadResponse.Membership(SquadResponseType.Leave, 0, 0, leavingPlayer, Some(leader), tplayer.Name, false, Some(None))) )
             SquadEvents.publish( SquadServiceResponse(s"/$leader/Squad", SquadResponse.Membership(SquadResponseType.Leave, 0, 0, leader, Some(leavingPlayer), "", true, Some(None))) )
-            LeaveSquad(tplayer, squad)
+            squadFeatures(squad.GUID).Refuse = leavingPlayer
+            LeaveSquad(leavingPlayer, squad)
           }
         }
 
-      case SquadAction.Membership(SquadRequestType.Reject, rejectingPlayer, optionalPlayer, _, _) =>
+      case SquadAction.Membership(SquadRequestType.Reject, rejectingPlayer, _, _, _) =>
         val rejectedBid = RemoveInvite(rejectingPlayer)
         //(A, B) -> person who made the rejection, person who was rejected
         (rejectedBid match {
           case Some(SpontaneousInvite(invitingPlayer)) =>
             //rejectingPlayer is the would-be squad member
             (Some(rejectingPlayer), Some(invitingPlayer.CharId))
-          case Some(VacancyInvite(invitingPlayer, _, guid)) if idToSquad(guid).Leader.CharId != rejectingPlayer =>
+          case Some(VacancyInvite(invitingPlayer, _, guid))
+            if squadFeatures.get(guid).nonEmpty && squadFeatures(guid).Squad.Leader.CharId != rejectingPlayer =>
             //rejectingPlayer is the would-be squad member
             (Some(rejectingPlayer), Some(invitingPlayer))
-          case Some(BidForPosition(_, guid, _)) if idToSquad(guid).Leader.CharId != rejectingPlayer =>
+          case Some(BidForRole(_, guid, _))
+            if squadFeatures.get(guid).nonEmpty && squadFeatures(guid).Squad.Leader.CharId == rejectingPlayer =>
             //rejectingPlayer is the squad leader
             (Some(rejectingPlayer), None)
+          case Some(InviteForRole(invitingPlayer, _, guid, position))
+            if squadFeatures.get(guid).nonEmpty && squadFeatures(guid).Squad.Leader.CharId != rejectingPlayer =>
+            //rejectingPlayer is the would-be squad member
+            val features = squadFeatures(guid)
+            features.Refuse = rejectingPlayer //do not bother this player anymore
+            features.ProxyInvites = features.ProxyInvites.filterNot(_ == rejectingPlayer)
+            if(features.ProxyInvites.isEmpty) {
+              features.SearchForRole = None
+            }
+            (None, None)
+          case Some(ProximityInvite(_, _, guid))
+            if squadFeatures.get(guid).nonEmpty && squadFeatures(guid).Squad.Leader.CharId != rejectingPlayer =>
+            //rejectingPlayer is the would-be squad member
+            val features = squadFeatures(guid)
+            features.Refuse = rejectingPlayer //do not bother this player anymore
+            features.ProxyInvites = features.ProxyInvites.filterNot(_ == rejectingPlayer)
+            if(features.ProxyInvites.isEmpty) {
+              //all invitations exhausted; this invitation is concluded
+              features.SearchForRole = None
+            }
+            (None, None)
           case _ => ;
             (None, None)
         }) match {
@@ -469,46 +565,11 @@ class SquadService extends Actor {
             SquadEvents.publish( SquadServiceResponse(s"/$rejected/Squad", SquadResponse.Membership(SquadResponseType.Reject, 0, 0, rejected, Some(rejected), "", true, Some(None))) )
           case _ => ;
         }
-        NextInvite(rejectingPlayer) match {
-          case Some(bid : BidForPosition) if rejectedBid.isEmpty || !rejectedBid.contains(bid) =>
-            HandleBidForPosition(bid, tplayer)
-          case Some(bid) if rejectedBid.isEmpty || !rejectedBid.contains(bid) =>
-            SquadEvents.publish(SquadServiceResponse(s"/$rejectingPlayer/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, bid.InviterCharId, Some(rejectingPlayer), bid.InviterName, false, Some(None))))
-          case None => ;
-        }
+        NextInviteAndRespond(rejectingPlayer)
 
-//      case SquadAction.Membership(SquadRequestType.Cancel, cancellingPlayer, _, _, _) =>
-//        //look for queued BidForPosition entries where we are the player who wants to join
-//        queuedInvites.foreach { case (leader, queueOfInvites) =>
-//          val list = queueOfInvites.filterNot { entry =>
-//              entry.isInstanceOf[BidForPosition] &&
-//                entry.asInstanceOf[BidForPosition].player.CharId == cancellingPlayer
-//          }
-//          if(list.nonEmpty && list.size != queueOfInvites.size) {
-//            queuedInvites(leader) = list
-//          }
-//          else if(list.isEmpty) {
-//            queuedInvites.remove(leader)
-//          }
-//        }
-//        //clean up active BidForPosition invite entries where we are the player who wants to join
-//        val list = invites.filter { case(_, entry) =>
-//          entry.isInstanceOf[BidForPosition] &&
-//            entry.asInstanceOf[BidForPosition].player.CharId == cancellingPlayer
-//        }
-//        list.foreach {
-//          case(charId, entry : BidForPosition) =>
-//            RemoveInvite(charId)
-//            SquadEvents.publish( SquadServiceResponse(s"/$charId/Squad", SquadResponse.Membership(SquadResponseType.Cancel, 0, 0, cancellingPlayer, None, entry.player.Name, false, None)))
-//            NextInvite(charId) match {
-//              case Some(bid : BidForPosition) =>
-//                HandleBidForPosition(bid, tplayer)
-//              case Some(bid) =>
-//                SquadEvents.publish(SquadServiceResponse(s"/$charId/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, bid.InviterCharId, Some(charId), bid.InviterName, false, Some(None))))
-//              case _ => ;
-//            }
-//          case _ => ;
-//        }
+      case SquadAction.Membership(SquadRequestType.Cancel, cancellingPlayer, _, _, _) =>
+        //huh?
+        log.warn(s"Huh? what does player $cancellingPlayer want to cancel?")
 
       case SquadAction.Membership(SquadRequestType.Promote, promotingPlayer, Some(promotedPlayer), _, _) =>
         (memberToSquad.get(promotingPlayer), memberToSquad.get(promotedPlayer)) match {
@@ -518,6 +579,27 @@ class SquadService extends Actor {
             val (member, index) = membership.zipWithIndex.find { case (_member, _) => _member.CharId == promotedPlayer }.get
             log.info(s"Player ${leader.Name} steps down from leading ${squad.Task}")
             SwapMemberPosition(squad, leader, member)
+            //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)
+                }
+            }
             log.info(s"Promoting player ${leader.Name} to be the leader of ${squad.Task}")
             membership.foreach { _member =>
               SquadEvents.publish(SquadServiceResponse(s"/${_member.CharId}/Squad", SquadResponse.PromoteMember(squad, promotedPlayer, index, position)))
@@ -539,7 +621,8 @@ class SquadService extends Actor {
                 ))
             )
 
-          case _ => ;
+          case msg =>
+            log.warn(s"Unsupported squad behavior: $msg")
         }
 
       case SquadAction.Waypoint(_, wtype, _, info) =>
@@ -555,7 +638,7 @@ class SquadService extends Actor {
             }
           case _ => (None, None)
         }) match {
-          case (Some(squad), Some(waypoint)) =>
+          case (Some(squad), Some(_)) =>
             //waypoint added or updated
             squad.Membership
               .filterNot { member => member.CharId == tplayer.CharId }
@@ -565,6 +648,7 @@ class SquadService extends Actor {
                   SquadServiceResponse(s"/$charId/Squad", SquadResponse.WaypointEvent(WaypointEventAction.Add, playerCharId, wtype, None, info, 1))
                 )
               }
+
           case (Some(squad), None) =>
             //waypoint removed?
             squad.Membership
@@ -575,10 +659,12 @@ class SquadService extends Actor {
                   SquadServiceResponse(s"/$charId/Squad", SquadResponse.WaypointEvent(WaypointEventAction.Remove, playerCharId, wtype, None, None, 0))
                 )
               }
-          case _ => ;
+
+          case msg =>
+            log.warn(s"Unsupported squad waypoint behavior: $msg")
         }
 
-      case SquadAction.Definition(zone, guid, line, action) =>
+      case SquadAction.Definition(guid, line, action) =>
         import net.psforever.packet.game.SquadAction._
         val pSquadOpt = GetParticipatingSquad(tplayer)
         val lSquadOpt = GetLeadingSquad(tplayer, pSquadOpt)
@@ -633,14 +719,11 @@ class SquadService extends Actor {
                 squad.Availability.update(position, false)
                 log.info(s"${tplayer.Name}-${tplayer.Faction} has closed the #$position position in squad")
                 val memberPosition = squad.Membership(position)
-                val listingChanged = if(memberPosition.Name.nonEmpty) {
-                  SquadInfo().Size(squad.Size).Capacity(squad.Capacity)
-                }
-                else {
-                  SquadInfo().Capacity(squad.Capacity)
+                if(memberPosition.CharId > 0) {
+                  LeaveSquad(memberPosition.CharId, squad)
                 }
                 memberPosition.Close()
-                UpdateSquadListWhenListed(squad, listingChanged)
+                UpdateSquadListWhenListed(squad, SquadInfo().Capacity(squad.Capacity))
                 UpdateSquadDetail(squad.GUID, squad,
                   SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail.Closed)))
                 )
@@ -716,6 +799,113 @@ class SquadService extends Actor {
             val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
             squad.AutoApproveInvitationRequests = state
 
+          case FindLfsSoldiersForRole(position) =>
+            lSquadOpt match {
+              case Some(squad) =>
+                val sguid = squad.GUID
+                val features = squadFeatures(sguid)
+                features.SearchForRole match {
+                  case Some(-1) =>
+                    //a proximity invitation has not yet cleared; nothing will be gained by trying to invite for a specific role
+                    log.debug("FindLfsSoldiersForRole: waiting for proximity invitations to clear")
+                  case _ =>
+                    //either no role has ever been recruited, or some other role has been recruited
+                    //normal LFS recruitment for the given position
+                    val excusedInvites = features.Refuse
+                    val faction = squad.Faction
+                    val requirementsToMeet = squad.Membership(position).Requirements
+                    val outstandingActiveInvites = features.SearchForRole match {
+                      case Some(pos) =>
+                        RemoveQueuedInvitesForSquadAndPosition(sguid, pos)
+                        invites.collect { case(charId, InviteForRole(_,_, squad_guid, role)) if squad_guid == sguid && role == pos => charId }
+                      case None =>
+                        List.empty[Long]
+                    }
+                    features.SearchForRole = position
+                    //this will update the role entry in the GUI to visually indicate being searched for; only one will be displayed at a time
+                    SquadEvents.publish(
+                      SquadServiceResponse(s"/${tplayer.CharId}/Squad", 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.LFS && memberToSquad.get(player.CharId).isEmpty &&
+                          requirementsToMeet.intersect(player.Certifications) == requirementsToMeet =>
+                        player.CharId
+                      }
+                      .partition { charId => outstandingActiveInvites.exists(charId == _) } match {
+                      case (Nil, Nil) =>
+                        outstandingActiveInvites foreach RemoveInvite
+                        features.ProxyInvites = Nil
+                        //TODO cancel the LFS search from the server so that the client updates properly; how?
+                        None
+                      case (outstandingPlayerList, invitedPlayerList) =>
+                        //players who were actively invited for the previous position and are eligible for the new position
+                        outstandingPlayerList.foreach { charId =>
+                          val bid = invites(charId).asInstanceOf[InviteForRole]
+                          invites(charId) = InviteForRole(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,
+                            InviteForRole(invitingPlayer, name, sguid, position),
+                            invitingPlayer,
+                            name
+                          )
+                        }
+                      case None => ;
+                    }
+                }
+
+              case _ => ;
+            }
+
+          case CancelFind() =>
+            lSquadOpt match {
+              case Some(squad) =>
+                val sguid = squad.GUID
+                val position = squadFeatures(sguid).SearchForRole
+                squadFeatures(sguid).SearchForRole = None
+                //remove active invites
+                invites.filter {
+                  case (_, InviteForRole(_, _, _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 InviteForRole(_, _, _guid, _) => _guid == sguid
+                    case _ => false
+                  }
+                  queuedInvites += charId -> filtered
+                  if(filtered.isEmpty) {
+                    queuedInvites.remove(charId)
+                  }
+                }
+                //remove yet-to-be invitedPlayers
+                squadFeatures(sguid).ProxyInvites = Nil
+              case _ => ;
+            }
+
           case RequestListSquad() =>
             val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
             if(!squad.Listed && squad.Task.nonEmpty && squad.ZoneId > 0) {
@@ -738,6 +928,7 @@ class SquadService extends Actor {
           case ResetAll() =>
             lSquadOpt match {
               case Some(squad) if squad.Size > 1 =>
+                val guid = squad.GUID
                 squad.Task = ""
                 squad.ZoneId = None
                 squad.Availability.indices.foreach { i =>
@@ -751,19 +942,172 @@ class SquadService extends Actor {
                 squad.LocationFollowsSquadLead = false
                 squad.AutoApproveInvitationRequests = false
                 UpdateSquadListWhenListed(squad, SquadInfo().Task("").ZoneId(None).Capacity(squad.Capacity))
-                UpdateSquadDetail(squad.GUID, squad)
+                UpdateSquadDetail(guid, squad)
                 sender ! SquadServiceResponse("", SquadResponse.AssociateWithSquad(PlanetSideGUID(0)))
-                if(!initialAssociation.contains(squad.GUID)) {
-                  initialAssociation += squad.GUID
-                }
+                squadFeatures(guid).InitialAssociation = true
                 //do not unlist an already listed squad
               case Some(squad) =>
                 //underutilized squad; just close it out
-                CloseOutSquad(squad)
+                CloseSquad(squad)
               case _ => ;
             }
 
           case _ =>
+            (pSquadOpt, action) match {
+              //the following action can be performed by the squad leader and maybe an unaffiliated player
+              case (Some(squad), SelectRoleForYourself(position)) =>
+                log.info(s"${tplayer.Name} would like the #${position+1} spot in the same squad")
+                val membership = squad.Membership.zipWithIndex
+                val toMember = squad.Membership(position)
+                if(squad.Leader.CharId == tplayer.CharId) {
+                  //TODO 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 && toMember.CharId == 0) || ValidOpenSquadPosition(squad, position, toMember, tplayer.Certifications)) {
+                  membership.find { case (member, _) => member.CharId == tplayer.CharId } match {
+                    case Some((fromMember, fromIndex)) =>
+                      SwapMemberPosition(squad, toMember, fromMember)
+                      if(fromIndex == squad.LeaderPositionIndex) {
+                        squad.LeaderPositionIndex = position
+                      }
+                      //RemoveInvite(tplayer.CharId).foreach { _ =>
+                      //close the old bids out
+                      //}
+                      membership
+                        .filter { case (_member, _) => _member.CharId > 0 }
+                        .foreach { case (_member, _) =>
+                          SquadEvents.publish(SquadServiceResponse(s"/${_member.CharId}/Squad", SquadResponse.AssignMember(squad, fromIndex, position)))
+                        }
+                      UpdateSquadDetail(squad.GUID, squad)
+                    case _ => ;
+                    //somehow, this is not our squad; do nothing, for now
+                  }
+                }
+                else {
+                  //not qualified for requested position
+                }
+
+              //the following action can be performed by an unaffiliated player
+              case (None, SelectRoleForYourself(position)) =>
+                //not a member of any squad, but we might become a member of this one
+                GetSquad(guid) match {
+                  case Some(squad) =>
+                    val toMember = squad.Membership(position)
+                    if(ValidOpenSquadPosition(squad, position, toMember, tplayer.Certifications)) {
+                      //we could join but we may need permission from the squad leader first
+                      log.info(s"${tplayer.Name} would like the #${position+1} spot in the squad ${squad.Task}.")
+                      AddInviteAndRespond(
+                        squad.Leader.CharId,
+                        BidForRole(tplayer, guid, position),
+                        invitingPlayer = 0L, //we ourselves technically are ...
+                        tplayer.Name
+                      )
+                    }
+                  case None => ;
+                  //squad does not exist? assume old local data; force update to correct discrepancy
+                }
+
+              //the following action can be performed by anyone who has tried to join a squad
+              case (_, CancelSelectRoleForYourself(_)) =>
+                val cancellingPlayer = tplayer.CharId
+                GetSquad(guid) match {
+                  case Some(squad) =>
+                    //assumption: a player who is cancelling will rarely end up with their invite queued
+                    val leaderCharId = squad.Leader.CharId
+                    //clean up any active BidForRole 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[BidForRole] &&
+                        entry.asInstanceOf[BidForRole].player.CharId == cancellingPlayer =>
+                        out
+                      case _ =>
+                        None
+                    }) match {
+                      case Some(entry : BidForRole) =>
+                        RemoveInvite(leaderCharId)
+                        SquadEvents.publish( SquadServiceResponse(s"/$leaderCharId/Squad", 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 BidForRole 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[BidForRole] &&
+                              entry.asInstanceOf[BidForRole].player.CharId == cancellingPlayer
+                          })
+                        case None =>
+                          (Nil, -1)
+                      }) match {
+                        case (_, -1) =>
+                          None //no change
+                        case (list, index) if list.size == 1 =>
+                          val entry = list.head.asInstanceOf[BidForRole]
+                          SquadEvents.publish( SquadServiceResponse(s"/$leaderCharId/Squad", SquadResponse.Membership(SquadResponseType.Cancel, 0, 0, cancellingPlayer, None, entry.player.Name, false, Some(None))))
+                          queuedInvites.remove(leaderCharId)
+                          Some(true)
+                        case (list, index) =>
+                          val entry = list(index).asInstanceOf[BidForRole]
+                          SquadEvents.publish( SquadServiceResponse(s"/$leaderCharId/Squad", SquadResponse.Membership(SquadResponseType.Cancel, 0, 0, cancellingPlayer, None, entry.player.Name, false, Some(None))))
+                          queuedInvites(leaderCharId) = list.take(index) ++ list.drop(index+1)
+                          Some(true)
+                      }
+                    )
+
+                  case _ => ;
+                }
+
+              //the following action can be performed by ???
+              case (Some(squad), AssignSquadMemberToRole(position, char_id)) =>
+                val membership = squad.Membership.zipWithIndex
+                (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(squad, toMember, fromMember)
+                    if(fromPosition == squad.LeaderPositionIndex) {
+                      squad.LeaderPositionIndex = position
+                    }
+                    membership
+                      .filter({ case (_member, _) => _member.CharId > 0 })
+                      .foreach { case (_member, _) =>
+                        SquadEvents.publish(SquadServiceResponse(s"/${_member.CharId}/Squad", SquadResponse.AssignMember(squad, fromPosition, position)))
+                      }
+                    UpdateSquadDetail(squad.GUID, squad,
+                      SquadDetail().Members(List(
+                        SquadPositionEntry(position, SquadPositionDetail().CharId(fromMember.CharId).Name(fromMember.Name)),
+                        SquadPositionEntry(fromPosition, SquadPositionDetail().CharId(char_id).Name(name))
+                      ))
+                    )
+                  case _ => ;
+                }
+
+              //the following action can be peprformed by anyone
+              case (_, SearchForSquadsWithParticularRole(_/*role*/, _/*requirements*/, _/*zone_id*/, _/*search_mode*/)) =>
+                //though we should be able correctly search squads as is intended
+                //I don't know how search results should be prioritized or even how to return search results to the user
+                sender ! SquadServiceResponse("", SquadResponse.SquadSearchResults())
+
+              //the following action can be performed by anyone
+              case (_, DisplaySquad()) =>
+                GetSquad(guid) match {
+                  case Some(squad) =>
+                    sender ! SquadServiceResponse("", SquadResponse.Detail(squad.GUID, SquadService.Detail.Publish(squad)))
+                  case None => ;
+                }
+
+              //the following message is feedback from a specific client, awaiting proper initialization
+              case (_, SquadMemberInitializationIssue()) =>
+              //            GetSquad(guid) match {
+              //              case Some(squad) =>
+              //                sender ! SquadServiceResponse("", SquadResponse.Detail(squad.GUID, SquadService.Detail.Publish(squad)))
+              //              case None => ;
+              //            }
+
+              case msg => ;
+                log.warn(s"Unsupported squad definition behavior: $msg")
+            }
         }
         // etc..
         (pSquadOpt, action) match {
@@ -803,20 +1147,18 @@ class SquadService extends Actor {
           //the following action can be performed by an unaffiliated player
           case (None, SelectRoleForYourself(position)) =>
             //not a member of any squad, but we might become a member of this one
-            idToSquad.get(guid) match {
+            GetSquad(guid) match {
               case Some(squad) =>
                 val toMember = squad.Membership(position)
                 if(ValidOpenSquadPosition(squad, position, toMember, tplayer.Certifications)) {
                   //we could join but we may need permission from the squad leader first
                   log.info(s"Player ${tplayer.Name} would like to join the squad ${squad.Task}.")
-                  val leader = squad.Leader
-                  val bid = BidForPosition(tplayer, guid, position)
-                  val leaderCharId = leader.CharId
-                  AddInvite(leaderCharId, bid) match {
-                    case out @ Some(_) if out.contains(bid) =>
-                      HandleBidForPosition(bid, tplayer)
-                    case _ => ;
-                  }
+                  AddInviteAndRespond(
+                    squad.Leader.CharId,
+                    BidForRole(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
@@ -825,38 +1167,32 @@ class SquadService extends Actor {
           //the following action can be performed by anyone who has tried to join a squad
           case (_, CancelSelectRoleForYourself(_)) =>
             val cancellingPlayer = tplayer.CharId
-            idToSquad.get(guid) match {
+            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 BidForPosition invite entry where we are the player who wants to join the leader's squad
+                //clean up any active BidForRole invite entry where we are the player who wants to join the leader's squad
                 ((invites.get(leaderCharId) match {
-                  case out @ Some(entry) if entry.isInstanceOf[BidForPosition] &&
-                    entry.asInstanceOf[BidForPosition].player.CharId == cancellingPlayer =>
+                  case out @ Some(entry) if entry.isInstanceOf[BidForRole] &&
+                    entry.asInstanceOf[BidForRole].player.CharId == cancellingPlayer =>
                     out
                   case _ =>
                     None
                 }) match {
-                  case Some(entry : BidForPosition) =>
+                  case Some(entry : BidForRole) =>
                     RemoveInvite(leaderCharId)
                     SquadEvents.publish( SquadServiceResponse(s"/$leaderCharId/Squad", SquadResponse.Membership(SquadResponseType.Cancel, 0, 0, cancellingPlayer, None, entry.player.Name, false, Some(None))))
-                    NextInvite(leaderCharId) match {
-                      case Some(bid : BidForPosition) =>
-                        HandleBidForPosition(bid, tplayer)
-                      case Some(bid) =>
-                        SquadEvents.publish(SquadServiceResponse(s"/$leaderCharId/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, bid.InviterCharId, Some(leaderCharId), bid.InviterName, false, Some(None))))
-                      case _ => ;
-                    }
+                    NextInviteAndRespond(leaderCharId)
                     Some(true)
                   case _ =>
                     None
                 }).orElse(
-                  //look for a queued BidForPosition entry where we are the player who wants to join the leader's squad
+                  //look for a queued BidForRole entry where we are the player who wants to join the leader's squad
                   (queuedInvites.get(leaderCharId) match {
                     case Some(_list) =>
                       (_list, _list.indexWhere { entry =>
-                        entry.isInstanceOf[BidForPosition] &&
-                          entry.asInstanceOf[BidForPosition].player.CharId == cancellingPlayer
+                        entry.isInstanceOf[BidForRole] &&
+                          entry.asInstanceOf[BidForRole].player.CharId == cancellingPlayer
                       })
                     case None =>
                       (Nil, -1)
@@ -864,12 +1200,12 @@ class SquadService extends Actor {
                     case (_, -1) =>
                       None //no change
                     case (list, index) if list.size == 1 =>
-                      val entry = list.head.asInstanceOf[BidForPosition]
+                      val entry = list.head.asInstanceOf[BidForRole]
                       SquadEvents.publish( SquadServiceResponse(s"/$leaderCharId/Squad", SquadResponse.Membership(SquadResponseType.Cancel, 0, 0, cancellingPlayer, None, entry.player.Name, false, Some(None))))
                       queuedInvites.remove(leaderCharId)
                       Some(true)
                     case (list, index) =>
-                      val entry = list(index).asInstanceOf[BidForPosition]
+                      val entry = list(index).asInstanceOf[BidForRole]
                       SquadEvents.publish( SquadServiceResponse(s"/$leaderCharId/Squad", SquadResponse.Membership(SquadResponseType.Cancel, 0, 0, cancellingPlayer, None, entry.player.Name, false, Some(None))))
                       queuedInvites(leaderCharId) = list.take(index) ++ list.drop(index+1)
                       Some(true)
@@ -905,23 +1241,22 @@ class SquadService extends Actor {
             }
 
           //the following action can be peprformed by anyone
-          case (_, SearchForSquadsWithParticularRole(role, requirements, zone_id, search_mode)) =>
+          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
             sender ! SquadServiceResponse("", SquadResponse.SquadSearchResults())
 
           //the following action can be performed by anyone
           case (_, DisplaySquad()) =>
-            idToSquad.get(guid) match {
+            GetSquad(guid) match {
               case Some(squad) =>
-                viewDetails(tplayer.CharId) = guid
                 sender ! SquadServiceResponse("", SquadResponse.Detail(squad.GUID, SquadService.Detail.Publish(squad)))
               case None => ;
             }
 
           //the following message is feedback from a specific client, awaiting proper initialization
           case (_, SquadMemberInitializationIssue()) =>
-//            idToSquad.get(guid) match {
+//            GetSquad(guid) match {
 //              case Some(squad) =>
 //                sender ! SquadServiceResponse("", SquadResponse.Detail(squad.GUID, SquadService.Detail.Publish(squad)))
 //              case None => ;
@@ -958,54 +1293,65 @@ class SquadService extends Actor {
   }
 
   /**
-    *
-    * @param invitedPlayer
-    * @param bid
+    * na
+    * @param invitedPlayer the person who will handle the invitation, eventually if not immediately
+    * @param invite the "new" invitation envelop object
     * @return an optional invite;
     *         if added to the active invite position, return the parameter bid;
     *         if added to the queued invite, return the invite in the active position;
     *         if not added, return `None`
     */
-  def AddInvite(invitedPlayer : Long, bid : Invitation) : Option[Invitation] = {
-    invites.get(invitedPlayer) match {
+  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) =>
-            if(_bid.InviterCharId != bid.InviterCharId && !bidList.exists { eachBid => eachBid.InviterCharId == bid.InviterCharId }) {
-              log.debug(s"Invite from ${bid.InviterCharId} to $invitedPlayer stored in queue while active invite request pending")
-              queuedInvites(invitedPlayer) = bid match {
-                case _: BidForPosition =>
-                  val (normals, others) = bidList.partition(_.isInstanceOf[BidForPosition])
-                  (normals :+ bid) ++ others
+            //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 }) {
+              log.debug(s"Invite from ${invite.InviterCharId} to $invitedPlayer stored in queue while active invite request pending")
+              queuedInvites(invitedPlayer) = invite match {
+                case _: BidForRole =>
+                  val (normals, others) = bidList.partition(_.isInstanceOf[BidForRole])
+                  (normals :+ invite) ++ others
                 case _ =>
-                  bidList :+ bid
+                  bidList :+ invite
               }
-              None
-            }
-            else {
               Some(_bid)
             }
-          case None =>
-            if(_bid.InviterCharId != bid.InviterCharId) {
-              log.debug(s"Invite from ${bid.InviterCharId} to $invitedPlayer stored while active invite request pending")
-              queuedInvites(invitedPlayer) = List[Invitation](bid)
+            else {
+              None
+            }
+          case None =>
+            if(_bid.InviterCharId != invite.InviterCharId) {
+              log.debug(s"Invite from ${invite.InviterCharId} to $invitedPlayer stored while active invite request pending")
+              queuedInvites(invitedPlayer) = List[Invitation](invite)
+              Some(_bid)
+            }
+            else {
+              None
             }
-            Some(_bid)
         }
 
       case None =>
-        invites(invitedPlayer) = bid
-        Some(bid)
+        invites(invitedPlayer) = invite
+        Some(invite)
     }
   }
 
   def RemoveInvite(invitedPlayer : Long) : Option[Invitation] = {
-    invites.remove(invitedPlayer)
+    invites.remove(invitedPlayer) match {
+      case out @ Some(invite) =>
+        previousInvites += invitedPlayer -> invite
+        out
+      case None =>
+        None
+    }
   }
 
   def RemoveQueuedInvites(invitedPlayer : Long) : List[Invitation] = {
     queuedInvites.remove(invitedPlayer) match {
-      case Some(_bidList) => _bidList.toList
+      case Some(_bidList) => _bidList
       case None => Nil
     }
   }
@@ -1032,6 +1378,7 @@ class SquadService extends Actor {
   }
 
   def NextInvite(invitedPlayer : Long) : Option[Invitation] = {
+    previousInvites.remove(invitedPlayer)
     invites.get(invitedPlayer) match {
       case None =>
         queuedInvites.get(invitedPlayer) match {
@@ -1059,7 +1406,7 @@ class SquadService extends Actor {
 
   def HandleVacancyInvite(squad_guid : PlanetSideGUID, invitedPlayer : Long, invitingPlayer : Long, recruit : Player) : Option[(Squad, Int)] = {
     //accepted an invitation to join an existing squad
-    if(idToSquad.get(squad_guid).isEmpty) {
+    if(squadFeatures.get(squad_guid).isEmpty) {
       log.warn(s"Accept->Invite: the squad #${squad_guid.guid} no longer exists")
       None
     }
@@ -1068,16 +1415,15 @@ class SquadService extends Actor {
       None
     }
     else {
-      val squad = idToSquad(squad_guid)
+      val squad = squadFeatures(squad_guid).Squad
       if(!squad.AutoApproveInvitationRequests && squad.Leader.CharId != invitingPlayer) {
         //the inviting player was not the squad leader and this decision should be bounced off the squad leader
-        val bid = IndirectVacancy(recruit, squad_guid)
-        AddInvite(squad.Leader.CharId, bid) match {
-          case out @ Some(_) if out.contains(bid) =>
-            SquadEvents.publish(SquadServiceResponse(s"/$invitingPlayer/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitingPlayer, Some(invitedPlayer), recruit.Name, false, Some(None))))
-            HandleBidForPosition(bid, recruit)
-          case _ => ;
-        }
+        AltAddInviteAndRespond(
+          squad.Leader.CharId,
+          IndirectInvite(recruit, squad_guid),
+          invitingPlayer,
+          name = ""
+        )
         log.info(s"Accept->Invite: ${recruit.Name} must await an invitation from the leader of squad #${squad_guid.guid}")
         None
       }
@@ -1103,44 +1449,44 @@ class SquadService extends Actor {
 
   def InitialAssociation(squad : Squad) : Boolean = {
     val guid = squad.GUID
-    initialAssociation.indexOf(guid) match {
-      case -1 => ;
-      case index =>
-        initialAssociation.remove(index)
-        val charId = squad.Leader.CharId
-        SquadEvents.publish(
-          SquadServiceResponse(s"/$charId/Squad", SquadResponse.AssociateWithSquad(guid))
-        )
-        SquadEvents.publish(
-          SquadServiceResponse(s"/$charId/Squad", SquadResponse.Detail(
-            guid,
-            SquadService.Detail.Publish(squad))
-          )
+    if(squadFeatures(guid).InitialAssociation) {
+      squadFeatures(guid).InitialAssociation = false
+      val charId = squad.Leader.CharId
+      SquadEvents.publish(
+        SquadServiceResponse(s"/$charId/Squad", SquadResponse.AssociateWithSquad(guid))
+      )
+      SquadEvents.publish(
+        SquadServiceResponse(s"/$charId/Squad", SquadResponse.Detail(
+          guid,
+          SquadService.Detail.Publish(squad))
         )
+      )
     }
     false
   }
 
-  def HandleBidForPosition(bid : BidForPosition, player : Player) : Unit = {
-    HandleBidForPosition(bid, bid.squad_guid, bid.player.Name, player)
+  def HandleBidForRole(bid : BidForRole, player : Player) : Boolean = {
+    HandleBidForRole(bid, bid.squad_guid, bid.player.Name, player)
   }
-  def HandleBidForPosition(bid : IndirectVacancy, player : Player) : Unit = {
-    HandleBidForPosition(bid, bid.squad_guid, bid.player.Name, player)
+  def HandleBidForRole(bid : IndirectInvite, player : Player) : Boolean = {
+    HandleBidForRole(bid, bid.squad_guid, bid.player.Name, player)
   }
 
-  def HandleBidForPosition(bid : Invitation, squad_guid : PlanetSideGUID, name : String, player : Player) : Unit = {
-    idToSquad.get(squad_guid) match {
+  def HandleBidForRole(bid : Invitation, squad_guid : PlanetSideGUID, name : String, player : Player) : Boolean = {
+    GetSquad(squad_guid) match {
       case Some(squad) =>
         val leaderCharId = squad.Leader.CharId
         if(squad.AutoApproveInvitationRequests) {
-          self ! SquadServiceMessage(player, SquadAction.Membership(SquadRequestType.Accept, leaderCharId, None, "", None))
+          self ! SquadServiceMessage(player, Zone.Nowhere, SquadAction.Membership(SquadRequestType.Accept, leaderCharId, None, "", None))
         }
         else {
           SquadEvents.publish(SquadServiceResponse(s"/$leaderCharId/Squad", SquadResponse.WantsSquadPosition(name)))
         }
+        true
       case _ =>
         //squad is missing; will this properly short-circuit?
         log.error(s"Attempted to process ${bid.InviterName}'s bid for a position in a squad (id:${squad_guid.guid}) that does not exist")
+        false
     }
   }
 
@@ -1201,27 +1547,31 @@ class SquadService extends Actor {
     }
   }
 
-  def LeaveSquad(player : Player, squad : Squad) : Boolean = {
-    val charId = player.CharId
-    val membership = squad.Membership.zipWithIndex
-    membership.find { case (_member, _) => _member.CharId == charId } match {
-      case Some((member, index)) =>
-        val updateList = membership.collect({ case (_member, _index) if _member.CharId > 0 => (_member.CharId, _index) }).toList
-        //member leaves the squad completely
-        memberToSquad.remove(charId)
-        member.Name = ""
-        member.CharId = 0
-        sender ! SquadServiceResponse("", SquadResponse.Leave(squad, updateList))
-        //other squad members see the member leaving
-        val leavingMember = List((charId, index))
-        membership
-          .filter { case (_member, _) => _member.CharId > 0 }
-          .foreach { case (_member, _) =>
-            SquadEvents.publish( SquadServiceResponse(s"/${_member.CharId}/Squad", SquadResponse.Leave(squad, leavingMember)) )
-          }
-        true
-      case None =>
-        false
+  def RemoveInvitesForSquadAndPosition(guid : PlanetSideGUID, position : Int) : Unit = {
+    //eliminate active invites for this role
+    invites.collect {
+      case(charId, InviteForRole(_,_, sguid, pos)) if sguid == guid && pos == position =>
+        RemoveInvite(charId)
+      case (charId, BidForRole(_, sguid, pos)) if sguid == guid && pos == position =>
+        RemoveInvite(charId)
+    }
+    RemoveQueuedInvitesForSquadAndPosition(guid, position)
+  }
+
+  def RemoveQueuedInvitesForSquadAndPosition(guid : PlanetSideGUID, position : Int) : Unit = {
+    //eliminate other invites for this role
+    queuedInvites.foreach { case(charId, queue) =>
+      val filtered = queue.filterNot {
+        case InviteForRole(_,_, sguid, pos) => sguid == guid && pos == position
+        case BidForRole(_, sguid, pos) => sguid == guid && pos == position
+        case _ => false
+      }
+      if(filtered.isEmpty) {
+        queuedInvites.remove(charId)
+      }
+      else if(queue.size != filtered.size) {
+        queuedInvites += charId -> filtered
+      }
     }
   }
 
@@ -1230,7 +1580,7 @@ class SquadService extends Actor {
       case None =>
         true
       case Some(squad) if squad.Size == 1 =>
-        CloseOutSquad(squad)
+        CloseSquad(squad)
         true
       case _ =>
         log.warn(msg)
@@ -1238,27 +1588,169 @@ class SquadService extends Actor {
     }
   }
 
-  def CloseOutSquad(squad : Squad) : Unit = {
+  def LeaveSquad(charId : Long, squad : Squad) : Boolean = {
     val membership = squad.Membership.zipWithIndex
-    CloseOutSquad(
-      squad,
-      membership,
-      membership.collect({ case (_member, _index) if _member.CharId > 0 => (_member.CharId, _index) }).toList
-    )
+    membership.find { case (_member, _) => _member.CharId == charId } match {
+      case Some((member, index)) =>
+        val entry = (charId, index)
+        val updateList = entry +: membership
+          .collect { case (_member, _index) if _member.CharId > 0 && _member.CharId != charId => (_member.CharId, _index) }
+          .toList
+        //member leaves the squad completely
+        memberToSquad.remove(charId)
+        member.Name = ""
+        member.CharId = 0
+        SquadEvents.publish( SquadServiceResponse(s"/$charId/Squad", SquadResponse.Leave(squad, updateList)))
+        //other squad members see the member leaving
+        val leavingMember = List(entry)
+        membership
+          .filter { case (_member, _) => _member.CharId > 0 }
+          .foreach { case (_member, _) =>
+            SquadEvents.publish( SquadServiceResponse(s"/${_member.CharId}/Squad", SquadResponse.Leave(squad, leavingMember)) )
+          }
+        UpdateSquadListWhenListed(squad, SquadInfo().Size(squad.Size))
+        UpdateSquadDetail(squad.GUID, squad,
+          SquadDetail().Members(List(SquadPositionEntry(index, SquadPositionDetail().Player(char_id = 0, name = ""))))
+        )
+        true
+      case None =>
+        false
+    }
   }
 
-  def CloseOutSquad(squad : Squad, membership : Iterable[(Member, Int)], updateList : List[(Long, Int)]) : Unit = {
-    membership.foreach {
-      case (member, _) =>
-        val charId = member.CharId
+  def CloseSquad(squad : Squad) : Unit = {
+    val guid = squad.GUID
+    val membership = squad.Membership.zipWithIndex
+    val (updateMembers, updateIndices) = membership
+      .collect { case (member, index) if member.CharId > 0 => ((member, member.CharId, index), (member.CharId, index)) }
+      .unzip
+    val updateIndicesList = updateIndices.toList
+    val completelyBlankSquadDetail = SquadDetail().Complete
+    updateMembers
+      .foreach { case (member, charId, index) =>
+        memberToSquad.remove(charId)
         member.Name = ""
         member.CharId = 0L
-        memberToSquad.remove(charId)
-        SquadEvents.publish( SquadServiceResponse(s"/$charId/Squad", SquadResponse.Leave(squad, updateList)) )
+        SquadEvents.publish( SquadServiceResponse(s"/$charId/Squad", SquadResponse.Leave(squad,
+          updateIndicesList.filterNot { case (_, outIndex) => outIndex == index } :+ (charId, index) //we need to be last to leave to see the events
+        )) )
+        SquadEvents.publish( SquadServiceResponse(s"/$charId/Squad", SquadResponse.AssociateWithSquad(PlanetSideGUID(0))) )
+        SquadEvents.publish( SquadServiceResponse(s"/$charId/Squad", SquadResponse.Detail(PlanetSideGUID(0), completelyBlankSquadDetail)) )
+      }
+    UpdateSquadListWhenListed(squad, None)
+    CleanupInvitesForSquad(guid)
+    squadFeatures.remove(guid).get.Stop
+    TryResetSquadId()
+  }
+
+  def DisbandSquad(squad : Squad) : Unit = {
+    CloseSquad(squad)
+    val leader = squad.Leader.CharId
+    SquadEvents.publish(SquadServiceResponse(s"/$leader/Squad", SquadResponse.Membership(SquadResponseType.Disband, 0, 0, leader, None, "", true, Some(None))))
+    squad.Membership
+      .collect { case member if member.CharId > 0 && member.CharId != leader => member.CharId }
+      .foreach { charId =>
+        SquadEvents.publish(SquadServiceResponse(s"/$charId/Squad", SquadResponse.Membership(SquadResponseType.Disband, 0, 0, charId, None, "", false, Some(None))))
+      }
+  }
+
+  def CleanupInvitesForSquad(squadGUID : PlanetSideGUID) : Unit = {
+    //clean up invites
+    invites.collect {
+      case (id, VacancyInvite(_, _, guid)) if squadGUID == guid =>
+        RemoveInvite(id)
+      case (id, IndirectInvite(_, guid)) if squadGUID == guid =>
+        RemoveInvite(id)
+      case (id, InviteForRole(_, _, guid, _)) if squadGUID == guid =>
+        RemoveInvite(id)
+      case (id, BidForRole(_, guid, _)) if squadGUID == guid =>
+        RemoveInvite(id)
+      case (id, ProximityInvite(_, _, guid)) if squadGUID == guid =>
+        RemoveInvite(id)
+    }
+    //tidy the queued invitations
+    queuedInvites.foreach { case(id, queue) =>
+      val filteredQueue = queue.filterNot {
+        case VacancyInvite(_, _, guid) => squadGUID == guid
+        case IndirectInvite(_, guid) => squadGUID == guid
+        case InviteForRole(_, _, guid, _) => squadGUID == guid
+        case BidForRole(_, guid, _) => squadGUID == guid
+        case ProximityInvite(_, _, guid) => squadGUID == guid
+        case _ => false
+      }
+      if(filteredQueue.isEmpty) {
+        queuedInvites.remove(id)
+      }
+      else if(filteredQueue.size != queue.size) {
+        queuedInvites.update(id, filteredQueue)
+      }
+    }
+    squadFeatures(squadGUID).SearchForRole match {
+      case None => ;
+      case Some(_) =>
+        squadFeatures(squadGUID).SearchForRole = None
+    }
+  }
+
+  def CleanupInvitesFromPlayer(charId : Long) : Unit = {
+    invites.remove(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, InviteForRole(_charId, _, _, _)) if _charId == charId =>
+        RemoveInvite(id)
+      case (id, BidForRole(player, _, _)) if player.CharId == charId =>
+        RemoveInvite(id)
+      case (id, ProximityInvite(_charId, _, _)) if 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 InviteForRole(player, _, _, _) => player == charId
+        case BidForRole(player, _, _) => player.CharId == charId
+        case ProximityInvite(_charId, _, _) => _charId == charId
+        case _ => false
+      }
+      if(filteredQueue.isEmpty) {
+        queuedInvites.remove(id)
+      }
+      else if(filteredQueue.size != queue.size) {
+        queuedInvites.update(id, filteredQueue)
+      }
+    }
+    previousInvites.remove(charId)
+  }
+
+  def CleanupInvitesToPosition(position : Int) : Unit = {
+    invites.collect {
+      case (id, InviteForRole(_, _, _, _position)) if _position == position =>
+        RemoveInvite(id)
+      case (id, BidForRole(_, _, _position)) if _position == position =>
+        RemoveInvite(id)
+    }
+    //tidy the queued invitations
+    queuedInvites.foreach { case(id, queue) =>
+      val filteredQueue = queue.filterNot {
+        case InviteForRole(_, _, _, _position) => _position == position
+        case BidForRole(_, _, _position) => _position == position
+        case _ => false
+      }
+      if(filteredQueue.isEmpty) {
+        queuedInvites.remove(id)
+      }
+      else if(filteredQueue.size != queue.size) {
+        queuedInvites.update(id, filteredQueue)
+      }
     }
-    idToSquad.remove(squad.GUID)
-    waypoints.remove(squad.GUID)
-    UpdateSquadList(squad, None)
   }
 
   def SwapMemberPosition(squad : Squad, toMember : Member, fromMember : Member) : Unit = {
@@ -1375,18 +1867,7 @@ class SquadService extends Actor {
   }
 
   def AddWaypoint(guid : PlanetSideGUID, waypointType : Int, info : WaypointInfo) : Option[WaypointData] = {
-    (waypoints.get(guid) match {
-      case Some(array) =>
-        array
-      case None if idToSquad.get(guid).nonEmpty =>
-        log.debug(s"initializing squad waypoint system for squad #${guid.guid}")
-        val array = Array.fill[WaypointData](5)(new WaypointData())
-        waypoints(guid) = array
-        array
-      case _ =>
-        log.warn(s"squad #${guid.guid} does not currently exist so it can not render waypoints")
-        Array.empty[WaypointData]
-    }).lift(waypointType) match {
+    squadFeatures(guid).Waypoints.lift(waypointType) match {
       case Some(point) =>
         //update the waypoint
         log.debug(s"rendering squad waypoint $waypointType for squad #${guid.guid}")
@@ -1400,25 +1881,28 @@ class SquadService extends Actor {
   }
 
   def RemoveWaypoint(guid : PlanetSideGUID, waypointType : Int) : Unit = {
-    (waypoints.get(guid) match {
-      case Some(array) =>
-        array
-      case None =>
-        Array.empty[WaypointData]
-    }).lift(waypointType) match {
-      case Some(point) =>
-        //update the waypoint
-        log.debug(s"removing squad waypoint $waypointType for squad #${guid.guid}")
-        point.zone_number = 1
-        point.pos = Vector3.z(1)
-      case _ => ;
+    squadFeatures.get(guid) match {
+      case Some(features) =>
+        features.Waypoints.lift(waypointType) match {
+          case Some(point) =>
+            //update the waypoint
+            log.debug(s"removing squad waypoint $waypointType for squad #${guid.guid}")
+            point.zone_number = 1
+            point.pos = Vector3.z(1)
+          case _ =>
+            log.warn(s"no squad waypoint $waypointType found")
+        }
+      case _ =>
+        log.warn(s"no squad #$guid found")
     }
   }
 
   def InitWaypoints(toCharId : Long, guid : PlanetSideGUID) : Unit = {
-    (idToSquad.get(guid), waypoints.get(guid)) match {
-      case (Some(squad), Some(list)) =>
-        val vz1 = Vector3.z(1)
+    squadFeatures.get(guid) match {
+      case Some(features) =>
+        val squad = features.Squad
+        val vz1 = Vector3.z(value = 1)
+        val list = features.Waypoints
         SquadEvents.publish(
           SquadServiceResponse(s"/$toCharId/Squad", SquadResponse.InitWaypoints(squad.Leader.CharId,
             list.zipWithIndex.collect { case (point, index) if point.pos != vz1 =>
@@ -1426,7 +1910,81 @@ class SquadService extends Actor {
             }
           ))
         )
-      case _ => ;
+      case None => ;
+    }
+  }
+
+  def indirectInviteResp(bid : IndirectInvite, player : Player, invitedPlayer : Long, invitingPlayer : Long, name : String) : Boolean = {
+    HandleBidForRole(bid, player)
+  }
+
+  def altIndirectInviteResp(bid : IndirectInvite, player : Player, invitedPlayer : Long, invitingPlayer : Long, name : String) : Boolean = {
+    SquadEvents.publish(SquadServiceResponse(s"/$invitingPlayer/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitingPlayer, Some(invitedPlayer), player.Name, false, Some(None))))
+    HandleBidForRole(bid, player)
+  }
+
+  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, _) =>
+          SquadEvents.publish(SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, charId, Some(invitedPlayer), _name, false, Some(None))))
+          SquadEvents.publish(SquadServiceResponse(s"/$charId/Squad", 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
+          SquadEvents.publish(SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, bidInvitingPlayer, Some(invitedPlayer), player.Name, false, Some(None))))
+          SquadEvents.publish(SquadServiceResponse(s"/$bidInvitingPlayer/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, invitedPlayer, Some(bidInvitingPlayer), player.Name, true, Some(None))))
+
+        case _bid @ BidForRole(player, _, _) =>
+          HandleBidForRole(_bid, player)
+
+        case InviteForRole(charId, _name, _, _) =>
+          SquadEvents.publish(SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, invitedPlayer, Some(charId), _name, false, Some(None))))
+
+        case ProximityInvite(charId, _name, _) =>
+          SquadEvents.publish(SquadServiceResponse(s"/$invitedPlayer/Squad", 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")
+      }
+    }
+  }
+
+  def AddInviteAndRespond(invitedPlayer : Long, targetInvite : Invitation, invitingPlayer : Long, name : String) : Unit = {
+    InviteResponseTemplate(indirectInviteResp)(
+      targetInvite,
+      AddInvite(invitedPlayer, targetInvite),
+      invitedPlayer,
+      invitingPlayer,
+      name
+    )
+  }
+
+  def AltAddInviteAndRespond(invitedPlayer : Long, targetInvite : Invitation, invitingPlayer : Long, name : String) : Unit = {
+    InviteResponseTemplate(altIndirectInviteResp)(
+      targetInvite,
+      AddInvite(invitedPlayer, targetInvite),
+      invitedPlayer,
+      invitingPlayer,
+      name
+    )
+  }
+
+  def NextInviteAndRespond(invitedPlayer : Long) : Unit = {
+    NextInvite(invitedPlayer) match {
+      case Some(invite) =>
+        InviteResponseTemplate(indirectInviteResp)(
+          invite,
+          Some(invite),
+          invitedPlayer,
+          invite.InviterCharId,
+          invite.InviterName
+        )
+      case None => ;
     }
   }
 }
@@ -1444,25 +2002,57 @@ object SquadService {
 
   /**
     * Utilized when one player attempts to join an existing squad in a specific role.
-    * @param player na
-    * @param squad_guid na
-    * @param position na
+    * 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 BidForPosition(player : Player, squad_guid : PlanetSideGUID, position : Int)
+  final case class BidForRole(player : Player, squad_guid : PlanetSideGUID, position : Int)
     extends Invitation(player.CharId, player.Name)
 
   /**
     * Utilized when one squad member issues an invite for some other player.
-    * @param char_id na
-    * @param name na
-    * @param squad_guid na
+    * 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)
 
-  final case class IndirectVacancy(player : Player, squad_guid : PlanetSideGUID)
+  /**
+    * 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 InviteForRole(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
@@ -1470,6 +2060,104 @@ object SquadService {
   final case class SpontaneousInvite(player : Player)
     extends Invitation(player.CharId, player.Name)
 
+  class SquadFeatures(val Squad : Squad) {
+    /**
+      * `initialAssociation` per squad is similar to "Does this squad want to recruit members?"
+      * The squad does not have to be flagged.
+      * Dispatches an `AssociateWithSquad` `SDAM` to the squad leader and ???
+      * and then a `SDDUM` that includes at least the squad owner name and char id.
+      * Dispatched only once when a squad is first listed
+      * or when the squad leader searches for recruits by proximity or for certain roles or by invite
+      * or when a spontaneous squad forms,
+      * whichever happens first.
+      * Additionally, the packets are also sent when the check is made when the continent is changed (or set).
+      */
+    private var initialAssociation : Boolean = true
+    /**
+      * na
+      */
+    private var switchboard : ActorRef = ActorRef.noSender
+    /**
+      * Waypoint data.
+      * The first four slots are used for squad waypoints.
+      * The fifth slot is used for the squad leader experience waypoint.
+      * @see `Start`
+      */
+    private var waypoints : Array[WaypointData] = Array[WaypointData]()
+    /**
+      * The particular position being recruited right at the moment.
+      * When `None`. no highlighted searches have been indicated.
+      * When a positive integer or 0, indicates distributed `InviteForRole` messages as recorded by `proxyInvites`.
+      * Only one position may bne actively recruited at a time in this case.
+      * When -1, indicates distributed `ProximityIvite` messages as recorded by `proxyInvites`.
+      * Previous efforts may or may not be forgotten if there is a switch between the two modes.
+      */
+    private var searchForRole : Option[Int] = None
+    /**
+      * Handle persistent data related to `ProximityInvite` and `InviteForRole` messages
+      */
+    private var proxyInvites : List[Long] = Nil
+    /**
+      * These useres rejected invitation to this 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.
+      */
+    private var refusedPlayers : List[Long] = Nil
+
+    def Start(implicit context : ActorContext) : SquadFeatures = {
+      switchboard = context.actorOf(Props[SquadSwitchboard], s"squad${Squad.GUID.guid}")
+      waypoints = Array.fill[WaypointData](5)(new WaypointData())
+      this
+    }
+
+    def Stop : SquadFeatures = {
+      switchboard ! akka.actor.PoisonPill
+      switchboard = Actor.noSender
+      waypoints = Array.empty
+      this
+    }
+
+    def InitialAssociation : Boolean = initialAssociation
+
+    def InitialAssociation_=(assoc : Boolean) : Boolean = {
+      initialAssociation = assoc
+      InitialAssociation
+    }
+
+    def Switchboard : ActorRef = switchboard
+
+    def Waypoints : Array[WaypointData] = waypoints
+
+    def SearchForRole : Option[Int] = searchForRole
+
+    def SearchForRole_=(role : Int) : Option[Int] = SearchForRole_=(Some(role))
+
+    def SearchForRole_=(role : Option[Int]) : Option[Int] = {
+      searchForRole = role
+      SearchForRole
+    }
+
+    def ProxyInvites : List[Long] = proxyInvites
+
+    def ProxyInvites_=(list : List[Long]) : List[Long] = {
+      proxyInvites = list
+      ProxyInvites
+    }
+
+    def Refuse : List[Long] = refusedPlayers
+
+    def Refuse_=(charId : Long) : List[Long] = {
+      Refuse_=(List(charId))
+    }
+
+    def Refuse_=(list : List[Long]) : List[Long] = {
+      refusedPlayers = list ++ refusedPlayers
+      Refuse
+    }
+  }
+
   object SquadList {
     def Publish(squad : Squad) : SquadInfo = {
       SquadInfo(
diff --git a/common/src/main/scala/services/teamwork/SquadServiceMessage.scala b/common/src/main/scala/services/teamwork/SquadServiceMessage.scala
index 1e6c5559..c1826303 100644
--- a/common/src/main/scala/services/teamwork/SquadServiceMessage.scala
+++ b/common/src/main/scala/services/teamwork/SquadServiceMessage.scala
@@ -2,8 +2,9 @@
 package services.teamwork
 
 import net.psforever.objects.Player
+import net.psforever.objects.zones.Zone
 
-final case class SquadServiceMessage(tplayer : Player, actionMessage : Any)
+final case class SquadServiceMessage(tplayer : Player, zone : Zone, actionMessage : Any)
 
 object SquadServiceMessage {
   final case class RecoverSquadMembership()
diff --git a/common/src/test/scala/game/SquadMembershipResponseTest.scala b/common/src/test/scala/game/SquadMembershipResponseTest.scala
index c640df04..e2727aee 100644
--- a/common/src/test/scala/game/SquadMembershipResponseTest.scala
+++ b/common/src/test/scala/game/SquadMembershipResponseTest.scala
@@ -3,7 +3,7 @@ package game
 
 import net.psforever.packet._
 import net.psforever.packet.game._
-import net.psforever.types.SquadRequestType
+import net.psforever.types.SquadResponseType
 import org.specs2.mutable._
 import scodec.bits._
 
@@ -30,7 +30,7 @@ class SquadMembershipResponseTest extends Specification {
     "decode (0-1)" in {
       PacketCoding.DecodePacket(string_01).require match {
         case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) =>
-          unk1 mustEqual SquadRequestType.Invite
+          unk1 mustEqual SquadResponseType.Invite
           unk2 mustEqual 0
           unk3 mustEqual 0
           unk4 mustEqual 42771010L
@@ -46,7 +46,7 @@ class SquadMembershipResponseTest extends Specification {
     "decode (0-2)" in {
       PacketCoding.DecodePacket(string_02).require match {
         case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) =>
-          unk1 mustEqual SquadRequestType.Invite
+          unk1 mustEqual SquadResponseType.Invite
           unk2 mustEqual 0
           unk3 mustEqual 0
           unk4 mustEqual 1176612L
@@ -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 SquadRequestType.Unk01
+          unk1 mustEqual SquadResponseType.Unk01
           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 SquadRequestType.Unk01
+          unk1 mustEqual SquadResponseType.Unk01
           unk2 mustEqual 18
           unk3 mustEqual 0
           unk4 mustEqual 41578085L
@@ -94,7 +94,7 @@ class SquadMembershipResponseTest extends Specification {
     "decode (2-1)" in {
       PacketCoding.DecodePacket(string_21).require match {
         case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) =>
-          unk1 mustEqual SquadRequestType.Accept
+          unk1 mustEqual SquadResponseType.Accept
           unk2 mustEqual 0
           unk3 mustEqual 0
           unk4 mustEqual 1300870L
@@ -110,7 +110,7 @@ class SquadMembershipResponseTest extends Specification {
     "decode (2-2)" in {
       PacketCoding.DecodePacket(string_22).require match {
         case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) =>
-          unk1 mustEqual SquadRequestType.Accept
+          unk1 mustEqual SquadResponseType.Accept
           unk2 mustEqual 0
           unk3 mustEqual 0
           unk4 mustEqual 1300870L
@@ -126,7 +126,7 @@ class SquadMembershipResponseTest extends Specification {
     "decode (3-1)" in {
       PacketCoding.DecodePacket(string_31).require match {
         case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) =>
-          unk1 mustEqual SquadRequestType.Reject
+          unk1 mustEqual SquadResponseType.Reject
           unk2 mustEqual 0
           unk3 mustEqual 3
           unk4 mustEqual 31035057L
@@ -142,7 +142,7 @@ class SquadMembershipResponseTest extends Specification {
     "decode (3-2)" in {
       PacketCoding.DecodePacket(string_32).require match {
         case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) =>
-          unk1 mustEqual SquadRequestType.Reject
+          unk1 mustEqual SquadResponseType.Reject
           unk2 mustEqual 0
           unk3 mustEqual 2
           unk4 mustEqual 31106913L
@@ -158,7 +158,7 @@ class SquadMembershipResponseTest extends Specification {
     "decode (4-1)" in {
       PacketCoding.DecodePacket(string_41).require match {
         case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) =>
-          unk1 mustEqual SquadRequestType.Cancel
+          unk1 mustEqual SquadResponseType.Cancel
           unk2 mustEqual 0
           unk3 mustEqual 2
           unk4 mustEqual 41578085L
@@ -174,7 +174,7 @@ class SquadMembershipResponseTest extends Specification {
     "decode (4-2)" in {
       PacketCoding.DecodePacket(string_42).require match {
         case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) =>
-          unk1 mustEqual SquadRequestType.Cancel
+          unk1 mustEqual SquadResponseType.Cancel
           unk2 mustEqual 0
           unk3 mustEqual 2
           unk4 mustEqual 41607396L
@@ -190,7 +190,7 @@ class SquadMembershipResponseTest extends Specification {
     "decode (5-1)" in {
       PacketCoding.DecodePacket(string_51).require match {
         case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) =>
-          unk1 mustEqual SquadRequestType.Leave
+          unk1 mustEqual SquadResponseType.Leave
           unk2 mustEqual 0
           unk3 mustEqual 1
           unk4 mustEqual 1176612L
@@ -206,7 +206,7 @@ class SquadMembershipResponseTest extends Specification {
     "decode (7-1)" in {
       PacketCoding.DecodePacket(string_71).require match {
         case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) =>
-          unk1 mustEqual SquadRequestType.Disband
+          unk1 mustEqual SquadResponseType.Disband
           unk2 mustEqual 2
           unk3 mustEqual 0
           unk4 mustEqual 1176612L
@@ -222,7 +222,7 @@ class SquadMembershipResponseTest extends Specification {
     "decode (7-2)" in {
       PacketCoding.DecodePacket(string_72).require match {
         case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) =>
-          unk1 mustEqual SquadRequestType.Disband
+          unk1 mustEqual SquadResponseType.Disband
           unk2 mustEqual 0
           unk3 mustEqual 0
           unk4 mustEqual 41578085L
@@ -238,7 +238,7 @@ class SquadMembershipResponseTest extends Specification {
     "decode (8-1)" in {
       PacketCoding.DecodePacket(string_81).require match {
         case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) =>
-          unk1 mustEqual SquadRequestType.PlatoonInvite
+          unk1 mustEqual SquadResponseType.PlatoonInvite
           unk2 mustEqual 0
           unk3 mustEqual 0
           unk4 mustEqual 30910985L
@@ -254,7 +254,7 @@ class SquadMembershipResponseTest extends Specification {
     "decode (9-1)" in {
       PacketCoding.DecodePacket(string_91).require match {
         case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) =>
-          unk1 mustEqual SquadRequestType.PlatoonAccept
+          unk1 mustEqual SquadResponseType.PlatoonAccept
           unk2 mustEqual 0
           unk3 mustEqual 0
           unk4 mustEqual 297025L
@@ -270,7 +270,7 @@ class SquadMembershipResponseTest extends Specification {
     "decode (9-2)" in {
       PacketCoding.DecodePacket(string_92).require match {
         case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) =>
-          unk1 mustEqual SquadRequestType.PlatoonAccept
+          unk1 mustEqual SquadResponseType.PlatoonAccept
           unk2 mustEqual 0
           unk3 mustEqual 0
           unk4 mustEqual 30910985L
@@ -286,7 +286,7 @@ class SquadMembershipResponseTest extends Specification {
     "decode (b-1)" in {
       PacketCoding.DecodePacket(string_b1).require match {
         case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) =>
-          unk1 mustEqual SquadRequestType.PlatoonCancel
+          unk1 mustEqual SquadResponseType.PlatoonCancel
           unk2 mustEqual 0
           unk3 mustEqual 1
           unk4 mustEqual 30910985L
@@ -300,119 +300,119 @@ class SquadMembershipResponseTest extends Specification {
     }
 
     "encode (0-1)" in {
-      val msg = SquadMembershipResponse(SquadRequestType.Invite, 0, 0, 42771010L, Some(1300870L), "HofD", false, None)
+      val msg = SquadMembershipResponse(SquadResponseType.Invite, 0, 0, 42771010L, Some(1300870L), "HofD", false, None)
       val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
       pkt mustEqual string_01
     }
 
     "encode (0-2)" in {
-      val msg = SquadMembershipResponse(SquadRequestType.Invite, 0, 0, 1176612L, Some(1004937L), "SPOILERS", true, None)
+      val msg = SquadMembershipResponse(SquadResponseType.Invite, 0, 0, 1176612L, Some(1004937L), "SPOILERS", true, None)
       val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
       pkt mustEqual string_02
     }
 
     "encode (1-1)" in {
-      val msg = SquadMembershipResponse(SquadRequestType.Unk01, 19, 0, 41530025L, Some(0L), "", true, Some(None))
+      val msg = SquadMembershipResponse(SquadResponseType.Unk01, 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(SquadRequestType.Unk01, 18, 0, 41578085L, Some(0L), "", true, Some(None))
+      val msg = SquadMembershipResponse(SquadResponseType.Unk01, 18, 0, 41578085L, Some(0L), "", true, Some(None))
       val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
       pkt mustEqual string_12
     }
 
     "encode (2-1)" in {
-      val msg = SquadMembershipResponse(SquadRequestType.Accept, 0, 0, 1300870L, Some(42771010L), "VirusGiver", true, Some(None))
+      val msg = SquadMembershipResponse(SquadResponseType.Accept, 0, 0, 1300870L, Some(42771010L), "VirusGiver", true, Some(None))
       val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
       pkt mustEqual string_21
     }
 
     "encode (2-2)" in {
-      val msg = SquadMembershipResponse(SquadRequestType.Accept, 0, 0, 1300870L, Some(30736877L), "VirusGiver", true, Some(None))
+      val msg = SquadMembershipResponse(SquadResponseType.Accept, 0, 0, 1300870L, Some(30736877L), "VirusGiver", true, Some(None))
       val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
       pkt mustEqual string_22
     }
 
     "encode (3-1)" in {
-      val msg = SquadMembershipResponse(SquadRequestType.Reject, 0, 3, 31035057L, Some(42771010L), "HMFIC", false, Some(None))
+      val msg = SquadMembershipResponse(SquadResponseType.Reject, 0, 3, 31035057L, Some(42771010L), "HMFIC", false, Some(None))
       val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
       pkt mustEqual string_31
     }
 
     "encode (3-2)" in {
-      val msg = SquadMembershipResponse(SquadRequestType.Reject, 0, 2, 31106913L, Some(42771010L), "DAN1111", false, Some(None))
+      val msg = SquadMembershipResponse(SquadResponseType.Reject, 0, 2, 31106913L, Some(42771010L), "DAN1111", false, Some(None))
       val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
       pkt mustEqual string_32
     }
 
     "encode (4-1)" in {
-      val msg = SquadMembershipResponse(SquadRequestType.Cancel, 0, 2, 41578085L, Some(41607133L), "SAraisVanu", false, Some(None))
+      val msg = SquadMembershipResponse(SquadResponseType.Cancel, 0, 2, 41578085L, Some(41607133L), "SAraisVanu", false, Some(None))
       val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
       pkt mustEqual string_41
     }
 
     "encode (4-2)" in {
-      val msg = SquadMembershipResponse(SquadRequestType.Cancel, 0, 2, 41607396L, Some(41324011L), "AirInjector", false, Some(None))
+      val msg = SquadMembershipResponse(SquadResponseType.Cancel, 0, 2, 41607396L, Some(41324011L), "AirInjector", false, Some(None))
       val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
       pkt mustEqual string_42
     }
 
     "encode (5-1)" in {
-      val msg = SquadMembershipResponse(SquadRequestType.Leave, 0, 1, 1176612L, Some(1176612L), "CCRIDER", true, Some(None))
+      val msg = SquadMembershipResponse(SquadResponseType.Leave, 0, 1, 1176612L, Some(1176612L), "CCRIDER", true, Some(None))
       val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
       pkt mustEqual string_51
     }
 
     "encode (7-1)" in {
-      val msg = SquadMembershipResponse(SquadRequestType.Disband, 2, 0, 1176612L, Some(0L), "", true, Some(None))
+      val msg = SquadMembershipResponse(SquadResponseType.Disband, 2, 0, 1176612L, Some(0L), "", true, Some(None))
       val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
       pkt mustEqual string_71
     }
 
     "encode (7-2)" in {
-      val msg = SquadMembershipResponse(SquadRequestType.Disband, 0, 0, 41578085L, Some(30910985L), "Wizkid45", true, Some(None))
+      val msg = SquadMembershipResponse(SquadResponseType.Disband, 0, 0, 41578085L, Some(30910985L), "Wizkid45", true, Some(None))
       val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
       pkt mustEqual string_72
     }
 
     "encode (8-1)" in {
-      val msg = SquadMembershipResponse(SquadRequestType.PlatoonInvite, 0, 0, 30910985L, Some(41578085L), "Wizkid45", false, Some(None))
+      val msg = SquadMembershipResponse(SquadResponseType.PlatoonInvite, 0, 0, 30910985L, Some(41578085L), "Wizkid45", false, Some(None))
       val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
       pkt mustEqual string_81
     }
 
     "encode (9-1)" in {
-      val msg = SquadMembershipResponse(SquadRequestType.PlatoonAccept, 0, 0, 297025L, Some(41605002L), "", true, Some(None))
+      val msg = SquadMembershipResponse(SquadResponseType.PlatoonAccept, 0, 0, 297025L, Some(41605002L), "", true, Some(None))
       val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
       pkt mustEqual string_91
     }
 
     "encode (9-2)" in {
-      val msg = SquadMembershipResponse(SquadRequestType.PlatoonAccept, 0, 0, 30910985L, Some(41578085L), "Wizkid45", false, Some(None))
+      val msg = SquadMembershipResponse(SquadResponseType.PlatoonAccept, 0, 0, 30910985L, Some(41578085L), "Wizkid45", false, Some(None))
       val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
       pkt mustEqual string_92
     }
 
     "encode (b-1)" in {
-      val msg = SquadMembershipResponse(SquadRequestType.PlatoonCancel, 0, 1, 30910985L, Some(41578085L), "SAraisVanu", false, Some(None))
+      val msg = SquadMembershipResponse(SquadResponseType.PlatoonCancel, 0, 1, 30910985L, Some(41578085L), "SAraisVanu", false, Some(None))
       val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
       pkt mustEqual string_b1
diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala
index 3fe21933..cacf4a97 100644
--- a/pslogin/src/main/scala/WorldSessionActor.scala
+++ b/pslogin/src/main/scala/WorldSessionActor.scala
@@ -155,6 +155,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
     galaxyService ! Service.Leave()
     LivePlayerList.Remove(sessionId)
     if(player != null && player.HasGUID) {
+      squadService ! Service.Leave(Some(player.CharId.toString))
       val player_guid = player.GUID
       //handle orphaned deployables
       DisownDeployables()
@@ -3561,7 +3562,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
           case None => false
         }
         avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlayerState(avatar_guid, msg, spectator, wepInHand))
-        //squadService ! SquadServiceMessage(tplayer, SquadAction.Update(tplayer.CharId, tplayer.Health, tplayer.MaxHealth, tplayer.Armor, tplayer.MaxArmor, pos, zone.Number))
+        //squadService ! SquadServiceMessage(tplayer, continent, SquadAction.Update(tplayer.CharId, tplayer.Health, tplayer.MaxHealth, tplayer.Armor, tplayer.MaxArmor, pos, zone.Number))
       }
 
     case msg @ ChildObjectStateMessage(object_guid, pitch, yaw) =>
@@ -5019,15 +5020,15 @@ class WorldSessionActor extends Actor with MDCContextAware {
 
     case msg @ SquadDefinitionActionMessage(u1, u2, action) =>
       log.info(s"SquadDefinitionAction: $msg")
-        squadService ! SquadServiceMessage(player, SquadServiceAction.Definition(continent, u1, u2, action))
+        squadService ! SquadServiceMessage(player, continent, SquadServiceAction.Definition(u1, u2, action))
 
     case msg @ SquadMembershipRequest(request_type, unk2, unk3, player_name, unk5) =>
       log.info(s"$msg")
-      squadService ! SquadServiceMessage(player, SquadServiceAction.Membership(request_type, unk2, unk3, player_name, unk5))
+      squadService ! SquadServiceMessage(player, continent, SquadServiceAction.Membership(request_type, unk2, unk3, player_name, unk5))
 
     case msg @ SquadWaypointRequest(request, _, wtype, unk, info) =>
       log.info(s"Waypoint Request: $msg")
-      squadService ! SquadServiceMessage(player, SquadServiceAction.Waypoint(request, wtype, unk, info))
+      squadService ! SquadServiceMessage(player, continent, SquadServiceAction.Waypoint(request, wtype, unk, info))
 
     case msg @ GenericCollisionMsg(u1, p, t, php, thp, pv, tv, ppos, tpos, u2, u3, u4) =>
       log.info("Ouch! " + msg)

From 2a93e6a8be6cd877f9a25a549e260ca6ac300a1d Mon Sep 17 00:00:00 2001
From: FateJH <Jason_DiDonato@yahoo.com>
Date: Tue, 1 Oct 2019 21:25:44 -0400
Subject: [PATCH 21/31] too much to itemize; still getting squads working, more
 or less

---
 .../scala/net/psforever/objects/Avatar.scala  |    9 +-
 .../psforever/objects/teamwork/Squad.scala    |   49 +-
 .../objects/teamwork/SquadFeatures.scala      |  154 +
 .../game/PlanetsideAttributeMessage.scala     |    5 +-
 .../game/SquadDefinitionActionMessage.scala   |    1 +
 .../scala/services/teamwork/SquadAction.scala |    5 +-
 .../services/teamwork/SquadResponse.scala     |    4 +-
 .../services/teamwork/SquadService.scala      | 3188 +++++++++++------
 .../teamwork/SquadServiceResponse.scala       |   11 +-
 .../src/main/scala/WorldSessionActor.scala    |  546 +--
 10 files changed, 2481 insertions(+), 1491 deletions(-)
 create mode 100644 common/src/main/scala/net/psforever/objects/teamwork/SquadFeatures.scala

diff --git a/common/src/main/scala/net/psforever/objects/Avatar.scala b/common/src/main/scala/net/psforever/objects/Avatar.scala
index 0416b6a9..4b689187 100644
--- a/common/src/main/scala/net/psforever/objects/Avatar.scala
+++ b/common/src/main/scala/net/psforever/objects/Avatar.scala
@@ -48,13 +48,8 @@ class Avatar(private val char_id : Long, val name : String, val faction : Planet
   private val deployables : DeployableToolbox = new DeployableToolbox
   /**
     * Looking For Squad:<br>
-    * Used to indicate both the marque that appears underneath a player's nameplate and an actual player state<br>
-    * This `Avatar`-specific "Looking for Squad" variable
-    * is used to indicate the active state of the LFS marque in the game
-    * and will change to `false` if the player is the member of a squad.
-    * A client-local version of "Looking for Squad" will maintain the real state of LFS
-    * once the player has joined a squad.
-    * The client-local version will restore the `Avatar`-local variable upon leaving the squad.
+    * Indicates both a player state and the text on the marquee under the player nameplate.
+    * Should only be valid when the player is not in a squad.
     */
   private var lfs : Boolean = false
 
diff --git a/common/src/main/scala/net/psforever/objects/teamwork/Squad.scala b/common/src/main/scala/net/psforever/objects/teamwork/Squad.scala
index 5f653dcd..3ff63035 100644
--- a/common/src/main/scala/net/psforever/objects/teamwork/Squad.scala
+++ b/common/src/main/scala/net/psforever/objects/teamwork/Squad.scala
@@ -11,11 +11,7 @@ class Squad(squadId : PlanetSideGUID, alignment : PlanetSideEmpire.Value) extend
   private var zoneId : Option[Int] = None
   private var task : String = ""
   private val membership : Array[Member] = Array.fill[Member](10)(new Member)
-  private val availability : Array[Boolean] = Array.fill[Boolean](10)(true)
-  private var listed : Boolean = false
-  private var leaderPositionIndex : Int = 0
-  private var autoApproveInvitationRequests : Boolean = false
-  private var locationFollowsSquadLead : Boolean = false
+  private val availability : Array[Boolean] = Array.fill[Boolean](10)(elem = true)
 
   override def GUID_=(d : PlanetSideGUID) : PlanetSideGUID = GUID
 
@@ -23,14 +19,7 @@ class Squad(squadId : PlanetSideGUID, alignment : PlanetSideEmpire.Value) extend
 
   def CustomZoneId : Boolean = zoneId.isDefined
 
-  def ZoneId : Int = zoneId.getOrElse({
-    membership.lift(leaderPositionIndex) match {
-      case Some(leader) =>
-        leader.ZoneId
-      case _ =>
-        0
-    }
-  })
+  def ZoneId : Int = zoneId.getOrElse(membership(0).ZoneId)
 
   def ZoneId_=(id : Int) : Int = {
     ZoneId_=(Some(id))
@@ -48,42 +37,12 @@ class Squad(squadId : PlanetSideGUID, alignment : PlanetSideEmpire.Value) extend
     Task
   }
 
-  def Listed : Boolean = listed
-
-  def Listed_=(announce : Boolean) : Boolean = {
-    listed = announce
-    Listed
-  }
-
-  def LocationFollowsSquadLead : Boolean = locationFollowsSquadLead
-
-  def LocationFollowsSquadLead_=(follow : Boolean) : Boolean = {
-    locationFollowsSquadLead = follow
-    LocationFollowsSquadLead
-  }
-
-  def AutoApproveInvitationRequests : Boolean = autoApproveInvitationRequests
-
-  def AutoApproveInvitationRequests_=(autoApprove : Boolean) : Boolean = {
-    autoApproveInvitationRequests = autoApprove
-    AutoApproveInvitationRequests
-  }
-
   def Membership : Array[Member] = membership
 
   def Availability : Array[Boolean] = availability
 
-  def LeaderPositionIndex : Int = leaderPositionIndex
-
-  def LeaderPositionIndex_=(position : Int) : Int = {
-    if(availability.lift(position).contains(true)) {
-      leaderPositionIndex = position
-    }
-    LeaderPositionIndex
-  }
-
   def Leader : Member = {
-    membership(leaderPositionIndex) match {
+    membership(0) match {
       case member if !member.Name.equals("") =>
         member
       case _ =>
@@ -102,9 +61,7 @@ object Squad {
     override def ZoneId_=(id : Int) : Int = 0
     override def ZoneId_=(id : Option[Int]) : Int = 0
     override def Task_=(assignment : String) : String =  ""
-    override def Listed_=(announce : Boolean) : Boolean = false
     override def Membership : Array[Member] = Array.empty[Member]
     override def Availability : Array[Boolean] = Array.fill[Boolean](10)(false)
-    override def LeaderPositionIndex_=(position : Int) : Int = 0
   }
 }
diff --git a/common/src/main/scala/net/psforever/objects/teamwork/SquadFeatures.scala b/common/src/main/scala/net/psforever/objects/teamwork/SquadFeatures.scala
new file mode 100644
index 00000000..a374f0ab
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/teamwork/SquadFeatures.scala
@@ -0,0 +1,154 @@
+// Copyright (c) 2019 PSForever
+package net.psforever.objects.teamwork
+
+import akka.actor.{Actor, ActorContext, ActorRef, Cancellable, Props}
+import net.psforever.objects.DefaultCancellable
+import services.teamwork.SquadService.WaypointData
+import services.teamwork.SquadSwitchboard
+
+class SquadFeatures(val Squad : Squad) {
+  /**
+    * `initialAssociation` per squad is similar to "Does this squad want to recruit members?"
+    * The squad does not have to be flagged.
+    * Dispatches an `AssociateWithSquad` `SDAM` to the squad leader and ???
+    * and then a `SDDUM` that includes at least the squad owner name and char id.
+    * Dispatched only once when a squad is first listed
+    * or when the squad leader searches for recruits by proximity or for certain roles or by invite
+    * or when a spontaneous squad forms,
+    * whichever happens first.
+    * Additionally, the packets are also sent when the check is made when the continent is changed (or set).
+    */
+  private var initialAssociation : Boolean = true
+  /**
+    * na
+    */
+  private var switchboard : ActorRef = ActorRef.noSender
+  /**
+    * Waypoint data.
+    * The first four slots are used for squad waypoints.
+    * The fifth slot is used for the squad leader experience waypoint.<br>
+    * <br>
+    * All of the waypoints constantly exist as long as the squad to which they are attached exists.
+    * They are merely "activated" and "deactivated."
+    * When "activated," the waypoint knows on which continent to appear and where on the map and in the game world to be positioned.
+    * Waypoints manifest in the game world as a 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 differentiating number where applicable.
+    * The squad leader experience rally, for example, does not have a number like the preceding four waypoints.
+    * @see `Start`
+    */
+  private var waypoints : Array[WaypointData] = Array[WaypointData]()
+  /**
+    * The particular position being recruited right at the moment.
+    * When `None`. no highlighted searches have been indicated.
+    * When a positive integer or 0, indicates distributed `LookingForSquadRoleInvite` messages as recorded by `proxyInvites`.
+    * Only one position may bne actively recruited at a time in this case.
+    * When -1, indicates distributed `ProximityIvite` messages as recorded by `proxyInvites`.
+    * Previous efforts may or may not be forgotten if there is a switch between the two modes.
+    */
+  private var searchForRole : Option[Int] = None
+  /**
+    * Handle persistent data related to `ProximityInvite` and `LookingForSquadRoleInvite` messages
+    */
+  private var proxyInvites : List[Long] = Nil
+
+  private var requestInvitePrompt : Cancellable = DefaultCancellable.obj
+  /**
+    * These useres rejected invitation to this 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.
+    */
+  private var refusedPlayers : List[Long] = Nil
+  private var autoApproveInvitationRequests : Boolean = true
+  private var locationFollowsSquadLead : Boolean = true
+
+  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}")
+    waypoints = Array.fill[WaypointData](5)(new WaypointData())
+    this
+  }
+
+  def Stop : SquadFeatures = {
+    switchboard ! akka.actor.PoisonPill
+    switchboard = Actor.noSender
+    waypoints = Array.empty
+    requestInvitePrompt.cancel
+    this
+  }
+
+  def InitialAssociation : Boolean = initialAssociation
+
+  def InitialAssociation_=(assoc : Boolean) : Boolean = {
+    initialAssociation = assoc
+    InitialAssociation
+  }
+
+  def Switchboard : ActorRef = switchboard
+
+  def Waypoints : Array[WaypointData] = waypoints
+
+  def SearchForRole : Option[Int] = searchForRole
+
+  def SearchForRole_=(role : Int) : Option[Int] = SearchForRole_=(Some(role))
+
+  def SearchForRole_=(role : Option[Int]) : Option[Int] = {
+    searchForRole = role
+    SearchForRole
+  }
+
+  def ProxyInvites : List[Long] = proxyInvites
+
+  def ProxyInvites_=(list : List[Long]) : List[Long] = {
+    proxyInvites = list
+    ProxyInvites
+  }
+
+  def Refuse : List[Long] = refusedPlayers
+
+  def Refuse_=(charId : Long) : List[Long] = {
+    Refuse_=(List(charId))
+  }
+
+  def Refuse_=(list : List[Long]) : List[Long] = {
+    refusedPlayers = list ++ refusedPlayers
+    Refuse
+  }
+
+  def LocationFollowsSquadLead : Boolean = locationFollowsSquadLead
+
+  def LocationFollowsSquadLead_=(follow : Boolean) : Boolean = {
+    locationFollowsSquadLead = follow
+    LocationFollowsSquadLead
+  }
+
+  def AutoApproveInvitationRequests : Boolean = autoApproveInvitationRequests
+
+  def AutoApproveInvitationRequests_=(autoApprove : Boolean) : Boolean = {
+    autoApproveInvitationRequests = autoApprove
+    AutoApproveInvitationRequests
+  }
+
+  def Listed : Boolean = listed
+
+  def Listed_=(announce : Boolean) : Boolean = {
+    listed = announce
+    Listed
+  }
+
+  def ToChannel : String = channel
+
+  def Prompt : Cancellable = requestInvitePrompt
+
+  def Prompt_=(callback: Cancellable) : Cancellable = {
+    if(requestInvitePrompt.isCancelled) {
+      requestInvitePrompt = callback
+    }
+    Prompt
+  }
+}
\ No newline at end of file
diff --git a/common/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala b/common/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala
index eec5c312..ac4b49ff 100644
--- a/common/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala
+++ b/common/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala
@@ -118,7 +118,10 @@ import scodec.codecs._
   * `27 - PA_JAMMED - plays jammed buzzing sound`<br>
   * `28 - PA_IMPLANT_ACTIVE - Plays implant sounds. Valid values seem to be up to 20.`<br>
   * `29 - PA_VAPORIZED - Visible ?! That's not the cloaked effect, Maybe for spectator mode ?. Value is 0 to visible, 1 to invisible.`<br>
-  * `31 - Info under avatar name : 0 = LFS, 1 = Looking For Squad Members`<br>
+  * `31 - Looking for Squad info (marquee and ui):<br>
+  * ` - 0 is LFS`<br>
+  * ` - 1 is LFSM (Looking for Squad Members)`<br>
+  * ` - n is the supplemental squad identifier number; same as "LFS;" for the leader, sets "LFSM" after the first manual flagging`<br>
   * `32 - Info under avatar name : 0 = Looking For Squad Members, 1 = LFS`<br>
   * `35 - BR. Value is the BR`<br>
   * `36 - CR. Value is the CR`<br>
diff --git a/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala b/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala
index 6fd99e69..eab055a9 100644
--- a/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala
+++ b/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala
@@ -360,6 +360,7 @@ object SquadAction{
   * &nbsp;&nbsp;&nbsp;&nbsp;`17` - Set List Squad (ui)<br>
   * &nbsp;&nbsp;&nbsp;&nbsp;`18` - UNKNOWN<br>
   * &nbsp;&nbsp;&nbsp;&nbsp;`26` - Reset All<br>
+  * &nbsp;&nbsp;&nbsp;&nbsp;`32` - UNKNOWN<br>
   * &nbsp;&nbsp;&nbsp;&nbsp;`35` - Cancel Squad Search<br>
   * &nbsp;&nbsp;&nbsp;&nbsp;`39` - No Squad Search Results<br>
   * &nbsp;&nbsp;&nbsp;&nbsp;`41` - Cancel Find<br>
diff --git a/common/src/main/scala/services/teamwork/SquadAction.scala b/common/src/main/scala/services/teamwork/SquadAction.scala
index 5b3ed90e..a0034da5 100644
--- a/common/src/main/scala/services/teamwork/SquadAction.scala
+++ b/common/src/main/scala/services/teamwork/SquadAction.scala
@@ -3,11 +3,14 @@ package services.teamwork
 
 import net.psforever.objects.zones.Zone
 import net.psforever.packet.game._
-import net.psforever.types.{SquadRequestType, Vector3}
+import net.psforever.types.{PlanetSideEmpire, SquadRequestType, Vector3}
 
 object SquadAction {
   trait Action
 
+  final case class InitSquadList() extends Action
+  final case class InitCharId() extends Action
+
   final case class Definition(guid : PlanetSideGUID, line : Int, action : SquadAction) extends Action
   final case class Membership(request_type : SquadRequestType.Value, unk2 : Long, unk3 : Option[Long], player_name : String, unk5 : Option[Option[String]]) extends Action
   final case class Waypoint(event_type : WaypointEventAction.Value, waypoint_type : Int, unk : Option[Long], waypoint_info : Option[WaypointInfo]) extends Action
diff --git a/common/src/main/scala/services/teamwork/SquadResponse.scala b/common/src/main/scala/services/teamwork/SquadResponse.scala
index 817d27e5..5860145c 100644
--- a/common/src/main/scala/services/teamwork/SquadResponse.scala
+++ b/common/src/main/scala/services/teamwork/SquadResponse.scala
@@ -20,8 +20,8 @@ object SquadResponse {
 
   final case class Membership(request_type : SquadResponseType.Value, unk1 : Int, unk2 : Int, unk3 : Long, unk4 : Option[Long], player_name : String, unk5 : Boolean, unk6 : Option[Option[String]]) extends Response //see SquadMembershipResponse
   final case class Invite(from_char_id : Long, to_char_id : Long, name : String) extends Response
-  final case class WantsSquadPosition(bid_name : String) extends Response
-  final case class Join(squad : Squad, positionsToUpdate : List[Int]) extends Response
+  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 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
diff --git a/common/src/main/scala/services/teamwork/SquadService.scala b/common/src/main/scala/services/teamwork/SquadService.scala
index 2c22886c..b837e7a1 100644
--- a/common/src/main/scala/services/teamwork/SquadService.scala
+++ b/common/src/main/scala/services/teamwork/SquadService.scala
@@ -1,11 +1,11 @@
 // Copyright (c) 2019 PSForever
 package services.teamwork
 
-import akka.actor.{Actor, ActorContext, ActorRef, Props}
-import net.psforever.objects.Player
+import akka.actor.{Actor, ActorRef, Terminated}
+import net.psforever.objects.{Avatar, LivePlayerList, Player}
 import net.psforever.objects.definition.converter.StatConverter
 import net.psforever.objects.loadouts.SquadLoadout
-import net.psforever.objects.teamwork.{Member, Squad}
+import net.psforever.objects.teamwork.{Member, Squad, SquadFeatures}
 import net.psforever.objects.zones.Zone
 import net.psforever.packet.game._
 import net.psforever.types._
@@ -15,8 +15,6 @@ import scala.collection.concurrent.TrieMap
 import scala.collection.mutable
 import scala.collection.mutable.ListBuffer
 
-//import scala.concurrent.duration._
-
 class SquadService extends Actor {
   import SquadService._
 
@@ -26,31 +24,120 @@ class SquadService extends Actor {
     * A squad of `PlanetSideGUID(0)` indicates both a nonexistent squad and the default no-squad for clients.
     */
   private var sid : Int = 1
-
-  private var memberToSquad : mutable.LongMap[Squad] = mutable.LongMap[Squad]()
-  private val invites : mutable.LongMap[Invitation] = mutable.LongMap[Invitation]()
-  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 invite is queued).
-    * Cleared when the next queued invite becomes active.
+    * All squads.<br>
+    * key - squad unique number; value - the squad wrapped around its attributes object
     */
-  private val previousInvites : mutable.LongMap[Invitation] = mutable.LongMap[Invitation]()
-
-  private var squadFeatures : TrieMap[PlanetSideGUID, SquadService.SquadFeatures] = new TrieMap[PlanetSideGUID, SquadService.SquadFeatures]()
-  private val publishedLists : TrieMap[PlanetSideEmpire.Value, ListBuffer[SquadInfo]] = TrieMap[PlanetSideEmpire.Value, ListBuffer[SquadInfo]](
+  private var 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
+    * and may have limited interaction with their squad definition windows.<br>
+    * key - squad unique number; value - the squad's unique identifier number
+    */
+  private val publishedLists : TrieMap[PlanetSideEmpire.Value, ListBuffer[PlanetSideGUID]] = TrieMap[PlanetSideEmpire.Value, ListBuffer[PlanetSideGUID]](
     PlanetSideEmpire.TR -> ListBuffer.empty,
     PlanetSideEmpire.NC -> ListBuffer.empty,
     PlanetSideEmpire.VS -> ListBuffer.empty
   )
 
+  /**
+    * 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]()
+  /**
+    * 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]]()
+  /**
+    * The gien player has refused participation into this other player's sqaud.<br>
+    * 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]]()
+  /**
+    * Players who are interested in updated details regarding a certain squad though they may not be a member of the squad.<br>
+    * 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 invite is queued).
+    * Cleared when the next queued invite becomes active.<br>
+    * 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.<br>
+    * key - a `PlanetSideEmpire` value; value - `ActorRef` reference<br>
+    * 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 who may become squad members.
+    * 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 `WorldSessionActor`.<br>
+    * 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 def debug(msg : String) : Unit = {
+    log.info(msg)
+  }
 
   override def preStart : Unit = {
     log.info("Starting...")
   }
 
+  override def postStop() : Unit = {
+    //invitations
+    invites.clear()
+    queuedInvites.clear()
+    previousInvites.clear()
+    refused.clear()
+    continueToMonitorDetails.clear()
+    //squads and members (users)
+    squadFeatures.foreach { case(_, features) =>
+      CloseSquad(features.Squad)
+    }
+    memberToSquad.clear()
+    publishedLists.clear()
+    UserEvents.foreach { case(_, actor) =>
+      SquadEvents.unsubscribe(actor)
+    }
+    UserEvents.clear()
+  }
+
+  /**
+    * Produce the next available unique squad identifier.
+    * The first number is always 1.
+    * The greatest possible identifier is 65535 (an unsigned 16-bit integer)
+    * before it wraps back around to 1.
+    * @return the current squad unique identifier number
+    */
   def GetNextSquadId() : PlanetSideGUID = {
     val out = sid
     val j = sid + 1
@@ -63,6 +150,10 @@ class SquadService extends Actor {
     PlanetSideGUID(out)
   }
 
+  /**
+    * Set the unique squad identifier back to the start (1) if no squads are active.
+    * @return `true`, if the identifier is reset; `false`, otherwise
+    */
   def TryResetSquadId() : Boolean = {
     if(squadFeatures.isEmpty) {
       sid = 1
@@ -73,233 +164,377 @@ class SquadService extends Actor {
     }
   }
 
-  def GetSquad(id : PlanetSideGUID) : Option[Squad] = {
-    squadFeatures.get(id) match {
-      case Some(features) => Some(features.Squad)
-      case None => None
-    }
+  /**
+    * If a squad exists for an identifier, return that squad.
+    * @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 GetParticipatingSquad(player : Player) : Option[Squad] = {
-    GetParticipatingSquad(player.CharId)
+  /**
+    * 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)
+
+  /**
+    * 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] = memberToSquad.get(charId) match {
+    case opt @ Some(_) =>
+      opt
+    case None =>
+      None
   }
 
-  def GetParticipatingSquad(charId : Long) : Option[Squad] = {
-    memberToSquad.get(charId) match {
-      case opt @ Some(_) =>
-        opt
-      case None =>
+  /**
+    * If this player is a member of any squad, discover that squad.
+    * @see `GetParticipatingSquad`
+    * @see `Squad::Leader`
+    * @param player the potential member
+    * @param opt an optional squad to check;
+    *            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)
+
+  /**
+    * If the player associated with this unique character identifier number is the leader of any squad, discover that squad.
+    * @see `GetParticipatingSquad`
+    * @see `Squad->Leader`
+    * @param charId the potential member identifier
+    * @param opt an optional squad to check;
+    *            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] = opt.orElse(GetParticipatingSquad(charId)) match {
+    case Some(squad) =>
+      if(squad.Leader.CharId == charId) {
+        Some(squad)
+      }
+      else {
         None
-    }
+      }
+    case _ =>
+      None
   }
 
-  def GetLeadingSquad(player : Player, opt : Option[Squad]) : Option[Squad] = {
-    val charId = player.CharId
-    opt match {
-      case Some(squad) =>
-        if(squad.Leader.CharId == charId) {
-          Some(squad)
-        }
-        else {
-          None
-        }
-
-      case None =>
-        memberToSquad.get(charId) match {
-          case Some(squad) if squad.Leader.CharId == charId =>
-            Some(squad)
-          case _ =>
-            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)
   }
-
-  def GetLeadingSquad(charId : Long, opt : Option[Squad]) : Option[Squad] = {
-    opt.orElse(memberToSquad.get(charId)) match {
-      case Some(squad) =>
-        if(squad.Leader.CharId == charId) {
-          Some(squad)
-        }
-        else {
-          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
+    * @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 _ =>
-        None
+        log.error(s"Publish(String): subscriber information is an unhandled format - $to")
     }
   }
-
-  def CreateSquad(player : Player) : Squad = {
-    val faction = player.Faction
-    val name = player.Name
-    val squad = new Squad(GetNextSquadId(), faction)
-    val leadPosition = squad.Membership(squad.LeaderPositionIndex)
-    leadPosition.Name = name
-    leadPosition.CharId = player.CharId
-    leadPosition.Health = player.Health
-    leadPosition.Armor = player.Armor
-    leadPosition.Position = player.Position
-    leadPosition.ZoneId = 1
-    log.info(s"$name-$faction has created a new squad")
-    squad
+  /**
+    * 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.error(s"Publish(Long): subscriber information can not be found - $to")
+    }
   }
-
-  def StartSquad(squad : Squad) : Squad = {
-    squadFeatures += squad.GUID -> new SquadService.SquadFeatures(squad).Start
-    memberToSquad += squad.Leader.CharId -> squad
-    squad
+  /**
+    * 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)
+    }
   }
-
-  def StartSquad(player : Player) : Squad = {
-    val squad = CreateSquad(player)
-    StartSquad(squad)
-    squad
+  /**
+    * 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")
   }
-
-  val SquadEvents = new GenericEventBus[SquadServiceResponse]
 
   def receive : Receive = {
     //subscribe to a faction's channel - necessary to receive updates about listed squads
     case Service.Join(faction) if "TRNCVS".indexOf(faction) > -1 =>
       val path = s"/$faction/Squad"
       val who = sender()
-      log.info(s"$who has joined $path")
+      debug(s"$who has joined $path")
       SquadEvents.subscribe(who, path)
-      //send initial squad catalog
-      sender ! SquadServiceResponse(s"/$faction/Squad", SquadResponse.InitList(publishedLists(PlanetSideEmpire(faction)).toVector))
 
     //subscribe to the player's personal channel - necessary for future and previous squad information
     case Service.Join(char_id) =>
-      val path = s"/$char_id/Squad"
-      val who = sender()
-      log.info(s"$who has joined $path")
-      SquadEvents.subscribe(who, path)
-      //check for renewable squad information
-      val longCharId = char_id.toLong
-      memberToSquad.get(longCharId) match {
-        case None => ;
-        case Some(squad) =>
-          val guid = squad.GUID
-          val indices = squad.Membership.zipWithIndex.collect({ case (member, index) if member.CharId != 0 => index }).toList
-          SquadEvents.publish(SquadServiceResponse(s"/$char_id/Squad", SquadResponse.AssociateWithSquad(guid)))
-          SquadEvents.publish(SquadServiceResponse(s"/$char_id/Squad", SquadResponse.Join(squad, indices)))
-          InitSquadDetail(guid, Seq(longCharId), squad)
-          InitWaypoints(longCharId, guid)
+      try {
+        val longCharId = char_id.toLong
+        val path = s"/$char_id/Squad"
+        val who = sender()
+        debug(s"$who has joined $path")
+        context.watch(who)
+        UserEvents += longCharId -> who
+        refused(longCharId) = Nil
+      }
+      catch {
+        case _ : ClassCastException =>
+          log.warn(s"Service.Join: tried $char_id as a unique character identifier, but it could not be casted")
+        case e : Exception =>
+          log.error(s"Service.Join: unexpected exception using $char_id as data - ${e.getLocalizedMessage}")
+          e.printStackTrace()
       }
 
-    case Service.Leave(Some(char_id)) =>
-      SquadEvents.unsubscribe(sender())
-      val longCharId = char_id.toLong
-      val pSquadOpt = GetParticipatingSquad(longCharId)
-      (pSquadOpt, GetLeadingSquad(longCharId, pSquadOpt)) match {
-        case (Some(_), Some(squad)) =>
-          //leader of a squad; the squad will be disbanded
-          DisbandSquad(squad)
-        case (Some(squad), None) if squad.Size == 2 =>
-          //one of the last two members of a squad; the squad will be disbanded
-          DisbandSquad(squad)
-        case (Some(squad), None) =>
-          //member of the squad; leave the squad
-          LeaveSquad(longCharId, squad)
-        case _ =>
-          //not a member of any squad; nothing to do here
-      }
-      CleanupInvitesFromPlayer(longCharId)
+    case Service.Leave(Some(char_id)) => LeaveService(char_id, sender)
 
-    case Service.Leave(None) | Service.LeaveAll() => ;
+    case Service.Leave(None) | Service.LeaveAll() => UserEvents find { case(_, subscription) => subscription == sender} match {
+      case Some((to, _)) =>
+        LeaveService(to, sender)
+      case _ => ;
+    }
+
+    case Terminated(actorRef) =>
+      context.unwatch(actorRef)
+      UserEvents find { case(_, subscription) => subscription eq actorRef} match {
+        case Some((to, _)) =>
+          LeaveService(to, sender)
+        case _ => ;
+      }
 
     case SquadServiceMessage(tplayer, zone, squad_action) => squad_action match {
-      case SquadAction.Membership(SquadRequestType.Invite, invitingPlayer, Some(invitedPlayer), _, _) =>
+      case SquadAction.InitSquadList() =>
+        Publish( sender, SquadResponse.InitList(PublishedLists(tplayer.Faction)) ) //send initial squad catalog
+
+      case SquadAction.InitCharId() =>
+        val charId = tplayer.CharId
+        memberToSquad.get(charId) match {
+          case None => ;
+          case Some(squad) =>
+            val guid = squad.GUID
+            val toChannel = s"/${squadFeatures(guid).ToChannel}/Squad"
+            val indices = squad.Membership.zipWithIndex.collect({ case (member, index) if member.CharId != 0 => index }).toList
+            Publish(charId, SquadResponse.AssociateWithSquad(guid))
+            Publish(charId, SquadResponse.Join(squad, indices, toChannel))
+            InitSquadDetail(guid, Seq(charId), squad)
+            InitWaypoints(charId, guid)
+        }
+
+      case SquadAction.Membership(SquadRequestType.Invite, invitingPlayer, Some(_invitedPlayer), invitedName, _) =>
         //this is just busy work; for actual joining operations, see SquadRequestType.Accept
-        (memberToSquad.get(invitingPlayer), memberToSquad.get(invitedPlayer)) match {
-          case (Some(squad1), Some(squad2))
-            if squad1.GUID == squad2.GUID =>
-            //both players are in the same squad; no need to do anything
+        (if(invitedName.nonEmpty) {
+          //validate player with name exists
+          LivePlayerList.WorldPopulation({ case (_, a : Avatar) => a.name == invitedName }).headOption match {
+            case Some(player) => UserEvents.keys.find(_ == player.CharId)
+            case None => None
+          }
+        }
+        else {
+          //validate player with id exists
+          UserEvents.keys.find(_ == _invitedPlayer)
+        }) match {
+          case Some(invitedPlayer) =>
+            (memberToSquad.get(invitingPlayer), memberToSquad.get(invitedPlayer)) match {
+              case (Some(squad1), Some(squad2)) if squad1.GUID == squad2.GUID =>
+              //both players are in the same squad; no need to do anything
 
-          case (Some(squad1), Some(squad2))
-            if squad1.Leader.CharId == invitingPlayer && squad2.Leader.CharId == invitedPlayer &&
-              squad1.Size > 1 && squad2.Size > 1 =>
-            //we might do some platoon chicanery with this case later
-            //TODO platoons
+              case (Some(squad1), Some(squad2)) if 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 && !squadFeatures(squad1.GUID).Refuse.contains(invitedPlayer) =>
-            //both players belong to squads, but the invitedplayer's squad (squad2) is underutilized by comparison
-            //treat the same as "the classic situation" using squad1
-            log.info(s"$invitedPlayer has been invited to squad ${squad1.Task} by $invitingPlayer")
-            val charId = tplayer.CharId
-            AddInviteAndRespond(
-              invitedPlayer,
-              VacancyInvite(charId, tplayer.Name, squad1.GUID),
-              charId,
-              tplayer.Name
-            )
+              case (Some(squad1), Some(squad2)) if squad2.Size == 1 =>
+                //both players belong to squads, but the invitedPlayer's squad (squad2) is underutilized by comparison
+                //treat the same as "the classic situation" using squad1
+                if(!Refused(invitedPlayer).contains(invitingPlayer)) {
+                  val charId = tplayer.CharId
+                  AddInviteAndRespond(
+                    invitedPlayer,
+                    VacancyInvite(charId, tplayer.Name, squad1.GUID),
+                    charId,
+                    tplayer.Name
+                  )
+                }
 
-          case (Some(squad1), Some(squad2))
-            if squad1.Size == 1 && !squadFeatures(squad2.GUID).Refuse.contains(invitingPlayer) =>
-            //both players belong to squads, but the invitingPlayer's squad is underutilized by comparison
-            //treat the same as "indirection ..." using squad2
-            log.warn(s"$invitedPlayer has asked $invitingPlayer for an invitation to squad ${squad2.Task}, but the squad leader may need to approve")
-            AddInviteAndRespond(
-              squad2.Leader.CharId,
-              IndirectInvite(tplayer, squad2.GUID),
-              invitingPlayer,
-              tplayer.Name
-            )
+              case (Some(squad1), Some(squad2)) if squad1.Size == 1 =>
+                //both players belong to squads, but the invitingPlayer's squad is underutilized by comparison
+                //treat the same as "indirection ..." using squad2
+                val leader = squad2.Leader.CharId
+                if(Refused(invitingPlayer).contains(invitedPlayer)) {
+                  debug(s"$invitedPlayer repeated a previous refusal to $invitingPlayer's invitation offer")
+                }
+                else if(Refused(invitingPlayer).contains(leader)) {
+                  debug(s"$invitedPlayer repeated a previous refusal to $leader's invitation offer")
+                }
+                else {
+                  AddInviteAndRespond(
+                    leader,
+                    IndirectInvite(tplayer, squad2.GUID),
+                    invitingPlayer,
+                    tplayer.Name
+                  )
+                }
 
-          case (Some(squad), None)
-            if !squadFeatures(squad.GUID).Refuse.contains(invitedPlayer) =>
-            //the classic situation
-            log.info(s"$invitedPlayer has been invited to squad ${squad.Task} by $invitingPlayer")
-            AddInviteAndRespond(
-              invitedPlayer,
-              VacancyInvite(tplayer.CharId, tplayer.Name, squad.GUID),
-              invitingPlayer,
-              tplayer.Name
-            )
+              case (Some(squad), None) =>
+                //the classic situation
+                if(!Refused(invitedPlayer).contains(invitingPlayer)) {
+                  AddInviteAndRespond(
+                    invitedPlayer,
+                    VacancyInvite(tplayer.CharId, tplayer.Name, squad.GUID),
+                    invitingPlayer,
+                    tplayer.Name
+                  )
+                }
+                else {
+                  debug(s"$invitedPlayer repeated a previous refusal to $invitingPlayer's invitation offer")
+                }
 
-          case (None, Some(squad))
-            if !squadFeatures(squad.GUID).Refuse.contains(invitingPlayer) =>
-            //indirection;  we're trying to invite ourselves to someone else's squad
-            log.warn(s"$invitedPlayer has asked $invitingPlayer for an invitation to squad ${squad.Task}, but the squad leader may need to approve")
-            AddInviteAndRespond(
-              squad.Leader.CharId,
-              IndirectInvite(tplayer, squad.GUID),
-              invitingPlayer,
-              tplayer.Name
-            )
+              case (None, Some(squad)) =>
+                //indirection;  we're trying to invite ourselves to someone else's squad
+                val leader = squad.Leader.CharId
+                if(Refused(invitingPlayer).contains(invitedPlayer)) {
+                  debug(s"$invitedPlayer repeated a previous refusal to $invitingPlayer's invitation offer")
+                }
+                else if(Refused(invitingPlayer).contains(leader)) {
+                  debug(s"$invitedPlayer repeated a previous refusal to $leader's invitation offer")
+                }
+                else {
+                  AddInviteAndRespond(
+                    squad.Leader.CharId,
+                    IndirectInvite(tplayer, squad.GUID),
+                    invitingPlayer,
+                    tplayer.Name
+                  )
+                }
 
-          case (None, None) =>
-            //neither the invited player nor the inviting player belong to any squad
-            log.info(s"$invitedPlayer has been invited to join $invitingPlayer's spontaneous squad")
-            AddInviteAndRespond(
-              invitedPlayer,
-              SpontaneousInvite(tplayer),
-              invitingPlayer,
-              tplayer.Name
-            )
+              case (None, None) =>
+                //neither the invited player nor the inviting player belong to any squad
+                if(Refused(invitingPlayer).contains(invitedPlayer)) {
+                  debug(s"$invitedPlayer repeated a previous refusal to $invitingPlayer's invitation offer")
+                }
+                else if(Refused(invitedPlayer).contains(invitingPlayer)) {
+                  debug(s"$invitingPlayer repeated a previous refusal to $invitingPlayer's invitation offer")
+                }
+                else {
+                  AddInviteAndRespond(
+                    invitedPlayer,
+                    SpontaneousInvite(tplayer),
+                    invitingPlayer,
+                    tplayer.Name
+                  )
+                }
 
-          case _ => ;
+              case _ => ;
+            }
+          case None => ;
         }
 
       case SquadAction.Membership(SquadRequestType.ProximityInvite, invitingPlayer, _, _, _) =>
-        memberToSquad.get(invitingPlayer) match {
+        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
-                log.info("ProximityInvite: waiting for existing proximity invitations to clear")
+                debug("ProximityInvite: wait for existing proximity invitations to clear")
               case _ =>
-                log.info("ProximityInvite: looking for invitation targets ...")
                 val outstandingActiveInvites = features.SearchForRole match {
                   case Some(pos) =>
                     RemoveQueuedInvitesForSquadAndPosition(sguid, pos)
-                    invites.collect { case(charId, InviteForRole(_,_, squad_guid, role)) if squad_guid == sguid && role == pos => charId }
+                    invites.collect { case(charId, LookingForSquadRoleInvite(_,_, squad_guid, role)) if squad_guid == sguid && role == pos => charId }
                   case None =>
                     List.empty[Long]
                 }
-                features.SearchForRole = Some(-1)
                 val faction = squad.Faction
                 val center = tplayer.Position
                 val excusedInvites = features.Refuse
@@ -312,13 +547,14 @@ class SquadService extends Actor {
                 - 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
+                - are a certain distance from the squad leader (n < 25m)
                  */
                 (zone.LivePlayers
                   .collect { case player
-                    if player.Faction == faction && player.LFS && memberToSquad.get(player.CharId).isEmpty &&
-                      !excusedInvites.contains(player.CharId) &&
-                      Vector3.DistanceSquared(player.Position, center) < 100f &&
+                    if player.Faction == faction && player.LFS &&
+                      (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 =>
@@ -331,15 +567,14 @@ class SquadService extends Actor {
                   .partition { charId => outstandingActiveInvites.exists(_ == charId) } match {
                   case (Nil, Nil) =>
                     //no one found
-                    log.info("ProximityInvite: no invitation targets 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
-                    log.info(s"ProximityInvite: found ${outstandingPlayerList.size} players already having been invited, and ${invitedPlayerList.size} players to invite")
+                    features.SearchForRole = Some(-1)
                     outstandingPlayerList.foreach { charId =>
-                      val bid = invites(charId).asInstanceOf[InviteForRole]
+                      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
@@ -368,40 +603,40 @@ class SquadService extends Actor {
 
       case SquadAction.Membership(SquadRequestType.Accept, invitedPlayer, _, _, _) =>
         val acceptedInvite = RemoveInvite(invitedPlayer)
-        val msg = "Accept: the invited player is already a member of a squad and can not join a second one"
         acceptedInvite match {
-          case Some(BidForRole(petitioner, guid, position)) if EnsureEmptySquad(petitioner.CharId, msg) && squadFeatures.get(guid).nonEmpty =>
+          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)
+            features.Prompt.cancel
             JoinSquad(petitioner, features.Squad, position)
             RemoveInvitesForSquadAndPosition(guid, position)
 
-          case Some(IndirectInvite(recruit, guid)) if EnsureEmptySquad(recruit.CharId, msg) =>
+          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)) =>
-                SquadEvents.publish(SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitedPlayer, Some(recruitCharId), recruit.Name, true, Some(None))))
+                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, msg) =>
+          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)) =>
-                SquadEvents.publish(SquadServiceResponse(s"/$invitingPlayer/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitingPlayer, Some(invitedPlayer), tplayer.Name, false, Some(None))))
-                SquadEvents.publish(SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitedPlayer, Some(invitingPlayer), "", true, Some(None))))
+                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, msg) =>
+          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 {
@@ -412,14 +647,14 @@ class SquadService extends Actor {
                 //generate a new squad, with invitingPlayer as the leader
                 val squad = StartSquad(invitingPlayer)
                 squad.Task = s"${invitingPlayer.Name}'s Squad"
-                SquadEvents.publish( SquadServiceResponse(s"/$invitingPlayerCharId/Squad", SquadResponse.AssociateWithSquad(squad.GUID)) )
+                Publish(invitingPlayerCharId, SquadResponse.AssociateWithSquad(squad.GUID))
                 Some(squad)
             }) match {
               case Some(squad) =>
-                HandleVacancyInvite(squad.GUID, tplayer.CharId, invitingPlayerCharId, tplayer) match {
+                HandleVacancyInvite(squad, tplayer.CharId, invitingPlayerCharId, tplayer) match {
                   case Some((_, line)) =>
-                    SquadEvents.publish( SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitedPlayer, Some(invitingPlayerCharId), "", true, Some(None))) )
-                    SquadEvents.publish( SquadServiceResponse(s"/$invitingPlayerCharId/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitingPlayerCharId, Some(invitedPlayer), tplayer.Name, false, Some(None))) )
+                    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 _ => ;
@@ -427,12 +662,12 @@ class SquadService extends Actor {
               case _ => ;
             }
 
-          case Some(InviteForRole(invitingPlayer, name, guid, position)) if EnsureEmptySquad(invitedPlayer, msg) =>
+          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
-                SquadEvents.publish( SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitedPlayer, Some(invitingPlayer), "", true, Some(None))) )
-                SquadEvents.publish( SquadServiceResponse(s"/$invitingPlayer/Squad", 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)))
+                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
@@ -441,35 +676,31 @@ class SquadService extends Actor {
               case Some(features) =>
                 //can not join squad; position is unavailable or other reasons block action
                 features.ProxyInvites = features.ProxyInvites.filterNot(_ == invitedPlayer)
-                NextInviteAndRespond(invitedPlayer)
 
               case _ =>
                 //squad no longer exists?
-                NextInviteAndRespond(invitedPlayer)
             }
 
-          case Some(ProximityInvite(invitingPlayer, _, guid)) if EnsureEmptySquad(invitedPlayer, msg) =>
+          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 = squad.Membership.zipWithIndex
-                    .collect { case (member, index) if member.CharId == 0 && squad.Availability(index) && {
-                      val requirementsToMeet = member.Requirements
-                      requirementsToMeet.intersect(tplayer.Certifications) == requirementsToMeet
-                    } =>
-                      (index, member.Requirements.size)
-                    }
-                    .sortBy({ case (_, requirements) => requirements })
+                  val positions = (for {
+                    (member, index) <- squad.Membership.zipWithIndex
+                    if ValidOpenSquadPosition(squad, index, member, tplayer.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 //(None, None)
+                    case _ => None
                   }) match {
                     case Some(position) if JoinSquad(tplayer, squad, position) =>
                       //join this squad
-                      SquadEvents.publish( SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitedPlayer, Some(invitingPlayer), "", true, Some(None))) )
-                      SquadEvents.publish( SquadServiceResponse(s"/$invitingPlayer/Squad", 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)))
+                      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 _ =>
@@ -481,104 +712,185 @@ class SquadService extends Actor {
                 }
                 else if(squad.Size == squad.Capacity) {
                   //all available squad positions filled; terminate all remaining invitations
-                  features.SearchForRole = None
-                  features.ProxyInvites = Nil
-                  CleanupInvitesForSquad(guid)
-                  //CleanupInvitesFromPlayer(invitingPlayer)
+                  RemoveProximityInvites(guid)
+                  RemoveAllInvitesToSquad(guid)
+                  //RemoveAllInvitesWithPlayer(invitingPlayer)
                 }
 
               case _ =>
                 //squad no longer exists?
-                NextInviteAndRespond(invitedPlayer)
             }
 
           case _ =>
-            //the invite either timed-out or was withdrawn or is now invalid; select a new one?
-            NextInviteAndRespond(invitedPlayer)
+            //the invite either timed-out or was withdrawn or is now invalid
+            (previousInvites.get(invitedPlayer) match {
+              case Some(SpontaneousInvite(leader)) => (leader.CharId, leader.Name)
+              case Some(VacancyInvite(charId, name, _)) => (charId, name)
+              case Some(ProximityInvite(charId, name, _)) => (charId, name)
+              case Some(LookingForSquadRoleInvite(charId, name, _, _)) => (charId, name)
+              case _ => (0L, "")
+            }) match {
+              case (0L, "") => ;
+              case (charId, name) =>
+                Publish(charId, SquadResponse.Membership(SquadResponseType.Cancel, 0, 0, charId, Some(0L), name, false, Some(None)))
+            }
         }
+        NextInviteAndRespond(invitedPlayer)
 
-      case SquadAction.Membership(SquadRequestType.Leave, leavingPlayer, optionalPlayer, _, _) =>
-        val squad = memberToSquad(leavingPlayer)
-        val leader = squad.Leader.CharId
-        if(leavingPlayer == leader || squad.Size == 2) {
-          //squad leader is leaving his own squad, so it will be disbanded
-          //alternately, squad is only composed of two people, so it will be closed-out when one of them leaves
-          DisbandSquad(squad)
-        }
-        else {
-          if(optionalPlayer.contains(leavingPlayer)) {
-            //leaving the squad of own accord
-            LeaveSquad(tplayer.CharId, squad)
-          }
-          else if(optionalPlayer.contains(leader)) {
-            //kicked by the squad leader
-            SquadEvents.publish( SquadServiceResponse(s"/$leavingPlayer/Squad", SquadResponse.Membership(SquadResponseType.Leave, 0, 0, leavingPlayer, Some(leader), tplayer.Name, false, Some(None))) )
-            SquadEvents.publish( SquadServiceResponse(s"/$leader/Squad", SquadResponse.Membership(SquadResponseType.Leave, 0, 0, leader, Some(leavingPlayer), "", true, Some(None))) )
-            squadFeatures(squad.GUID).Refuse = leavingPlayer
-            LeaveSquad(leavingPlayer, squad)
-          }
+      case SquadAction.Membership(SquadRequestType.Leave, leavingPlayer, optionalPlayer, name, _) =>
+        GetParticipatingSquad(leavingPlayer) match {
+          case Some(squad) =>
+            val kickedPlayer = if(name.nonEmpty) {
+              //validate player with name
+              LivePlayerList.WorldPopulation({ case (_, a : Avatar) => a.name == name }).headOption match {
+                case Some(player) => UserEvents.keys.find(_ == player.CharId)
+                case None => None
+              }
+            }
+            else {
+              //validate player with id
+              optionalPlayer match {
+                case Some(id) => UserEvents.keys.find(_ == id)
+                case None => None
+              }
+            }
+            val leader = squad.Leader.CharId
+            if(leavingPlayer == leader || squad.Size == 2) {
+              //squad leader is leaving his own squad, so it will be disbanded
+              //alternately, squad is only composed of two people, so it will be closed-out when one of them leaves
+              DisbandSquad(squad)
+            }
+            else {
+              if(kickedPlayer.isEmpty || kickedPlayer.contains(leavingPlayer)) {
+                //leaving the squad of own accord
+                LeaveSquad(tplayer.CharId, squad)
+              }
+              else if(kickedPlayer.contains(leader)) {
+                //kicked by the squad leader
+                Publish(leavingPlayer, SquadResponse.Membership(SquadResponseType.Leave, 0, 0, leavingPlayer, Some(leader), tplayer.Name, false, Some(None)))
+                Publish(leader, SquadResponse.Membership(SquadResponseType.Leave, 0, 0, leader, Some(leavingPlayer), "", true, Some(None)))
+                squadFeatures(squad.GUID).Refuse = leavingPlayer
+                LeaveSquad(leavingPlayer, squad)
+              }
+            }
+
+          case None =>
         }
 
       case SquadAction.Membership(SquadRequestType.Reject, rejectingPlayer, _, _, _) =>
         val rejectedBid = RemoveInvite(rejectingPlayer)
         //(A, B) -> person who made the rejection, person who was rejected
         (rejectedBid match {
-          case Some(SpontaneousInvite(invitingPlayer)) =>
-            //rejectingPlayer is the would-be squad member
-            (Some(rejectingPlayer), Some(invitingPlayer.CharId))
-          case Some(VacancyInvite(invitingPlayer, _, guid))
+          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
-            (Some(rejectingPlayer), Some(invitingPlayer))
-          case Some(BidForRole(_, guid, _))
-            if squadFeatures.get(guid).nonEmpty && squadFeatures(guid).Squad.Leader.CharId == rejectingPlayer =>
-            //rejectingPlayer is the squad leader
-            (Some(rejectingPlayer), None)
-          case Some(InviteForRole(invitingPlayer, _, guid, position))
-            if squadFeatures.get(guid).nonEmpty && squadFeatures(guid).Squad.Leader.CharId != rejectingPlayer =>
-            //rejectingPlayer is the would-be squad member
-            val features = squadFeatures(guid)
-            features.Refuse = rejectingPlayer //do not bother this player anymore
-            features.ProxyInvites = features.ProxyInvites.filterNot(_ == rejectingPlayer)
-            if(features.ProxyInvites.isEmpty) {
-              features.SearchForRole = None
-            }
-            (None, None)
+            //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
+            //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
-            features.ProxyInvites = features.ProxyInvites.filterNot(_ == rejectingPlayer)
-            if(features.ProxyInvites.isEmpty) {
-              //all invitations exhausted; this invitation is concluded
+            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(candidate, 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
+            features.Prompt.cancel
+            (Some(rejectingPlayer), None)
+
           case _ => ;
             (None, None)
         }) match {
           case (Some(rejected), Some(invited)) =>
-            SquadEvents.publish( SquadServiceResponse(s"/$rejected/Squad", SquadResponse.Membership(SquadResponseType.Reject, 0, 0, rejected, Some(invited), "", true, Some(None))) )
-            SquadEvents.publish( SquadServiceResponse(s"/$invited/Squad", SquadResponse.Membership(SquadResponseType.Reject, 0, 0, invited, Some(rejected), tplayer.Name, false, Some(None))) )
+            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) =>
-            SquadEvents.publish( SquadServiceResponse(s"/$rejected/Squad", SquadResponse.Membership(SquadResponseType.Reject, 0, 0, rejected, Some(rejected), "", true, Some(None))) )
+            Publish(rejected, SquadResponse.Membership(SquadResponseType.Reject, 0, 0, rejected, Some(rejected), "", true, Some(None)))
           case _ => ;
         }
         NextInviteAndRespond(rejectingPlayer)
 
-      case SquadAction.Membership(SquadRequestType.Cancel, cancellingPlayer, _, _, _) =>
-        //huh?
-        log.warn(s"Huh? what does player $cancellingPlayer want to cancel?")
+      case SquadAction.Membership(SquadRequestType.Disband, char_id, _, _, _) =>
+        GetLeadingSquad(char_id, None) match {
+          case Some(squad) =>
+            DisbandSquad(squad)
+          case None => ;
+        }
 
-      case SquadAction.Membership(SquadRequestType.Promote, promotingPlayer, Some(promotedPlayer), _, _) =>
-        (memberToSquad.get(promotingPlayer), memberToSquad.get(promotedPlayer)) match {
-          case (Some(squad), Some(squad2)) if squad.GUID == squad2.GUID && squad.Leader.CharId == promotingPlayer =>
+      case SquadAction.Membership(SquadRequestType.Cancel, cancellingPlayer, _, _, _) =>
+        //get rid of SpontaneousInvite objects and VacancyInvite objects
+        invites.collect {
+          case (id, invite : SpontaneousInvite) if invite.InviterCharId == cancellingPlayer =>
+            RemoveInvite(id)
+          case (id, invite : VacancyInvite) if invite.InviterCharId == cancellingPlayer =>
+            RemoveInvite(id)
+          case (id, invite : LookingForSquadRoleInvite) if invite.InviterCharId == cancellingPlayer =>
+            RemoveInvite(id)
+        }
+        queuedInvites.foreach { case (id : Long, inviteList) =>
+          val inList = inviteList.filterNot {
+            case invite : SpontaneousInvite if invite.InviterCharId == cancellingPlayer => true
+            case invite : VacancyInvite if invite.InviterCharId == cancellingPlayer => true
+            case invite : LookingForSquadRoleInvite if invite.InviterCharId == cancellingPlayer => true
+            case _ => false
+          }
+          if(inList.isEmpty) {
+            queuedInvites.remove(id)
+          }
+          else {
+            queuedInvites(id) = inList
+          }
+        }
+        //get rid of ProximityInvite objects
+        RemoveProximityInvites(cancellingPlayer)
+
+      case SquadAction.Membership(SquadRequestType.Promote, promotingPlayer, Some(_promotedPlayer), promotedName, _) =>
+        val promotedPlayer = (if(promotedName.nonEmpty) {
+          //validate player with name exists
+          LivePlayerList.WorldPopulation({ case (_, a : Avatar) => a.name == promotedName }).headOption match {
+            case Some(player) => UserEvents.keys.find(_ == player.CharId)
+            case None => Some(_promotedPlayer)
+          }
+        }
+        else {
+          Some(_promotedPlayer)
+        }) match {
+          case Some(player) => player
+          case None => -1L
+        }
+        (GetLeadingSquad(promotingPlayer, None), GetParticipatingSquad(promotedPlayer)) match {
+          case (Some(squad), Some(squad2)) if squad.GUID == squad2.GUID =>
             val membership = squad.Membership.filter { _member => _member.CharId > 0 }
-            val (leader, position) = (squad.Leader, 0)
+            val leader = squad.Leader
             val (member, index) = membership.zipWithIndex.find { case (_member, _) => _member.CharId == promotedPlayer }.get
-            log.info(s"Player ${leader.Name} steps down from leading ${squad.Task}")
-            SwapMemberPosition(squad, leader, member)
+            val features = squadFeatures(squad.GUID)
+            SwapMemberPosition(leader, member)
+            //cancel previous leader invite prompt, if any
+            features.Prompt.cancel
             //move around invites so that the proper squad leader deals with them
             val leaderInvite = invites.remove(promotingPlayer)
             val leaderQueuedInvites = queuedInvites.remove(promotingPlayer).toList.flatten
@@ -600,31 +912,33 @@ class SquadService extends Actor {
                     queuedInvites += promotedPlayer -> (xs ++ queuedInvites.remove(promotedPlayer).toList.flatten)
                 }
             }
-            log.info(s"Promoting player ${leader.Name} to be the leader of ${squad.Task}")
-            membership.foreach { _member =>
-              SquadEvents.publish(SquadServiceResponse(s"/${_member.CharId}/Squad", SquadResponse.PromoteMember(squad, promotedPlayer, index, position)))
+            debug(s"Promoting player ${leader.Name} to be the leader of ${squad.Task}")
+            Publish(features.ToChannel, SquadResponse.PromoteMember(squad, promotedPlayer, index, 0))
+            if(features.Listed) {
+              Publish(promotingPlayer, SquadResponse.SetListSquad(PlanetSideGUID(0)))
+              Publish(promotedPlayer, SquadResponse.SetListSquad(squad.GUID))
             }
-            SquadEvents.publish(SquadServiceResponse(s"/$promotingPlayer/Squad", SquadResponse.AssociateWithSquad(PlanetSideGUID(0))))
-            SquadEvents.publish(SquadServiceResponse(s"/$promotedPlayer/Squad", SquadResponse.AssociateWithSquad(squad.GUID)))
             UpdateSquadListWhenListed(
-              squad,
+              features,
               SquadInfo().Leader(leader.Name)
             )
-            UpdateSquadDetail(squad.GUID, squad,
+            UpdateSquadDetail(
+              squad.GUID,
               SquadDetail()
                 .LeaderCharId(leader.CharId)
                 .Field3(value = 0L)
                 .LeaderName(leader.Name)
                 .Members(List(
-                  SquadPositionEntry(position, SquadPositionDetail().CharId(member.CharId).Name(member.Name)),
-                  SquadPositionEntry(index, SquadPositionDetail().CharId(leader.CharId).Name(leader.Name))
+                  SquadPositionEntry(0, SquadPositionDetail().CharId(leader.CharId).Name(leader.Name)),
+                  SquadPositionEntry(index, SquadPositionDetail().CharId(member.CharId).Name(member.Name))
                 ))
             )
-
-          case msg =>
-            log.warn(s"Unsupported squad behavior: $msg")
+          case _ => ;
         }
 
+      case SquadAction.Membership(event, _, _, _, _) =>
+        debug(s"SquadAction.Membership: $event is not yet supported")
+
       case SquadAction.Waypoint(_, wtype, _, info) =>
         val playerCharId = tplayer.CharId
         (GetLeadingSquad(tplayer, None) match {
@@ -640,25 +954,11 @@ class SquadService extends Actor {
         }) match {
           case (Some(squad), Some(_)) =>
             //waypoint added or updated
-            squad.Membership
-              .filterNot { member => member.CharId == tplayer.CharId }
-              .foreach { member =>
-                val charId = member.CharId
-                SquadEvents.publish(
-                  SquadServiceResponse(s"/$charId/Squad", SquadResponse.WaypointEvent(WaypointEventAction.Add, playerCharId, wtype, None, info, 1))
-                )
-              }
+            SquadServiceResponse(s"/${squadFeatures(squad.GUID).ToChannel}/Squad", tplayer.CharId, SquadResponse.WaypointEvent(WaypointEventAction.Add, playerCharId, wtype, None, info, 1))
 
           case (Some(squad), None) =>
             //waypoint removed?
-            squad.Membership
-              .filterNot { member => member.CharId == tplayer.CharId }
-              .foreach { member =>
-                val charId = member.CharId
-                SquadEvents.publish(
-                  SquadServiceResponse(s"/$charId/Squad", SquadResponse.WaypointEvent(WaypointEventAction.Remove, playerCharId, wtype, None, None, 0))
-                )
-              }
+            SquadServiceResponse(s"/${squadFeatures(squad.GUID).ToChannel}/Squad", tplayer.CharId, SquadResponse.WaypointEvent(WaypointEventAction.Remove, playerCharId, wtype, None, None, 0))
 
           case msg =>
             log.warn(s"Unsupported squad waypoint behavior: $msg")
@@ -674,57 +974,60 @@ class SquadService extends Actor {
             val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
             if(squad.Task.nonEmpty && squad.ZoneId > 0) {
               tplayer.SquadLoadouts.SaveLoadout(squad, squad.Task, line)
-              sender ! SquadServiceResponse("", SquadResponse.ListSquadFavorite(line, squad.Task))
+              Publish(sender, SquadResponse.ListSquadFavorite(line, squad.Task))
             }
 
           case LoadSquadFavorite() =>
-            val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
-            tplayer.SquadLoadouts.LoadLoadout(line) match {
-              case Some(loadout : SquadLoadout) if squad.Size == 1 =>
-                log.info(s"${tplayer.Name} is loading a squad composition: $loadout")
-                SquadService.LoadSquadDefinition(squad, loadout)
-                sender ! SquadServiceResponse("", SquadResponse.AssociateWithSquad(squad.GUID))
-                UpdateSquadList(squad, SquadService.SquadList.Publish(squad))
-                UpdateSquadDetail(PlanetSideGUID(0), squad)
-              case _ =>
+            if(pSquadOpt.isEmpty || pSquadOpt == lSquadOpt) {
+              val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
+              tplayer.SquadLoadouts.LoadLoadout(line) match {
+                case Some(loadout : SquadLoadout) if squad.Size == 1 =>
+                  SquadService.LoadSquadDefinition(squad, loadout)
+                  UpdateSquadListWhenListed(squadFeatures(squad.GUID), SquadService.SquadList.Publish(squad))
+                  Publish(sender, SquadResponse.AssociateWithSquad(PlanetSideGUID(0)))
+                  InitSquadDetail(PlanetSideGUID(0), Seq(tplayer.CharId), squad)
+                  UpdateSquadDetail(squad)
+                  Publish(sender, SquadResponse.AssociateWithSquad(squad.GUID))
+                case _ =>
+              }
             }
 
           case DeleteSquadFavorite() =>
             tplayer.SquadLoadouts.DeleteLoadout(line)
-            sender ! SquadServiceResponse("", SquadResponse.ListSquadFavorite(line, ""))
+            Publish(sender, SquadResponse.ListSquadFavorite(line, ""))
 
           case ChangeSquadPurpose(purpose) =>
-            log.info(s"${tplayer.Name}-${tplayer.Faction} has changed his squad's task to $purpose")
             val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
             squad.Task = purpose
-            UpdateSquadListWhenListed(squad, SquadInfo().Task(purpose))
-            UpdateSquadDetail(squad.GUID, squad, SquadDetail().Task(purpose))
+            UpdateSquadListWhenListed(squadFeatures(squad.GUID), SquadInfo().Task(purpose))
+            UpdateSquadDetail(squad.GUID, SquadDetail().Task(purpose))
 
           case ChangeSquadZone(zone_id) =>
-            log.info(s"${tplayer.Name}-${tplayer.Faction} has changed squad's ops zone to $zone_id")
             val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
             squad.ZoneId = zone_id.zoneId.toInt
-            UpdateSquadListWhenListed(squad, SquadInfo().ZoneId(zone_id))
+            UpdateSquadListWhenListed(squadFeatures(squad.GUID), SquadInfo().ZoneId(zone_id))
             InitialAssociation(squad)
-            sender ! SquadServiceResponse("", SquadResponse.Detail(
+            Publish(sender, SquadResponse.Detail(squad.GUID, SquadService.Detail.Publish(squad)))
+            UpdateSquadDetail(
               squad.GUID,
-              SquadService.Detail.Publish(squad))
+              squad.GUID,
+              Seq(squad.Leader.CharId),
+              SquadDetail().ZoneId(zone_id)
             )
-            UpdateSquadDetail(squad.GUID, squad.Membership.map { _m => _m.CharId }.filterNot { _ == squad.Leader.CharId }, SquadDetail().ZoneId(zone_id))
 
           case CloseSquadMemberPosition(position) =>
             val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
             squad.Availability.lift(position) match {
-              case Some(true) =>
+              case Some(true) if position > 0 => //do not close squad leader position; undefined behavior
                 squad.Availability.update(position, false)
-                log.info(s"${tplayer.Name}-${tplayer.Faction} has closed the #$position position in squad")
                 val memberPosition = squad.Membership(position)
                 if(memberPosition.CharId > 0) {
                   LeaveSquad(memberPosition.CharId, squad)
                 }
                 memberPosition.Close()
-                UpdateSquadListWhenListed(squad, SquadInfo().Capacity(squad.Capacity))
-                UpdateSquadDetail(squad.GUID, squad,
+                UpdateSquadListWhenListed(squadFeatures(squad.GUID), SquadInfo().Capacity(squad.Capacity))
+                UpdateSquadDetail(
+                  squad.GUID,
                   SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail.Closed)))
                 )
               case Some(false) | None => ;
@@ -734,10 +1037,10 @@ class SquadService extends Actor {
             val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
             squad.Availability.lift(position) match {
               case Some(false) =>
-                log.info(s"${tplayer.Name}-${tplayer.Faction} has opened the #$position position in squad")
                 squad.Availability.update(position, true)
-                UpdateSquadListWhenListed(squad, SquadInfo().Capacity(squad.Capacity))
-                UpdateSquadDetail(squad.GUID, squad,
+                UpdateSquadListWhenListed(squadFeatures(squad.GUID), SquadInfo().Capacity(squad.Capacity))
+                UpdateSquadDetail(
+                  squad.GUID,
                   SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail.Open)))
                 )
               case Some(true) | None => ;
@@ -747,9 +1050,9 @@ class SquadService extends Actor {
             val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
             squad.Availability.lift(position) match {
               case Some(true) =>
-                log.info(s"${tplayer.Name}-${tplayer.Faction} has changed the role of squad position #$position")
                 squad.Membership(position).Role = role
-                UpdateSquadDetail(squad.GUID, squad,
+                UpdateSquadDetail(
+                  squad.GUID,
                   SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail().Role(role))))
                 )
               case Some(false) | None => ;
@@ -759,9 +1062,9 @@ class SquadService extends Actor {
             val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
             squad.Availability.lift(position) match {
               case Some(true) =>
-                log.info(s"${tplayer.Name}-${tplayer.Faction} has changed the orders for squad position #$position")
                 squad.Membership(position).Orders = orders
-                UpdateSquadDetail(squad.GUID, squad,
+                UpdateSquadDetail(
+                  squad.GUID,
                   SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail().DetailedOrders(orders))))
                 )
               case Some(false) | None => ;
@@ -771,33 +1074,21 @@ class SquadService extends Actor {
             val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
             squad.Availability.lift(position) match {
               case Some(true) =>
-                log.info(s"${tplayer.Name}-${tplayer.Faction} has changed the requirements for squad position #$position")
                 squad.Membership(position).Requirements = certs
-                UpdateSquadDetail(squad.GUID, squad,
+                UpdateSquadDetail(
+                  squad.GUID,
                   SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail().Requirements(certs))))
                 )
               case Some(false) | None => ;
             }
 
           case LocationFollowsSquadLead(state) =>
-            if(state) {
-              log.info(s"${tplayer.Name}-${tplayer.Faction} has moves the rally to the leader's position")
-            }
-            else {
-              log.info(s"${tplayer.Name}-${tplayer.Faction} has let the rally move freely")
-            }
-            val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
-            squad.LocationFollowsSquadLead = state
+            val features = squadFeatures(lSquadOpt.getOrElse(StartSquad(tplayer)).GUID)
+            features.LocationFollowsSquadLead = state
 
           case AutoApproveInvitationRequests(state) =>
-            if(state) {
-              log.info(s"${tplayer.Name}-${tplayer.Faction} is allowing all requests to join the squad")
-            }
-            else {
-              log.info(s"${tplayer.Name}-${tplayer.Faction} has started screening invitation requests")
-            }
-            val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
-            squad.AutoApproveInvitationRequests = state
+            val features = squadFeatures(lSquadOpt.getOrElse(StartSquad(tplayer)).GUID)
+            features.AutoApproveInvitationRequests = state
 
           case FindLfsSoldiersForRole(position) =>
             lSquadOpt match {
@@ -807,7 +1098,7 @@ class SquadService extends Actor {
                 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
-                    log.debug("FindLfsSoldiersForRole: waiting for proximity invitations to clear")
+                    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
@@ -817,17 +1108,18 @@ class SquadService extends Actor {
                     val outstandingActiveInvites = features.SearchForRole match {
                       case Some(pos) =>
                         RemoveQueuedInvitesForSquadAndPosition(sguid, pos)
-                        invites.collect { case(charId, InviteForRole(_,_, squad_guid, role)) if squad_guid == sguid && role == pos => charId }
+                        invites.collect { case(charId, LookingForSquadRoleInvite(_,_, squad_guid, role)) if squad_guid == sguid && role == pos => charId }
                       case None =>
                         List.empty[Long]
                     }
                     features.SearchForRole = position
                     //this will update the role entry in the GUI to visually indicate being searched for; only one will be displayed at a time
-                    SquadEvents.publish(
-                      SquadServiceResponse(s"/${tplayer.CharId}/Squad", SquadResponse.Detail(
+                    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)
@@ -849,8 +1141,8 @@ class SquadService extends Actor {
                       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[InviteForRole]
-                          invites(charId) = InviteForRole(bid.char_id, bid.name, sguid, position)
+                          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
@@ -864,7 +1156,7 @@ class SquadService extends Actor {
                         invitedPlayers.foreach { invitedPlayer =>
                           AddInviteAndRespond(
                             invitedPlayer,
-                            InviteForRole(invitingPlayer, name, sguid, position),
+                            LookingForSquadRoleInvite(invitingPlayer, name, sguid, position),
                             invitingPlayer,
                             name
                           )
@@ -884,7 +1176,7 @@ class SquadService extends Actor {
                 squadFeatures(sguid).SearchForRole = None
                 //remove active invites
                 invites.filter {
-                  case (_, InviteForRole(_, _, _guid, pos)) => _guid == sguid && position.contains(pos)
+                  case (_, LookingForSquadRoleInvite(_, _, _guid, pos)) => _guid == sguid && position.contains(pos)
                   case _ => false
                 }
                   .keys.foreach { charId =>
@@ -893,7 +1185,7 @@ class SquadService extends Actor {
                 //remove queued invites
                 queuedInvites.foreach { case (charId, queue) =>
                   val filtered = queue.filterNot {
-                    case InviteForRole(_, _, _guid, _) => _guid == sguid
+                    case LookingForSquadRoleInvite(_, _, _guid, _) => _guid == sguid
                     case _ => false
                   }
                   queuedInvites += charId -> filtered
@@ -908,20 +1200,20 @@ class SquadService extends Actor {
 
           case RequestListSquad() =>
             val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
-            if(!squad.Listed && squad.Task.nonEmpty && squad.ZoneId > 0) {
-              log.info(s"${tplayer.Name}-${tplayer.Faction} has opened public recruitment for squad ${squad.Task}")
-              squad.Listed = true
+            val features = squadFeatures(squad.GUID)
+            if(!features.Listed && squad.Task.nonEmpty && squad.ZoneId > 0) {
+              features.Listed = true
               InitialAssociation(squad)
-              sender ! SquadServiceResponse("", SquadResponse.SetListSquad(squad.GUID))
+              Publish(sender, SquadResponse.SetListSquad(squad.GUID))
               UpdateSquadList(squad, None)
             }
 
           case StopListSquad() =>
             val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
-            if(squad.Listed) {
-              log.info(s"${tplayer.Name}-${tplayer.Faction} has closed public recruitment for squad ${squad.Task}")
-              squad.Listed = false
-              sender ! SquadServiceResponse("", SquadResponse.AssociateWithSquad(PlanetSideGUID(0)))
+            val features = squadFeatures(squad.GUID)
+            if(features.Listed) {
+              features.Listed = false
+              Publish(sender, SquadResponse.SetListSquad(PlanetSideGUID(0)))
               UpdateSquadList(squad, None)
             }
 
@@ -939,11 +1231,12 @@ class SquadService extends Actor {
                   position.Orders = ""
                   position.Requirements = Set()
                 })
-                squad.LocationFollowsSquadLead = false
-                squad.AutoApproveInvitationRequests = false
-                UpdateSquadListWhenListed(squad, SquadInfo().Task("").ZoneId(None).Capacity(squad.Capacity))
-                UpdateSquadDetail(guid, squad)
-                sender ! SquadServiceResponse("", SquadResponse.AssociateWithSquad(PlanetSideGUID(0)))
+                val features = squadFeatures(squad.GUID)
+                features.LocationFollowsSquadLead = false
+                features.AutoApproveInvitationRequests = false
+                UpdateSquadListWhenListed(features, SquadInfo().Task("").ZoneId(None).Capacity(squad.Capacity))
+                UpdateSquadDetail(squad)
+                Publish(sender, SquadResponse.AssociateWithSquad(PlanetSideGUID(0)))
                 squadFeatures(guid).InitialAssociation = true
                 //do not unlist an already listed squad
               case Some(squad) =>
@@ -956,49 +1249,35 @@ class SquadService extends Actor {
             (pSquadOpt, action) match {
               //the following action can be performed by the squad leader and maybe an unaffiliated player
               case (Some(squad), SelectRoleForYourself(position)) =>
-                log.info(s"${tplayer.Name} would like the #${position+1} spot in the same squad")
-                val membership = squad.Membership.zipWithIndex
-                val toMember = squad.Membership(position)
-                if(squad.Leader.CharId == tplayer.CharId) {
-                  //TODO 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 && toMember.CharId == 0) || ValidOpenSquadPosition(squad, position, toMember, tplayer.Certifications)) {
-                  membership.find { case (member, _) => member.CharId == tplayer.CharId } match {
-                    case Some((fromMember, fromIndex)) =>
-                      SwapMemberPosition(squad, toMember, fromMember)
-                      if(fromIndex == squad.LeaderPositionIndex) {
-                        squad.LeaderPositionIndex = position
-                      }
-                      //RemoveInvite(tplayer.CharId).foreach { _ =>
-                      //close the old bids out
-                      //}
-                      membership
-                        .filter { case (_member, _) => _member.CharId > 0 }
-                        .foreach { case (_member, _) =>
-                          SquadEvents.publish(SquadServiceResponse(s"/${_member.CharId}/Squad", SquadResponse.AssignMember(squad, fromIndex, position)))
-                        }
-                      UpdateSquadDetail(squad.GUID, squad)
-                    case _ => ;
-                    //somehow, this is not our squad; do nothing, for now
-                  }
-                }
-                else {
-                  //not qualified for requested position
-                }
+                //TODO should be possible, but doesn't work correctly due to UI squad cards not updating as expected
+//                if(squad.Leader.CharId == tplayer.CharId) {
+//                  //squad leader currently disallowed
+//                } else
+//                //the squad leader may swap to any open position; a normal member has to validate against requirements
+//                if(squad.Leader.CharId == tplayer.CharId || ValidOpenSquadPosition(squad, position, tplayer.Certifications)) {
+//                  squad.Membership.zipWithIndex.find { case (member, _) => member.CharId == tplayer.CharId } match {
+//                    case Some((fromMember, fromIndex)) =>
+//                      SwapMemberPosition(squad.Membership(position), fromMember)
+//                      Publish(squadFeatures(squad.GUID).ToChannel, SquadResponse.AssignMember(squad, fromIndex, position))
+//                      UpdateSquadDetail(squad)
+//                    case _ => ;
+//                    //somehow, this is not our squad; do nothing, for now
+//                  }
+//                }
+//                else {
+//                  //not qualified for requested position
+//                }
 
               //the following action can be performed by an unaffiliated player
               case (None, SelectRoleForYourself(position)) =>
                 //not a member of any squad, but we might become a member of this one
                 GetSquad(guid) match {
                   case Some(squad) =>
-                    val toMember = squad.Membership(position)
-                    if(ValidOpenSquadPosition(squad, position, toMember, tplayer.Certifications)) {
+                    if(ValidOpenSquadPosition(squad, position, tplayer.Certifications)) {
                       //we could join but we may need permission from the squad leader first
-                      log.info(s"${tplayer.Name} would like the #${position+1} spot in the squad ${squad.Task}.")
                       AddInviteAndRespond(
                         squad.Leader.CharId,
-                        BidForRole(tplayer, guid, position),
+                        RequestRole(tplayer, guid, position),
                         invitingPlayer = 0L, //we ourselves technically are ...
                         tplayer.Name
                       )
@@ -1014,42 +1293,42 @@ class SquadService extends Actor {
                   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 BidForRole invite entry where we are the player who wants to join the leader's squad
+                    //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[BidForRole] &&
-                        entry.asInstanceOf[BidForRole].player.CharId == cancellingPlayer =>
+                      case out @ Some(entry) if entry.isInstanceOf[RequestRole] &&
+                        entry.asInstanceOf[RequestRole].player.CharId == cancellingPlayer =>
                         out
                       case _ =>
                         None
                     }) match {
-                      case Some(entry : BidForRole) =>
+                      case Some(entry : RequestRole) =>
                         RemoveInvite(leaderCharId)
-                        SquadEvents.publish( SquadServiceResponse(s"/$leaderCharId/Squad", SquadResponse.Membership(SquadResponseType.Cancel, 0, 0, cancellingPlayer, None, entry.player.Name, false, Some(None))))
+                        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 BidForRole entry where we are the player who wants to join the leader's squad
+                      //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[BidForRole] &&
-                              entry.asInstanceOf[BidForRole].player.CharId == cancellingPlayer
+                            entry.isInstanceOf[RequestRole] &&
+                              entry.asInstanceOf[RequestRole].player.CharId == cancellingPlayer
                           })
                         case None =>
                           (Nil, -1)
                       }) match {
                         case (_, -1) =>
                           None //no change
-                        case (list, index) if list.size == 1 =>
-                          val entry = list.head.asInstanceOf[BidForRole]
-                          SquadEvents.publish( SquadServiceResponse(s"/$leaderCharId/Squad", SquadResponse.Membership(SquadResponseType.Cancel, 0, 0, cancellingPlayer, None, entry.player.Name, false, Some(None))))
+                        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[BidForRole]
-                          SquadEvents.publish( SquadServiceResponse(s"/$leaderCharId/Squad", SquadResponse.Membership(SquadResponseType.Cancel, 0, 0, cancellingPlayer, None, entry.player.Name, false, Some(None))))
+                          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)
                       }
@@ -1065,16 +1344,10 @@ class SquadService extends Actor {
                   //TODO squad leader currently disallowed
                   case (Some((fromMember, fromPosition)), (toMember, _)) if fromPosition != 0 =>
                     val name = fromMember.Name
-                    SwapMemberPosition(squad, toMember, fromMember)
-                    if(fromPosition == squad.LeaderPositionIndex) {
-                      squad.LeaderPositionIndex = position
-                    }
-                    membership
-                      .filter({ case (_member, _) => _member.CharId > 0 })
-                      .foreach { case (_member, _) =>
-                        SquadEvents.publish(SquadServiceResponse(s"/${_member.CharId}/Squad", SquadResponse.AssignMember(squad, fromPosition, position)))
-                      }
-                    UpdateSquadDetail(squad.GUID, squad,
+                    SwapMemberPosition(toMember, fromMember)
+                    Publish(squadFeatures(guid).ToChannel, SquadResponse.AssignMember(squad, fromPosition, position))
+                    UpdateSquadDetail(
+                      squad.GUID,
                       SquadDetail().Members(List(
                         SquadPositionEntry(position, SquadPositionDetail().CharId(fromMember.CharId).Name(fromMember.Name)),
                         SquadPositionEntry(fromPosition, SquadPositionDetail().CharId(char_id).Name(name))
@@ -1087,183 +1360,32 @@ class SquadService extends Actor {
               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
-                sender ! SquadServiceResponse("", SquadResponse.SquadSearchResults())
+                Publish(sender, SquadResponse.SquadSearchResults())
 
               //the following action can be performed by anyone
               case (_, DisplaySquad()) =>
+                val charId = tplayer.CharId
                 GetSquad(guid) match {
+                  case Some(squad) if memberToSquad.get(charId).isEmpty =>
+                    continueToMonitorDetails += charId -> squad.GUID
+                    Publish(sender, SquadResponse.Detail(squad.GUID, SquadService.Detail.Publish(squad)))
                   case Some(squad) =>
-                    sender ! SquadServiceResponse("", SquadResponse.Detail(squad.GUID, SquadService.Detail.Publish(squad)))
-                  case None => ;
+                    Publish(sender, SquadResponse.Detail(squad.GUID, SquadService.Detail.Publish(squad)))
+                  case _ => ;
                 }
 
               //the following message is feedback from a specific client, awaiting proper initialization
               case (_, SquadMemberInitializationIssue()) =>
-              //            GetSquad(guid) match {
-              //              case Some(squad) =>
-              //                sender ! SquadServiceResponse("", SquadResponse.Detail(squad.GUID, SquadService.Detail.Publish(squad)))
-              //              case None => ;
-              //            }
+//            GetSquad(guid) match {
+//              case Some(squad) =>
+//                Publish(sender, SquadResponse.Detail(squad.GUID, SquadService.Detail.Publish(squad)))
+//              case None => ;
+//            }
 
               case msg => ;
                 log.warn(s"Unsupported squad definition behavior: $msg")
             }
         }
-        // etc..
-        (pSquadOpt, action) match {
-          //the following action can be performed by the squad leader and maybe an unaffiliated player
-          case (Some(squad), SelectRoleForYourself(position)) =>
-            log.info(s"${tplayer.Name} would like the #${position+1} spot in this squad")
-            val membership = squad.Membership.zipWithIndex
-            val toMember = squad.Membership(position)
-            if(squad.Leader.CharId == tplayer.CharId) {
-              //TODO 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 && toMember.CharId == 0) || ValidOpenSquadPosition(squad, position, toMember, tplayer.Certifications)) {
-              membership.find { case (member, _) => member.CharId == tplayer.CharId } match {
-                case Some((fromMember, fromIndex)) =>
-                  SwapMemberPosition(squad, toMember, fromMember)
-                  if(fromIndex == squad.LeaderPositionIndex) {
-                    squad.LeaderPositionIndex = position
-                  }
-                  //RemoveInvite(tplayer.CharId).foreach { _ =>
-                    //close the old bids out
-                  //}
-                  membership
-                    .filter { case (_member, _) => _member.CharId > 0 }
-                    .foreach { case (_member, _) =>
-                      SquadEvents.publish(SquadServiceResponse(s"/${_member.CharId}/Squad", SquadResponse.AssignMember(squad, fromIndex, position)))
-                    }
-                  UpdateSquadDetail(squad.GUID, squad)
-                case _ => ;
-                //somehow, this is not our squad; do nothing, for now
-              }
-            }
-            else {
-              //not qualified for requested position
-            }
-
-          //the following action can be performed by an unaffiliated player
-          case (None, SelectRoleForYourself(position)) =>
-            //not a member of any squad, but we might become a member of this one
-            GetSquad(guid) match {
-              case Some(squad) =>
-                val toMember = squad.Membership(position)
-                if(ValidOpenSquadPosition(squad, position, toMember, tplayer.Certifications)) {
-                  //we could join but we may need permission from the squad leader first
-                  log.info(s"Player ${tplayer.Name} would like to join the squad ${squad.Task}.")
-                  AddInviteAndRespond(
-                    squad.Leader.CharId,
-                    BidForRole(tplayer, guid, position),
-                    invitingPlayer = 0L, //we ourselves technically are ...
-                    tplayer.Name
-                  )
-                }
-              case None => ;
-              //squad does not exist? assume old local data; force update to correct discrepancy
-            }
-
-          //the following action can be performed by anyone who has tried to join a squad
-          case (_, CancelSelectRoleForYourself(_)) =>
-            val cancellingPlayer = tplayer.CharId
-            GetSquad(guid) match {
-              case Some(squad) =>
-                //assumption: a player who is cancelling will rarely end up with their invite queued
-                val leaderCharId = squad.Leader.CharId
-                //clean up any active BidForRole 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[BidForRole] &&
-                    entry.asInstanceOf[BidForRole].player.CharId == cancellingPlayer =>
-                    out
-                  case _ =>
-                    None
-                }) match {
-                  case Some(entry : BidForRole) =>
-                    RemoveInvite(leaderCharId)
-                    SquadEvents.publish( SquadServiceResponse(s"/$leaderCharId/Squad", 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 BidForRole 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[BidForRole] &&
-                          entry.asInstanceOf[BidForRole].player.CharId == cancellingPlayer
-                      })
-                    case None =>
-                      (Nil, -1)
-                  }) match {
-                    case (_, -1) =>
-                      None //no change
-                    case (list, index) if list.size == 1 =>
-                      val entry = list.head.asInstanceOf[BidForRole]
-                      SquadEvents.publish( SquadServiceResponse(s"/$leaderCharId/Squad", SquadResponse.Membership(SquadResponseType.Cancel, 0, 0, cancellingPlayer, None, entry.player.Name, false, Some(None))))
-                      queuedInvites.remove(leaderCharId)
-                      Some(true)
-                    case (list, index) =>
-                      val entry = list(index).asInstanceOf[BidForRole]
-                      SquadEvents.publish( SquadServiceResponse(s"/$leaderCharId/Squad", SquadResponse.Membership(SquadResponseType.Cancel, 0, 0, cancellingPlayer, None, entry.player.Name, false, Some(None))))
-                      queuedInvites(leaderCharId) = list.take(index) ++ list.drop(index+1)
-                      Some(true)
-                  }
-                )
-
-              case _ => ;
-            }
-
-          //the following action can be performed by ???
-          case (Some(squad), AssignSquadMemberToRole(position, char_id)) =>
-            val membership = squad.Membership.zipWithIndex
-            (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(squad, toMember, fromMember)
-                if(fromPosition == squad.LeaderPositionIndex) {
-                  squad.LeaderPositionIndex = position
-                }
-                membership
-                  .filter({ case (_member, _) => _member.CharId > 0 })
-                  .foreach { case (_member, _) =>
-                    SquadEvents.publish(SquadServiceResponse(s"/${_member.CharId}/Squad", SquadResponse.AssignMember(squad, fromPosition, position)))
-                  }
-                UpdateSquadDetail(squad.GUID, squad,
-                  SquadDetail().Members(List(
-                    SquadPositionEntry(position, SquadPositionDetail().CharId(fromMember.CharId).Name(fromMember.Name)),
-                    SquadPositionEntry(fromPosition, SquadPositionDetail().CharId(char_id).Name(name))
-                  ))
-                )
-              case _ => ;
-            }
-
-          //the following action can be peprformed by anyone
-          case (_, SearchForSquadsWithParticularRole(_/*role*/, _/*requirements*/, _/*zone_id*/, _/*search_mode*/)) =>
-            //though we should be able correctly search squads as is intended
-            //I don't know how search results should be prioritized or even how to return search results to the user
-            sender ! SquadServiceResponse("", SquadResponse.SquadSearchResults())
-
-          //the following action can be performed by anyone
-          case (_, DisplaySquad()) =>
-            GetSquad(guid) match {
-              case Some(squad) =>
-                sender ! SquadServiceResponse("", SquadResponse.Detail(squad.GUID, SquadService.Detail.Publish(squad)))
-              case None => ;
-            }
-
-          //the following message is feedback from a specific client, awaiting proper initialization
-          case (_, SquadMemberInitializationIssue()) =>
-//            GetSquad(guid) match {
-//              case Some(squad) =>
-//                sender ! SquadServiceResponse("", SquadResponse.Detail(squad.GUID, SquadService.Detail.Publish(squad)))
-//              case None => ;
-//            }
-
-          case _ => ;
-        }
 
       case SquadAction.Update(char_id, health, max_health, armor, max_armor, pos, zone_number) =>
         memberToSquad.get(char_id) match {
@@ -1274,13 +1396,16 @@ class SquadService extends Actor {
                 member.Armor = StatConverter.Health(armor, max_armor, min=1, max=64)
                 member.Position = pos
                 member.ZoneId = zone_number
-                sender ! SquadServiceResponse("", 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
-                ))
+                Publish(
+                  sender,
+                  SquadResponse.UpdateMembers(
+                    squad,
+                    squad.Membership
+                      .filterNot { _.CharId == 0 }
+                      .map { member => SquadAction.Update(member.CharId, member.Health, 0, member.Armor, 0, member.Position, member.ZoneId) }
+                      .toList
+                  )
+                )
               case _ => ;
             }
 
@@ -1288,18 +1413,72 @@ class SquadService extends Actor {
         }
 
       case msg =>
-        log.info(s"Unhandled message $msg from $sender")
+        debug(s"Unhandled message $msg from $sender")
+    }
+
+    case data @ SquadResponse.WantsSquadPosition(leader_char_id, _) =>
+      Publish(leader_char_id, data)
+
+    case msg =>
+      debug(s"Unhandled message $msg from $sender")
+  }
+
+  /**
+    * 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
     }
   }
 
   /**
-    * na
-    * @param invitedPlayer the person who will handle the invitation, eventually if not immediately
+    * 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)
+      case None =>
+        Nil
+    }
+  }
+
+  /**
+    * Assign a provided invitation object to either the active or inactive position for a player.<br>
+    * <br>
+    * 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;
-    *         if added to the active invite position, return the parameter bid;
-    *         if added to the queued invite, return the invite in the active position;
-    *         if not added, return `None`
+    *         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 {
@@ -1309,10 +1488,9 @@ class SquadService extends Actor {
           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 }) {
-              log.debug(s"Invite from ${invite.InviterCharId} to $invitedPlayer stored in queue while active invite request pending")
               queuedInvites(invitedPlayer) = invite match {
-                case _: BidForRole =>
-                  val (normals, others) = bidList.partition(_.isInstanceOf[BidForRole])
+                case _: RequestRole =>
+                  val (normals, others) = bidList.partition(_.isInstanceOf[RequestRole])
                   (normals :+ invite) ++ others
                 case _ =>
                   bidList :+ invite
@@ -1324,7 +1502,6 @@ class SquadService extends Actor {
             }
           case None =>
             if(_bid.InviterCharId != invite.InviterCharId) {
-              log.debug(s"Invite from ${invite.InviterCharId} to $invitedPlayer stored while active invite request pending")
               queuedInvites(invitedPlayer) = List[Invitation](invite)
               Some(_bid)
             }
@@ -1339,44 +1516,150 @@ class SquadService extends Actor {
     }
   }
 
-  def RemoveInvite(invitedPlayer : Long) : Option[Invitation] = {
-    invites.remove(invitedPlayer) match {
-      case out @ Some(invite) =>
-        previousInvites += invitedPlayer -> invite
-        out
-      case None =>
-        None
+  /**
+    * 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.<br>
+    * <br>
+    * 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")
+      }
     }
   }
 
-  def RemoveQueuedInvites(invitedPlayer : Long) : List[Invitation] = {
-    queuedInvites.remove(invitedPlayer) match {
-      case Some(_bidList) => _bidList
-      case None => Nil
-    }
+  /**
+    * 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
+    )
   }
 
-  def RemoveInvites(invitedPlayer : Long, invitingPlayer : Long) : Unit = {
-    queuedInvites.get(invitedPlayer) match {
-      case Some(bidList) =>
-        val list = bidList.filterNot { _.InviterCharId == invitingPlayer }
-        if(list.nonEmpty) {
-          queuedInvites(invitedPlayer) = list
-        }
-        else {
-          queuedInvites.remove(invitedPlayer)
-        }
-      case None => ;
-    }
-    invites.get(invitedPlayer) match {
-      case Some(_bid) =>
-        if(_bid.InviterCharId == invitingPlayer) {
-          //drop bid, try reload new bid
-        }
-      case None => ;
-    }
+  /**
+    * 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.<br>
+    * <br>
+    * 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 {
@@ -1404,166 +1687,106 @@ class SquadService extends Actor {
     }
   }
 
-  def HandleVacancyInvite(squad_guid : PlanetSideGUID, invitedPlayer : Long, invitingPlayer : Long, recruit : Player) : Option[(Squad, Int)] = {
-    //accepted an invitation to join an existing squad
-    if(squadFeatures.get(squad_guid).isEmpty) {
-      log.warn(s"Accept->Invite: the squad #${squad_guid.guid} no longer exists")
-      None
-    }
-    else if(memberToSquad.get(invitedPlayer).nonEmpty) {
-      log.warn(s"Accept->Invite: ${recruit.Name} is already a member of a squad and can not join squad #${squad_guid.guid}")
-      None
-    }
-    else {
-      val squad = squadFeatures(squad_guid).Squad
-      if(!squad.AutoApproveInvitationRequests && squad.Leader.CharId != invitingPlayer) {
-        //the inviting player was not the squad leader and this decision should be bounced off the squad leader
-        AltAddInviteAndRespond(
-          squad.Leader.CharId,
-          IndirectInvite(recruit, squad_guid),
-          invitingPlayer,
-          name = ""
+  /**
+    * 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
         )
-        log.info(s"Accept->Invite: ${recruit.Name} must await an invitation from the leader of squad #${squad_guid.guid}")
+      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
-      }
-      else {
-        //if a suitable position in the squad can be found, player may occupy it
-        squad.Membership.zipWithIndex.find({ case (member, index) =>
-          ValidOpenSquadPosition(squad, index, member, recruit.Certifications)
-        }) match {
-          case Some((_, line)) =>
-            Some((squad, line))
-          case _ =>
-            if(squad.Size == squad.Capacity) {
-              log.warn(s"Accept->Invite: squad #${squad_guid.guid} is already full and ${recruit.Name} can not join it")
-            }
-            else {
-              log.warn(s"Accept->Invite: squad #${squad_guid.guid} has no positions available that satisfy ${recruit.Name}")
-            }
-            None
-        }
-      }
     }
   }
 
-  def InitialAssociation(squad : Squad) : Boolean = {
-    val guid = squad.GUID
-    if(squadFeatures(guid).InitialAssociation) {
-      squadFeatures(guid).InitialAssociation = false
-      val charId = squad.Leader.CharId
-      SquadEvents.publish(
-        SquadServiceResponse(s"/$charId/Squad", SquadResponse.AssociateWithSquad(guid))
-      )
-      SquadEvents.publish(
-        SquadServiceResponse(s"/$charId/Squad", SquadResponse.Detail(
-          guid,
-          SquadService.Detail.Publish(squad))
-        )
-      )
-    }
-    false
-  }
-
-  def HandleBidForRole(bid : BidForRole, player : Player) : Boolean = {
-    HandleBidForRole(bid, bid.squad_guid, bid.player.Name, player)
-  }
-  def HandleBidForRole(bid : IndirectInvite, player : Player) : Boolean = {
-    HandleBidForRole(bid, bid.squad_guid, bid.player.Name, player)
-  }
-
-  def HandleBidForRole(bid : Invitation, squad_guid : PlanetSideGUID, name : String, player : Player) : Boolean = {
-    GetSquad(squad_guid) match {
-      case Some(squad) =>
-        val leaderCharId = squad.Leader.CharId
-        if(squad.AutoApproveInvitationRequests) {
-          self ! SquadServiceMessage(player, Zone.Nowhere, SquadAction.Membership(SquadRequestType.Accept, leaderCharId, None, "", None))
-        }
-        else {
-          SquadEvents.publish(SquadServiceResponse(s"/$leaderCharId/Squad", SquadResponse.WantsSquadPosition(name)))
-        }
-        true
-      case _ =>
-        //squad is missing; will this properly short-circuit?
-        log.error(s"Attempted to process ${bid.InviterName}'s bid for a position in a squad (id:${squad_guid.guid}) that does not exist")
-        false
-    }
-  }
-
-  def JoinSquad(player : Player, squad : Squad, line : Int) : Boolean = {
-    val charId = player.CharId
-    val position = squad.Membership(line)
-    if(ValidOpenSquadPosition(squad, line, position, player.Certifications)) {
-      log.info(s"Player ${player.Name} will join the squad ${squad.Task} at position ${line+1}!")
-      position.Name = player.Name
-      position.CharId = charId
-      position.Health = StatConverter.Health(player.Health, player.MaxHealth, min=1, max=64)
-      position.Armor = StatConverter.Health(player.Armor, player.MaxArmor, min=1, max=64)
-      position.Position = player.Position
-      position.ZoneId = 13
-      memberToSquad(charId) = squad
-
-      InitialAssociation(squad)
-      SquadEvents.publish( SquadServiceResponse(s"/$charId/Squad", SquadResponse.AssociateWithSquad(squad.GUID)) )
-      val size = squad.Size
-      if(size == 1) {
-        //leader joins the squad? do nothing?
-        squad.LeaderPositionIndex = line
-      }
-      else if(size == 2) {
-        //first squad member after leader; both members fully initialize
-        val indices = squad.Membership.zipWithIndex
-          .collect({ case (member, index) if member.CharId != 0 => index }).toList
-        squad.Membership
-          .filterNot { _.CharId == 0 }
-          .foreach { member =>
-            SquadEvents.publish(SquadServiceResponse(s"/${member.CharId}/Squad", SquadResponse.Join(squad, indices)))
-            InitWaypoints(member.CharId, squad.GUID)
-          }
-        //fully update for all users
-        UpdateSquadDetail(squad.GUID, squad)
-      }
-      else {
-        //joining an active squad; everybody updates differently
-        //new member gets full squad UI updates
-        val indices = squad.Membership.zipWithIndex.collect({ case (member, index) if member.CharId != 0 => index }).toList
-        SquadEvents.publish(SquadServiceResponse(s"/$charId/Squad", SquadResponse.Join(squad, indices)))
-        InitSquadDetail(squad.GUID, Seq(charId), squad)
-        InitWaypoints(charId, squad.GUID)
-        //other squad members see new member joining the squad
-        val updatedIndex = List(line)
-        val otherMembers = squad.Membership.filterNot { member => member.CharId == 0 || member.CharId == charId }.map{ _.CharId }
-        otherMembers.foreach { member =>
-          SquadEvents.publish(SquadServiceResponse(s"/$member/Squad", SquadResponse.Join(squad, updatedIndex)))
-        }
-        val details = SquadDetail().Members(List(SquadPositionEntry(line, SquadPositionDetail().CharId(charId).Name(player.Name))))
-        UpdateSquadDetail(squad.GUID, otherMembers, details)
-      }
-      UpdateSquadListWhenListed(squad, SquadInfo().Size(size))
-      true
-    }
-    else {
-      false
+  /**
+    * 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, InviteForRole(_,_, sguid, pos)) if sguid == guid && pos == position =>
+      case(charId, LookingForSquadRoleInvite(_,_, sguid, pos)) if sguid == guid && pos == position =>
         RemoveInvite(charId)
-      case (charId, BidForRole(_, sguid, pos)) if sguid == guid && pos == position =>
+      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 InviteForRole(_,_, sguid, pos) => sguid == guid && pos == position
-        case BidForRole(_, sguid, pos) => sguid == guid && pos == position
+        case LookingForSquadRoleInvite(_,_, sguid, pos) => sguid == guid && pos == position
+        case RequestRole(_, sguid, pos) => sguid == guid && pos == position
         case _ => false
       }
       if(filtered.isEmpty) {
@@ -1575,107 +1798,40 @@ class SquadService extends Actor {
     }
   }
 
-  def EnsureEmptySquad(char_id : Long, msg : String = "default warning message") : Boolean = {
-    memberToSquad.get(char_id) match {
-      case None =>
-        true
-      case Some(squad) if squad.Size == 1 =>
-        CloseSquad(squad)
-        true
-      case _ =>
-        log.warn(msg)
-        false
-    }
-  }
-
-  def LeaveSquad(charId : Long, squad : Squad) : Boolean = {
-    val membership = squad.Membership.zipWithIndex
-    membership.find { case (_member, _) => _member.CharId == charId } match {
-      case Some((member, index)) =>
-        val entry = (charId, index)
-        val updateList = entry +: membership
-          .collect { case (_member, _index) if _member.CharId > 0 && _member.CharId != charId => (_member.CharId, _index) }
-          .toList
-        //member leaves the squad completely
-        memberToSquad.remove(charId)
-        member.Name = ""
-        member.CharId = 0
-        SquadEvents.publish( SquadServiceResponse(s"/$charId/Squad", SquadResponse.Leave(squad, updateList)))
-        //other squad members see the member leaving
-        val leavingMember = List(entry)
-        membership
-          .filter { case (_member, _) => _member.CharId > 0 }
-          .foreach { case (_member, _) =>
-            SquadEvents.publish( SquadServiceResponse(s"/${_member.CharId}/Squad", SquadResponse.Leave(squad, leavingMember)) )
-          }
-        UpdateSquadListWhenListed(squad, SquadInfo().Size(squad.Size))
-        UpdateSquadDetail(squad.GUID, squad,
-          SquadDetail().Members(List(SquadPositionEntry(index, SquadPositionDetail().Player(char_id = 0, name = ""))))
-        )
-        true
-      case None =>
-        false
-    }
-  }
-
-  def CloseSquad(squad : Squad) : Unit = {
-    val guid = squad.GUID
-    val membership = squad.Membership.zipWithIndex
-    val (updateMembers, updateIndices) = membership
-      .collect { case (member, index) if member.CharId > 0 => ((member, member.CharId, index), (member.CharId, index)) }
-      .unzip
-    val updateIndicesList = updateIndices.toList
-    val completelyBlankSquadDetail = SquadDetail().Complete
-    updateMembers
-      .foreach { case (member, charId, index) =>
-        memberToSquad.remove(charId)
-        member.Name = ""
-        member.CharId = 0L
-        SquadEvents.publish( SquadServiceResponse(s"/$charId/Squad", SquadResponse.Leave(squad,
-          updateIndicesList.filterNot { case (_, outIndex) => outIndex == index } :+ (charId, index) //we need to be last to leave to see the events
-        )) )
-        SquadEvents.publish( SquadServiceResponse(s"/$charId/Squad", SquadResponse.AssociateWithSquad(PlanetSideGUID(0))) )
-        SquadEvents.publish( SquadServiceResponse(s"/$charId/Squad", SquadResponse.Detail(PlanetSideGUID(0), completelyBlankSquadDetail)) )
-      }
-    UpdateSquadListWhenListed(squad, None)
-    CleanupInvitesForSquad(guid)
-    squadFeatures.remove(guid).get.Stop
-    TryResetSquadId()
-  }
-
-  def DisbandSquad(squad : Squad) : Unit = {
-    CloseSquad(squad)
-    val leader = squad.Leader.CharId
-    SquadEvents.publish(SquadServiceResponse(s"/$leader/Squad", SquadResponse.Membership(SquadResponseType.Disband, 0, 0, leader, None, "", true, Some(None))))
-    squad.Membership
-      .collect { case member if member.CharId > 0 && member.CharId != leader => member.CharId }
-      .foreach { charId =>
-        SquadEvents.publish(SquadServiceResponse(s"/$charId/Squad", SquadResponse.Membership(SquadResponseType.Disband, 0, 0, charId, None, "", false, Some(None))))
-      }
-  }
-
-  def CleanupInvitesForSquad(squadGUID : PlanetSideGUID) : Unit = {
+  /**
+    * 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 squadGUID == guid =>
+      case (id, VacancyInvite(_, _, guid)) if sguid == guid =>
         RemoveInvite(id)
-      case (id, IndirectInvite(_, guid)) if squadGUID == guid =>
+      case (id, IndirectInvite(_, guid)) if sguid == guid =>
         RemoveInvite(id)
-      case (id, InviteForRole(_, _, guid, _)) if squadGUID == guid =>
+      case (id, LookingForSquadRoleInvite(_, _, guid, _)) if sguid == guid =>
         RemoveInvite(id)
-      case (id, BidForRole(_, guid, _)) if squadGUID == guid =>
+      case (id, RequestRole(_, guid, _)) if sguid == guid =>
         RemoveInvite(id)
-      case (id, ProximityInvite(_, _, guid)) if squadGUID == guid =>
+      case (id, ProximityInvite(_, _, guid)) if sguid == guid =>
         RemoveInvite(id)
     }
     //tidy the queued invitations
     queuedInvites.foreach { case(id, queue) =>
       val filteredQueue = queue.filterNot {
-        case VacancyInvite(_, _, guid) => squadGUID == guid
-        case IndirectInvite(_, guid) => squadGUID == guid
-        case InviteForRole(_, _, guid, _) => squadGUID == guid
-        case BidForRole(_, guid, _) => squadGUID == guid
-        case ProximityInvite(_, _, guid) => squadGUID == guid
+        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) {
@@ -1685,15 +1841,32 @@ class SquadService extends Actor {
         queuedInvites.update(id, filteredQueue)
       }
     }
-    squadFeatures(squadGUID).SearchForRole match {
+    squadFeatures(sguid).ProxyInvites = Nil
+    squadFeatures(sguid).SearchForRole match {
       case None => ;
       case Some(_) =>
-        squadFeatures(squadGUID).SearchForRole = None
+        squadFeatures(sguid).SearchForRole = None
+    }
+    continueToMonitorDetails.collect {
+      case (charId, guid) if sguid == guid =>
+        continueToMonitorDetails.remove(charId)
     }
   }
 
-  def CleanupInvitesFromPlayer(charId : Long) : Unit = {
-    invites.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)
@@ -1701,11 +1874,9 @@ class SquadService extends Actor {
         RemoveInvite(id)
       case (id, IndirectInvite(player, _)) if player.CharId == charId =>
         RemoveInvite(id)
-      case (id, InviteForRole(_charId, _, _, _)) if _charId == charId =>
+      case (id, LookingForSquadRoleInvite(_charId, _, _, _)) if _charId == charId =>
         RemoveInvite(id)
-      case (id, BidForRole(player, _, _)) if player.CharId == charId =>
-        RemoveInvite(id)
-      case (id, ProximityInvite(_charId, _, _)) if charId == _charId =>
+      case (id, RequestRole(player, _, _)) if player.CharId == charId =>
         RemoveInvite(id)
     }
     //tidy the queued invitations
@@ -1715,9 +1886,8 @@ class SquadService extends Actor {
         case SpontaneousInvite(player) => player.CharId == charId
         case VacancyInvite(player, _, _) => player == charId
         case IndirectInvite(player, _) => player.CharId == charId
-        case InviteForRole(player, _, _, _) => player == charId
-        case BidForRole(player, _, _) => player.CharId == charId
-        case ProximityInvite(_charId, _, _) => _charId == charId
+        case LookingForSquadRoleInvite(player, _, _, _) => player == charId
+        case RequestRole(player, _, _) => player.CharId == charId
         case _ => false
       }
       if(filteredQueue.isEmpty) {
@@ -1727,33 +1897,589 @@ class SquadService extends Actor {
         queuedInvites.update(id, filteredQueue)
       }
     }
-    previousInvites.remove(charId)
+    continueToMonitorDetails.remove(charId)
+    RemoveProximityInvites(charId)
   }
 
-  def CleanupInvitesToPosition(position : Int) : Unit = {
-    invites.collect {
-      case (id, InviteForRole(_, _, _, _position)) if _position == position =>
+  /**
+    * 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) = invites.collect {
+      case (id, ProximityInvite(inviterCharId, inviterName, squadGUID)) if inviterCharId == invitingPlayer =>
         RemoveInvite(id)
-      case (id, BidForRole(_, _, _position)) if _position == position =>
-        RemoveInvite(id)
-    }
-    //tidy the queued invitations
-    queuedInvites.foreach { case(id, queue) =>
-      val filteredQueue = queue.filterNot {
-        case InviteForRole(_, _, _, _position) => _position == position
-        case BidForRole(_, _, _position) => _position == position
+        ((id, squadGUID), (id, inviterName))
+    }.unzip
+    RemoveProximityInvites(removedInvites)
+    //queued
+    RemoveProximityInvites(queuedInvites.flatMap { case (id : Long, inviteList) =>
+      val (outList, inList) = inviteList.partition {
+        case ProximityInvite(inviterCharId, _, _) if inviterCharId == invitingPlayer => true
         case _ => false
       }
-      if(filteredQueue.isEmpty) {
+      if(inList.isEmpty) {
         queuedInvites.remove(id)
       }
-      else if(filteredQueue.size != queue.size) {
-        queuedInvites.update(id, filteredQueue)
+      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 (ids, 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
+          }
+        case _ => ;
       }
     }
   }
 
-  def SwapMemberPosition(squad : Squad, toMember : Member, fromMember : Member) : Unit = {
+  /**
+    * 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) = 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) =>
+      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
+  }
+
+  /**
+    * 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 {
+      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.<br>
+    * <br>
+    * 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`
+    * @see `ValidOpenSquadPosition`
+    * @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 (member, index) =>
+      ValidOpenSquadPosition(squad, index, member, recruit.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
+        }
+        else {
+          Some((squad, line))
+        }
+      case _ =>
+        None
+    }
+  }
+
+  /**
+    * 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.<br>
+    * <br>
+    * 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 of 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 {
+      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 {
+          import scala.concurrent.duration._
+          import scala.concurrent.ExecutionContext.Implicits.global
+          features.Prompt = context.system.scheduler.schedule(1 milliseconds, 45000 milliseconds, self, SquadResponse.WantsSquadPosition(leaderCharId, player.Name))
+        }
+        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.Detail.Publish`
+    * @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.Detail.Publish(squad)))
+    }
+  }
+
+  /**
+    * Establish a new squad.
+    * Create all of the support structures for the squad and link into them.
+    * At a minimum, by default, the squad needs a squad leader
+    * and a stronger, more exposed connection between the squad and leader needs to be recognized.<br>
+    * <br>
+    * Usually, a squad is created by modifying some aspect of its necessary fields.
+    * The primary necessary fields required for a squad include the squad's task and the squad's zone of operation.
+    * @see `GetNextSquadId`
+    * @see `Squad`
+    * @see `SquadFeatures`
+    * @see `SquadFeatures::Start`
+    * @param player the player who would become the squad leader
+    * @return the squad that has been created
+    */
+  def StartSquad(player : Player) : Squad = {
+    val faction = player.Faction
+    val name = player.Name
+    val squad = new Squad(GetNextSquadId(), faction)
+    val leadPosition = squad.Membership(0)
+    leadPosition.Name = name
+    leadPosition.CharId = player.CharId
+    leadPosition.Health = player.Health
+    leadPosition.Armor = player.Armor
+    leadPosition.Position = player.Position
+    leadPosition.ZoneId = 1
+    squadFeatures += squad.GUID -> new SquadFeatures(squad).Start
+    memberToSquad += squad.Leader.CharId -> squad
+    debug(s"$name-$faction has created a new squad")
+    squad
+  }
+
+  /**
+    * Behaviors and exchanges necessary to complete the fulfilled recruitment process for the squad role.<br>
+    * <br>
+    * 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`
+    * 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 `ValidOpenSquadPosition`
+    * @param player the new squad member;
+    *               this player is NOT the squad leader
+    * @param squad 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 = {
+    val charId = player.CharId
+    val role = squad.Membership(position)
+    if(UserEvents.get(charId).nonEmpty && squad.Leader.CharId != charId && ValidOpenSquadPosition(squad, position, role, player.Certifications)) {
+      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 = s"/${features.ToChannel}/Squad"
+        memberCharIds.foreach { charId =>
+          SquadEvents.subscribe(UserEvents(charId), toChannel)
+          Publish(charId, SquadResponse.Join(squad, indices, toChannel))
+          InitWaypoints(charId, squad.GUID)
+        }
+        //fully update for all users
+        InitSquadDetail(squad)
+      }
+      else {
+        //joining an active squad; everybody updates differently
+        val updatedIndex = List(position)
+        val toChannel = s"/${features.ToChannel}/Squad"
+        //new member gets full squad UI updates
+        Publish(
+          charId,
+          SquadResponse.Join(
+            squad,
+            squad.Membership.zipWithIndex.collect({ case (member, index) if member.CharId > 0 => index }).toList,
+            toChannel
+          )
+        )
+        //other squad members see new member joining the squad
+        Publish(features.ToChannel, SquadResponse.Join(squad, updatedIndex, ""))
+        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(UserEvents(charId), toChannel)
+      }
+      UpdateSquadListWhenListed(features, SquadInfo().Size(size))
+      true
+    }
+    else {
+      false
+    }
+  }
+
+  /**
+    * Determine whether a player is sufficiently unemployed
+    * and has no grand delusions of being a squad leader.
+    * @see `CloseSquad`
+    * @param charId the player
+    * @return `true`, if the target player possesses no squad or a squad that is suitably nonexistent;
+    *        `false`, otherwise
+    */
+  def EnsureEmptySquad(charId : Long) : Boolean = {
+    memberToSquad.get(charId) match {
+      case None =>
+        true
+      case Some(squad) if squad.Size == 1 =>
+        CloseSquad(squad)
+        true
+      case _ =>
+        log.warn("EnsureEmptySquad: the invited player is already a member of a squad and can not join a second one")
+        false
+    }
+  }
+
+  /**
+    * Behaviors and exchanges necessary to undo the recruitment process for the squad role.
+    * @see `PanicLeaveSquad`
+    * @see `Publish`
+    * @param charId the player
+    * @param squad 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 = {
+    val membership = squad.Membership.zipWithIndex
+    membership.find { case (_member, _) => _member.CharId == charId } match {
+      case data @ Some((_, 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")
+        true
+      case _ =>
+        false
+    }
+  }
+
+  /**
+    * Behaviors and exchanges necessary to undo the recruitment process for the squad role.<br>
+    * <br>
+    * 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)) =>
+        val entry = (charId, index)
+        //member leaves the squad completely
+        memberToSquad.remove(charId)
+        member.Name = ""
+        member.CharId = 0
+        //other squad members see the member leaving
+        Publish(squadFeatures(squad.GUID).ToChannel, SquadResponse.Leave(squad, List(entry)))
+        UpdateSquadListWhenListed(squadFeatures(squad.GUID), SquadInfo().Size(squad.Size))
+        UpdateSquadDetail(
+          squad.GUID,
+          SquadDetail().Members(List(SquadPositionEntry(index, SquadPositionDetail().Player(char_id = 0, name = ""))))
+        )
+        true
+      case None =>
+        false
+    }
+  }
+
+  /**
+    * All players are made to leave the squad and the squad will stop existing.
+    * Any member of the squad missing an `ActorRef` object used to message the player's client
+    * 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 `SquadDetail`
+    * @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)) }
+      .unzip
+    val updateIndicesList = updateIndices.toList
+    val completelyBlankSquadDetail = SquadDetail().Complete
+    val channel = s"/${squadFeatures(squad.GUID).ToChannel}/Squad"
+    updateMembers
+      .foreach {
+        case (member, charId, _, None) =>
+          memberToSquad.remove(charId)
+          member.Name = ""
+          member.CharId = 0L
+        case (member, charId, index, Some(actor)) =>
+          memberToSquad.remove(charId)
+          member.Name = ""
+          member.CharId = 0L
+          SquadEvents.unsubscribe(actor, channel)
+          Publish(
+            charId,
+            SquadResponse.Leave(
+              squad,
+              updateIndicesList.filterNot { case (_, outIndex) => outIndex == index } :+ (charId, index) //we need to be last
+            )
+          )
+          Publish(charId, SquadResponse.AssociateWithSquad(PlanetSideGUID(0)))
+          Publish(charId, SquadResponse.Detail(PlanetSideGUID(0), completelyBlankSquadDetail))
+      }
+    UpdateSquadListWhenListed(
+      squadFeatures.remove(guid).get.Stop,
+      None
+    )
+    TryResetSquadId()
+  }
+
+  /**
+    * All players are made to leave the squad and the squad will stop existing.
+    * 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
+    */
+  def DisbandSquad(squad : Squad) : Unit = {
+    val leader = squad.Leader.CharId
+    PanicDisbandSquad(
+      squad,
+      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
+    Publish(leader, SquadResponse.Membership(SquadResponseType.Disband, 0, 0, leader, None, "", true, Some(None)))
+  }
+
+  /**
+    * All players are made to leave the squad and the squad will stop existing.<br>
+    * <br>
+    * The complement of the prior `DisbandSquad` method.
+    * This method deals entirely with other squad members observing the squad become abandoned.
+    * 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 `CloseSquad`
+    * @see `DisbandSquad`
+    * @see `Publish`
+    * @see `SquadResponse.Membership`
+    * @param squad 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 = {
+    CloseSquad(squad)
+    membership.foreach { charId =>
+      Publish(charId, SquadResponse.Membership(SquadResponseType.Disband, 0, 0, charId, None, "", false, Some(None)))
+    }
+  }
+
+  /**
+    * 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
@@ -1775,119 +2501,56 @@ class SquadService extends Actor {
     toMember.Armor = armor
   }
 
-  def UpdateSquadList(faction : PlanetSideEmpire.Value): Unit = {
-    val factionListings = publishedLists(faction)
-    SquadEvents.publish(
-      SquadServiceResponse(s"/$faction/Squad", SquadResponse.InitList(factionListings.toVector))
-    )
-  }
-
-  def UpdateSquadList(squad : Squad, changes : SquadInfo) : Unit = {
-    UpdateSquadList(squad, Some(changes))
-  }
-
-  def UpdateSquadListWhenListed(squad : Squad, changes : SquadInfo) : Unit = {
-    UpdateSquadListWhenListed(squad, Some(changes))
-  }
-
-  def UpdateSquadListWhenListed(squad : Squad, changes : Option[SquadInfo]) : Unit = {
-    if(squad.Listed || squad.Size > 1) {
-      UpdateSquadList(squad, changes)
-    }
-  }
-
-  def UpdateSquadList(squad : Squad, changes : Option[SquadInfo]) : Unit = {
-    val faction = squad.Faction
-    val factionListings = publishedLists(faction)
-    factionListings.find(info => {
-      info.squad_guid match {
-        case Some(sguid) => sguid == squad.GUID
-        case _ => false
-      }
-    }) match {
-      case Some(listedSquad) =>
-        val index = factionListings.indexOf(listedSquad)
-        changes match {
-          case Some(changedFields) =>
-            //squad information update
-            log.info(s"Squad will be updated")
-            factionListings(index) = SquadService.SquadList.Publish(squad)
-            SquadEvents.publish(
-              SquadServiceResponse(s"/$faction/Squad", SquadResponse.UpdateList(Seq((index, changedFields))))
-            )
+  /**
+    * Display the indicated waypoint.<br>
+    * <br>
+    * 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."
+    * @see `SquadWaypointRequest`
+    * @see `WaypointInfo`
+    * @param guid the squad's unique identifier
+    * @param waypointType the type of the waypoint as an integer;
+    *                     0-4 are squad waypoints;
+    *                     5 is the squad leader's experience 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 : Int, info : WaypointInfo) : Option[WaypointData] = {
+    squadFeatures.get(guid) match {
+      case Some(features) =>
+        features.Waypoints.lift(waypointType) match {
+          case Some(point) =>
+            point.zone_number = info.zone_number
+            point.pos = info.pos
+            Some(point)
           case None =>
-            //remove squad from listing
-            log.info(s"Squad will be removed")
-            factionListings.remove(index)
-            SquadEvents.publish(
-              //SquadServiceResponse(s"$faction/Squad", SquadResponse.RemoveFromList(Seq(index)))
-              SquadServiceResponse(s"/$faction/Squad", SquadResponse.InitList(factionListings.toVector))
-            )
+            log.error(s"no squad waypoint $waypointType found")
+            None
         }
       case None =>
-        //first time being published
-        log.info(s"Squad will be introduced")
-        factionListings += SquadService.SquadList.Publish(squad)
-        SquadEvents.publish(
-          SquadServiceResponse(s"/$faction/Squad", SquadResponse.InitList(factionListings.toVector))
-        )
-    }
-  }
-
-  def InitSquadDetail(squad : Squad) : Unit = {
-    InitSquadDetail(squad.GUID, squad.Membership.map { member => member.CharId }, squad)
-  }
-
-  def InitSquadDetail(guid : PlanetSideGUID, squad : Squad) : Unit = {
-    InitSquadDetail(guid, squad.Membership.map { member => member.CharId }, squad)
-  }
-
-  def InitSquadDetail(guid : PlanetSideGUID, toMembers : Iterable[Long], squad : Squad) : Unit = {
-    val output = SquadResponse.Detail(guid, SquadService.Detail.Publish(squad))
-    toMembers.foreach { charId => SquadEvents.publish(SquadServiceResponse(s"/$charId/Squad", output)) }
-  }
-
-  def UpdateSquadDetail(guid : PlanetSideGUID, squad : Squad) : Unit = {
-    UpdateSquadDetail(guid, squad, SquadService.Detail.Publish(squad))
-  }
-
-  def UpdateSquadDetail(squad : Squad, details : SquadDetail) : Unit = {
-    UpdateSquadDetail(squad.GUID, squad.Membership.map { member => member.CharId }, details)
-  }
-
-  def UpdateSquadDetail(guid : PlanetSideGUID, squad : Squad, details : SquadDetail) : Unit = {
-    UpdateSquadDetail(guid, squad.Membership.map { member => member.CharId }, details)
-  }
-
-  def UpdateSquadDetail(guid : PlanetSideGUID, toMembers : Iterable[Long], details : SquadDetail) : Unit = {
-    if(toMembers.nonEmpty) {
-      val output = SquadResponse.Detail(guid, details)
-      toMembers.foreach { charId => SquadEvents.publish(SquadServiceResponse(s"/$charId/Squad", output)) }
-    }
-  }
-
-  def AddWaypoint(guid : PlanetSideGUID, waypointType : Int, info : WaypointInfo) : Option[WaypointData] = {
-    squadFeatures(guid).Waypoints.lift(waypointType) match {
-      case Some(point) =>
-        //update the waypoint
-        log.debug(s"rendering squad waypoint $waypointType for squad #${guid.guid}")
-        point.zone_number = info.zone_number
-        point.pos = info.pos
-        Some(point)
-      case _ =>
-        log.warn(s"no squad waypoint $waypointType found")
+        log.error(s"no squad waypoint $waypointType found")
         None
     }
   }
 
+  /**
+    * Hide the indicated waypoint.
+    * Unused waypoints are marked by having a non-zero z-coordinate.<br>
+    * <br>
+    * 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 as an integer;
+    *                     0-4 are squad waypoints;
+    *                     5 is the squad leader's experience waypoint
+    */
   def RemoveWaypoint(guid : PlanetSideGUID, waypointType : Int) : Unit = {
     squadFeatures.get(guid) match {
       case Some(features) =>
         features.Waypoints.lift(waypointType) match {
           case Some(point) =>
-            //update the waypoint
-            log.debug(s"removing squad waypoint $waypointType for squad #${guid.guid}")
-            point.zone_number = 1
             point.pos = Vector3.z(1)
           case _ =>
             log.warn(s"no squad waypoint $waypointType found")
@@ -1897,104 +2560,301 @@ class SquadService extends Actor {
     }
   }
 
+  /**
+    * 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
-        SquadEvents.publish(
-          SquadServiceResponse(s"/$toCharId/Squad", SquadResponse.InitWaypoints(squad.Leader.CharId,
+        Publish(
+          toCharId, SquadResponse.InitWaypoints(squad.Leader.CharId,
             list.zipWithIndex.collect { case (point, index) if point.pos != vz1 =>
               (index, WaypointInfo(point.zone_number, point.pos), 1)
             }
-          ))
+          )
         )
       case None => ;
     }
   }
 
-  def indirectInviteResp(bid : IndirectInvite, player : Player, invitedPlayer : Long, invitingPlayer : Long, name : String) : Boolean = {
-    HandleBidForRole(bid, player)
+  def LeaveService(charId : String, sender : ActorRef) : Unit = {
+    LeaveService(charId.toLong, sender)
   }
 
-  def altIndirectInviteResp(bid : IndirectInvite, player : Player, invitedPlayer : Long, invitingPlayer : Long, name : String) : Boolean = {
-    SquadEvents.publish(SquadServiceResponse(s"/$invitingPlayer/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitingPlayer, Some(invitedPlayer), player.Name, false, Some(None))))
-    HandleBidForRole(bid, player)
+  def LeaveService(charId : Long, sender : ActorRef) : Unit = {
+    refused.remove(charId)
+    continueToMonitorDetails.remove(charId)
+    RemoveAllInvitesWithPlayer(charId)
+    val pSquadOpt = GetParticipatingSquad(charId)
+    val lSquadOpt = GetLeadingSquad(charId, pSquadOpt)
+    pSquadOpt match {
+      //member of the squad; leave the squad
+      case Some(squad) =>
+        val size = squad.Size
+        SquadEvents.unsubscribe(UserEvents(charId), s"/${squadFeatures(squad.GUID).ToChannel}/Squad")
+        UserEvents.remove(charId)
+        lSquadOpt 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 })
+        }
+      case None =>
+        //not a member of any squad; nothing to do here
+        UserEvents.remove(charId)
+    }
+    SquadEvents.unsubscribe(sender) //just to make certain
   }
 
-  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, _) =>
-          SquadEvents.publish(SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, charId, Some(invitedPlayer), _name, false, Some(None))))
-          SquadEvents.publish(SquadServiceResponse(s"/$charId/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, invitedPlayer, Some(charId), _name, true, Some(None))))
+  /**
+    * Dispatch a message entailing the composition of this squad
+    * and focus on any specific aspects of it, purported as being changed recently.
+    * @see `SquadInfo`
+    * @see `UpdateSquadList(Squad, Option[SquadInfo])`
+    * @param squad the squad
+    * @param changes the highlighted aspects of the squad;
+    *                these "changes" do not have to reflect the actual squad but are related to the contents of the message
+    */
+  private def UpdateSquadList(squad : Squad, changes : SquadInfo) : Unit = {
+    UpdateSquadList(squad, Some(changes))
+  }
 
-        case _bid @ IndirectInvite(player, _) =>
-          indirectVacancyFunc(_bid, player, invitedPlayer, invitingPlayer, name)
+  /**
+    * Dispatch a message entailing the composition of this squad when that squad is publicly available
+    * and focus on any specific aspects of it, purported as being changed recently.
+    * @see `SquadInfo`
+    * @see `UpdateSquadList(Squad, Option[SquadInfo])`
+    * @param features the related information about the squad
+    * @param changes the highlighted aspects of the squad;
+    *                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 : SquadInfo) : Unit = {
+    UpdateSquadListWhenListed(features, Some(changes))
+  }
 
-        case _bid @ SpontaneousInvite(player) =>
-          val bidInvitingPlayer = _bid.InviterCharId
-          SquadEvents.publish(SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, bidInvitingPlayer, Some(invitedPlayer), player.Name, false, Some(None))))
-          SquadEvents.publish(SquadServiceResponse(s"/$bidInvitingPlayer/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, invitedPlayer, Some(bidInvitingPlayer), player.Name, true, Some(None))))
+  /**
+    * Dispatch a message entailing the composition of this squad when that squad is publicly available
+    * and focus on any specific aspects of it, purported as being changed recently.
+    * The only requirement is that the squad is publicly available for recruitment ("listed").
+    * @see `SquadInfo`
+    * @see `UpdateSquadList(Squad, Option[SquadInfo])`
+    * @param features the related information about 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
+    */
+  private def UpdateSquadListWhenListed(features : SquadFeatures, changes : Option[SquadInfo]) : Unit = {
+    val squad = features.Squad
+    if(features.Listed) {
+      UpdateSquadList(squad, changes)
+    }
+  }
 
-        case _bid @ BidForRole(player, _, _) =>
-          HandleBidForRole(_bid, player)
+  /**
+    * Dispatch a message entailing the composition of this squad
+    * and focus on any specific aspects of it, purported as being changed recently.<br>
+    * <br>
+    * What sort of message is dispatched is not only based on the input parameters
+    * but also on the state of previously listed squad information.
+    * Listed squad information is queued when it is first published, organized first by faction affinity, then by chronology.
+    * The output is first determinate on whether the squad had previously been listed as available.
+    * If so, it will either update its data to all valid faction associated entities with the provided changed data;
+    * or, it will be removed from the list of available squads, if there is no provided change data.
+    * If the squad can not be found,
+    * the change data, whatever it is, is unimportant, and the squad will be listed in full for the first time.<br>
+    * <br>
+    * When a squad is first introduced to the aforementioned list,
+    * thus first being published to all faction-associated parties,
+    * the entirety of the squad list for that faction will be updated in one go.
+    * It is not necessary to do this, but doing so saves index and unique squad identifier management
+    * at the cost of the size of the packet to be dispatched.
+    * When a squad is removed to the aforementioned list,
+    * the same process occurs where the full list for that faction affiliation is sent as an update.
+    * The procedure for updating individual squad fields is precise and targeted,
+    * and has been or should be prepared in advance by the caller to this method.
+    * As a consequence, when updating the entry for that squad,
+    * the information used as the update does not necessarily reflect the actual information currently in the squad.
+    * @see `SquadResponse.InitList`
+    * @see `SquadResponse.UpdateList`
+    * @see `SquadService.SquadList.Publish`
+    * @param squad 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 = {
+    val guid = squad.GUID
+    val faction = squad.Faction
+    val factionListings = publishedLists(faction)
+    factionListings.find(_ == guid) match {
+      case Some(listedSquad) =>
+        val index = factionListings.indexOf(listedSquad)
+        changes match {
+          case Some(changedFields) =>
+            //squad information update
+            Publish(faction, SquadResponse.UpdateList(Seq((index, changedFields))))
+          case None =>
+            //remove squad from listing
+            factionListings.remove(index)
+            //Publish(faction, SquadResponse.RemoveFromList(Seq(index)))
+            Publish(faction, SquadResponse.InitList(PublishedLists(factionListings)))
+        }
+      case None =>
+        //first time being published
+        factionListings += guid
+        Publish(faction, SquadResponse.InitList(PublishedLists(factionListings)))
+    }
+  }
 
-        case InviteForRole(charId, _name, _, _) =>
-          SquadEvents.publish(SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, invitedPlayer, Some(charId), _name, false, Some(None))))
+  /**
+    * 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
+    )
+  }
 
-        case ProximityInvite(charId, _name, _) =>
-          SquadEvents.publish(SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, invitedPlayer, Some(charId), _name, false, Some(None))))
+  /**
+    * Dispatch an intial message entailing the strategic information and the composition of this squad.
+    * The details of the squad will be updated in full and be sent to all indicated observers.
+    * @see `SquadService.Detail.Publish`
+    * @param guid the unique squad identifier to be used when composing the details for this message
+    * @param to the unique character identifier numbers of the players who will receive this message
+    * @param squad the squad from which the squad details shall be composed
+    */
+  def InitSquadDetail(guid : PlanetSideGUID, to : Iterable[Long], squad : Squad) : Unit = {
+    val output = SquadResponse.Detail(guid, SquadService.Detail.Publish(squad))
+    to.foreach { Publish(_, output) }
+  }
 
-        case _ =>
-          log.warn(s"AddInviteAndRespond: can not parse discovered unhandled invitation type - $targetInvite")
+  /**
+    * Send a message entailing the strategic information and the composition of the squad to the existing members of the squad.
+    * @see `SquadService.Detail.Publish`
+    * @see `UpdateSquadDetail(PlanetSideGUID, PlanetSideGUID, List[Long], SquadDetail)`
+    * @param squad the squad
+    */
+  def UpdateSquadDetail(squad : Squad) : Unit = {
+    UpdateSquadDetail(
+      squad.GUID,
+      squad.GUID,
+      Nil,
+      SquadService.Detail.Publish(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.Detail.Publish`
+    * @see `UpdateSquadDetail(PlanetSideGUID, PlanetSideGUID, List[Long], SquadDetail)`
+    * @param squad the squad
+    */
+  def UpdateSquadDetail(guid : PlanetSideGUID, squad : Squad) : Unit = {
+    UpdateSquadDetail(
+      guid,
+      squad.GUID,
+      Nil,
+      SquadService.Detail.Publish(squad)
+    )
+  }
+
+  /**
+    * Send 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, sguid) if sguid == guid && !excluding.exists(_ == charId) =>
+        Publish(charId, output, Nil)
       }
-    }
   }
 
-  def AddInviteAndRespond(invitedPlayer : Long, targetInvite : Invitation, invitingPlayer : Long, name : String) : Unit = {
-    InviteResponseTemplate(indirectInviteResp)(
-      targetInvite,
-      AddInvite(invitedPlayer, targetInvite),
-      invitedPlayer,
-      invitingPlayer,
-      name
-    )
+  /**
+    * 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))
   }
-
-  def AltAddInviteAndRespond(invitedPlayer : Long, targetInvite : Invitation, invitingPlayer : Long, name : String) : Unit = {
-    InviteResponseTemplate(altIndirectInviteResp)(
-      targetInvite,
-      AddInvite(invitedPlayer, targetInvite),
-      invitedPlayer,
-      invitingPlayer,
-      name
-    )
-  }
-
-  def NextInviteAndRespond(invitedPlayer : Long) : Unit = {
-    NextInvite(invitedPlayer) match {
-      case Some(invite) =>
-        InviteResponseTemplate(indirectInviteResp)(
-          invite,
-          Some(invite),
-          invitedPlayer,
-          invite.InviterCharId,
-          invite.InviterName
-        )
-      case None => ;
-    }
+  /**
+    * 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
+    * @return a `Vector` of transformed squad data
+    */
+  def PublishedLists(guids : Iterable[PlanetSideGUID]) : Vector[SquadInfo] = {
+    guids.map {guid => SquadService.SquadList.Publish(squadFeatures(guid).Squad) }.toVector
   }
 }
 
 object SquadService {
+
+  /**
+    * 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
   }
 
+  /**
+    * 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
+    */
   abstract class Invitation(char_id : Long, name : String) {
     def InviterCharId : Long = char_id
     def InviterName : String = name
@@ -2007,7 +2867,7 @@ object SquadService {
     * @param squad_guid the squad with the role
     * @param position the index of the role
     */
-  final case class BidForRole(player : Player, squad_guid : PlanetSideGUID, position : Int)
+  final case class RequestRole(player : Player, squad_guid : PlanetSideGUID, position : Int)
     extends Invitation(player.CharId, player.Name)
 
   /**
@@ -2050,7 +2910,7 @@ object SquadService {
     * @param squad_guid the squad with the role
     * @param position the index of the role
     */
-  final case class InviteForRole(char_id : Long, name : String, squad_guid : PlanetSideGUID, position : Int)
+  final case class LookingForSquadRoleInvite(char_id : Long, name : String, squad_guid : PlanetSideGUID, position : Int)
     extends Invitation(char_id, name)
 
   /**
@@ -2060,105 +2920,13 @@ object SquadService {
   final case class SpontaneousInvite(player : Player)
     extends Invitation(player.CharId, player.Name)
 
-  class SquadFeatures(val Squad : Squad) {
-    /**
-      * `initialAssociation` per squad is similar to "Does this squad want to recruit members?"
-      * The squad does not have to be flagged.
-      * Dispatches an `AssociateWithSquad` `SDAM` to the squad leader and ???
-      * and then a `SDDUM` that includes at least the squad owner name and char id.
-      * Dispatched only once when a squad is first listed
-      * or when the squad leader searches for recruits by proximity or for certain roles or by invite
-      * or when a spontaneous squad forms,
-      * whichever happens first.
-      * Additionally, the packets are also sent when the check is made when the continent is changed (or set).
-      */
-    private var initialAssociation : Boolean = true
-    /**
-      * na
-      */
-    private var switchboard : ActorRef = ActorRef.noSender
-    /**
-      * Waypoint data.
-      * The first four slots are used for squad waypoints.
-      * The fifth slot is used for the squad leader experience waypoint.
-      * @see `Start`
-      */
-    private var waypoints : Array[WaypointData] = Array[WaypointData]()
-    /**
-      * The particular position being recruited right at the moment.
-      * When `None`. no highlighted searches have been indicated.
-      * When a positive integer or 0, indicates distributed `InviteForRole` messages as recorded by `proxyInvites`.
-      * Only one position may bne actively recruited at a time in this case.
-      * When -1, indicates distributed `ProximityIvite` messages as recorded by `proxyInvites`.
-      * Previous efforts may or may not be forgotten if there is a switch between the two modes.
-      */
-    private var searchForRole : Option[Int] = None
-    /**
-      * Handle persistent data related to `ProximityInvite` and `InviteForRole` messages
-      */
-    private var proxyInvites : List[Long] = Nil
-    /**
-      * These useres rejected invitation to this 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.
-      */
-    private var refusedPlayers : List[Long] = Nil
-
-    def Start(implicit context : ActorContext) : SquadFeatures = {
-      switchboard = context.actorOf(Props[SquadSwitchboard], s"squad${Squad.GUID.guid}")
-      waypoints = Array.fill[WaypointData](5)(new WaypointData())
-      this
-    }
-
-    def Stop : SquadFeatures = {
-      switchboard ! akka.actor.PoisonPill
-      switchboard = Actor.noSender
-      waypoints = Array.empty
-      this
-    }
-
-    def InitialAssociation : Boolean = initialAssociation
-
-    def InitialAssociation_=(assoc : Boolean) : Boolean = {
-      initialAssociation = assoc
-      InitialAssociation
-    }
-
-    def Switchboard : ActorRef = switchboard
-
-    def Waypoints : Array[WaypointData] = waypoints
-
-    def SearchForRole : Option[Int] = searchForRole
-
-    def SearchForRole_=(role : Int) : Option[Int] = SearchForRole_=(Some(role))
-
-    def SearchForRole_=(role : Option[Int]) : Option[Int] = {
-      searchForRole = role
-      SearchForRole
-    }
-
-    def ProxyInvites : List[Long] = proxyInvites
-
-    def ProxyInvites_=(list : List[Long]) : List[Long] = {
-      proxyInvites = list
-      ProxyInvites
-    }
-
-    def Refuse : List[Long] = refusedPlayers
-
-    def Refuse_=(charId : Long) : List[Long] = {
-      Refuse_=(List(charId))
-    }
-
-    def Refuse_=(list : List[Long]) : List[Long] = {
-      refusedPlayers = list ++ refusedPlayers
-      Refuse
-    }
-  }
-
   object SquadList {
+    /**
+      * Produce complete squad information.
+      * @see `SquadInfo`
+      * @param squad the squad
+      * @return the squad's information to be used in the squad list
+      */
     def Publish(squad : Squad) : SquadInfo = {
       SquadInfo(
         squad.Leader.Name,
@@ -2172,6 +2940,12 @@ object SquadService {
   }
 
   object Detail {
+    /**
+      * Produce complete squad membership details.
+      * @see `SquadDetail`
+      * @param squad the squad
+      * @return the squad's information to be used in the squad's detail window
+      */
     def Publish(squad : Squad) : SquadDetail = {
       SquadDetail()
         .Field1(squad.GUID.guid)
@@ -2193,6 +2967,11 @@ object SquadService {
     }
   }
 
+  /**
+    * 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)
@@ -2211,11 +2990,62 @@ object SquadService {
     }
   }
 
-  def ValidOpenSquadPosition(squad : Squad, position : Int, member : Member, reqs : Set[CertificationType.Value]) : Boolean = {
-    ValidSquadPosition(squad, position, member, reqs) && member.CharId == 0
+  /**
+    * Determine if the indicated squad role can be used for a player trying to join the squad.
+    * @see `ValidOpenSquadPosition`
+    * @param squad the squad
+    * @param position the position of the squad role being tested
+    * @param certs the certifications being offered for comparison testing
+    * @return `true`, if the squad role may accept the player with the given certification permissions;
+    *        `false`, otherwise
+    */
+  def ValidOpenSquadPosition(squad : Squad, position : Int, certs : Set[CertificationType.Value]) : Boolean = {
+    squad.Membership.lift(position) match {
+      case Some(member) =>
+        ValidOpenSquadPosition(squad, position, member, certs)
+      case None =>
+        false
+    }
   }
 
-  def ValidSquadPosition(squad : Squad, position : Int, member : Member, reqs : Set[CertificationType.Value]) : Boolean = {
-    squad.Availability(position) && reqs.intersect(member.Requirements) == member.Requirements
+  /**
+    * Determine if the indicated squad role can be used for a player trying to join the squad.
+    * @see `AvailableSquadPosition`
+    * @param squad the squad
+    * @param position the position of the squad role being tested
+    * @param member the squad role being tested
+    * @param certs the certifications being offered for comparison testing
+    * @return `true`, if the squad role may accept the player with the given certification permissions;
+    *        `false`, otherwise
+    */
+  def ValidOpenSquadPosition(squad : Squad, position : Int, member : Member, certs : Set[CertificationType.Value]) : Boolean = {
+    AvailableSquadPosition(squad, position, member) && certs.intersect(member.Requirements) == member.Requirements
+  }
+
+  /**
+    * Determine if the indicated squad role is unoccupied but is also capable of being occupied.
+    * @see `AvailableSquadPosition(Squad, Int, Member)`
+    * @param squad the squad
+    * @param position the position of the squad role being tested
+    * @return `true`, if the squad role is unoccupied;
+    *        `false`, otherwise
+    */
+  def AvailableSquadPosition(squad : Squad, position : Int) : Boolean = squad.Membership.lift(position) match {
+    case Some(member) =>
+      AvailableSquadPosition(squad, position, member)
+    case None =>
+      false
+  }
+
+  /**
+    * Determine if the indicated squad role is unoccupied but is also capable of being occupied.
+    * @param squad the squad
+    * @param position the position of the squad role being tested
+    * @param member the squad role being tested
+    * @return `true`, if the squad role is unoccupied;
+    *        `false`, otherwise
+    */
+  def AvailableSquadPosition(squad : Squad, position : Int, member : Member) : Boolean = {
+    squad.Availability(position) && member.CharId == 0
   }
 }
diff --git a/common/src/main/scala/services/teamwork/SquadServiceResponse.scala b/common/src/main/scala/services/teamwork/SquadServiceResponse.scala
index 5e6bc998..47cf0387 100644
--- a/common/src/main/scala/services/teamwork/SquadServiceResponse.scala
+++ b/common/src/main/scala/services/teamwork/SquadServiceResponse.scala
@@ -1,7 +1,14 @@
 // Copyright (c) 2019 PSForever
 package services.teamwork
 
-import net.psforever.packet.game.SquadInfo
 import services.GenericEventBusMsg
 
-final case class SquadServiceResponse(toChannel : String, response : SquadResponse.Response) extends GenericEventBusMsg
+final case class SquadServiceResponse(toChannel : String, exclude : Iterable[Long], response : SquadResponse.Response) extends GenericEventBusMsg
+
+object SquadServiceResponse {
+  def apply(toChannel : String, response : SquadResponse.Response) : SquadServiceResponse =
+    SquadServiceResponse(toChannel, Nil, response)
+
+  def apply(toChannel : String, exclude : Long, response : SquadResponse.Response) : SquadServiceResponse =
+    SquadServiceResponse(toChannel, Seq(exclude), response)
+}
diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala
index cacf4a97..2f5a03a8 100644
--- a/pslogin/src/main/scala/WorldSessionActor.scala
+++ b/pslogin/src/main/scala/WorldSessionActor.scala
@@ -101,6 +101,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
   var whenUsedLastKit : Long = 0
   val projectiles : Array[Option[Projectile]] = Array.fill[Option[Projectile]](Projectile.RangeUID - Projectile.BaseUID)(None)
   var drawDeloyableIcon : PlanetSideGameObject with Deployable => Unit = RedrawDeployableIcons
+  var updateSquad : () => Unit = NoSquadUpdates
   var recentTeleportAttempt : Long = 0
   var lastTerminalOrderFulfillment : Boolean = true  /**
     * used during zone transfers to maintain reference to seated vehicle (which does not yet exist in the new zone)
@@ -118,12 +119,18 @@ class WorldSessionActor extends Actor with MDCContextAware {
     */
   var interstellarFerryTopLevelGUID : Option[PlanetSideGUID] = None
   val squadUI : LongMap[SquadUIElement] = new LongMap[SquadUIElement]()
+  val squad_supplement_id : Int = 11
   /**
     * `AvatarConverter` can only rely on the `Avatar`-local Looking For Squad variable.
-    * When joining or creating a squad, the original state of the avatar's LFS variable is stored here.
-    * Upon leaving or disbanding a squad, this value is restored to the avatar's LFS variable.
+    * When joining or creating a squad, the original state of the avatar's local LFS variable is blanked.
+    * This `WSA`-local variable is then used to indicate the ongoing state of the LFS UI component,
+    * now called "Looking for Squad Member."
+    * Upon leaving or disbanding a squad, this value is made false.
+    * Control switching between the `Avatar`-local and the `WSA`-local variable is contingent on `squadUI` being populated.
     */
   var lfs : Boolean = false
+  var squadChannel : Option[String] = None
+  var squadSetup : () => Unit = FirstTimeSquadSetup
 
   var amsSpawnPoints : List[SpawnPoint] = Nil
   var clientKeepAlive : Cancellable = DefaultCancellable.obj
@@ -168,9 +175,6 @@ class WorldSessionActor extends Actor with MDCContextAware {
           ) ++ player.Inventory.Items)
         .filterNot({ case InventoryItem(obj, _) => obj.isInstanceOf[BoomerTrigger] || obj.isInstanceOf[Telepad] })
       //put any temporary value back into the avatar
-      if(squadUI.nonEmpty) {
-        avatar.LFS = lfs
-      }
       //TODO final character save before doing any of this (use equipment)
       continent.Population ! Zone.Population.Release(avatar)
       if(player.isAlive) {
@@ -356,216 +360,221 @@ class WorldSessionActor extends Actor with MDCContextAware {
     case VehicleServiceResponse(toChannel, guid, reply) =>
       HandleVehicleServiceResponse(toChannel, guid, reply)
 
-    case SquadServiceResponse(toChannel, response) =>
-      response match {
-        case SquadResponse.ListSquadFavorite(line, task) =>
-          sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), line, SquadAction.ListSquadFavorite(task)))
+    case SquadServiceResponse(_, excluded, response) =>
+      if(!excluded.exists(_ == avatar.CharId)) {
+        response match {
+          case SquadResponse.ListSquadFavorite(line, task) =>
+            sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), line, SquadAction.ListSquadFavorite(task)))
 
-        case SquadResponse.InitList(infos) if infos.nonEmpty =>
-          sendResponse(ReplicationStreamMessage(infos))
+          case SquadResponse.InitList(infos) =>
+            sendResponse(ReplicationStreamMessage(infos))
 
-        case SquadResponse.UpdateList(infos) if infos.nonEmpty =>
-          val o = ReplicationStreamMessage(6, None,
-            infos.map { case (index, squadInfo) =>
-              SquadListing(index, squadInfo)
-            }.toVector
-          )
-          sendResponse(
-            ReplicationStreamMessage(6, None,
-              infos.map { case (index, squadInfo) =>
-                SquadListing(index, squadInfo)
-              }.toVector
-            )
-          )
-
-        case SquadResponse.RemoveFromList(infos) if infos.nonEmpty =>
-          sendResponse(
-            ReplicationStreamMessage(1, None,
-              infos.map { index =>
-                SquadListing(index, None)
-              }.toVector
-            )
-          )
-
-        case SquadResponse.Detail(guid, detail) =>
-          sendResponse(SquadDetailDefinitionUpdateMessage(guid, detail))
-
-        case SquadResponse.AssociateWithSquad(squad_guid) =>
-          sendResponse(SquadDefinitionActionMessage(squad_guid, 0, SquadAction.AssociateWithSquad()))
-
-        case SquadResponse.SetListSquad(squad_guid) =>
-          sendResponse(SquadDefinitionActionMessage(squad_guid, 0, SquadAction.SetListSquad()))
-
-        case SquadResponse.Unknown17(squad, char_id) =>
-          sendResponse(
-            SquadDefinitionActionMessage(squad.GUID, 0, SquadAction.Unknown(33))
-          )
-
-        case SquadResponse.Membership(request_type, unk1, unk2, char_id, 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
-              LivePlayerList.WorldPopulation({ case (_, a : Avatar) => char_id == a.CharId }).headOption match {
-                case Some(player) =>
-                  player.name
-                case None =>
-                  player_name
-              }
-            case _ =>
-              player_name
-          }
-          sendResponse(SquadMembershipResponse(request_type, unk1, unk2, char_id, opt_char_id, name, unk5, unk6))
-
-        case SquadResponse.Invite(from_char_id, to_char_id, name) =>
-          sendResponse(SquadMembershipResponse(SquadResponseType.Invite, 0, 0, from_char_id, Some(to_char_id), s"$name", false, Some(None)))
-
-        case SquadResponse.WantsSquadPosition(name : String) =>
-          sendResponse(
-            ChatMsg(
-              ChatMessageType.CMT_TELL, true, "",
-              s"\\#6[SQUAD] \\#3$name\\#6 would like to join your squad. (respond with \\#3/accept\\#6 or \\#3/reject\\#6)",
-              None
-            )
-          )
-
-        case SquadResponse.Join(squad, positionsToUpdate) =>
-          val leader = squad.Leader
-          val id = 11
-          val membershipPositions = squad.Membership
-            .zipWithIndex
-            .filter { case (_, index ) => positionsToUpdate.contains(index) }
-          membershipPositions.find({ case(member, _) => member.CharId == avatar.CharId }) match {
-            case Some((ourMember, ourIndex)) =>
-              //we are joining the squad
-              //load each member's entry (our own too)
-              membershipPositions.foreach { case(member, index) =>
-                sendResponse(SquadMemberEvent.Add(id, member.CharId, index, member.Name, member.ZoneId, unk7 = 0))
-                squadUI(member.CharId) = SquadUIElement(member.Name, index, member.ZoneId, member.Health, member.Armor, member.Position)
-              }
-              //initialization
-              sendResponse(SquadMemberEvent.Add(id, ourMember.CharId, ourIndex, ourMember.Name, ourMember.ZoneId, unk7 = 0)) //repeat of our entry
-              sendResponse(PlanetsideAttributeMessage(player.GUID, 31, id)) //associate with squad?
-              sendResponse(PlanetsideAttributeMessage(player.GUID, 32, ourIndex)) //associate with member position in squad?
-              //a finalization? what does this do?
-              sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(18)))
-              //lfs state management (always OFF)
-              if(avatar.LFS) {
-                lfs = avatar.LFS
-                avatar.LFS = false
-                sendResponse(PlanetsideAttributeMessage(player.GUID, 53, 0))
-              }
-            case _ =>
-              //other player is joining our squad
-              //load each member's entry
-              membershipPositions.foreach { case(member, index) =>
-                sendResponse(SquadMemberEvent.Add(id, member.CharId, index, member.Name, member.ZoneId, unk7 = 0))
-                squadUI(member.CharId) = SquadUIElement(member.Name, index, member.ZoneId, member.Health, member.Armor, member.Position)
-              }
-          }
-          //send an initial dummy update for map icon(s)
-          sendResponse(SquadState(PlanetSideGUID(id),
-            membershipPositions
-              .filterNot { case (member, _) => member.CharId == avatar.CharId }
-              .map{ case (member, _) => SquadStateInfo(member.CharId, member.Health, member.Armor, member.Position, 2,2, false, 429, None,None) }
-              .toList
-          ))
-          log.info(s"SquadCards: ${squadUI.map { case(id, card) => s"[${card.index}:${card.name}/$id]" }.mkString}")
-
-        case SquadResponse.Leave(_, positionsToUpdate) =>
-          val id = 11
-          positionsToUpdate.find({ case(member, _) => member == avatar.CharId }) match {
-            case Some((ourMember, ourIndex)) =>
-              //we are leaving the squad
-              //remove each member's entry (our own too)
-              positionsToUpdate.foreach { case(member, index) =>
-                sendResponse(SquadMemberEvent.Remove(id, member, index))
-                squadUI.remove(member)
-              }
-              //uninitialize
-              sendResponse(SquadMemberEvent.Remove(id, ourMember, ourIndex)) //repeat of our entry
-              sendResponse(PlanetsideAttributeMessage(player.GUID, 31, 0)) //disassociate with squad?
-              sendResponse(PlanetsideAttributeMessage(player.GUID, 32, 0)) //disassociate with member position in squad?
-              sendResponse(PlanetsideAttributeMessage(player.GUID, 34, 4294967295L)) //unknown, perhaps unrelated?
-              //a finalization? what does this do?
-              sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(18)))
-              //lfs state management (maybe ON)
-              if(lfs) {
-                avatar.LFS = lfs
-                sendResponse(PlanetsideAttributeMessage(player.GUID, 53, 1))
-                lfs = false
-              }
-            case _ =>
-              //remove each member's entry
-              positionsToUpdate.foreach { case(member, index) =>
-                sendResponse(SquadMemberEvent.Remove(id, member, index))
-                squadUI.remove(member)
-              }
-              log.info(s"SquadCards: ${squadUI.map { case(id, card) => s"[${card.index}:${card.name}/$id]" }.mkString}")
-          }
-
-        case SquadResponse.UpdateMembers(squad, positions) =>
-          import services.teamwork.SquadAction.{Update => SAUpdate}
-          val id = 11
-          val pairedEntries = positions.collect {
-            case entry if squadUI.contains(entry.char_id) =>
-              (entry, squadUI(entry.char_id))
-          }
-          //prune entries
-          val updatedEntries = pairedEntries
-            .collect({
-              case (entry, element) if entry.zone_number != element.zone =>
-                //zone gets updated for these entries
-                sendResponse(SquadMemberEvent.UpdateZone(11, 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)
-                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)
-                entry
-            })
-            .filterNot(_.char_id == avatar.CharId) //we want to update our backend, but not our frontend
-          if(updatedEntries.nonEmpty) {
+          case SquadResponse.UpdateList(infos) if infos.nonEmpty =>
             sendResponse(
-              SquadState(
-                PlanetSideGUID(id),
-                updatedEntries.map { entry => SquadStateInfo(entry.char_id, entry.health, entry.armor, entry.pos, 2,2, false, 429, None,None)}
+              ReplicationStreamMessage(6, None,
+                infos.map { case (index, squadInfo) =>
+                  SquadListing(index, squadInfo)
+                }.toVector
               )
             )
-          }
 
-        case SquadResponse.AssignMember(squad, from_index, to_index) =>
-          //we've already swapped position internally; now we swap the cards
-          SwapSquadUIElements(squad, from_index, to_index)
-          log.info(s"SquadCards: ${squadUI.map { case(id, card) => s"[${card.index}:${card.name}/$id]" }.mkString}")
+          case SquadResponse.RemoveFromList(infos) if infos.nonEmpty =>
+            sendResponse(
+              ReplicationStreamMessage(1, None,
+                infos.map { index =>
+                  SquadListing(index, None)
+                }.toVector
+              )
+            )
 
-        case SquadResponse.PromoteMember(squad, char_id, from_index, to_index) =>
-          //promotion will swap cards visually, but we must fix the backend
-          val id = 11
-          sendResponse(SquadMemberEvent.Promote(id, char_id))
-          SwapSquadUIElements(squad, from_index, to_index)
-          log.info(s"SquadCards: ${squadUI.map { case(id, card) => s"[${card.index}:${card.name}/$id]" }.mkString}")
+          case SquadResponse.Detail(guid, detail) =>
+            sendResponse(SquadDetailDefinitionUpdateMessage(guid, detail))
 
-        case SquadResponse.SquadSearchResults() =>
-          //I don't actually know how to return search results
-          sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.NoSquadSearchResults()))
+          case SquadResponse.AssociateWithSquad(squad_guid) =>
+            sendResponse(SquadDefinitionActionMessage(squad_guid, 0, SquadAction.AssociateWithSquad()))
 
-        case SquadResponse.InitWaypoints(char_id, waypoints) =>
-          val id = 11
-          StartBundlingPackets()
-          waypoints.foreach { case (waypoint_type, info, unk) =>
-            sendResponse(SquadWaypointEvent.Add(id, char_id, waypoint_type, WaypointEvent(info.zone_number, info.pos, unk)))
-          }
-          StopBundlingPackets()
+          case SquadResponse.SetListSquad(squad_guid) =>
+            sendResponse(SquadDefinitionActionMessage(squad_guid, 0, SquadAction.SetListSquad()))
 
-        case SquadResponse.WaypointEvent(WaypointEventAction.Add, char_id, waypoint_type, _, Some(info), unk) =>
-          val id = 11
-          sendResponse(SquadWaypointEvent.Add(id, char_id, waypoint_type, WaypointEvent(info.zone_number, info.pos, unk)))
+          case SquadResponse.Unknown17(squad, char_id) =>
+            sendResponse(
+              SquadDefinitionActionMessage(squad.GUID, 0, SquadAction.Unknown(33))
+            )
 
-        case SquadResponse.WaypointEvent(WaypointEventAction.Remove, char_id, waypoint_type, _, _, _) =>
-          val id = 11
-          sendResponse(SquadWaypointEvent.Remove(id, char_id, waypoint_type))
+          case SquadResponse.Membership(request_type, unk1, unk2, char_id, 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
+                LivePlayerList.WorldPopulation({ case (_, a : Avatar) => char_id == a.CharId }).headOption match {
+                  case Some(player) =>
+                    player.name
+                  case None =>
+                    player_name
+                }
+              case _ =>
+                player_name
+            }
+            sendResponse(SquadMembershipResponse(request_type, unk1, unk2, char_id, opt_char_id, name, unk5, unk6))
 
-        case _ => ;
+          case SquadResponse.Invite(from_char_id, to_char_id, name) =>
+            sendResponse(SquadMembershipResponse(SquadResponseType.Invite, 0, 0, from_char_id, Some(to_char_id), s"$name", false, Some(None)))
+
+          case SquadResponse.WantsSquadPosition(_, name) =>
+            sendResponse(
+              ChatMsg(
+                ChatMessageType.CMT_SQUAD, true, name,
+                s"\\#6 would like to join your squad. (respond with \\#3/accept\\#6 or \\#3/reject\\#6)",
+                None
+              )
+            )
+
+          case SquadResponse.Join(squad, positionsToUpdate, toChannel) =>
+            val leader = squad.Leader
+            val membershipPositions = squad.Membership
+              .zipWithIndex
+              .filter { case (_, index ) => positionsToUpdate.contains(index) }
+            membershipPositions.find({ case(member, _) => member.CharId == avatar.CharId }) match {
+              case Some((ourMember, ourIndex)) =>
+                //we are joining the squad
+                //load each member's entry (our own too)
+                membershipPositions.foreach { case(member, index) =>
+                  sendResponse(SquadMemberEvent.Add(squad_supplement_id, member.CharId, index, member.Name, member.ZoneId, unk7 = 0))
+                  squadUI(member.CharId) = SquadUIElement(member.Name, index, member.ZoneId, member.Health, member.Armor, member.Position)
+                }
+                //repeat our entry
+                sendResponse(SquadMemberEvent.Add(squad_supplement_id, ourMember.CharId, ourIndex, ourMember.Name, ourMember.ZoneId, unk7 = 0)) //repeat of our entry
+                //turn lfs off
+                val factionOnContinentChannel = s"${continent.Id}/${player.Faction}"
+                if(avatar.LFS) {
+                  avatar.LFS = false
+                  sendResponse(PlanetsideAttributeMessage(player.GUID, 53, 0))
+                  avatarService ! AvatarServiceMessage(factionOnContinentChannel, AvatarAction.PlanetsideAttribute(player.GUID, 53, 0))
+                }
+                //squad colors
+                sendResponse(PlanetsideAttributeMessage(player.GUID, 31, squad_supplement_id))
+                avatarService ! AvatarServiceMessage(factionOnContinentChannel, AvatarAction.PlanetsideAttribute(player.GUID, 31, squad_supplement_id))
+                sendResponse(PlanetsideAttributeMessage(player.GUID, 32, ourIndex)) //associate with member position in squad
+                //a finalization? what does this do?
+                sendResponse(SquadDefinitionActionMessage(squad.GUID, 0, SquadAction.Unknown(18)))
+                updateSquad = UpdatesWhenEnrolledInSquad
+                squadChannel = Some(toChannel)
+              case _ =>
+                //other player is joining our squad
+                //load each member's entry
+                membershipPositions.foreach { case(member, index) =>
+                  sendResponse(SquadMemberEvent.Add(squad_supplement_id, member.CharId, index, member.Name, member.ZoneId, unk7 = 0))
+                  squadUI(member.CharId) = SquadUIElement(member.Name, index, member.ZoneId, member.Health, member.Armor, member.Position)
+                }
+            }
+            //send an initial dummy update for map icon(s)
+            sendResponse(SquadState(PlanetSideGUID(squad_supplement_id),
+              membershipPositions
+                .filterNot { case (member, _) => member.CharId == avatar.CharId }
+                .map{ case (member, _) => SquadStateInfo(member.CharId, member.Health, member.Armor, member.Position, 2,2, false, 429, None,None) }
+                .toList
+            ))
+
+          case SquadResponse.Leave(squad, positionsToUpdate) =>
+            positionsToUpdate.find({ case(member, _) => member == avatar.CharId }) match {
+              case Some((ourMember, ourIndex)) =>
+                //we are leaving the squad
+                //remove each member's entry (our own too)
+                positionsToUpdate.foreach { case(member, index) =>
+                  sendResponse(SquadMemberEvent.Remove(squad_supplement_id, member, index))
+                  squadUI.remove(member)
+                }
+                //uninitialize
+                sendResponse(SquadMemberEvent.Remove(squad_supplement_id, ourMember, ourIndex)) //repeat of our entry
+                sendResponse(PlanetsideAttributeMessage(player.GUID, 31, 0)) //disassociate with squad?
+                sendResponse(PlanetsideAttributeMessage(player.GUID, 32, 0)) //disassociate with member position in squad?
+                sendResponse(PlanetsideAttributeMessage(player.GUID, 34, 4294967295L)) //unknown, perhaps unrelated?
+                lfs = false
+                //a finalization? what does this do?
+                sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(18)))
+                updateSquad = NoSquadUpdates
+                squadChannel = None
+              case _ =>
+                //remove each member's entry
+                positionsToUpdate.foreach { case(member, index) =>
+                  sendResponse(SquadMemberEvent.Remove(squad_supplement_id, member, index))
+                  squadUI.remove(member)
+                }
+            }
+
+          case SquadResponse.AssignMember(squad, from_index, to_index) =>
+            //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 factionOnContinentChannel = s"${continent.Id}/${player.Faction}"
+            //are we being demoted?
+            if(squadUI(charId).index == 0) {
+              //lfsm -> lfs
+              if(lfs) {
+                sendResponse(PlanetsideAttributeMessage(guid, 53, 0))
+                avatarService ! AvatarServiceMessage(factionOnContinentChannel, AvatarAction.PlanetsideAttribute(guid, 53, 0))
+              }
+              lfs = 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
+            }
+            avatarService ! AvatarServiceMessage(factionOnContinentChannel, AvatarAction.PlanetsideAttribute(guid, 31, squad_supplement_id))
+            //we must fix the squad cards backend
+            SwapSquadUIElements(squad, from_index, to_index)
+
+          case SquadResponse.UpdateMembers(squad, positions) =>
+            import services.teamwork.SquadAction.{Update => SAUpdate}
+            val pairedEntries = positions.collect {
+              case entry if squadUI.contains(entry.char_id) =>
+                (entry, squadUI(entry.char_id))
+            }
+            //prune entries
+            val updatedEntries = pairedEntries
+              .collect({
+                case (entry, element) if entry.zone_number != element.zone =>
+                  //zone gets updated for these entries
+                  sendResponse(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)
+                  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)
+                  entry
+              })
+              .filterNot(_.char_id == avatar.CharId) //we want to update our backend, but not our frontend
+            if(updatedEntries.nonEmpty) {
+              sendResponse(
+                SquadState(
+                  PlanetSideGUID(squad_supplement_id),
+                  updatedEntries.map { entry => SquadStateInfo(entry.char_id, entry.health, entry.armor, entry.pos, 2,2, false, 429, None,None)}
+                )
+              )
+            }
+
+          case SquadResponse.SquadSearchResults() =>
+            //I don't actually know how to return search results
+            sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.NoSquadSearchResults()))
+
+          case SquadResponse.InitWaypoints(char_id, waypoints) =>
+            StartBundlingPackets()
+            waypoints.foreach { case (waypoint_type, info, unk) =>
+              sendResponse(SquadWaypointEvent.Add(squad_supplement_id, char_id, waypoint_type, WaypointEvent(info.zone_number, info.pos, unk)))
+            }
+            StopBundlingPackets()
+
+          case SquadResponse.WaypointEvent(WaypointEventAction.Add, char_id, waypoint_type, _, Some(info), unk) =>
+            sendResponse(SquadWaypointEvent.Add(squad_supplement_id, char_id, waypoint_type, WaypointEvent(info.zone_number, info.pos, unk)))
+
+          case SquadResponse.WaypointEvent(WaypointEventAction.Remove, char_id, waypoint_type, _, _, _) =>
+            sendResponse(SquadWaypointEvent.Remove(squad_supplement_id, char_id, waypoint_type))
+
+          case _ => ;
+        }
       }
 
     case Deployment.CanDeploy(obj, state) =>
@@ -1040,6 +1049,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
       galaxyService ! Service.Join(s"${avatar.faction}") //for hotspots
       squadService ! Service.Join(s"${avatar.faction}") //channel will be player.Faction
       squadService ! Service.Join(s"${avatar.CharId}") //channel will be player.CharId (in order to work with packets)
+      squadService ! Service.Join(s"${avatar.faction}") //channel will be player.Faction
       cluster ! InterstellarCluster.GetWorld("home3")
 
     case InterstellarCluster.GiveWorld(zoneId, zone) =>
@@ -3034,7 +3044,15 @@ class WorldSessionActor extends Actor with MDCContextAware {
     sendResponse(SetChatFilterMessage(ChatChannel.Local, false, ChatChannel.values.toList)) //TODO will not always be "on" like this
     deadState = DeadState.Alive
     sendResponse(AvatarDeadStateMessage(DeadState.Alive, 0, 0, tplayer.Position, player.Faction, true))
-    sendResponse(PlanetsideAttributeMessage(guid, 53, tplayer.LFS))
+    //looking for squad (members)
+    if(squadUI.nonEmpty && squadUI(avatar.CharId).index == 0) {
+      sendResponse(PlanetsideAttributeMessage(guid, 31, 1))
+      avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(guid, 31, 1))
+    }
+    if(tplayer.LFS || lfs) {
+      sendResponse(PlanetsideAttributeMessage(guid, 53, 1))
+      avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(guid, 53, 1))
+    }
     sendResponse(AvatarSearchCriteriaMessage(guid, List(0, 0, 0, 0, 0, 0)))
     (1 to 73).foreach(i => {
       // not all GUID's are set, and not all of the set ones will always be zero; what does this section do?
@@ -3047,14 +3065,8 @@ class WorldSessionActor extends Actor with MDCContextAware {
     //AvatarAwardMessage
     //DisplayAwardMessage
     sendResponse(PlanetsideStringAttributeMessage(guid, 0, "Outfit Name"))
-    sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(6)))
-    (0 to 9).foreach(line => {
-      sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), line, SquadAction.ListSquadFavorite("")))
-    })
-    sendResponse(SquadDetailDefinitionUpdateMessage.Init)
-    sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0,SquadAction.AssociateWithSquad()))
-    sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0,SquadAction.SetListSquad()))
-    sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0,SquadAction.Unknown(18)))
+    //squad stuff (loadouts, assignment)
+    squadSetup()
     //MapObjectStateBlockMessage and ObjectCreateMessage?
     //TacticsMessage?
     //change the owner on our deployables (re-draw the icons for our deployables too)
@@ -3089,7 +3101,30 @@ class WorldSessionActor extends Actor with MDCContextAware {
         interstellarFerryTopLevelGUID = None
       case _ => ;
     }
-    squadService ! Service.Join(s"${avatar.faction}") //channel will be player.Faction
+  }
+
+  def FirstTimeSquadSetup() : Unit = {
+    sendResponse(SquadDetailDefinitionUpdateMessage.Init)
+    sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(6)))
+    //only need to load these once
+    avatar.SquadLoadouts.Loadouts.foreach {
+      case (index, loadout : SquadLoadout) =>
+        sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), index, SquadAction.ListSquadFavorite(loadout.task)))
+    }
+    sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.AssociateWithSquad()))
+    sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.SetListSquad()))
+    sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(18)))
+    squadService ! SquadServiceMessage(player, continent, SquadServiceAction.InitSquadList())
+    squadService ! SquadServiceMessage(player, continent, SquadServiceAction.InitCharId())
+    squadSetup = SubsequentSpawnSquadSetup
+  }
+
+  def SubsequentSpawnSquadSetup() : Unit = {
+    if(squadUI.nonEmpty) {
+      //sendResponse(PlanetsideAttributeMessage(player.GUID, 31, squad_supplement_id))
+      val (_, ourCard) = squadUI.find { case (id, card) => id == player.CharId }.get
+      sendResponse(PlanetsideAttributeMessage(player.GUID, 32, ourCard.index))
+    }
   }
 
   def handleControlPkt(pkt : PlanetSideControlPacket) = {
@@ -3503,23 +3538,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
     case msg @ PlayerStateMessageUpstream(avatar_guid, pos, vel, yaw, pitch, yaw_upper, seq_time, unk3, is_crouching, is_jumping, unk4, is_cloaking, unk5, unk6) =>
       if(deadState == DeadState.Alive) {
         if(!player.Crouching && is_crouching) { //SQUAD TESTING CODE
-          sendResponse(
-            CharacterKnowledgeMessage(
-              41577140L,
-              CharacterKnowledgeInfo(
-                "Degrado",
-                Set(
-                  CertificationType.StandardAssault,
-                  CertificationType.AgileExoSuit,
-                  CertificationType.StandardExoSuit
-                ),
-                37,
-                5,
-                PlanetSideGUID(7)
-              )
-            )
-          )
-          sendResponse(SquadInvitationRequestMessage(PlanetSideGUID(1), 4, 41577140L, "Degrado"))
+          //...
         }
         player.Position = pos
         player.Velocity = vel
@@ -3562,7 +3581,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
           case None => false
         }
         avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlayerState(avatar_guid, msg, spectator, wepInHand))
-        //squadService ! SquadServiceMessage(tplayer, continent, SquadAction.Update(tplayer.CharId, tplayer.Health, tplayer.MaxHealth, tplayer.Armor, tplayer.MaxArmor, pos, zone.Number))
+        updateSquad()
       }
 
     case msg @ ChildObjectStateMessage(object_guid, pitch, yaw) =>
@@ -3603,6 +3622,8 @@ class WorldSessionActor extends Actor with MDCContextAware {
               }
               vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.VehicleState(player.GUID, vehicle_guid, unk1, pos, ang, vel, flight, unk6, unk7, wheels, unk9, unkA))
             }
+            //vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.VehicleState(player.GUID, vehicle_guid, unk1, pos, ang, vel, flight, unk6, unk7, wheels, unk9, unkA))
+            updateSquad()
           case (None, _) =>
             //log.error(s"VehicleState: no vehicle $vehicle_guid found in zone")
             //TODO placing a "not driving" warning here may trigger as we are disembarking the vehicle
@@ -4704,24 +4725,28 @@ class WorldSessionActor extends Actor with MDCContextAware {
             log.info(s"GenericObject: $player is MAX with an unexpected weapon - ${definition.Name}")
         }
       }
-      else if(action == 37) { //Looking For Squad OFF
-        if(squadUI.nonEmpty) {
-          lfs = false
-        }
-        else if(avatar.LFS) {
-          avatar.LFS = false
-          avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(player.GUID, 53, 0))
-          log.info(s"GenericObject: ${player.Name} is no longer looking for a squad to join")
-        }
-      }
       else if(action == 36) { //Looking For Squad ON
         if(squadUI.nonEmpty) {
-          lfs = true
+          if(!lfs && squadUI(player.CharId).index == 0) {
+            lfs = true
+            avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(player.GUID, 53, 1))
+          }
         }
         else if(!avatar.LFS) {
           avatar.LFS = true
           avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(player.GUID, 53, 1))
-          log.info(s"GenericObject: ${player.Name} has made himself available to join a squad")
+        }
+      }
+      else if(action == 37) { //Looking For Squad OFF
+        if(squadUI.nonEmpty) {
+          if(lfs && squadUI(player.CharId).index == 0) {
+            lfs = false
+            avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(player.GUID, 53, 0))
+          }
+        }
+        else if(avatar.LFS) {
+          avatar.LFS = false
+          avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(player.GUID, 53, 0))
         }
       }
 
@@ -9185,17 +9210,15 @@ class WorldSessionActor extends Actor with MDCContextAware {
 
   def SwapSquadUIElements(squad : Squad, fromIndex : Int, toIndex : Int) : Unit = {
     if(squadUI.nonEmpty) {
-      val fromMember = squad.Membership(fromIndex)
+      val fromMember = squad.Membership(toIndex) //the players have already been swapped in the backend object
       val fromCharId = fromMember.CharId
-      val toMember = squad.Membership(toIndex)
+      val toMember = squad.Membership(fromIndex) //the players have already been swapped in the backend object
       val toCharId = toMember.CharId
       val id = 11
-      if(fromCharId > 0) {
+      if(toCharId > 0) {
         //toMember and fromMember have swapped places
         val fromElem = squadUI(fromCharId)
         val toElem = squadUI(toCharId)
-        sendResponse(SquadMemberEvent.Remove(id, toCharId, fromIndex))
-        sendResponse(SquadMemberEvent.Remove(id, fromCharId, toIndex))
         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))
@@ -9212,14 +9235,14 @@ class WorldSessionActor extends Actor with MDCContextAware {
       }
       else {
         //previous fromMember has moved toMember
-        val elem = squadUI(toCharId)
-        sendResponse(SquadMemberEvent.Remove(id, toCharId, fromIndex))
-        squadUI(toCharId) = SquadUIElement(elem.name, toIndex, elem.zone, elem.health, elem.armor, elem.position)
-        sendResponse(SquadMemberEvent.Add(id, toCharId, toIndex, elem.name, elem.zone, unk7 = 0))
+        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))
         sendResponse(
           SquadState(
             PlanetSideGUID(id),
-            List(SquadStateInfo(toCharId, elem.health, elem.armor, elem.position, 2, 2, false, 429, None, None))
+            List(SquadStateInfo(fromCharId, elem.health, elem.armor, elem.position, 2, 2, false, 429, None, None))
           )
         )
       }
@@ -9233,6 +9256,23 @@ class WorldSessionActor extends Actor with MDCContextAware {
     }
   }
 
+  def NoSquadUpdates() : Unit = { }
+
+  def UpdatesWhenEnrolledInSquad() : Unit = {
+    squadService ! SquadServiceMessage(
+      player,
+      continent,
+      continent.GUID(player.VehicleSeated) match {
+        case Some(vehicle : Vehicle) =>
+          SquadServiceAction.Update(player.CharId, vehicle.Health, vehicle.MaxHealth, vehicle.Shields, vehicle.MaxShields, vehicle.Position, continent.Number)
+        case Some(obj : PlanetSideGameObject with WeaponTurret) =>
+          SquadServiceAction.Update(player.CharId, obj.Health, obj.MaxHealth, 0, 0, obj.Position, continent.Number)
+        case _ =>
+          SquadServiceAction.Update(player.CharId, player.Health, player.MaxHealth, player.Armor, player.MaxArmor, player.Position, continent.Number)
+      }
+    )
+  }
+
   def failWithError(error : String) = {
     log.error(error)
     sendResponse(ConnectionClose())

From f7098618d791122e32e550bc7249f05d4147365c Mon Sep 17 00:00:00 2001
From: FateJH <Jason_DiDonato@yahoo.com>
Date: Mon, 7 Oct 2019 21:41:29 -0400
Subject: [PATCH 22/31] Members and Squads now deal with their own inherent
 availabilities; code to transfer knowledge of character squad association by
 color; old code

---
 .../psforever/objects/teamwork/Member.scala   |  15 +-
 .../psforever/objects/teamwork/Squad.scala    |  20 ++-
 .../game/SquadDefinitionActionMessage.scala   |  54 +------
 .../services/teamwork/SquadResponse.scala     |   1 -
 .../services/teamwork/SquadService.scala      | 110 +++----------
 .../src/main/scala/WorldSessionActor.scala    | 149 +++++++++++++-----
 6 files changed, 160 insertions(+), 189 deletions(-)

diff --git a/common/src/main/scala/net/psforever/objects/teamwork/Member.scala b/common/src/main/scala/net/psforever/objects/teamwork/Member.scala
index 1ddfc46d..a2406a51 100644
--- a/common/src/main/scala/net/psforever/objects/teamwork/Member.scala
+++ b/common/src/main/scala/net/psforever/objects/teamwork/Member.scala
@@ -79,14 +79,11 @@ class Member {
     Position
   }
 
-  def Close() : Unit = {
-    role = ""
-    requirements = Set()
-    //about the individual filling the position
-    name = ""
-    health = 0
-    armor = 0
-    zoneId = 0
-    position = Vector3.Zero
+  def isAvailable : Boolean = {
+    charId == 0
+  }
+
+  def isAvailable(certs : Set[CertificationType.Value]) : Boolean = {
+    isAvailable && certs.intersect(requirements) == requirements
   }
 }
diff --git a/common/src/main/scala/net/psforever/objects/teamwork/Squad.scala b/common/src/main/scala/net/psforever/objects/teamwork/Squad.scala
index 3ff63035..0ecd2e20 100644
--- a/common/src/main/scala/net/psforever/objects/teamwork/Squad.scala
+++ b/common/src/main/scala/net/psforever/objects/teamwork/Squad.scala
@@ -3,7 +3,7 @@ package net.psforever.objects.teamwork
 
 import net.psforever.objects.entity.IdentifiableEntity
 import net.psforever.packet.game.PlanetSideGUID
-import net.psforever.types.PlanetSideEmpire
+import net.psforever.types.{CertificationType, PlanetSideEmpire}
 
 class Squad(squadId : PlanetSideGUID, alignment : PlanetSideEmpire.Value) extends IdentifiableEntity {
   super.GUID_=(squadId)
@@ -53,6 +53,24 @@ class Squad(squadId : PlanetSideGUID, alignment : PlanetSideEmpire.Value) extend
   def Size : Int = membership.count(member => member.CharId != 0)
 
   def Capacity : Int = availability.count(open => open)
+
+  def isAvailable(role : Int) : Boolean = {
+    availability.lift(role) match {
+      case Some(true) =>
+        membership(role).isAvailable
+      case _ =>
+        false
+    }
+  }
+
+  def isAvailable(role : Int, certs : Set[CertificationType.Value]) : Boolean = {
+    availability.lift(role) match {
+      case Some(true) =>
+        membership(role).isAvailable(certs)
+      case _ =>
+        false
+    }
+  }
 }
 
 object Squad {
diff --git a/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala b/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala
index eab055a9..a0a02c7d 100644
--- a/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala
+++ b/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala
@@ -370,7 +370,7 @@ object SquadAction{
   * &nbsp;&nbsp;&nbsp;&nbsp;`28` - Auto-approve Requests for Invitation<br>
   * &nbsp;&nbsp;&nbsp;&nbsp;`29` - UNKNOWN<br>
   * &nbsp;&nbsp;&nbsp;&nbsp;`30` - UNKNOWN<br>
-  * &nbsp;&nbsp;&nbsp;&nbsp;`31` - Location Follows Squad Lead<br>
+  * &nbsp;&nbsp;&nbsp;&nbsp;`31` - Location Follows Squad Leader<br>
   * &nbsp;&nbsp;`Int`<br>
   * &nbsp;&nbsp;&nbsp;&nbsp;`10` - Select this Role for Yourself<br>
   * &nbsp;&nbsp;&nbsp;&nbsp;`11` - UNKNOWN<br>
@@ -452,10 +452,9 @@ object SquadDefinitionActionMessage extends Marshallable[SquadDefinitionActionMe
       case 39 => noSquadSearchResultsCodec
       case 40 => findLfsSoldiersForRoleCodec
       case 41 => cancelFindCodec
-      case 2 | 6 | 11 |
-           12 | 13 | 14 |
-           18 | 29 | 30 | 32 | 33 |
-           36 | 37 | 42 | 43 => unknownCodec(code)
+      case  2 |  6 | 11 | 12 | 13 |
+           14 | 18 | 29 | 30 | 32 |
+           33 | 36 | 37 | 42 | 43 => unknownCodec(code)
       case _ => failureCodec(code)
     }).asInstanceOf[Codec[SquadAction]]
   }
@@ -477,48 +476,3 @@ object SquadDefinitionActionMessage extends Marshallable[SquadDefinitionActionMe
     }
   )
 }
-
-/*
-("change" specifically indicates the perspective is from the SL; "update" indicates squad members other than the oen who made the change
-("[#]" indicates the mode is detected but not properly parsed; the length of the combined fields may follow
-
-[0] - clicking on a squad listed in the "Find Squad" tab / cancel squad search (6 bits/pad?)
-[2] - ? (6 bits/pad?)
-[3] - save squad favorite (6 bits/pad?)
-[4] - load a squad definition favorite (6 bits/pad?)
-[6] - ? (6 bits/pad?)
-7 - ?
-[8] - list squad (6 bits/pad?)
-10 - select this role for yourself
-11 - ?
-12 - ?
-13 - ?
-14 - ?
-15 - ?
-[16] - ? (6 bits/pad?)
-[17] - ? (6 bits/pad?)
-[18] - ? (6 bits/pad?)
-19 - change purpose
-20 - change zone
-21 - change/close squad member position
-22 - change/add squad member position
-23 - change squad member req role
-24 - change squad member req detailed orders
-25 - change squad member req weapons
-[26] - reset all (6 bits/pad?)
-28 - auto-approve requests for invitation
-29 -
-30 -
-31 - location follows squad lead
-[32] - ? (6 bits/pad?)
-33 -
-34 - search for squads with a particular role
-36 -
-37 -
-38 -
-[39] - ? (?)
-40 - find LFS soldiers that meet the requirements for this role
-[41] - cancel search for LFS soldiers (6 bits)
-[42] - ? (6 bits/pad?)
-[43] - ? (6 bits/pad?)
-*/
diff --git a/common/src/main/scala/services/teamwork/SquadResponse.scala b/common/src/main/scala/services/teamwork/SquadResponse.scala
index 5860145c..3ad55b6c 100644
--- a/common/src/main/scala/services/teamwork/SquadResponse.scala
+++ b/common/src/main/scala/services/teamwork/SquadResponse.scala
@@ -16,7 +16,6 @@ object SquadResponse {
 
   final case class AssociateWithSquad(squad_guid : PlanetSideGUID) extends Response
   final case class SetListSquad(squad_guid : PlanetSideGUID) extends Response
-  final case class Unknown17(squad : Squad, char_id : Long) extends Response
 
   final case class Membership(request_type : SquadResponseType.Value, unk1 : Int, unk2 : Int, unk3 : Long, unk4 : Option[Long], player_name : String, unk5 : Boolean, unk6 : Option[Option[String]]) extends Response //see SquadMembershipResponse
   final case class Invite(from_char_id : Long, to_char_id : Long, name : String) extends Response
diff --git a/common/src/main/scala/services/teamwork/SquadService.scala b/common/src/main/scala/services/teamwork/SquadService.scala
index b837e7a1..11f4eaa5 100644
--- a/common/src/main/scala/services/teamwork/SquadService.scala
+++ b/common/src/main/scala/services/teamwork/SquadService.scala
@@ -55,7 +55,7 @@ class SquadService extends Actor {
     */
   private val queuedInvites : mutable.LongMap[List[Invitation]] = mutable.LongMap[List[Invitation]]()
   /**
-    * The gien player has refused participation into this other player's sqaud.<br>
+    * The given player has refused participation into this other player's squad.<br>
     * 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]]()
@@ -65,9 +65,10 @@ class SquadService extends Actor {
     */
   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.
+    * 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 invite is queued).
+    * Checked when trying to add a new invite (if found, the new invite is queued).
     * Cleared when the next queued invite becomes active.<br>
     * key - unique character identifier number; value, unique character identifier number
     */
@@ -91,10 +92,10 @@ class SquadService extends Actor {
     */
   private val SquadEvents = new GenericEventBus[SquadServiceResponse]
   /**
-    * This collection contains the message-sending contact reference for individuals who may become squad members.
+    * 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.
+    * and the origin `ActorRef` is added as a subscription.
     * It is maintained until they disconnect entirely.
     * The subscription is anticipated to belong to an instance of `WorldSessionActor`.<br>
     * key - unique character identifier number; value - `ActorRef` reference for that character
@@ -688,7 +689,7 @@ class SquadService extends Actor {
                 if(squad.Size < squad.Capacity) {
                   val positions = (for {
                     (member, index) <- squad.Membership.zipWithIndex
-                    if ValidOpenSquadPosition(squad, index, member, tplayer.Certifications)
+                    if squad.isAvailable(index, tplayer.Certifications)
                   } yield (index, member.Requirements.size))
                     .toList
                     .sortBy({ case (_, reqs) => reqs })
@@ -814,7 +815,7 @@ class SquadService extends Actor {
             }
             (None, None)
 
-          case Some(RequestRole(candidate, guid, _))
+          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)
@@ -1024,7 +1025,6 @@ class SquadService extends Actor {
                 if(memberPosition.CharId > 0) {
                   LeaveSquad(memberPosition.CharId, squad)
                 }
-                memberPosition.Close()
                 UpdateSquadListWhenListed(squadFeatures(squad.GUID), SquadInfo().Capacity(squad.Capacity))
                 UpdateSquadDetail(
                   squad.GUID,
@@ -1232,11 +1232,11 @@ class SquadService extends Actor {
                   position.Requirements = Set()
                 })
                 val features = squadFeatures(squad.GUID)
-                features.LocationFollowsSquadLead = false
-                features.AutoApproveInvitationRequests = false
+                features.LocationFollowsSquadLead = true
+                features.AutoApproveInvitationRequests = true
                 UpdateSquadListWhenListed(features, SquadInfo().Task("").ZoneId(None).Capacity(squad.Capacity))
                 UpdateSquadDetail(squad)
-                Publish(sender, SquadResponse.AssociateWithSquad(PlanetSideGUID(0)))
+                InitialAssociation(squad)
                 squadFeatures(guid).InitialAssociation = true
                 //do not unlist an already listed squad
               case Some(squad) =>
@@ -1248,13 +1248,13 @@ class SquadService extends Actor {
           case _ =>
             (pSquadOpt, action) match {
               //the following action can be performed by the squad leader and maybe an unaffiliated player
-              case (Some(squad), SelectRoleForYourself(position)) =>
+              case (Some(_), SelectRoleForYourself(_)) =>
                 //TODO should be possible, but doesn't work correctly due to UI squad cards not updating as expected
 //                if(squad.Leader.CharId == tplayer.CharId) {
 //                  //squad leader currently disallowed
 //                } else
 //                //the squad leader may swap to any open position; a normal member has to validate against requirements
-//                if(squad.Leader.CharId == tplayer.CharId || ValidOpenSquadPosition(squad, position, tplayer.Certifications)) {
+//                if(squad.Leader.CharId == tplayer.CharId || squad.isAvailable(position, tplayer.Certifications)) {
 //                  squad.Membership.zipWithIndex.find { case (member, _) => member.CharId == tplayer.CharId } match {
 //                    case Some((fromMember, fromIndex)) =>
 //                      SwapMemberPosition(squad.Membership(position), fromMember)
@@ -1273,7 +1273,7 @@ class SquadService extends Actor {
                 //not a member of any squad, but we might become a member of this one
                 GetSquad(guid) match {
                   case Some(squad) =>
-                    if(ValidOpenSquadPosition(squad, position, tplayer.Certifications)) {
+                    if(squad.isAvailable(position, tplayer.Certifications)) {
                       //we could join but we may need permission from the squad leader first
                       AddInviteAndRespond(
                         squad.Leader.CharId,
@@ -1940,7 +1940,7 @@ class SquadService extends Actor {
     * @param list a list of players to squads with expected entry redundancy
     */
   def RemoveProximityInvites(list : Iterable[(Long, PlanetSideGUID)]) : Unit = {
-    val (ids, squads) = list.unzip
+    val (_, squads) = list.unzip
     squads.toSeq.distinct.foreach { squad =>
       squadFeatures.get(squad) match {
         case Some(features) =>
@@ -2039,7 +2039,6 @@ class SquadService extends Actor {
     * @see `IndirectInvite`
     * @see `SquadFeatures::AutoApproveInvitationRequests`
     * @see `VacancyInvite`
-    * @see `ValidOpenSquadPosition`
     * @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
@@ -2049,8 +2048,8 @@ class SquadService extends Actor {
     */
   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 (member, index) =>
-      ValidOpenSquadPosition(squad, index, member, recruit.Certifications)
+    squad.Membership.zipWithIndex.find({ case (_, index) =>
+      squad.isAvailable(index, recruit.Certifications)
     }) match {
       case Some((_, line)) =>
         //position in squad found
@@ -2206,7 +2205,6 @@ class SquadService extends Actor {
     * @see `SquadResponse.Join`
     * @see `StatConverter.Health`
     * @see `UpdateSquadListWhenListed`
-    * @see `ValidOpenSquadPosition`
     * @param player the new squad member;
     *               this player is NOT the squad leader
     * @param squad the squad the player is joining
@@ -2217,7 +2215,7 @@ class SquadService extends Actor {
   def JoinSquad(player : Player, squad : Squad, position : Int) : Boolean = {
     val charId = player.CharId
     val role = squad.Membership(position)
-    if(UserEvents.get(charId).nonEmpty && squad.Leader.CharId != charId && ValidOpenSquadPosition(squad, position, role, player.Certifications)) {
+    if(UserEvents.get(charId).nonEmpty && squad.Leader.CharId != charId && squad.isAvailable(position, player.Certifications)) {
       role.Name = player.Name
       role.CharId = charId
       role.Health = StatConverter.Health(player.Health, player.MaxHealth, min=1, max=64)
@@ -2616,19 +2614,6 @@ class SquadService extends Actor {
     SquadEvents.unsubscribe(sender) //just to make certain
   }
 
-  /**
-    * Dispatch a message entailing the composition of this squad
-    * and focus on any specific aspects of it, purported as being changed recently.
-    * @see `SquadInfo`
-    * @see `UpdateSquadList(Squad, Option[SquadInfo])`
-    * @param squad the squad
-    * @param changes the highlighted aspects of the squad;
-    *                these "changes" do not have to reflect the actual squad but are related to the contents of the message
-    */
-  private def UpdateSquadList(squad : Squad, changes : SquadInfo) : Unit = {
-    UpdateSquadList(squad, Some(changes))
-  }
-
   /**
     * Dispatch a message entailing the composition of this squad when that squad is publicly available
     * and focus on any specific aspects of it, purported as being changed recently.
@@ -2989,63 +2974,4 @@ object SquadService {
       member.Requirements = position.requirements
     }
   }
-
-  /**
-    * Determine if the indicated squad role can be used for a player trying to join the squad.
-    * @see `ValidOpenSquadPosition`
-    * @param squad the squad
-    * @param position the position of the squad role being tested
-    * @param certs the certifications being offered for comparison testing
-    * @return `true`, if the squad role may accept the player with the given certification permissions;
-    *        `false`, otherwise
-    */
-  def ValidOpenSquadPosition(squad : Squad, position : Int, certs : Set[CertificationType.Value]) : Boolean = {
-    squad.Membership.lift(position) match {
-      case Some(member) =>
-        ValidOpenSquadPosition(squad, position, member, certs)
-      case None =>
-        false
-    }
-  }
-
-  /**
-    * Determine if the indicated squad role can be used for a player trying to join the squad.
-    * @see `AvailableSquadPosition`
-    * @param squad the squad
-    * @param position the position of the squad role being tested
-    * @param member the squad role being tested
-    * @param certs the certifications being offered for comparison testing
-    * @return `true`, if the squad role may accept the player with the given certification permissions;
-    *        `false`, otherwise
-    */
-  def ValidOpenSquadPosition(squad : Squad, position : Int, member : Member, certs : Set[CertificationType.Value]) : Boolean = {
-    AvailableSquadPosition(squad, position, member) && certs.intersect(member.Requirements) == member.Requirements
-  }
-
-  /**
-    * Determine if the indicated squad role is unoccupied but is also capable of being occupied.
-    * @see `AvailableSquadPosition(Squad, Int, Member)`
-    * @param squad the squad
-    * @param position the position of the squad role being tested
-    * @return `true`, if the squad role is unoccupied;
-    *        `false`, otherwise
-    */
-  def AvailableSquadPosition(squad : Squad, position : Int) : Boolean = squad.Membership.lift(position) match {
-    case Some(member) =>
-      AvailableSquadPosition(squad, position, member)
-    case None =>
-      false
-  }
-
-  /**
-    * Determine if the indicated squad role is unoccupied but is also capable of being occupied.
-    * @param squad the squad
-    * @param position the position of the squad role being tested
-    * @param member the squad role being tested
-    * @return `true`, if the squad role is unoccupied;
-    *        `false`, otherwise
-    */
-  def AvailableSquadPosition(squad : Squad, position : Int, member : Member) : Boolean = {
-    squad.Availability(position) && member.CharId == 0
-  }
 }
diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala
index 2f5a03a8..b19f72bd 100644
--- a/pslogin/src/main/scala/WorldSessionActor.scala
+++ b/pslogin/src/main/scala/WorldSessionActor.scala
@@ -119,7 +119,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
     */
   var interstellarFerryTopLevelGUID : Option[PlanetSideGUID] = None
   val squadUI : LongMap[SquadUIElement] = new LongMap[SquadUIElement]()
-  val squad_supplement_id : Int = 11
+  var squad_supplement_id : Int = 0
   /**
     * `AvatarConverter` can only rely on the `Avatar`-local Looking For Squad variable.
     * When joining or creating a squad, the original state of the avatar's local LFS variable is blanked.
@@ -396,11 +396,6 @@ class WorldSessionActor extends Actor with MDCContextAware {
           case SquadResponse.SetListSquad(squad_guid) =>
             sendResponse(SquadDefinitionActionMessage(squad_guid, 0, SquadAction.SetListSquad()))
 
-          case SquadResponse.Unknown17(squad, char_id) =>
-            sendResponse(
-              SquadDefinitionActionMessage(squad.GUID, 0, SquadAction.Unknown(33))
-            )
-
           case SquadResponse.Membership(request_type, unk1, unk2, char_id, opt_char_id, player_name, unk5, unk6) =>
             val name = request_type match {
               case SquadResponseType.Invite if unk5 =>
@@ -433,27 +428,31 @@ class WorldSessionActor extends Actor with MDCContextAware {
             val membershipPositions = squad.Membership
               .zipWithIndex
               .filter { case (_, index ) => positionsToUpdate.contains(index) }
+            StartBundlingPackets()
             membershipPositions.find({ case(member, _) => member.CharId == avatar.CharId }) match {
               case Some((ourMember, ourIndex)) =>
                 //we are joining the squad
                 //load each member's entry (our own too)
+                squad_supplement_id = squad.GUID.guid + 1
                 membershipPositions.foreach { case(member, index) =>
                   sendResponse(SquadMemberEvent.Add(squad_supplement_id, member.CharId, index, member.Name, member.ZoneId, unk7 = 0))
                   squadUI(member.CharId) = SquadUIElement(member.Name, index, member.ZoneId, member.Health, member.Armor, member.Position)
                 }
                 //repeat our entry
                 sendResponse(SquadMemberEvent.Add(squad_supplement_id, ourMember.CharId, ourIndex, ourMember.Name, ourMember.ZoneId, unk7 = 0)) //repeat of our entry
+                val playerGuid = player.GUID
                 //turn lfs off
                 val factionOnContinentChannel = s"${continent.Id}/${player.Faction}"
                 if(avatar.LFS) {
                   avatar.LFS = false
-                  sendResponse(PlanetsideAttributeMessage(player.GUID, 53, 0))
-                  avatarService ! AvatarServiceMessage(factionOnContinentChannel, AvatarAction.PlanetsideAttribute(player.GUID, 53, 0))
+                  sendResponse(PlanetsideAttributeMessage(playerGuid, 53, 0))
+                  avatarService ! AvatarServiceMessage(factionOnContinentChannel, AvatarAction.PlanetsideAttribute(playerGuid, 53, 0))
                 }
                 //squad colors
-                sendResponse(PlanetsideAttributeMessage(player.GUID, 31, squad_supplement_id))
-                avatarService ! AvatarServiceMessage(factionOnContinentChannel, AvatarAction.PlanetsideAttribute(player.GUID, 31, squad_supplement_id))
-                sendResponse(PlanetsideAttributeMessage(player.GUID, 32, ourIndex)) //associate with member position in squad
+                GiveSquadColorsInZone()
+                avatarService ! AvatarServiceMessage(factionOnContinentChannel, AvatarAction.PlanetsideAttribute(playerGuid, 31, 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)))
                 updateSquad = UpdatesWhenEnrolledInSquad
@@ -461,11 +460,16 @@ class WorldSessionActor extends Actor with MDCContextAware {
               case _ =>
                 //other player is joining our squad
                 //load each member's entry
-                membershipPositions.foreach { case(member, index) =>
-                  sendResponse(SquadMemberEvent.Add(squad_supplement_id, member.CharId, index, member.Name, member.ZoneId, unk7 = 0))
-                  squadUI(member.CharId) = SquadUIElement(member.Name, index, member.ZoneId, member.Health, member.Armor, member.Position)
-                }
+                GiveSquadColorsInZone(
+                  membershipPositions.map { case(member, index) =>
+                    val charId = member.CharId
+                    sendResponse(SquadMemberEvent.Add(squad_supplement_id, charId, index, member.Name, member.ZoneId, unk7 = 0))
+                    squadUI(charId) = SquadUIElement(member.Name, index, member.ZoneId, member.Health, member.Armor, member.Position)
+                    charId
+                  }
+                )
             }
+            StopBundlingPackets()
             //send an initial dummy update for map icon(s)
             sendResponse(SquadState(PlanetSideGUID(squad_supplement_id),
               membershipPositions
@@ -475,6 +479,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
             ))
 
           case SquadResponse.Leave(squad, positionsToUpdate) =>
+            StartBundlingPackets()
             positionsToUpdate.find({ case(member, _) => member == avatar.CharId }) match {
               case Some((ourMember, ourIndex)) =>
                 //we are leaving the squad
@@ -484,10 +489,12 @@ class WorldSessionActor extends Actor with MDCContextAware {
                   squadUI.remove(member)
                 }
                 //uninitialize
+                val playerGuid = player.GUID
                 sendResponse(SquadMemberEvent.Remove(squad_supplement_id, ourMember, ourIndex)) //repeat of our entry
-                sendResponse(PlanetsideAttributeMessage(player.GUID, 31, 0)) //disassociate with squad?
-                sendResponse(PlanetsideAttributeMessage(player.GUID, 32, 0)) //disassociate with member position in squad?
-                sendResponse(PlanetsideAttributeMessage(player.GUID, 34, 4294967295L)) //unknown, perhaps unrelated?
+                sendResponse(PlanetsideAttributeMessage(playerGuid, 31, 0)) //disassociate with squad?
+                avatarService ! AvatarServiceMessage(s"${continent.Id}/${player.Faction}", AvatarAction.PlanetsideAttribute(playerGuid, 31, 0))
+                sendResponse(PlanetsideAttributeMessage(playerGuid, 32, 0)) //disassociate with member position in squad?
+                sendResponse(PlanetsideAttributeMessage(playerGuid, 34, 4294967295L)) //unknown, perhaps unrelated?
                 lfs = false
                 //a finalization? what does this do?
                 sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(18)))
@@ -495,11 +502,17 @@ class WorldSessionActor extends Actor with MDCContextAware {
                 squadChannel = None
               case _ =>
                 //remove each member's entry
-                positionsToUpdate.foreach { case(member, index) =>
-                  sendResponse(SquadMemberEvent.Remove(squad_supplement_id, member, index))
-                  squadUI.remove(member)
-                }
+                GiveSquadColorsInZone(
+                  positionsToUpdate.map { case(member, index) =>
+                    sendResponse(SquadMemberEvent.Remove(squad_supplement_id, member, index))
+                    squadUI.remove(member)
+                    member
+                  },
+                  value = 0
+                )
             }
+            StopBundlingPackets()
+            squad_supplement_id = 0
 
           case SquadResponse.AssignMember(squad, from_index, to_index) =>
             //we've already swapped position internally; now we swap the cards
@@ -3045,10 +3058,6 @@ class WorldSessionActor extends Actor with MDCContextAware {
     deadState = DeadState.Alive
     sendResponse(AvatarDeadStateMessage(DeadState.Alive, 0, 0, tplayer.Position, player.Faction, true))
     //looking for squad (members)
-    if(squadUI.nonEmpty && squadUI(avatar.CharId).index == 0) {
-      sendResponse(PlanetsideAttributeMessage(guid, 31, 1))
-      avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(guid, 31, 1))
-    }
     if(tplayer.LFS || lfs) {
       sendResponse(PlanetsideAttributeMessage(guid, 53, 1))
       avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(guid, 53, 1))
@@ -3103,30 +3112,97 @@ class WorldSessionActor extends Actor with MDCContextAware {
     }
   }
 
+  /**
+    * These messages are dispatched when first starting up the client and connecting to the server for the first time.
+    * While many of thee messages will be reused for other situations, they appear in this order only during startup.
+    */
   def FirstTimeSquadSetup() : Unit = {
     sendResponse(SquadDetailDefinitionUpdateMessage.Init)
+    sendResponse(ReplicationStreamMessage(5, Some(6), Vector.empty)) //clear squad list
     sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(6)))
-    //only need to load these once
+    //only need to load these once - they persist between zone transfers and respawns
     avatar.SquadLoadouts.Loadouts.foreach {
       case (index, loadout : SquadLoadout) =>
         sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), index, SquadAction.ListSquadFavorite(loadout.task)))
     }
+    //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.SetListSquad()))
     sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(18)))
     squadService ! SquadServiceMessage(player, continent, SquadServiceAction.InitSquadList())
     squadService ! SquadServiceMessage(player, continent, SquadServiceAction.InitCharId())
-    squadSetup = SubsequentSpawnSquadSetup
+    squadSetup = RespawnSquadSetup
   }
 
-  def SubsequentSpawnSquadSetup() : Unit = {
+  /**
+    * These messages are used during each subsequent respawn to reset the squad colors on player nameplates and marquees.
+    * By using `squadUI` to maintain relevant information about squad members,
+    * especially the unique character identifier number,
+    * only the zone-specific squad members will receive the important messages about their squad member's spawn.
+    */
+  def RespawnSquadSetup() : Unit = {
     if(squadUI.nonEmpty) {
-      //sendResponse(PlanetsideAttributeMessage(player.GUID, 31, squad_supplement_id))
-      val (_, ourCard) = squadUI.find { case (id, card) => id == player.CharId }.get
-      sendResponse(PlanetsideAttributeMessage(player.GUID, 32, ourCard.index))
+      sendResponse(PlanetsideAttributeMessage(player.GUID, 31, squad_supplement_id))
+      avatarService ! AvatarServiceMessage(s"${continent.Id}/${player.Faction}", AvatarAction.PlanetsideAttribute(player.GUID, 31, squad_supplement_id))
+      sendResponse(PlanetsideAttributeMessage(player.GUID, 32, squadUI(player.CharId).index))
     }
   }
 
+  /**
+    * These messages are used during each subsequent respawn to reset the squad colors on player nameplates and marquees.
+    * During a zone change,
+    * on top of other squad mates in the zone needing to have their knowledge of this player's squad colors changed,
+    * the player must also set squad colors for each other squad members.
+    * Default respawn functionality may resume afterwards.
+    */
+  def ZoneChangeSquadSetup() : Unit = {
+    RespawnSquadSetup()
+    GiveSquadColorsInZone()
+    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)
+  }
+
+  /**
+    * 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)
+  }
+
+  /**
+    * Allocate the listed squad members in zone and give their nameplates and their marquees the appropriate squad color.
+    * @see `PlanetsideAttributeMessage`
+    * @param members members of the squad to target
+    * @param value the assignment value
+    */
+  def GiveSquadColorsInZone(members : Iterable[Long], value : Long) : Unit = {
+    SquadMembersInZone(members).foreach {
+      members => sendResponse(PlanetsideAttributeMessage(members.GUID, 31, value))
+    }
+  }
+
+  /**
+    * For the listed squad member unique character identifier numbers,
+    * find and return all squad members in the current zone.
+    * @param members members of the squad to target
+    * @return a list of `Player` objects
+    */
+  def SquadMembersInZone(members : Iterable[Long]) : Iterable[Player] = {
+    val players = continent.LivePlayers
+    for {
+      charId <- members
+      player = players.find { _.CharId == charId }
+      if player.nonEmpty
+    } yield player.get
+  }
+
   def handleControlPkt(pkt : PlanetSideControlPacket) = {
     pkt match {
       case sync @ ControlSync(diff, _, _, _, _, _, fa, fb) =>
@@ -4729,24 +4805,24 @@ class WorldSessionActor extends Actor with MDCContextAware {
         if(squadUI.nonEmpty) {
           if(!lfs && squadUI(player.CharId).index == 0) {
             lfs = true
-            avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(player.GUID, 53, 1))
+            avatarService ! AvatarServiceMessage(s"${continent.Id}/${player.Faction}", AvatarAction.PlanetsideAttribute(player.GUID, 53, 1))
           }
         }
         else if(!avatar.LFS) {
           avatar.LFS = true
-          avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(player.GUID, 53, 1))
+          avatarService ! AvatarServiceMessage(s"${continent.Id}/${player.Faction}", AvatarAction.PlanetsideAttribute(player.GUID, 53, 1))
         }
       }
       else if(action == 37) { //Looking For Squad OFF
         if(squadUI.nonEmpty) {
           if(lfs && squadUI(player.CharId).index == 0) {
             lfs = false
-            avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(player.GUID, 53, 0))
+            avatarService ! AvatarServiceMessage(s"${continent.Id}/${player.Faction}", AvatarAction.PlanetsideAttribute(player.GUID, 53, 0))
           }
         }
         else if(avatar.LFS) {
           avatar.LFS = false
-          avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(player.GUID, 53, 0))
+          avatarService ! AvatarServiceMessage(s"${continent.Id}/${player.Faction}", AvatarAction.PlanetsideAttribute(player.GUID, 53, 0))
         }
       }
 
@@ -8739,6 +8815,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
     })
     DisownDeployables()
     drawDeloyableIcon = RedrawDeployableIcons //important for when SetCurrentAvatar initializes the UI next zone
+    squadSetup = ZoneChangeSquadSetup
     continent.Population ! Zone.Population.Leave(avatar)
   }
 

From 5a1527ae1016370d74ef6c04e2242056e2182b9c Mon Sep 17 00:00:00 2001
From: FateJH <Jason_DiDonato@yahoo.com>
Date: Tue, 8 Oct 2019 18:47:03 -0400
Subject: [PATCH 23/31] small touch-up

---
 .../objects/teamwork/SquadFeatures.scala      |  4 +--
 .../game/PlanetsideAttributeMessage.scala     |  3 ++-
 .../services/teamwork/SquadResponse.scala     |  1 -
 .../services/teamwork/SquadService.scala      | 25 +++++++++++++++++--
 .../DetailedCharacterDataTest.scala           |  2 +-
 .../src/main/scala/WorldSessionActor.scala    |  9 ++-----
 6 files changed, 30 insertions(+), 14 deletions(-)

diff --git a/common/src/main/scala/net/psforever/objects/teamwork/SquadFeatures.scala b/common/src/main/scala/net/psforever/objects/teamwork/SquadFeatures.scala
index a374f0ab..29b3ad78 100644
--- a/common/src/main/scala/net/psforever/objects/teamwork/SquadFeatures.scala
+++ b/common/src/main/scala/net/psforever/objects/teamwork/SquadFeatures.scala
@@ -15,7 +15,7 @@ class SquadFeatures(val Squad : Squad) {
     * Dispatched only once when a squad is first listed
     * or when the squad leader searches for recruits by proximity or for certain roles or by invite
     * or when a spontaneous squad forms,
-    * whichever happens first.
+    * whatever happens first.
     * Additionally, the packets are also sent when the check is made when the continent is changed (or set).
     */
   private var initialAssociation : Boolean = true
@@ -151,4 +151,4 @@ class SquadFeatures(val Squad : Squad) {
     }
     Prompt
   }
-}
\ No newline at end of file
+}
diff --git a/common/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala b/common/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala
index ac4b49ff..4c33eab1 100644
--- a/common/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala
+++ b/common/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala
@@ -122,7 +122,8 @@ import scodec.codecs._
   * ` - 0 is LFS`<br>
   * ` - 1 is LFSM (Looking for Squad Members)`<br>
   * ` - n is the supplemental squad identifier number; same as "LFS;" for the leader, sets "LFSM" after the first manual flagging`<br>
-  * `32 - Info under avatar name : 0 = Looking For Squad Members, 1 = LFS`<br>
+  * `32 - Maintain the squad role index, when a member of a squad;<br>
+  *  - OLD: "Info under avatar name : 0 = Looking For Squad Members, 1 = LFS`"<br>
   * `35 - BR. Value is the BR`<br>
   * `36 - CR. Value is the CR`<br>
   * `43 - Info on avatar name : 0 = Nothing, 1 = "(LD)" message`<br>
diff --git a/common/src/main/scala/services/teamwork/SquadResponse.scala b/common/src/main/scala/services/teamwork/SquadResponse.scala
index 3ad55b6c..1b3ae6fb 100644
--- a/common/src/main/scala/services/teamwork/SquadResponse.scala
+++ b/common/src/main/scala/services/teamwork/SquadResponse.scala
@@ -18,7 +18,6 @@ object SquadResponse {
   final case class SetListSquad(squad_guid : PlanetSideGUID) extends Response
 
   final case class Membership(request_type : SquadResponseType.Value, unk1 : Int, unk2 : Int, unk3 : Long, unk4 : Option[Long], player_name : String, unk5 : Boolean, unk6 : Option[Option[String]]) extends Response //see SquadMembershipResponse
-  final case class Invite(from_char_id : Long, to_char_id : Long, name : String) extends Response
   final case class WantsSquadPosition(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 Leave(squad : Squad, positionsToUpdate : List[(Long, Int)]) extends Response
diff --git a/common/src/main/scala/services/teamwork/SquadService.scala b/common/src/main/scala/services/teamwork/SquadService.scala
index 11f4eaa5..a678742d 100644
--- a/common/src/main/scala/services/teamwork/SquadService.scala
+++ b/common/src/main/scala/services/teamwork/SquadService.scala
@@ -2240,7 +2240,13 @@ class SquadService extends Actor {
         val toChannel = s"/${features.ToChannel}/Squad"
         memberCharIds.foreach { charId =>
           SquadEvents.subscribe(UserEvents(charId), toChannel)
-          Publish(charId, SquadResponse.Join(squad, indices, toChannel))
+          Publish(charId,
+            SquadResponse.Join(
+              squad,
+              indices.filterNot(_ == position) :+ position,
+              toChannel
+            )
+          )
           InitWaypoints(charId, squad.GUID)
         }
         //fully update for all users
@@ -2255,7 +2261,11 @@ class SquadService extends Actor {
           charId,
           SquadResponse.Join(
             squad,
-            squad.Membership.zipWithIndex.collect({ case (member, index) if member.CharId > 0 => index }).toList,
+            squad.Membership
+              .zipWithIndex
+              .collect({ case (member, index) if member.CharId > 0 => index })
+              .filterNot(_== position)
+              .toList :+ position,
             toChannel
           )
         )
@@ -2580,10 +2590,20 @@ class SquadService extends Actor {
     }
   }
 
+  /**
+    * na
+    * @param charId the player's unique character identifier number
+    * @param sender the `ActorRef` associated with this character
+    */
   def LeaveService(charId : String, sender : ActorRef) : Unit = {
     LeaveService(charId.toLong, sender)
   }
 
+  /**
+    * na
+    * @param charId the player's unique character identifier number
+    * @param sender the `ActorRef` associated with this character
+    */
   def LeaveService(charId : Long, sender : ActorRef) : Unit = {
     refused.remove(charId)
     continueToMonitorDetails.remove(charId)
@@ -2690,6 +2710,7 @@ class SquadService extends Actor {
             //remove squad from listing
             factionListings.remove(index)
             //Publish(faction, SquadResponse.RemoveFromList(Seq(index)))
+            squadFeatures(squad.GUID).Refuse
             Publish(faction, SquadResponse.InitList(PublishedLists(factionListings)))
         }
       case None =>
diff --git a/common/src/test/scala/game/objectcreatedetailed/DetailedCharacterDataTest.scala b/common/src/test/scala/game/objectcreatedetailed/DetailedCharacterDataTest.scala
index 34175c27..2843386d 100644
--- a/common/src/test/scala/game/objectcreatedetailed/DetailedCharacterDataTest.scala
+++ b/common/src/test/scala/game/objectcreatedetailed/DetailedCharacterDataTest.scala
@@ -1315,7 +1315,7 @@ class DetailedCharacterDataTest extends Specification {
                   a.data.v5.isEmpty mustEqual true
                   a.exosuit mustEqual ExoSuitType.Standard
                   a.unk5 mustEqual 0
-                  a.unk6 mustEqual 1267466L
+                  a.char_id mustEqual 1267466L
                   a.unk7 mustEqual 3
                   a.unk8 mustEqual 3
                   a.unk9 mustEqual 0
diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala
index b19f72bd..ce21779b 100644
--- a/pslogin/src/main/scala/WorldSessionActor.scala
+++ b/pslogin/src/main/scala/WorldSessionActor.scala
@@ -411,9 +411,6 @@ class WorldSessionActor extends Actor with MDCContextAware {
             }
             sendResponse(SquadMembershipResponse(request_type, unk1, unk2, char_id, opt_char_id, name, unk5, unk6))
 
-          case SquadResponse.Invite(from_char_id, to_char_id, name) =>
-            sendResponse(SquadMembershipResponse(SquadResponseType.Invite, 0, 0, from_char_id, Some(to_char_id), s"$name", false, Some(None)))
-
           case SquadResponse.WantsSquadPosition(_, name) =>
             sendResponse(
               ChatMsg(
@@ -425,9 +422,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
 
           case SquadResponse.Join(squad, positionsToUpdate, toChannel) =>
             val leader = squad.Leader
-            val membershipPositions = squad.Membership
-              .zipWithIndex
-              .filter { case (_, index ) => positionsToUpdate.contains(index) }
+            val membershipPositions = positionsToUpdate map squad.Membership.zipWithIndex
             StartBundlingPackets()
             membershipPositions.find({ case(member, _) => member.CharId == avatar.CharId }) match {
               case Some((ourMember, ourIndex)) =>
@@ -6592,7 +6587,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
             sendResponse(ObjectDetachMessage(tool.GUID, previousBox.GUID, Vector3.Zero, 0f))
             sendResponse(ObjectDetachMessage(player.GUID, box.GUID, Vector3.Zero, 0f))
             obj.Inventory -= x.start //remove replacement ammo from inventory
-          val ammoSlotIndex = tool.FireMode.AmmoSlotIndex
+            val ammoSlotIndex = tool.FireMode.AmmoSlotIndex
             tool.AmmoSlots(ammoSlotIndex).Box = box //put replacement ammo in tool
             sendResponse(ObjectAttachMessage(tool.GUID, box.GUID, ammoSlotIndex))
 

From 7d9d5105f6ab6e84bbce6485b910529cebdfe271 Mon Sep 17 00:00:00 2001
From: FateJH <Jason_DiDonato@yahoo.com>
Date: Tue, 8 Oct 2019 20:01:52 -0400
Subject: [PATCH 24/31] was going to try blocking characters who have been
 refused from squad from getting squad updates but forgot it wouldn't be safe
 to do this here

---
 common/src/main/scala/services/teamwork/SquadService.scala | 1 -
 1 file changed, 1 deletion(-)

diff --git a/common/src/main/scala/services/teamwork/SquadService.scala b/common/src/main/scala/services/teamwork/SquadService.scala
index a678742d..aba14c73 100644
--- a/common/src/main/scala/services/teamwork/SquadService.scala
+++ b/common/src/main/scala/services/teamwork/SquadService.scala
@@ -2710,7 +2710,6 @@ class SquadService extends Actor {
             //remove squad from listing
             factionListings.remove(index)
             //Publish(faction, SquadResponse.RemoveFromList(Seq(index)))
-            squadFeatures(squad.GUID).Refuse
             Publish(faction, SquadResponse.InitList(PublishedLists(factionListings)))
         }
       case None =>

From 16afb63b3447ed847b347e7e93f44f65d5d08342 Mon Sep 17 00:00:00 2001
From: FateJH <Jason_DiDonato@yahoo.com>
Date: Mon, 14 Oct 2019 16:20:51 -0400
Subject: [PATCH 25/31] removed redundant subscription; made sure to publish
 waypoint events properly

---
 .../scala/services/teamwork/SquadService.scala    | 15 +++++++++++----
 pslogin/src/main/scala/WorldSessionActor.scala    |  1 -
 2 files changed, 11 insertions(+), 5 deletions(-)

diff --git a/common/src/main/scala/services/teamwork/SquadService.scala b/common/src/main/scala/services/teamwork/SquadService.scala
index aba14c73..0f2ac6a6 100644
--- a/common/src/main/scala/services/teamwork/SquadService.scala
+++ b/common/src/main/scala/services/teamwork/SquadService.scala
@@ -955,11 +955,18 @@ class SquadService extends Actor {
         }) match {
           case (Some(squad), Some(_)) =>
             //waypoint added or updated
-            SquadServiceResponse(s"/${squadFeatures(squad.GUID).ToChannel}/Squad", tplayer.CharId, SquadResponse.WaypointEvent(WaypointEventAction.Add, playerCharId, wtype, None, info, 1))
-
+            Publish(
+              s"${squadFeatures(squad.GUID).ToChannel}",
+              SquadResponse.WaypointEvent(WaypointEventAction.Add, playerCharId, wtype, None, info, 1),
+              Seq(tplayer.CharId)
+            )
           case (Some(squad), None) =>
-            //waypoint removed?
-            SquadServiceResponse(s"/${squadFeatures(squad.GUID).ToChannel}/Squad", tplayer.CharId, SquadResponse.WaypointEvent(WaypointEventAction.Remove, playerCharId, wtype, None, None, 0))
+            //waypoint removed
+            Publish(
+              s"${squadFeatures(squad.GUID).ToChannel}",
+              SquadResponse.WaypointEvent(WaypointEventAction.Remove, playerCharId, wtype, None, None, 0),
+              Seq(tplayer.CharId)
+            )
 
           case msg =>
             log.warn(s"Unsupported squad waypoint behavior: $msg")
diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala
index ce21779b..5ea8dabd 100644
--- a/pslogin/src/main/scala/WorldSessionActor.scala
+++ b/pslogin/src/main/scala/WorldSessionActor.scala
@@ -1057,7 +1057,6 @@ class WorldSessionActor extends Actor with MDCContextAware {
       galaxyService ! Service.Join(s"${avatar.faction}") //for hotspots
       squadService ! Service.Join(s"${avatar.faction}") //channel will be player.Faction
       squadService ! Service.Join(s"${avatar.CharId}") //channel will be player.CharId (in order to work with packets)
-      squadService ! Service.Join(s"${avatar.faction}") //channel will be player.Faction
       cluster ! InterstellarCluster.GetWorld("home3")
 
     case InterstellarCluster.GiveWorld(zoneId, zone) =>

From 1f578c9e5debd7a00bb588131adb1ede1fd7efb8 Mon Sep 17 00:00:00 2001
From: FateJH <Jason_DiDonato@yahoo.com>
Date: Mon, 14 Oct 2019 18:54:03 -0400
Subject: [PATCH 26/31] removed squad testing crouches

---
 pslogin/src/main/scala/WorldSessionActor.scala | 3 ---
 1 file changed, 3 deletions(-)

diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala
index 5ea8dabd..e80016dd 100644
--- a/pslogin/src/main/scala/WorldSessionActor.scala
+++ b/pslogin/src/main/scala/WorldSessionActor.scala
@@ -3607,9 +3607,6 @@ class WorldSessionActor extends Actor with MDCContextAware {
 
     case msg @ PlayerStateMessageUpstream(avatar_guid, pos, vel, yaw, pitch, yaw_upper, seq_time, unk3, is_crouching, is_jumping, unk4, is_cloaking, unk5, unk6) =>
       if(deadState == DeadState.Alive) {
-        if(!player.Crouching && is_crouching) { //SQUAD TESTING CODE
-          //...
-        }
         player.Position = pos
         player.Velocity = vel
         player.Orientation = Vector3(player.Orientation.x, pitch, yaw)

From 35a8d879419234cc634685582e5567f4594917b4 Mon Sep 17 00:00:00 2001
From: FateJH <Jason_DiDonato@yahoo.com>
Date: Wed, 16 Oct 2019 15:26:55 -0400
Subject: [PATCH 27/31] /kick support; fixing a bug with the supplemental squad
 id

---
 .../services/teamwork/SquadService.scala      | 91 +++++++++++--------
 .../src/main/scala/WorldSessionActor.scala    |  2 +-
 2 files changed, 53 insertions(+), 40 deletions(-)

diff --git a/common/src/main/scala/services/teamwork/SquadService.scala b/common/src/main/scala/services/teamwork/SquadService.scala
index 0f2ac6a6..0fc6291c 100644
--- a/common/src/main/scala/services/teamwork/SquadService.scala
+++ b/common/src/main/scala/services/teamwork/SquadService.scala
@@ -378,7 +378,7 @@ class SquadService extends Actor {
 
     case Service.Leave(Some(char_id)) => LeaveService(char_id, sender)
 
-    case Service.Leave(None) | Service.LeaveAll() => UserEvents find { case(_, subscription) => subscription == sender} match {
+    case Service.Leave(None) | Service.LeaveAll() => UserEvents find { case(_, subscription) => subscription.path.equals(sender.path)} match {
       case Some((to, _)) =>
         LeaveService(to, sender)
       case _ => ;
@@ -414,14 +414,17 @@ class SquadService extends Actor {
         //this is just busy work; for actual joining operations, see SquadRequestType.Accept
         (if(invitedName.nonEmpty) {
           //validate player with name exists
-          LivePlayerList.WorldPopulation({ case (_, a : Avatar) => a.name == invitedName }).headOption match {
+          LivePlayerList.WorldPopulation({ case (_, a : Avatar) => a.name.equalsIgnoreCase(invitedName) && a.faction == tplayer.Faction }).headOption match {
             case Some(player) => UserEvents.keys.find(_ == player.CharId)
             case None => None
           }
         }
         else {
           //validate player with id exists
-          UserEvents.keys.find(_ == _invitedPlayer)
+          LivePlayerList.WorldPopulation({ case (_, a : Avatar) => a.CharId == _invitedPlayer && a.faction == tplayer.Faction }).headOption match {
+            case Some(player) => Some(_invitedPlayer)
+            case None => None
+          }
         }) match {
           case Some(invitedPlayer) =>
             (memberToSquad.get(invitingPlayer), memberToSquad.get(invitedPlayer)) match {
@@ -738,44 +741,54 @@ class SquadService extends Actor {
         }
         NextInviteAndRespond(invitedPlayer)
 
-      case SquadAction.Membership(SquadRequestType.Leave, leavingPlayer, optionalPlayer, name, _) =>
-        GetParticipatingSquad(leavingPlayer) match {
+      case SquadAction.Membership(SquadRequestType.Leave, actingPlayer, _leavingPlayer, name, _) =>
+        GetParticipatingSquad(actingPlayer) match {
           case Some(squad) =>
-            val kickedPlayer = if(name.nonEmpty) {
+            val leader = squad.Leader.CharId
+            (if(name.nonEmpty) {
               //validate player with name
-              LivePlayerList.WorldPopulation({ case (_, a : Avatar) => a.name == name }).headOption match {
-                case Some(player) => UserEvents.keys.find(_ == player.CharId)
+              LivePlayerList.WorldPopulation({ case (_, a : Avatar) => a.name.equalsIgnoreCase(name) }).headOption match {
+                case Some(a) => UserEvents.keys.find(_ == a.CharId)
                 case None => None
               }
             }
             else {
               //validate player with id
-              optionalPlayer match {
+              _leavingPlayer match {
                 case Some(id) => UserEvents.keys.find(_ == id)
                 case None => None
               }
-            }
-            val leader = squad.Leader.CharId
-            if(leavingPlayer == leader || squad.Size == 2) {
-              //squad leader is leaving his own squad, so it will be disbanded
-              //alternately, squad is only composed of two people, so it will be closed-out when one of them leaves
-              DisbandSquad(squad)
-            }
-            else {
-              if(kickedPlayer.isEmpty || kickedPlayer.contains(leavingPlayer)) {
-                //leaving the squad of own accord
-                LeaveSquad(tplayer.CharId, squad)
-              }
-              else if(kickedPlayer.contains(leader)) {
-                //kicked by the squad leader
-                Publish(leavingPlayer, SquadResponse.Membership(SquadResponseType.Leave, 0, 0, leavingPlayer, Some(leader), tplayer.Name, false, Some(None)))
-                Publish(leader, SquadResponse.Membership(SquadResponseType.Leave, 0, 0, leader, Some(leavingPlayer), "", true, Some(None)))
-                squadFeatures(squad.GUID).Refuse = leavingPlayer
-                LeaveSquad(leavingPlayer, squad)
-              }
-            }
+            }) match {
+              case out @ Some(leavingPlayer) if GetParticipatingSquad(leavingPlayer).contains(squad) => //kicked player must be in the same squad
+                log.info(s"leader=$leader, acting=$actingPlayer, leaving=$leavingPlayer")
+                if(actingPlayer == leader) {
+                  if(leavingPlayer == leader || squad.Size == 2) {
+                    //squad leader is leaving his own squad, so it will be disbanded
+                    //OR squad is only composed of two people, so it will be closed-out when one of them leaves
+                    DisbandSquad(squad)
+                  }
+                  else {
+                    //kicked by the squad leader
+                    Publish(leavingPlayer, SquadResponse.Membership(SquadResponseType.Leave, 0, 0, leavingPlayer, Some(leader), tplayer.Name, false, Some(None)))
+                    Publish(leader, SquadResponse.Membership(SquadResponseType.Leave, 0, 0, leader, Some(leavingPlayer), "", true, Some(None)))
+                    squadFeatures(squad.GUID).Refuse = leavingPlayer
+                    LeaveSquad(leavingPlayer, squad)
+                  }
+                }
+                else if(leavingPlayer == actingPlayer) {
+                  if(squad.Size == 2) {
+                    //squad is only composed of two people, so it will be closed-out when one of them leaves
+                    DisbandSquad(squad)
+                  }
+                  else {
+                    //leaving the squad of own accord
+                    LeaveSquad(actingPlayer, squad)
+                  }
+                }
 
-          case None =>
+              case _ => ;
+            }
+          case _ => ;
         }
 
       case SquadAction.Membership(SquadRequestType.Reject, rejectingPlayer, _, _, _) =>
@@ -2244,9 +2257,9 @@ class SquadService extends Actor {
           .filterNot { case (member, _) => member.CharId == 0 }
           .toList
           .unzip { case (member, index) => (member.CharId, index) }
-        val toChannel = s"/${features.ToChannel}/Squad"
+        val toChannel = features.ToChannel
         memberCharIds.foreach { charId =>
-          SquadEvents.subscribe(UserEvents(charId), toChannel)
+          SquadEvents.subscribe(UserEvents(charId), s"/$toChannel/Squad")
           Publish(charId,
             SquadResponse.Join(
               squad,
@@ -2262,29 +2275,29 @@ class SquadService extends Actor {
       else {
         //joining an active squad; everybody updates differently
         val updatedIndex = List(position)
-        val toChannel = s"/${features.ToChannel}/Squad"
+        val toChannel = features.ToChannel
         //new member gets full squad UI updates
         Publish(
           charId,
           SquadResponse.Join(
             squad,
-            squad.Membership
+            position +: squad.Membership
               .zipWithIndex
               .collect({ case (member, index) if member.CharId > 0 => index })
-              .filterNot(_== position)
-              .toList :+ position,
+              .filterNot(_ == position)
+              .toList,
             toChannel
           )
         )
         //other squad members see new member joining the squad
-        Publish(features.ToChannel, SquadResponse.Join(squad, updatedIndex, ""))
+        Publish(toChannel, SquadResponse.Join(squad, updatedIndex, ""))
         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(UserEvents(charId), toChannel)
+        SquadEvents.subscribe(UserEvents(charId), s"/$toChannel/Squad")
       }
       UpdateSquadListWhenListed(features, SquadInfo().Size(size))
       true
@@ -2378,7 +2391,7 @@ class SquadService extends Actor {
         member.Name = ""
         member.CharId = 0
         //other squad members see the member leaving
-        Publish(squadFeatures(squad.GUID).ToChannel, SquadResponse.Leave(squad, List(entry)))
+        Publish(squadFeatures(squad.GUID).ToChannel, SquadResponse.Leave(squad, List(entry)), Seq(charId))
         UpdateSquadListWhenListed(squadFeatures(squad.GUID), SquadInfo().Size(squad.Size))
         UpdateSquadDetail(
           squad.GUID,
diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala
index e80016dd..cfb10565 100644
--- a/pslogin/src/main/scala/WorldSessionActor.scala
+++ b/pslogin/src/main/scala/WorldSessionActor.scala
@@ -493,6 +493,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
                 lfs = false
                 //a finalization? what does this do?
                 sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(18)))
+                squad_supplement_id = 0
                 updateSquad = NoSquadUpdates
                 squadChannel = None
               case _ =>
@@ -507,7 +508,6 @@ class WorldSessionActor extends Actor with MDCContextAware {
                 )
             }
             StopBundlingPackets()
-            squad_supplement_id = 0
 
           case SquadResponse.AssignMember(squad, from_index, to_index) =>
             //we've already swapped position internally; now we swap the cards

From 0c105f38261236c08716fde9db4faee82485740c Mon Sep 17 00:00:00 2001
From: FateJH <Jason_DiDonato@yahoo.com>
Date: Thu, 17 Oct 2019 16:27:19 -0400
Subject: [PATCH 28/31] repaired some broken tests; LoadoutManager respects
 length; squad ids attempt to reset only when no players are logged in

---
 .../objects/avatar/LoadoutManager.scala       |  6 +-
 .../services/teamwork/SquadService.scala      |  6 +-
 .../game/SquadMembershipResponseTest.scala    | 24 +++----
 .../src/test/scala/objects/AvatarTest.scala   | 70 +++++++++----------
 .../src/test/scala/objects/PlayerTest.scala   | 40 +++++------
 .../test/scala/PacketCodingActorTest.scala    |  2 +-
 6 files changed, 75 insertions(+), 73 deletions(-)

diff --git a/common/src/main/scala/net/psforever/objects/avatar/LoadoutManager.scala b/common/src/main/scala/net/psforever/objects/avatar/LoadoutManager.scala
index c5978723..44a7ab77 100644
--- a/common/src/main/scala/net/psforever/objects/avatar/LoadoutManager.scala
+++ b/common/src/main/scala/net/psforever/objects/avatar/LoadoutManager.scala
@@ -11,7 +11,7 @@ class LoadoutManager(size : Int) {
 
   def SaveLoadout(owner : Any, label : String, line : Int) : Unit = {
     Loadout.Create(owner, label) match {
-      case Success(loadout) =>
+      case Success(loadout) if entries.length > line =>
         entries(line) = Some(loadout)
       case _ => ;
     }
@@ -20,7 +20,9 @@ class LoadoutManager(size : Int) {
   def LoadLoadout(line : Int) : Option[Loadout] = entries.lift(line).flatten
 
   def DeleteLoadout(line : Int) : Unit = {
-    entries(line) = None
+    if(entries.length > line) {
+      entries(line) = None
+    }
   }
 
   def Loadouts : Seq[(Int, Loadout)] = entries.zipWithIndex.collect { case(Some(loadout), index) => (index, loadout) } toSeq
diff --git a/common/src/main/scala/services/teamwork/SquadService.scala b/common/src/main/scala/services/teamwork/SquadService.scala
index 0fc6291c..c0a69fbf 100644
--- a/common/src/main/scala/services/teamwork/SquadService.scala
+++ b/common/src/main/scala/services/teamwork/SquadService.scala
@@ -152,11 +152,11 @@ class SquadService extends Actor {
   }
 
   /**
-    * Set the unique squad identifier back to the start (1) if no squads are active.
+    * Set the unique squad identifier back to the start (1) if no squads are active and no players are logged on.
     * @return `true`, if the identifier is reset; `false`, otherwise
     */
   def TryResetSquadId() : Boolean = {
-    if(squadFeatures.isEmpty) {
+    if(UserEvents.isEmpty && squadFeatures.isEmpty) {
       sid = 1
       true
     }
@@ -2451,7 +2451,6 @@ class SquadService extends Actor {
       squadFeatures.remove(guid).get.Stop,
       None
     )
-    TryResetSquadId()
   }
 
   /**
@@ -2652,6 +2651,7 @@ class SquadService extends Actor {
         UserEvents.remove(charId)
     }
     SquadEvents.unsubscribe(sender) //just to make certain
+    TryResetSquadId()
   }
 
   /**
diff --git a/common/src/test/scala/game/SquadMembershipResponseTest.scala b/common/src/test/scala/game/SquadMembershipResponseTest.scala
index e2727aee..f9ff10c2 100644
--- a/common/src/test/scala/game/SquadMembershipResponseTest.scala
+++ b/common/src/test/scala/game/SquadMembershipResponseTest.scala
@@ -206,7 +206,7 @@ class SquadMembershipResponseTest extends Specification {
     "decode (7-1)" in {
       PacketCoding.DecodePacket(string_71).require match {
         case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) =>
-          unk1 mustEqual SquadResponseType.Disband
+          unk1 mustEqual SquadResponseType.PlatoonInvite
           unk2 mustEqual 2
           unk3 mustEqual 0
           unk4 mustEqual 1176612L
@@ -222,7 +222,7 @@ class SquadMembershipResponseTest extends Specification {
     "decode (7-2)" in {
       PacketCoding.DecodePacket(string_72).require match {
         case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) =>
-          unk1 mustEqual SquadResponseType.Disband
+          unk1 mustEqual SquadResponseType.PlatoonInvite
           unk2 mustEqual 0
           unk3 mustEqual 0
           unk4 mustEqual 41578085L
@@ -238,7 +238,7 @@ class SquadMembershipResponseTest extends Specification {
     "decode (8-1)" in {
       PacketCoding.DecodePacket(string_81).require match {
         case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) =>
-          unk1 mustEqual SquadResponseType.PlatoonInvite
+          unk1 mustEqual SquadResponseType.PlatoonAccept
           unk2 mustEqual 0
           unk3 mustEqual 0
           unk4 mustEqual 30910985L
@@ -254,7 +254,7 @@ class SquadMembershipResponseTest extends Specification {
     "decode (9-1)" in {
       PacketCoding.DecodePacket(string_91).require match {
         case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) =>
-          unk1 mustEqual SquadResponseType.PlatoonAccept
+          unk1 mustEqual SquadResponseType.PlatoonReject
           unk2 mustEqual 0
           unk3 mustEqual 0
           unk4 mustEqual 297025L
@@ -270,7 +270,7 @@ class SquadMembershipResponseTest extends Specification {
     "decode (9-2)" in {
       PacketCoding.DecodePacket(string_92).require match {
         case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) =>
-          unk1 mustEqual SquadResponseType.PlatoonAccept
+          unk1 mustEqual SquadResponseType.PlatoonReject
           unk2 mustEqual 0
           unk3 mustEqual 0
           unk4 mustEqual 30910985L
@@ -286,7 +286,7 @@ class SquadMembershipResponseTest extends Specification {
     "decode (b-1)" in {
       PacketCoding.DecodePacket(string_b1).require match {
         case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) =>
-          unk1 mustEqual SquadResponseType.PlatoonCancel
+          unk1 mustEqual SquadResponseType.PlatoonLeave
           unk2 mustEqual 0
           unk3 mustEqual 1
           unk4 mustEqual 30910985L
@@ -377,42 +377,42 @@ class SquadMembershipResponseTest extends Specification {
     }
 
     "encode (7-1)" in {
-      val msg = SquadMembershipResponse(SquadResponseType.Disband, 2, 0, 1176612L, Some(0L), "", true, Some(None))
+      val msg = SquadMembershipResponse(SquadResponseType.PlatoonInvite, 2, 0, 1176612L, Some(0L), "", true, Some(None))
       val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
       pkt mustEqual string_71
     }
 
     "encode (7-2)" in {
-      val msg = SquadMembershipResponse(SquadResponseType.Disband, 0, 0, 41578085L, Some(30910985L), "Wizkid45", true, Some(None))
+      val msg = SquadMembershipResponse(SquadResponseType.PlatoonInvite, 0, 0, 41578085L, Some(30910985L), "Wizkid45", true, Some(None))
       val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
       pkt mustEqual string_72
     }
 
     "encode (8-1)" in {
-      val msg = SquadMembershipResponse(SquadResponseType.PlatoonInvite, 0, 0, 30910985L, Some(41578085L), "Wizkid45", false, Some(None))
+      val msg = SquadMembershipResponse(SquadResponseType.PlatoonAccept, 0, 0, 30910985L, Some(41578085L), "Wizkid45", false, Some(None))
       val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
       pkt mustEqual string_81
     }
 
     "encode (9-1)" in {
-      val msg = SquadMembershipResponse(SquadResponseType.PlatoonAccept, 0, 0, 297025L, Some(41605002L), "", true, Some(None))
+      val msg = SquadMembershipResponse(SquadResponseType.PlatoonReject, 0, 0, 297025L, Some(41605002L), "", true, Some(None))
       val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
       pkt mustEqual string_91
     }
 
     "encode (9-2)" in {
-      val msg = SquadMembershipResponse(SquadResponseType.PlatoonAccept, 0, 0, 30910985L, Some(41578085L), "Wizkid45", false, Some(None))
+      val msg = SquadMembershipResponse(SquadResponseType.PlatoonReject, 0, 0, 30910985L, Some(41578085L), "Wizkid45", false, Some(None))
       val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
       pkt mustEqual string_92
     }
 
     "encode (b-1)" in {
-      val msg = SquadMembershipResponse(SquadResponseType.PlatoonCancel, 0, 1, 30910985L, Some(41578085L), "SAraisVanu", false, Some(None))
+      val msg = SquadMembershipResponse(SquadResponseType.PlatoonLeave, 0, 1, 30910985L, Some(41578085L), "SAraisVanu", false, Some(None))
       val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
       pkt mustEqual string_b1
diff --git a/common/src/test/scala/objects/AvatarTest.scala b/common/src/test/scala/objects/AvatarTest.scala
index 33b53ce7..6e1502b1 100644
--- a/common/src/test/scala/objects/AvatarTest.scala
+++ b/common/src/test/scala/objects/AvatarTest.scala
@@ -121,19 +121,19 @@ class AvatarTest extends Specification {
     obj.Implants(0).Active mustEqual false
     obj.Implants(0).Implant mustEqual ImplantType.None
     obj.Implant(0) mustEqual ImplantType.None
-    obj.Implants(0).Installed mustEqual None
+    obj.Implants(0).Installed.isEmpty mustEqual true
     obj.Implants(1).Unlocked mustEqual false
     obj.Implants(1).Initialized mustEqual false
     obj.Implants(1).Active mustEqual false
     obj.Implants(1).Implant mustEqual ImplantType.None
     obj.Implant(1) mustEqual ImplantType.None
-    obj.Implants(1).Installed mustEqual None
+    obj.Implants(1).Installed.isEmpty mustEqual true
     obj.Implants(2).Unlocked mustEqual false
     obj.Implants(2).Initialized mustEqual false
     obj.Implants(2).Active mustEqual false
     obj.Implants(2).Implant mustEqual ImplantType.None
     obj.Implant(2) mustEqual ImplantType.None
-    obj.Implants(2).Installed mustEqual None
+    obj.Implants(2).Installed.isEmpty mustEqual true
 
     obj.Implant(3) mustEqual ImplantType.None //invalid slots beyond the third always reports as ImplantType.None
   }
@@ -142,10 +142,10 @@ class AvatarTest extends Specification {
     val testplant : ImplantDefinition = ImplantDefinition(1)
     val obj = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5)
     obj.Implants(0).Unlocked = true
-    obj.InstallImplant(testplant) mustEqual Some(0)
+    obj.InstallImplant(testplant).contains(0) mustEqual true
     obj.Implants.find({p => p.Implant == ImplantType(1)}) match { //find the installed implant
       case Some(slot) =>
-        slot.Installed mustEqual Some(testplant)
+        slot.Installed.contains(testplant) mustEqual true
       case _ =>
         ko
     }
@@ -159,8 +159,8 @@ class AvatarTest extends Specification {
     obj.Implants(0).Unlocked = true
     obj.Implants(1).Unlocked = true
 
-    obj.InstallImplant(testplant1) mustEqual Some(0)
-    obj.InstallImplant(testplant2) mustEqual Some(1)
+    obj.InstallImplant(testplant1).contains(0) mustEqual true
+    obj.InstallImplant(testplant2).contains(1) mustEqual true
   }
 
   "can not install the same type of implant twice" in {
@@ -170,8 +170,8 @@ class AvatarTest extends Specification {
     obj.Implants(0).Unlocked = true
     obj.Implants(1).Unlocked = true
 
-    obj.InstallImplant(testplant1) mustEqual Some(0)
-    obj.InstallImplant(testplant2) mustEqual None
+    obj.InstallImplant(testplant1).contains(0) mustEqual true
+    obj.InstallImplant(testplant2).isEmpty mustEqual true
   }
 
   "can not install more implants than slots available (two unlocked)" in {
@@ -182,9 +182,9 @@ class AvatarTest extends Specification {
     obj.Implants(0).Unlocked = true
     obj.Implants(1).Unlocked = true
 
-    obj.InstallImplant(testplant1) mustEqual Some(0)
-    obj.InstallImplant(testplant2) mustEqual Some(1)
-    obj.InstallImplant(testplant3) mustEqual None
+    obj.InstallImplant(testplant1).contains(0) mustEqual true
+    obj.InstallImplant(testplant2).contains(1) mustEqual true
+    obj.InstallImplant(testplant3).isEmpty mustEqual true
   }
 
   "can not install more implants than slots available (four implants)" in {
@@ -197,21 +197,21 @@ class AvatarTest extends Specification {
     obj.Implants(1).Unlocked = true
     obj.Implants(2).Unlocked = true
 
-    obj.InstallImplant(testplant1) mustEqual Some(0)
-    obj.InstallImplant(testplant2) mustEqual Some(1)
-    obj.InstallImplant(testplant3) mustEqual Some(2)
-    obj.InstallImplant(testplant4) mustEqual None
+    obj.InstallImplant(testplant1).contains(0) mustEqual true
+    obj.InstallImplant(testplant2).contains(1) mustEqual true
+    obj.InstallImplant(testplant3).contains(2) mustEqual true
+    obj.InstallImplant(testplant4).isEmpty mustEqual true
   }
 
   "can uninstall an implant" in {
     val testplant : ImplantDefinition = ImplantDefinition(1)
     val obj = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5)
     obj.Implants(0).Unlocked = true
-    obj.InstallImplant(testplant) mustEqual Some(0)
-    obj.Implants(0).Installed mustEqual Some(testplant)
+    obj.InstallImplant(testplant).contains(0) mustEqual true
+    obj.Implants(0).Installed.contains(testplant) mustEqual true
 
-    obj.UninstallImplant(testplant.Type) mustEqual Some(0)
-    obj.Implants(0).Installed mustEqual None
+    obj.UninstallImplant(testplant.Type).contains(0) mustEqual true
+    obj.Implants(0).Installed.isEmpty mustEqual true
   }
 
   "can uninstall just a specific implant" in {
@@ -222,14 +222,14 @@ class AvatarTest extends Specification {
     obj.Implants(0).Unlocked = true
     obj.Implants(1).Unlocked = true
     obj.Implants(2).Unlocked = true
-    obj.InstallImplant(testplant1) mustEqual Some(0)
-    obj.InstallImplant(testplant2) mustEqual Some(1)
-    obj.InstallImplant(testplant3) mustEqual Some(2)
+    obj.InstallImplant(testplant1).contains(0) mustEqual true
+    obj.InstallImplant(testplant2).contains(1) mustEqual true
+    obj.InstallImplant(testplant3).contains(2) mustEqual true
 
     obj.Implant(0) mustEqual testplant1.Type
     obj.Implant(1) mustEqual testplant2.Type
     obj.Implant(2) mustEqual testplant3.Type
-    obj.UninstallImplant(testplant2.Type) mustEqual Some(1)
+    obj.UninstallImplant(testplant2.Type).contains(1) mustEqual true
     obj.Implant(0) mustEqual testplant1.Type
     obj.Implant(1) mustEqual ImplantType.None
     obj.Implant(2) mustEqual testplant3.Type
@@ -243,16 +243,16 @@ class AvatarTest extends Specification {
     obj.Implants(0).Unlocked = true
     obj.Implants(1).Unlocked = true
     obj.Implants(2).Unlocked = true
-    obj.InstallImplant(testplant1) mustEqual Some(0)
-    obj.InstallImplant(testplant2) mustEqual Some(1)
-    obj.InstallImplant(testplant3) mustEqual Some(2)
-    obj.UninstallImplant(testplant2.Type) mustEqual Some(1)
+    obj.InstallImplant(testplant1).contains(0) mustEqual true
+    obj.InstallImplant(testplant2).contains(1) mustEqual true
+    obj.InstallImplant(testplant3).contains(2) mustEqual true
+    obj.UninstallImplant(testplant2.Type).contains(1) mustEqual true
     obj.Implant(0) mustEqual testplant1.Type
     obj.Implant(1) mustEqual ImplantType.None
     obj.Implant(2) mustEqual testplant3.Type
 
     val testplant4 : ImplantDefinition = ImplantDefinition(4)
-    obj.InstallImplant(testplant4) mustEqual Some(1)
+    obj.InstallImplant(testplant4).contains(1) mustEqual true
     obj.Implant(0) mustEqual testplant1.Type
     obj.Implant(1) mustEqual testplant4.Type
     obj.Implant(2) mustEqual testplant3.Type
@@ -264,8 +264,8 @@ class AvatarTest extends Specification {
     val obj = Avatar("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5)
     obj.Implants(0).Unlocked = true
     obj.Implants(1).Unlocked = true
-    obj.InstallImplant(testplant1) mustEqual Some(0)
-    obj.InstallImplant(testplant2) mustEqual Some(1)
+    obj.InstallImplant(testplant1).contains(0) mustEqual true
+    obj.InstallImplant(testplant2).contains(1) mustEqual true
     obj.Implants(0).Initialized = true
     obj.Implants(0).Active = true
     obj.Implants(1).Initialized = true
@@ -281,7 +281,7 @@ class AvatarTest extends Specification {
 
   "does not have any loadout specifications by default" in {
     val (_, avatar) = CreatePlayer()
-    (0 to 9).foreach { avatar.EquipmentLoadouts.LoadLoadout(_) mustEqual None }
+    (0 to 9).foreach { avatar.EquipmentLoadouts.LoadLoadout(_).isEmpty mustEqual true }
     ok
   }
 
@@ -337,9 +337,9 @@ class AvatarTest extends Specification {
 
   "try to save player's current inventory as a loadout, but will not save to an invalid slot" in {
     val (obj, avatar) = CreatePlayer()
-    avatar.EquipmentLoadouts.SaveLoadout(obj, "test", 10)
+    avatar.EquipmentLoadouts.SaveLoadout(obj, "test", 50)
 
-    avatar.EquipmentLoadouts.LoadLoadout(10) mustEqual None
+    avatar.EquipmentLoadouts.LoadLoadout(50).isEmpty mustEqual true
   }
 
   "save player's current inventory as a loadout, without inventory contents" in {
@@ -384,7 +384,7 @@ class AvatarTest extends Specification {
 
     avatar.EquipmentLoadouts.LoadLoadout(0).isDefined mustEqual true
     avatar.EquipmentLoadouts.DeleteLoadout(0)
-    avatar.EquipmentLoadouts.LoadLoadout(0) mustEqual None
+    avatar.EquipmentLoadouts.LoadLoadout(0).isEmpty mustEqual true
   }
 
   "the fifth slot is the locker wrapped in an EquipmentSlot" in {
diff --git a/common/src/test/scala/objects/PlayerTest.scala b/common/src/test/scala/objects/PlayerTest.scala
index c1471800..f579e4fc 100644
--- a/common/src/test/scala/objects/PlayerTest.scala
+++ b/common/src/test/scala/objects/PlayerTest.scala
@@ -245,34 +245,34 @@ class PlayerTest extends Specification {
       val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5)
       obj.Slot(-1).Equipment = wep
 
-      obj.Slot(-1).Equipment mustEqual None
+      obj.Slot(-1).Equipment.isEmpty mustEqual true
     }
 
     "search for the smallest available slot in which to store equipment" in {
       val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5)
       obj.Inventory.Resize(3,3) //fits one item
 
-      obj.Fit(Tool(GlobalDefinitions.beamer)) mustEqual Some(0)
+      obj.Fit(Tool(GlobalDefinitions.beamer)).contains(0) mustEqual true
 
-      obj.Fit(Tool(GlobalDefinitions.suppressor)) mustEqual Some(2)
+      obj.Fit(Tool(GlobalDefinitions.suppressor)).contains(2) mustEqual true
 
       val ammo = AmmoBox(GlobalDefinitions.bullet_9mm)
       val ammo2 = AmmoBox(GlobalDefinitions.bullet_9mm)
       val ammo3 = AmmoBox(GlobalDefinitions.bullet_9mm)
-      obj.Fit(ammo) mustEqual Some(6)
+      obj.Fit(ammo).contains(6) mustEqual true
       obj.Slot(6).Equipment = ammo
-      obj.Fit(ammo2) mustEqual Some(Player.FreeHandSlot)
+      obj.Fit(ammo2).contains(Player.FreeHandSlot) mustEqual true
       obj.Slot(Player.FreeHandSlot).Equipment = ammo2
-      obj.Fit(ammo3) mustEqual None
+      obj.Fit(ammo3).isEmpty mustEqual true
     }
 
     "can use their free hand to hold things" in {
       val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5)
       val ammo = AmmoBox(GlobalDefinitions.bullet_9mm)
-      obj.FreeHand.Equipment mustEqual None
+      obj.FreeHand.Equipment.isEmpty mustEqual true
 
       obj.FreeHand.Equipment = ammo
-      obj.FreeHand.Equipment mustEqual Some(ammo)
+      obj.FreeHand.Equipment.contains(ammo) mustEqual true
     }
 
     "can access the player's locker-space" in {
@@ -308,12 +308,12 @@ class PlayerTest extends Specification {
         item
       }
 
-      obj.Find(PlanetSideGUID(1)) mustEqual Some(0) //holsters
-      obj.Find(PlanetSideGUID(2)) mustEqual Some(4) //holsters, melee
-      obj.Find(PlanetSideGUID(3)) mustEqual Some(6) //inventory
-      obj.Find(PlanetSideGUID(4)) mustEqual None //can not find in locker-space
-      obj.Find(PlanetSideGUID(5)) mustEqual Some(Player.FreeHandSlot) //free hand
-      obj.Find(PlanetSideGUID(6)) mustEqual None //not here
+      obj.Find(PlanetSideGUID(1)).contains(0) mustEqual true //holsters
+      obj.Find(PlanetSideGUID(2)).contains(4) mustEqual true //holsters, melee
+      obj.Find(PlanetSideGUID(3)).contains(6) mustEqual true //inventory
+      obj.Find(PlanetSideGUID(4)).isEmpty mustEqual true //can not find in locker-space
+      obj.Find(PlanetSideGUID(5)).contains(Player.FreeHandSlot) mustEqual true //free hand
+      obj.Find(PlanetSideGUID(6)).isEmpty mustEqual true //not here
     }
 
     "does equipment collision checking (are we already holding something there?)" in {
@@ -437,20 +437,20 @@ class PlayerTest extends Specification {
 
     "seat in a vehicle" in {
       val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5)
-      obj.VehicleSeated mustEqual None
+      obj.VehicleSeated.isEmpty mustEqual true
       obj.VehicleSeated = PlanetSideGUID(65)
-      obj.VehicleSeated mustEqual Some(PlanetSideGUID(65))
+      obj.VehicleSeated.contains(PlanetSideGUID(65)) mustEqual true
       obj.VehicleSeated = None
-      obj.VehicleSeated mustEqual None
+      obj.VehicleSeated.isEmpty mustEqual true
     }
 
     "own in a vehicle" in {
       val obj = TestPlayer("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, CharacterVoice.Voice5)
-      obj.VehicleOwned mustEqual None
+      obj.VehicleOwned.isEmpty mustEqual true
       obj.VehicleOwned = PlanetSideGUID(65)
-      obj.VehicleOwned mustEqual Some(PlanetSideGUID(65))
+      obj.VehicleOwned.contains(PlanetSideGUID(65)) mustEqual true
       obj.VehicleOwned = None
-      obj.VehicleOwned mustEqual None
+      obj.VehicleOwned.isEmpty mustEqual true
     }
 
     "remember what zone he is in" in {
diff --git a/pslogin/src/test/scala/PacketCodingActorTest.scala b/pslogin/src/test/scala/PacketCodingActorTest.scala
index 5efe312b..e055d325 100644
--- a/pslogin/src/test/scala/PacketCodingActorTest.scala
+++ b/pslogin/src/test/scala/PacketCodingActorTest.scala
@@ -485,7 +485,7 @@ class PacketCodingActorITest extends ActorTest {
   )
   val obj = DetailedPlayerData(pos, app, char, InventoryData(Nil), DrawnSlot.None)
   val pkt = MultiPacketBundle(List(ObjectCreateDetailedMessage(0x79, PlanetSideGUID(75), obj)))
-  val string_hex = hex"000900001879060000bc84b000000000000000000002040000097049006c006c006c004900490049006c006c006c0049006c0049006c006c0049006c006c006c0049006c006c00490084524000000000000000000000000000000020000007f35703fffffffffffffffffffffffffffffffc000000000000000000000000000000000000000190019000640000000000c800c80000000000000000000000000000000000000001c00042c54686c7000000000000000000000000000000000000000000000000000000000000000000000400e0"
+  val string_hex = hex"00090000186c060000bc84b000000000000000000002040000097049006c006c006c004900490049006c006c006c0049006c0049006c006c0049006c006c006c0049006c006c00490084524000000000000000000000000000000020000007f35703fffffffffffffffffffffffffffffffc000000000000000000000000000000000000000190019000640000000000c800c80000000000000000000000000000000000000001c00042c54686c7000000000000000000000000000000000000000000000000000000000000100000000400e0"
 
   "PacketCodingActor" should {
     "bundle an r-originating packet into an l-facing SlottedMetaPacket byte stream data (SlottedMetaPacket)" in {

From aa2e0089a49c197e1f7527dfc1b1d26c46ddb9d2 Mon Sep 17 00:00:00 2001
From: FateJH <Jason_DiDonato@yahoo.com>
Date: Sat, 19 Oct 2019 17:24:12 -0400
Subject: [PATCH 29/31] stop player from inviting himself to own squad;
 periodic squad updates; fixed squad listing hangup; fixed damage calculation
 tests

---
 .../services/teamwork/SquadService.scala      | 21 ++++++++++++-------
 .../test/scala/objects/DamageModelTests.scala |  4 ++--
 .../src/main/scala/WorldSessionActor.scala    | 12 +++++++++--
 3 files changed, 26 insertions(+), 11 deletions(-)

diff --git a/common/src/main/scala/services/teamwork/SquadService.scala b/common/src/main/scala/services/teamwork/SquadService.scala
index c0a69fbf..07b5d037 100644
--- a/common/src/main/scala/services/teamwork/SquadService.scala
+++ b/common/src/main/scala/services/teamwork/SquadService.scala
@@ -426,7 +426,7 @@ class SquadService extends Actor {
             case None => None
           }
         }) match {
-          case Some(invitedPlayer) =>
+          case Some(invitedPlayer) if invitingPlayer != invitedPlayer =>
             (memberToSquad.get(invitingPlayer), memberToSquad.get(invitedPlayer)) match {
               case (Some(squad1), Some(squad2)) if squad1.GUID == squad2.GUID =>
               //both players are in the same squad; no need to do anything
@@ -519,7 +519,7 @@ class SquadService extends Actor {
 
               case _ => ;
             }
-          case None => ;
+          case _ => ;
         }
 
       case SquadAction.Membership(SquadRequestType.ProximityInvite, invitingPlayer, _, _, _) =>
@@ -760,7 +760,6 @@ class SquadService extends Actor {
               }
             }) match {
               case out @ Some(leavingPlayer) if GetParticipatingSquad(leavingPlayer).contains(squad) => //kicked player must be in the same squad
-                log.info(s"leader=$leader, acting=$actingPlayer, leaving=$leavingPlayer")
                 if(actingPlayer == leader) {
                   if(leavingPlayer == leader || squad.Size == 2) {
                     //squad leader is leaving his own squad, so it will be disbanded
@@ -1254,11 +1253,15 @@ class SquadService extends Actor {
                 val features = squadFeatures(squad.GUID)
                 features.LocationFollowsSquadLead = true
                 features.AutoApproveInvitationRequests = true
-                UpdateSquadListWhenListed(features, SquadInfo().Task("").ZoneId(None).Capacity(squad.Capacity))
+                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
-                //do not unlist an already listed squad
               case Some(squad) =>
                 //underutilized squad; just close it out
                 CloseSquad(squad)
@@ -2159,7 +2162,7 @@ class SquadService extends Actor {
     * 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 ``ResetAll
     * @see `SquadResponse.AssociateWithSquad`
     * @see `SquadResponse.Detail`
     * @see `SquadService.Detail.Publish`
@@ -2425,7 +2428,11 @@ class SquadService extends Actor {
       .unzip
     val updateIndicesList = updateIndices.toList
     val completelyBlankSquadDetail = SquadDetail().Complete
-    val channel = s"/${squadFeatures(squad.GUID).ToChannel}/Squad"
+    val features = squadFeatures(guid)
+    val channel = s"/${features.ToChannel}/Squad"
+    if(features.Listed) {
+      Publish(squad.Leader.CharId, SquadResponse.SetListSquad(PlanetSideGUID(0)))
+    }
     updateMembers
       .foreach {
         case (member, charId, _, None) =>
diff --git a/common/src/test/scala/objects/DamageModelTests.scala b/common/src/test/scala/objects/DamageModelTests.scala
index 1230b043..5a477c88 100644
--- a/common/src/test/scala/objects/DamageModelTests.scala
+++ b/common/src/test/scala/objects/DamageModelTests.scala
@@ -70,11 +70,11 @@ class DamageCalculationsTests extends Specification {
     }
 
     "calculate distance between target and source" in {
-      DistanceBetweenTargetandSource(resprojectile) mustEqual 10
+      DistanceBetweenTargetandSource(resprojectile) mustEqual 67.38225f
     }
 
     "calculate distance between target and explosion (splash)" in {
-      DistanceFromExplosionToTarget(resprojectile) mustEqual 64.03124f
+      DistanceFromExplosionToTarget(resprojectile) mustEqual 63.031242f
     }
 
     "calculate no damage from components" in {
diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala
index cfb10565..8d263f31 100644
--- a/pslogin/src/main/scala/WorldSessionActor.scala
+++ b/pslogin/src/main/scala/WorldSessionActor.scala
@@ -131,6 +131,8 @@ class WorldSessionActor extends Actor with MDCContextAware {
   var lfs : Boolean = false
   var squadChannel : Option[String] = None
   var squadSetup : () => Unit = FirstTimeSquadSetup
+  var squadUpdateCounter : Int = 0
+  val queuedSquadActions : Seq[() => Unit] = Seq(SquadUpdates, NoSquadUpdates, NoSquadUpdates, NoSquadUpdates)
 
   var amsSpawnPoints : List[SpawnPoint] = Nil
   var clientKeepAlive : Cancellable = DefaultCancellable.obj
@@ -450,7 +452,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
                 sendResponse(PlanetsideAttributeMessage(playerGuid, 32, ourIndex))
                 //a finalization? what does this do?
                 sendResponse(SquadDefinitionActionMessage(squad.GUID, 0, SquadAction.Unknown(18)))
-                updateSquad = UpdatesWhenEnrolledInSquad
+                updateSquad = PeriodicUpdatesWhenEnrolledInSquad
                 squadChannel = Some(toChannel)
               case _ =>
                 //other player is joining our squad
@@ -494,6 +496,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
                 //a finalization? what does this do?
                 sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(18)))
                 squad_supplement_id = 0
+                squadUpdateCounter = 0
                 updateSquad = NoSquadUpdates
                 squadChannel = None
               case _ =>
@@ -9326,7 +9329,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
 
   def NoSquadUpdates() : Unit = { }
 
-  def UpdatesWhenEnrolledInSquad() : Unit = {
+  def SquadUpdates() : Unit = {
     squadService ! SquadServiceMessage(
       player,
       continent,
@@ -9341,6 +9344,11 @@ class WorldSessionActor extends Actor with MDCContextAware {
     )
   }
 
+  def PeriodicUpdatesWhenEnrolledInSquad() : Unit = {
+    queuedSquadActions(squadUpdateCounter)()
+    squadUpdateCounter = (squadUpdateCounter + 1) % queuedSquadActions.length
+  }
+
   def failWithError(error : String) = {
     log.error(error)
     sendResponse(ConnectionClose())

From 93f2264f613c054d0810aadefcb3391da76ecf3f Mon Sep 17 00:00:00 2001
From: FateJH <Jason_DiDonato@yahoo.com>
Date: Sun, 20 Oct 2019 15:34:01 -0400
Subject: [PATCH 30/31] added enumeration for squad waypoints and changed field
 datatypes where appriopriate; added assertions to squad detail packet;
 removed old logic from squad request and squad response enumerations; WSA lfs
 -> lfsm

---
 .../objects/teamwork/SquadFeatures.scala      |  3 ++-
 .../SquadDetailDefinitionUpdateMessage.scala  |  5 +++-
 .../packet/game/SquadWaypointEvent.scala      | 14 +++++------
 .../packet/game/SquadWaypointRequest.scala    | 12 ++++-----
 .../psforever/types/SquadRequestType.scala    |  7 ------
 .../psforever/types/SquadResponseType.scala   |  7 ------
 .../net/psforever/types/SquadWaypoints.scala  | 18 +++++++++++++
 .../scala/services/teamwork/SquadAction.scala |  4 +--
 .../services/teamwork/SquadResponse.scala     |  6 ++---
 .../services/teamwork/SquadService.scala      | 18 ++++++-------
 .../scala/game/SquadWaypointEventTest.scala   | 20 +++++++--------
 .../src/main/scala/WorldSessionActor.scala    | 25 +++++++++----------
 12 files changed, 71 insertions(+), 68 deletions(-)
 create mode 100644 common/src/main/scala/net/psforever/types/SquadWaypoints.scala

diff --git a/common/src/main/scala/net/psforever/objects/teamwork/SquadFeatures.scala b/common/src/main/scala/net/psforever/objects/teamwork/SquadFeatures.scala
index 29b3ad78..a1562302 100644
--- a/common/src/main/scala/net/psforever/objects/teamwork/SquadFeatures.scala
+++ b/common/src/main/scala/net/psforever/objects/teamwork/SquadFeatures.scala
@@ -3,6 +3,7 @@ package net.psforever.objects.teamwork
 
 import akka.actor.{Actor, ActorContext, ActorRef, Cancellable, Props}
 import net.psforever.objects.DefaultCancellable
+import net.psforever.types.SquadWaypoints
 import services.teamwork.SquadService.WaypointData
 import services.teamwork.SquadSwitchboard
 
@@ -70,7 +71,7 @@ class SquadFeatures(val Squad : Squad) {
 
   def Start(implicit context : ActorContext) : SquadFeatures = {
     switchboard = context.actorOf(Props[SquadSwitchboard], s"squad${Squad.GUID.guid}")
-    waypoints = Array.fill[WaypointData](5)(new WaypointData())
+    waypoints = Array.fill[WaypointData](SquadWaypoints.values.size)(new WaypointData())
     this
   }
 
diff --git a/common/src/main/scala/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala b/common/src/main/scala/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala
index 225489b7..055a031a 100644
--- a/common/src/main/scala/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala
+++ b/common/src/main/scala/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala
@@ -114,7 +114,10 @@ final case class SquadPositionDetail(is_closed : Option[Boolean],
   *              when 255, this indicated the end of enumerated squad position data and the data for that position is absent
   * @param info the squad position field data
   */
-final case class SquadPositionEntry(index : Int, info : Option[SquadPositionDetail])
+final case class SquadPositionEntry(index : Int, info : Option[SquadPositionDetail]) {
+  assert((index > -1 && index < 10) || index == 255, "index value is out of range 0=>n<=9 or n=255")
+  assert(if(index == 255) { info.isEmpty } else { true }, "index=255 indicates end of stream exclusively and field data should be blank")
+}
 
 /**
   * Information regarding a squad's position as a series of common fields.
diff --git a/common/src/main/scala/net/psforever/packet/game/SquadWaypointEvent.scala b/common/src/main/scala/net/psforever/packet/game/SquadWaypointEvent.scala
index 4fe9f649..ff466782 100644
--- a/common/src/main/scala/net/psforever/packet/game/SquadWaypointEvent.scala
+++ b/common/src/main/scala/net/psforever/packet/game/SquadWaypointEvent.scala
@@ -1,8 +1,8 @@
-// Copyright (c) 2017 PSForever
+// Copyright (c) 2019 PSForever
 package net.psforever.packet.game
 
 import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
-import net.psforever.types.Vector3
+import net.psforever.types.{SquadWaypoints, Vector3}
 import scodec.{Attempt, Codec, Err}
 import scodec.codecs._
 import shapeless.{::, HNil}
@@ -14,7 +14,7 @@ final case class WaypointEvent(zone_number : Int,
 final case class SquadWaypointEvent(event_type : WaypointEventAction.Value,
                                     unk : Int,
                                     char_id : Long,
-                                    waypoint_type : Int,
+                                    waypoint_type : SquadWaypoints.Value,
                                     unk5 : Option[Long],
                                     waypoint_info : Option[WaypointEvent])
   extends PlanetSideGamePacket {
@@ -24,13 +24,13 @@ final case class SquadWaypointEvent(event_type : WaypointEventAction.Value,
 }
 
 object SquadWaypointEvent extends Marshallable[SquadWaypointEvent] {
-  def Add(unk : Int, char_id : Long, waypoint_type : Int, waypoint : WaypointEvent) : SquadWaypointEvent =
+  def Add(unk : Int, char_id : Long, waypoint_type : SquadWaypoints.Value, waypoint : WaypointEvent) : SquadWaypointEvent =
     SquadWaypointEvent(WaypointEventAction.Add, unk, char_id, waypoint_type, None, Some(waypoint))
 
-  def Unknown1(unk : Int, char_id : Long, waypoint_type : Int, unk_a : Long) : SquadWaypointEvent =
+  def Unknown1(unk : Int, char_id : Long, waypoint_type : SquadWaypoints.Value, unk_a : Long) : SquadWaypointEvent =
     SquadWaypointEvent(WaypointEventAction.Unknown1, unk, char_id, waypoint_type, Some(unk_a), None)
 
-  def Remove(unk : Int, char_id : Long, waypoint_type : Int) : SquadWaypointEvent =
+  def Remove(unk : Int, char_id : Long, waypoint_type : SquadWaypoints.Value) : SquadWaypointEvent =
     SquadWaypointEvent(WaypointEventAction.Remove, unk, char_id, waypoint_type, None, None)
 
   private val waypoint_codec : Codec[WaypointEvent] = (
@@ -43,7 +43,7 @@ object SquadWaypointEvent extends Marshallable[SquadWaypointEvent] {
     ("event_type" | WaypointEventAction.codec) >>:~ { event_type =>
       ("unk" | uint16L) ::
       ("char_id" | uint32L) ::
-        ("waypoint_type" | uint8L) ::
+        ("waypoint_type" | SquadWaypoints.codec) ::
         ("unk5" | conditional(event_type == WaypointEventAction.Unknown1, uint32L)) ::
         ("waypoint_info" | conditional(event_type == WaypointEventAction.Add, waypoint_codec))
     }
diff --git a/common/src/main/scala/net/psforever/packet/game/SquadWaypointRequest.scala b/common/src/main/scala/net/psforever/packet/game/SquadWaypointRequest.scala
index 5b7ebd57..3384f8b5 100644
--- a/common/src/main/scala/net/psforever/packet/game/SquadWaypointRequest.scala
+++ b/common/src/main/scala/net/psforever/packet/game/SquadWaypointRequest.scala
@@ -2,7 +2,7 @@
 package net.psforever.packet.game
 
 import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
-import net.psforever.types.Vector3
+import net.psforever.types.{SquadWaypoints, Vector3}
 import scodec.{Attempt, Codec, Err}
 import scodec.codecs._
 import shapeless.{::, HNil}
@@ -46,7 +46,7 @@ final case class WaypointInfo(zone_number : Int,
   */
 final case class SquadWaypointRequest(request_type : WaypointEventAction.Value,
                                       char_id : Long,
-                                      waypoint_type : Int,
+                                      waypoint_type : SquadWaypoints.Value,
                                       unk4 : Option[Long],
                                       waypoint_info : Option[WaypointInfo])
   extends PlanetSideGamePacket {
@@ -56,13 +56,13 @@ final case class SquadWaypointRequest(request_type : WaypointEventAction.Value,
 }
 
 object SquadWaypointRequest extends Marshallable[SquadWaypointRequest] {
-  def Add(char_id : Long, waypoint_type : Int, waypoint : WaypointInfo) : SquadWaypointRequest =
+  def Add(char_id : Long, waypoint_type : SquadWaypoints.Value, waypoint : WaypointInfo) : SquadWaypointRequest =
     SquadWaypointRequest(WaypointEventAction.Add, char_id, waypoint_type, None, Some(waypoint))
 
-  def Unknown1(char_id : Long, waypoint_type : Int, unk_a : Long) : SquadWaypointRequest =
+  def Unknown1(char_id : Long, waypoint_type : SquadWaypoints.Value, unk_a : Long) : SquadWaypointRequest =
     SquadWaypointRequest(WaypointEventAction.Unknown1, char_id, waypoint_type, Some(unk_a), None)
 
-  def Remove(char_id : Long, waypoint_type : Int) : SquadWaypointRequest =
+  def Remove(char_id : Long, waypoint_type : SquadWaypoints.Value) : SquadWaypointRequest =
     SquadWaypointRequest(WaypointEventAction.Remove, char_id, waypoint_type, None, None)
 
   private val waypoint_codec : Codec[WaypointInfo] = (
@@ -80,7 +80,7 @@ object SquadWaypointRequest extends Marshallable[SquadWaypointRequest] {
   implicit val codec : Codec[SquadWaypointRequest] = (
     ("request_type" | WaypointEventAction.codec) >>:~ { request_type =>
       ("char_id" | uint32L) ::
-        ("waypoint_type" | uint8L) ::
+        ("waypoint_type" | SquadWaypoints.codec) ::
         ("unk4" | conditional(request_type == WaypointEventAction.Unknown1, uint32L)) ::
         ("waypoint" | conditional(request_type == WaypointEventAction.Add, waypoint_codec))
     }
diff --git a/common/src/main/scala/net/psforever/types/SquadRequestType.scala b/common/src/main/scala/net/psforever/types/SquadRequestType.scala
index a9c35f7a..1947abe7 100644
--- a/common/src/main/scala/net/psforever/types/SquadRequestType.scala
+++ b/common/src/main/scala/net/psforever/types/SquadRequestType.scala
@@ -24,11 +24,4 @@ object SquadRequestType extends Enumeration {
   = Value
 
   implicit val codec = PacketHelpers.createEnumerationCodec(this, uint4L)
-
-  def toResponse(request : SquadRequestType.Value) : SquadResponseType.Value = {
-    val id = request.id
-    if(id < 6) SquadResponseType(id)
-    else if(id > 6) SquadResponseType(id - 1)
-    else throw new NoSuchElementException("request does not have an applicable response")
-  }
 }
diff --git a/common/src/main/scala/net/psforever/types/SquadResponseType.scala b/common/src/main/scala/net/psforever/types/SquadResponseType.scala
index 1f73ccc4..84ab57ad 100644
--- a/common/src/main/scala/net/psforever/types/SquadResponseType.scala
+++ b/common/src/main/scala/net/psforever/types/SquadResponseType.scala
@@ -23,11 +23,4 @@ object SquadResponseType extends Enumeration {
   = Value
 
   implicit val codec = PacketHelpers.createEnumerationCodec(this, uint4L)
-
-  def fromRequest(response : SquadResponseType.Value) : SquadRequestType.Value = {
-    val id = response.id
-    if(id < 6) SquadRequestType(id)
-    else if(id > 5) SquadRequestType(id + 1)
-    else throw new NoSuchElementException("response does not stem from an applicable request")
-  }
 }
diff --git a/common/src/main/scala/net/psforever/types/SquadWaypoints.scala b/common/src/main/scala/net/psforever/types/SquadWaypoints.scala
new file mode 100644
index 00000000..5a11ea95
--- /dev/null
+++ b/common/src/main/scala/net/psforever/types/SquadWaypoints.scala
@@ -0,0 +1,18 @@
+// Copyright (c) 2019 PSForever
+package net.psforever.types
+
+import net.psforever.packet.PacketHelpers
+import scodec.codecs._
+
+object SquadWaypoints extends Enumeration {
+  type Type = Value
+  val
+  One,
+  Two,
+  Three,
+  Four,
+  ExperienceRally
+  = Value
+
+  implicit val codec = PacketHelpers.createEnumerationCodec(this, uint8L)
+}
diff --git a/common/src/main/scala/services/teamwork/SquadAction.scala b/common/src/main/scala/services/teamwork/SquadAction.scala
index a0034da5..0a1b9971 100644
--- a/common/src/main/scala/services/teamwork/SquadAction.scala
+++ b/common/src/main/scala/services/teamwork/SquadAction.scala
@@ -3,7 +3,7 @@ package services.teamwork
 
 import net.psforever.objects.zones.Zone
 import net.psforever.packet.game._
-import net.psforever.types.{PlanetSideEmpire, SquadRequestType, Vector3}
+import net.psforever.types.{PlanetSideEmpire, SquadRequestType, SquadWaypoints, Vector3}
 
 object SquadAction {
   trait Action
@@ -13,6 +13,6 @@ object SquadAction {
 
   final case class Definition(guid : PlanetSideGUID, line : Int, action : SquadAction) extends Action
   final case class Membership(request_type : SquadRequestType.Value, unk2 : Long, unk3 : Option[Long], player_name : String, unk5 : Option[Option[String]]) extends Action
-  final case class Waypoint(event_type : WaypointEventAction.Value, waypoint_type : Int, unk : Option[Long], waypoint_info : Option[WaypointInfo]) extends Action
+  final case class Waypoint(event_type : WaypointEventAction.Value, waypoint_type : SquadWaypoints.Value, unk : Option[Long], waypoint_info : Option[WaypointInfo]) extends Action
   final case class Update(char_id : Long, health : Int, max_health : Int, armor : Int, max_armor : Int, pos : Vector3, zone_number : Int) extends Action
 }
diff --git a/common/src/main/scala/services/teamwork/SquadResponse.scala b/common/src/main/scala/services/teamwork/SquadResponse.scala
index 1b3ae6fb..159f6fe2 100644
--- a/common/src/main/scala/services/teamwork/SquadResponse.scala
+++ b/common/src/main/scala/services/teamwork/SquadResponse.scala
@@ -3,7 +3,7 @@ package services.teamwork
 
 import net.psforever.objects.teamwork.Squad
 import net.psforever.packet.game._
-import net.psforever.types.SquadResponseType
+import net.psforever.types.{SquadResponseType, SquadWaypoints}
 
 object SquadResponse {
   trait Response
@@ -27,8 +27,8 @@ object SquadResponse {
 
   final case class Detail(guid : PlanetSideGUID, squad_detail : SquadDetail) extends Response
 
-  final case class InitWaypoints(char_id : Long, waypoints : Iterable[(Int, WaypointInfo, Int)]) extends Response
-  final case class WaypointEvent(event_type : WaypointEventAction.Value, char_id : Long, waypoint_type : Int, unk5 : Option[Long], waypoint_info : Option[WaypointInfo], unk : Int) extends Response
+  final case class InitWaypoints(char_id : Long, waypoints : Iterable[(SquadWaypoints.Value, WaypointInfo, Int)]) extends Response
+  final case class WaypointEvent(event_type : WaypointEventAction.Value, char_id : Long, waypoint_type : SquadWaypoints.Value, unk5 : Option[Long], waypoint_info : Option[WaypointInfo], unk : Int) extends Response
 
   final case class SquadSearchResults() extends Response
 }
diff --git a/common/src/main/scala/services/teamwork/SquadService.scala b/common/src/main/scala/services/teamwork/SquadService.scala
index 07b5d037..72267a6e 100644
--- a/common/src/main/scala/services/teamwork/SquadService.scala
+++ b/common/src/main/scala/services/teamwork/SquadService.scala
@@ -2544,16 +2544,14 @@ class SquadService extends Actor {
     * @see `SquadWaypointRequest`
     * @see `WaypointInfo`
     * @param guid the squad's unique identifier
-    * @param waypointType the type of the waypoint as an integer;
-    *                     0-4 are squad waypoints;
-    *                     5 is the squad leader's experience waypoint
+    * @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 : Int, info : WaypointInfo) : Option[WaypointData] = {
+  def AddWaypoint(guid : PlanetSideGUID, waypointType : SquadWaypoints.Value, info : WaypointInfo) : Option[WaypointData] = {
     squadFeatures.get(guid) match {
       case Some(features) =>
-        features.Waypoints.lift(waypointType) match {
+        features.Waypoints.lift(waypointType.id) match {
           case Some(point) =>
             point.zone_number = info.zone_number
             point.pos = info.pos
@@ -2576,14 +2574,12 @@ class SquadService extends Actor {
     * 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 as an integer;
-    *                     0-4 are squad waypoints;
-    *                     5 is the squad leader's experience waypoint
+    * @param waypointType the type of the waypoint
     */
-  def RemoveWaypoint(guid : PlanetSideGUID, waypointType : Int) : Unit = {
+  def RemoveWaypoint(guid : PlanetSideGUID, waypointType : SquadWaypoints.Value) : Unit = {
     squadFeatures.get(guid) match {
       case Some(features) =>
-        features.Waypoints.lift(waypointType) match {
+        features.Waypoints.lift(waypointType.id) match {
           case Some(point) =>
             point.pos = Vector3.z(1)
           case _ =>
@@ -2608,7 +2604,7 @@ class SquadService extends Actor {
         Publish(
           toCharId, SquadResponse.InitWaypoints(squad.Leader.CharId,
             list.zipWithIndex.collect { case (point, index) if point.pos != vz1 =>
-              (index, WaypointInfo(point.zone_number, point.pos), 1)
+              (SquadWaypoints(index), WaypointInfo(point.zone_number, point.pos), 1)
             }
           )
         )
diff --git a/common/src/test/scala/game/SquadWaypointEventTest.scala b/common/src/test/scala/game/SquadWaypointEventTest.scala
index 5903a997..199b0bb1 100644
--- a/common/src/test/scala/game/SquadWaypointEventTest.scala
+++ b/common/src/test/scala/game/SquadWaypointEventTest.scala
@@ -3,8 +3,8 @@ package game
 
 import org.specs2.mutable._
 import net.psforever.packet._
-import net.psforever.packet.game.{SquadWaypointEvent, WaypointEventAction, WaypointEvent}
-import net.psforever.types.Vector3
+import net.psforever.packet.game.{SquadWaypointEvent, WaypointEvent, WaypointEventAction}
+import net.psforever.types.{SquadWaypoints, Vector3}
 import scodec.bits._
 
 class SquadWaypointEventTest extends Specification {
@@ -19,7 +19,7 @@ class SquadWaypointEventTest extends Specification {
         unk1 mustEqual WaypointEventAction.Remove
         unk2 mustEqual 11
         unk3 mustEqual 31155863L
-        unk4 mustEqual 0
+        unk4 mustEqual SquadWaypoints.One
         unk5.isEmpty mustEqual true
         unk6.isEmpty mustEqual true
       case _ =>
@@ -33,7 +33,7 @@ class SquadWaypointEventTest extends Specification {
         unk1 mustEqual WaypointEventAction.Remove
         unk2 mustEqual 10
         unk3 mustEqual 0L
-        unk4 mustEqual 4
+        unk4 mustEqual SquadWaypoints.ExperienceRally
         unk5.isEmpty mustEqual true
         unk6.isEmpty mustEqual true
       case _ =>
@@ -47,7 +47,7 @@ class SquadWaypointEventTest extends Specification {
         unk1 mustEqual WaypointEventAction.Add
         unk2 mustEqual 3
         unk3 mustEqual 41581052L
-        unk4 mustEqual 1
+        unk4 mustEqual SquadWaypoints.Two
         unk5.isEmpty mustEqual true
         unk6.contains( WaypointEvent(10, Vector3(3457.9688f, 5514.4688f, 0.0f), 1) ) mustEqual true
       case _ =>
@@ -61,7 +61,7 @@ class SquadWaypointEventTest extends Specification {
         unk1 mustEqual WaypointEventAction.Unknown1
         unk2 mustEqual 3
         unk3 mustEqual 41581052L
-        unk4 mustEqual 1
+        unk4 mustEqual SquadWaypoints.Two
         unk5.contains( 4L ) mustEqual true
         unk6.isEmpty mustEqual true
       case _ =>
@@ -70,28 +70,28 @@ class SquadWaypointEventTest extends Specification {
   }
 
   "encode (1)" in {
-    val msg = SquadWaypointEvent.Remove(11, 31155863L, 0)
+    val msg = SquadWaypointEvent.Remove(11, 31155863L, SquadWaypoints.One)
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_1
   }
 
   "encode (2)" in {
-    val msg = SquadWaypointEvent.Remove(10, 0L, 4)
+    val msg = SquadWaypointEvent.Remove(10, 0L, SquadWaypoints.ExperienceRally)
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_2
   }
 
   "encode (3)" in {
-    val msg = SquadWaypointEvent.Add(3, 41581052L, 1, WaypointEvent(10, Vector3(3457.9688f, 5514.4688f, 0.0f), 1))
+    val msg = SquadWaypointEvent.Add(3, 41581052L, SquadWaypoints.Two, WaypointEvent(10, Vector3(3457.9688f, 5514.4688f, 0.0f), 1))
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_3
   }
 
   "encode (4)" in {
-    val msg = SquadWaypointEvent.Unknown1(3, 41581052L, 1, 4L)
+    val msg = SquadWaypointEvent.Unknown1(3, 41581052L, SquadWaypoints.Two, 4L)
     val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
 
     pkt mustEqual string_4
diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala
index 8d263f31..e48f1c4d 100644
--- a/pslogin/src/main/scala/WorldSessionActor.scala
+++ b/pslogin/src/main/scala/WorldSessionActor.scala
@@ -121,14 +121,14 @@ class WorldSessionActor extends Actor with MDCContextAware {
   val squadUI : LongMap[SquadUIElement] = new LongMap[SquadUIElement]()
   var squad_supplement_id : Int = 0
   /**
-    * `AvatarConverter` can only rely on the `Avatar`-local Looking For Squad variable.
-    * When joining or creating a squad, the original state of the avatar's local LFS variable is blanked.
+    * When joining or creating a squad, the original state of the avatar's internal LFS variable is blanked.
     * This `WSA`-local variable is then used to indicate the ongoing state of the LFS UI component,
     * now called "Looking for Squad Member."
+    * Only the squad leader may toggle the LFSM marquee.
     * Upon leaving or disbanding a squad, this value is made false.
-    * Control switching between the `Avatar`-local and the `WSA`-local variable is contingent on `squadUI` being populated.
+    * Control switching between the `Avatar`-local and the `WorldSessionActor`-local variable is contingent on `squadUI` being populated.
     */
-  var lfs : Boolean = false
+  var lfsm : Boolean = false
   var squadChannel : Option[String] = None
   var squadSetup : () => Unit = FirstTimeSquadSetup
   var squadUpdateCounter : Int = 0
@@ -492,7 +492,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
                 avatarService ! AvatarServiceMessage(s"${continent.Id}/${player.Faction}", AvatarAction.PlanetsideAttribute(playerGuid, 31, 0))
                 sendResponse(PlanetsideAttributeMessage(playerGuid, 32, 0)) //disassociate with member position in squad?
                 sendResponse(PlanetsideAttributeMessage(playerGuid, 34, 4294967295L)) //unknown, perhaps unrelated?
-                lfs = false
+                lfsm = false
                 //a finalization? what does this do?
                 sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(18)))
                 squad_supplement_id = 0
@@ -523,11 +523,11 @@ class WorldSessionActor extends Actor with MDCContextAware {
             //are we being demoted?
             if(squadUI(charId).index == 0) {
               //lfsm -> lfs
-              if(lfs) {
+              if(lfsm) {
                 sendResponse(PlanetsideAttributeMessage(guid, 53, 0))
                 avatarService ! AvatarServiceMessage(factionOnContinentChannel, AvatarAction.PlanetsideAttribute(guid, 53, 0))
               }
-              lfs = false
+              lfsm = false
               sendResponse(PlanetsideAttributeMessage(guid, 32, from_index)) //associate with member position in squad
             }
             //are we being promoted?
@@ -3055,7 +3055,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
     deadState = DeadState.Alive
     sendResponse(AvatarDeadStateMessage(DeadState.Alive, 0, 0, tplayer.Position, player.Faction, true))
     //looking for squad (members)
-    if(tplayer.LFS || lfs) {
+    if(tplayer.LFS || lfsm) {
       sendResponse(PlanetsideAttributeMessage(guid, 53, 1))
       avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(guid, 53, 1))
     }
@@ -3692,7 +3692,6 @@ class WorldSessionActor extends Actor with MDCContextAware {
               }
               vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.VehicleState(player.GUID, vehicle_guid, unk1, pos, ang, vel, flight, unk6, unk7, wheels, unk9, unkA))
             }
-            //vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.VehicleState(player.GUID, vehicle_guid, unk1, pos, ang, vel, flight, unk6, unk7, wheels, unk9, unkA))
             updateSquad()
           case (None, _) =>
             //log.error(s"VehicleState: no vehicle $vehicle_guid found in zone")
@@ -4797,8 +4796,8 @@ class WorldSessionActor extends Actor with MDCContextAware {
       }
       else if(action == 36) { //Looking For Squad ON
         if(squadUI.nonEmpty) {
-          if(!lfs && squadUI(player.CharId).index == 0) {
-            lfs = true
+          if(!lfsm && squadUI(player.CharId).index == 0) {
+            lfsm = true
             avatarService ! AvatarServiceMessage(s"${continent.Id}/${player.Faction}", AvatarAction.PlanetsideAttribute(player.GUID, 53, 1))
           }
         }
@@ -4809,8 +4808,8 @@ class WorldSessionActor extends Actor with MDCContextAware {
       }
       else if(action == 37) { //Looking For Squad OFF
         if(squadUI.nonEmpty) {
-          if(lfs && squadUI(player.CharId).index == 0) {
-            lfs = false
+          if(lfsm && squadUI(player.CharId).index == 0) {
+            lfsm = false
             avatarService ! AvatarServiceMessage(s"${continent.Id}/${player.Faction}", AvatarAction.PlanetsideAttribute(player.GUID, 53, 0))
           }
         }

From 5916fa68be43bee7834d4acafe03b2ce5e5c773f Mon Sep 17 00:00:00 2001
From: FateJH <Jason_DiDonato@yahoo.com>
Date: Mon, 21 Oct 2019 19:41:29 -0400
Subject: [PATCH 31/31] SquadAction and SquadResponse were worked into
 SquadServiceMessage and SquadServiceResponse, respectively, so that traits
 could be sealed; the continuous join request prompt has been exorcised from
 squad operations; switching to auto-approve for squads will now resolve all
 pending join requests; enumerated shared cases in SDAM

---
 .../objects/teamwork/SquadFeatures.scala      | 19 ++--------
 .../game/SquadDefinitionActionMessage.scala   | 19 ++++++++--
 .../SquadDetailDefinitionUpdateMessage.scala  | 38 -------------------
 .../scala/services/teamwork/SquadAction.scala | 18 ---------
 .../services/teamwork/SquadResponse.scala     | 34 -----------------
 .../services/teamwork/SquadService.scala      | 33 +++++++++++-----
 .../teamwork/SquadServiceMessage.scala        | 14 +++++++
 .../teamwork/SquadServiceResponse.scala       | 31 +++++++++++++++
 .../src/main/scala/WorldSessionActor.scala    |  5 ++-
 9 files changed, 91 insertions(+), 120 deletions(-)
 delete mode 100644 common/src/main/scala/services/teamwork/SquadAction.scala
 delete mode 100644 common/src/main/scala/services/teamwork/SquadResponse.scala

diff --git a/common/src/main/scala/net/psforever/objects/teamwork/SquadFeatures.scala b/common/src/main/scala/net/psforever/objects/teamwork/SquadFeatures.scala
index a1562302..2e1a5f05 100644
--- a/common/src/main/scala/net/psforever/objects/teamwork/SquadFeatures.scala
+++ b/common/src/main/scala/net/psforever/objects/teamwork/SquadFeatures.scala
@@ -1,8 +1,7 @@
 // Copyright (c) 2019 PSForever
 package net.psforever.objects.teamwork
 
-import akka.actor.{Actor, ActorContext, ActorRef, Cancellable, Props}
-import net.psforever.objects.DefaultCancellable
+import akka.actor.{Actor, ActorContext, ActorRef, Props}
 import net.psforever.types.SquadWaypoints
 import services.teamwork.SquadService.WaypointData
 import services.teamwork.SquadSwitchboard
@@ -11,8 +10,8 @@ class SquadFeatures(val Squad : Squad) {
   /**
     * `initialAssociation` per squad is similar to "Does this squad want to recruit members?"
     * The squad does not have to be flagged.
-    * Dispatches an `AssociateWithSquad` `SDAM` to the squad leader and ???
-    * and then a `SDDUM` that includes at least the squad owner name and char id.
+    * Dispatches an `AssociateWithSquad` `SquadDefinitionActionMessage` packet to the squad leader and ???
+    * and then a `SquadDetailDefinitionUpdateMessage` that includes at least the squad owner name and char id.
     * Dispatched only once when a squad is first listed
     * or when the squad leader searches for recruits by proximity or for certain roles or by invite
     * or when a spontaneous squad forms,
@@ -52,8 +51,6 @@ class SquadFeatures(val Squad : Squad) {
     * Handle persistent data related to `ProximityInvite` and `LookingForSquadRoleInvite` messages
     */
   private var proxyInvites : List[Long] = Nil
-
-  private var requestInvitePrompt : Cancellable = DefaultCancellable.obj
   /**
     * These useres rejected invitation to this squad.
     * For the purposes of wide-searches for membership
@@ -79,7 +76,6 @@ class SquadFeatures(val Squad : Squad) {
     switchboard ! akka.actor.PoisonPill
     switchboard = Actor.noSender
     waypoints = Array.empty
-    requestInvitePrompt.cancel
     this
   }
 
@@ -143,13 +139,4 @@ class SquadFeatures(val Squad : Squad) {
   }
 
   def ToChannel : String = channel
-
-  def Prompt : Cancellable = requestInvitePrompt
-
-  def Prompt_=(callback: Cancellable) : Cancellable = {
-    if(requestInvitePrompt.isCancelled) {
-      requestInvitePrompt = callback
-    }
-    Prompt
-  }
 }
diff --git a/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala b/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala
index a0a02c7d..ccdc41b5 100644
--- a/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala
+++ b/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala
@@ -426,16 +426,23 @@ object SquadDefinitionActionMessage extends Marshallable[SquadDefinitionActionMe
     ((code : @switch) match {
       case 0 => displaySquadCodec
       case 1 => squadMemberInitializationIssueCodec
+      case 2 => unknownCodec(action = 2)
       case 3 => saveSquadFavoriteCodec
       case 4 => loadSquadFavoriteCodec
       case 5 => deleteSquadFavoriteCodec
+      case 6 => unknownCodec(action = 6)
       case 7 => listSquadFavoriteCodec
       case 8 => requestListSquadCodec
       case 9 => stopListSquadCodec
       case 10 => selectRoleForYourselfCodec
+      case 11 => unknownCodec(action = 11)
+      case 12 => unknownCodec(action = 12)
+      case 13 => unknownCodec(action = 13)
+      case 14 => unknownCodec(action = 14)
       case 15 => cancelSelectRoleForYourselfCodec
       case 16 => associateWithSquadCodec
       case 17 => setListSquadCodec
+      case 18 => unknownCodec(action = 18)
       case 19 => changeSquadPurposeCodec
       case 20 => changeSquadZoneCodec
       case 21 => closeSquadMemberPositionCodec
@@ -444,17 +451,23 @@ object SquadDefinitionActionMessage extends Marshallable[SquadDefinitionActionMe
       case 24 => changeSquadMemberRequirementsDetailedOrdersCodec
       case 25 => changeSquadMemberRequirementsCertificationsCodec
       case 26 => resetAllCodec
+      //case 27 => ?
       case 28 => autoApproveInvitationRequestsCodec
+      case 29 => unknownCodec(action = 29)
+      case 30 => unknownCodec(action = 30)
       case 31 => locationFollowsSquadLeadCodec
+      case 32 => unknownCodec(action = 32)
+      case 33 => unknownCodec(action = 33)
       case 34 => searchForSquadsWithParticularRoleCodec
       case 35 => cancelSquadSearchCodec
+      case 36 => unknownCodec(action = 36)
+      case 37 => unknownCodec(action = 37)
       case 38 => assignSquadMemberToRoleCodec
       case 39 => noSquadSearchResultsCodec
       case 40 => findLfsSoldiersForRoleCodec
       case 41 => cancelFindCodec
-      case  2 |  6 | 11 | 12 | 13 |
-           14 | 18 | 29 | 30 | 32 |
-           33 | 36 | 37 | 42 | 43 => unknownCodec(code)
+      case 42 => unknownCodec(action = 42)
+      case 43 => unknownCodec(action = 43)
       case _ => failureCodec(code)
     }).asInstanceOf[Codec[SquadAction]]
   }
diff --git a/common/src/main/scala/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala b/common/src/main/scala/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala
index 055a031a..7c60d048 100644
--- a/common/src/main/scala/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala
+++ b/common/src/main/scala/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala
@@ -657,44 +657,6 @@ object SquadDetailDefinitionUpdateMessage extends Marshallable[SquadDetailDefini
         }
       )
     }
-    //TODO while this pattern looks elegant, bitsOverByte does not accumulate properly with the either(bool, L, R); why?
-//    private def membersCodec(bitsOverByte : StreamLengthToken) : Codec[SquadDetail] = {
-//      import shapeless.::
-//      either(bool,
-//        { //false
-//          bitsOverByte.Add(3)
-//          uint2 :: FullyPopulatedPositions.codec(bitsOverByte)
-//        },
-//        { //true
-//          bitsOverByte.Add(4)
-//          uint(3) :: vector(ItemizedPositions.codec(bitsOverByte))
-//        }
-//      ).exmap[SquadDetail] (
-//        {
-//          case Left(_ :: member_list :: HNil) =>
-//            Attempt.successful(SquadDetail(None, None, None, None, None, None, None, None, Some(ignoreTerminatingEntry(member_list.toList))))
-//          case Right(_ :: member_list :: HNil) =>
-//            Attempt.successful(SquadDetail(None, None, None, None, None, None, None, None, Some(ignoreTerminatingEntry(member_list.toList))))
-//        },
-//        {
-//          case SquadDetail(_, _, _, _, _, _, _, _, Some(member_list)) =>
-//            if(member_list
-//              .collect { case position if position.info.nonEmpty =>
-//                val info = position.info.get
-//                List(info.is_closed, info.role, info.detailed_orders, info.requirements, info.char_id, info.name)
-//              }
-//              .flatten
-//              .count(_.isEmpty) == 0) {
-//              Attempt.successful(Left(2 :: ensureTerminatingEntry(member_list).toVector :: HNil))
-//            }
-//            else {
-//              Attempt.successful(Right(4 :: ensureTerminatingEntry(member_list).toVector :: HNil))
-//            }
-//          case _ =>
-//            Attempt.failure(Err("failed to encode squad data for members"))
-//        }
-//      )
-//    }
     /**
       * A failing pattern for when the coded value is not tied to a known field pattern.
       * This pattern does not read or write any bit data.
diff --git a/common/src/main/scala/services/teamwork/SquadAction.scala b/common/src/main/scala/services/teamwork/SquadAction.scala
deleted file mode 100644
index 0a1b9971..00000000
--- a/common/src/main/scala/services/teamwork/SquadAction.scala
+++ /dev/null
@@ -1,18 +0,0 @@
-// Copyright (c) 2019 PSForever
-package services.teamwork
-
-import net.psforever.objects.zones.Zone
-import net.psforever.packet.game._
-import net.psforever.types.{PlanetSideEmpire, SquadRequestType, SquadWaypoints, Vector3}
-
-object SquadAction {
-  trait Action
-
-  final case class InitSquadList() extends Action
-  final case class InitCharId() extends Action
-
-  final case class Definition(guid : PlanetSideGUID, line : Int, action : SquadAction) extends Action
-  final case class Membership(request_type : SquadRequestType.Value, unk2 : Long, unk3 : Option[Long], player_name : String, unk5 : Option[Option[String]]) extends Action
-  final case class Waypoint(event_type : WaypointEventAction.Value, waypoint_type : SquadWaypoints.Value, unk : Option[Long], waypoint_info : Option[WaypointInfo]) extends Action
-  final case class Update(char_id : Long, health : Int, max_health : Int, armor : Int, max_armor : Int, pos : Vector3, zone_number : Int) extends Action
-}
diff --git a/common/src/main/scala/services/teamwork/SquadResponse.scala b/common/src/main/scala/services/teamwork/SquadResponse.scala
deleted file mode 100644
index 159f6fe2..00000000
--- a/common/src/main/scala/services/teamwork/SquadResponse.scala
+++ /dev/null
@@ -1,34 +0,0 @@
-// Copyright (c) 2019 PSForever
-package services.teamwork
-
-import net.psforever.objects.teamwork.Squad
-import net.psforever.packet.game._
-import net.psforever.types.{SquadResponseType, SquadWaypoints}
-
-object SquadResponse {
-  trait Response
-
-  final case class ListSquadFavorite(line : Int, task : String) extends Response
-
-  final case class InitList(info : Vector[SquadInfo]) extends Response
-  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 SetListSquad(squad_guid : PlanetSideGUID) extends Response
-
-  final case class Membership(request_type : SquadResponseType.Value, unk1 : Int, unk2 : Int, unk3 : Long, unk4 : Option[Long], player_name : String, unk5 : Boolean, unk6 : Option[Option[String]]) extends Response //see SquadMembershipResponse
-  final case class 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 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 Detail(guid : PlanetSideGUID, squad_detail : SquadDetail) extends Response
-
-  final case class InitWaypoints(char_id : Long, waypoints : Iterable[(SquadWaypoints.Value, WaypointInfo, Int)]) extends Response
-  final case class WaypointEvent(event_type : WaypointEventAction.Value, char_id : Long, waypoint_type : SquadWaypoints.Value, unk5 : Option[Long], waypoint_info : Option[WaypointInfo], unk : Int) extends Response
-
-  final case class SquadSearchResults() extends Response
-}
diff --git a/common/src/main/scala/services/teamwork/SquadService.scala b/common/src/main/scala/services/teamwork/SquadService.scala
index 72267a6e..ad717e59 100644
--- a/common/src/main/scala/services/teamwork/SquadService.scala
+++ b/common/src/main/scala/services/teamwork/SquadService.scala
@@ -612,7 +612,6 @@ class SquadService extends Actor {
             //player requested to join a squad's specific position
             //invitedPlayer is actually the squad leader; petitioner is the actual "invitedPlayer"
             val features = squadFeatures(guid)
-            features.Prompt.cancel
             JoinSquad(petitioner, features.Squad, position)
             RemoveInvitesForSquadAndPosition(guid, position)
 
@@ -832,7 +831,6 @@ class SquadService extends Actor {
             //rejectingPlayer is the squad leader; candidate is the would-be squad member who was rejected
             val features = squadFeatures(guid)
             features.Refuse = rejectingPlayer
-            features.Prompt.cancel
             (Some(rejectingPlayer), None)
 
           case _ => ;
@@ -902,8 +900,6 @@ class SquadService extends Actor {
             val (member, index) = membership.zipWithIndex.find { case (_member, _) => _member.CharId == promotedPlayer }.get
             val features = squadFeatures(squad.GUID)
             SwapMemberPosition(leader, member)
-            //cancel previous leader invite prompt, if any
-            features.Prompt.cancel
             //move around invites so that the proper squad leader deals with them
             val leaderInvite = invites.remove(promotingPlayer)
             val leaderQueuedInvites = queuedInvites.remove(promotingPlayer).toList.flatten
@@ -1108,6 +1104,28 @@ class SquadService extends Actor {
           case AutoApproveInvitationRequests(state) =>
             val features = squadFeatures(lSquadOpt.getOrElse(StartSquad(tplayer)).GUID)
             features.AutoApproveInvitationRequests = state
+            if(state) {
+              //allowed auto-approval - resolve the requests (only)
+              val charId = tplayer.CharId
+              val (requests, others) = (invites.get(charId).toList ++ queuedInvites.get(charId).toList)
+                .partition({ case _ : RequestRole => true})
+              invites.remove(charId)
+              queuedInvites.remove(charId)
+              previousInvites.remove(charId)
+              requests.foreach {
+                case request : RequestRole =>
+                  JoinSquad(request.player, features.Squad, request.position)
+                case _ => ;
+              }
+              others.collect { case invite : Invitation => invite } match {
+                case Nil => ;
+                case x :: Nil =>
+                  AddInviteAndRespond(charId, x, x.InviterCharId, x.InviterName)
+                case x :: xs =>
+                  AddInviteAndRespond(charId, x, x.InviterCharId, x.InviterName)
+                  queuedInvites += charId -> xs
+              }
+            }
 
           case FindLfsSoldiersForRole(position) =>
             lSquadOpt match {
@@ -1439,9 +1457,6 @@ class SquadService extends Actor {
         debug(s"Unhandled message $msg from $sender")
     }
 
-    case data @ SquadResponse.WantsSquadPosition(leader_char_id, _) =>
-      Publish(leader_char_id, data)
-
     case msg =>
       debug(s"Unhandled message $msg from $sender")
   }
@@ -2145,9 +2160,7 @@ class SquadService extends Actor {
           self ! SquadServiceMessage(player, Zone.Nowhere, SquadAction.Membership(SquadRequestType.Accept, leaderCharId, None, "", None))
         }
         else {
-          import scala.concurrent.duration._
-          import scala.concurrent.ExecutionContext.Implicits.global
-          features.Prompt = context.system.scheduler.schedule(1 milliseconds, 45000 milliseconds, self, SquadResponse.WantsSquadPosition(leaderCharId, player.Name))
+          Publish(leaderCharId, SquadResponse.WantsSquadPosition(leaderCharId, player.Name))
         }
         true
       case _ =>
diff --git a/common/src/main/scala/services/teamwork/SquadServiceMessage.scala b/common/src/main/scala/services/teamwork/SquadServiceMessage.scala
index c1826303..fd856ae0 100644
--- a/common/src/main/scala/services/teamwork/SquadServiceMessage.scala
+++ b/common/src/main/scala/services/teamwork/SquadServiceMessage.scala
@@ -3,9 +3,23 @@ package services.teamwork
 
 import net.psforever.objects.Player
 import net.psforever.objects.zones.Zone
+import net.psforever.packet.game.{PlanetSideGUID, SquadAction => PacketSquadAction, WaypointEventAction, WaypointInfo}
+import net.psforever.types.{SquadRequestType, SquadWaypoints, Vector3}
 
 final case class SquadServiceMessage(tplayer : Player, zone : Zone, actionMessage : Any)
 
 object SquadServiceMessage {
   final case class RecoverSquadMembership()
 }
+
+object SquadAction {
+  sealed trait Action
+
+  final case class InitSquadList() extends Action
+  final case class InitCharId() extends Action
+
+  final case class Definition(guid : PlanetSideGUID, line : Int, action : PacketSquadAction) extends Action
+  final case class Membership(request_type : SquadRequestType.Value, unk2 : Long, unk3 : Option[Long], player_name : String, unk5 : Option[Option[String]]) extends Action
+  final case class Waypoint(event_type : WaypointEventAction.Value, waypoint_type : SquadWaypoints.Value, unk : Option[Long], waypoint_info : Option[WaypointInfo]) extends Action
+  final case class Update(char_id : Long, health : Int, max_health : Int, armor : Int, max_armor : Int, pos : Vector3, zone_number : Int) extends Action
+}
diff --git a/common/src/main/scala/services/teamwork/SquadServiceResponse.scala b/common/src/main/scala/services/teamwork/SquadServiceResponse.scala
index 47cf0387..82fd1339 100644
--- a/common/src/main/scala/services/teamwork/SquadServiceResponse.scala
+++ b/common/src/main/scala/services/teamwork/SquadServiceResponse.scala
@@ -1,6 +1,9 @@
 // Copyright (c) 2019 PSForever
 package services.teamwork
 
+import net.psforever.objects.teamwork.Squad
+import net.psforever.packet.game._
+import net.psforever.types.{SquadResponseType, SquadWaypoints}
 import services.GenericEventBusMsg
 
 final case class SquadServiceResponse(toChannel : String, exclude : Iterable[Long], response : SquadResponse.Response) extends GenericEventBusMsg
@@ -12,3 +15,31 @@ object SquadServiceResponse {
   def apply(toChannel : String, exclude : Long, response : SquadResponse.Response) : SquadServiceResponse =
     SquadServiceResponse(toChannel, Seq(exclude), response)
 }
+
+object SquadResponse {
+  sealed trait Response
+
+  final case class ListSquadFavorite(line : Int, task : String) extends Response
+
+  final case class InitList(info : Vector[SquadInfo]) extends Response
+  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 SetListSquad(squad_guid : PlanetSideGUID) extends Response
+
+  final case class Membership(request_type : SquadResponseType.Value, unk1 : Int, unk2 : Int, unk3 : Long, unk4 : Option[Long], player_name : String, unk5 : Boolean, unk6 : Option[Option[String]]) extends Response //see SquadMembershipResponse
+  final case class 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 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 Detail(guid : PlanetSideGUID, squad_detail : SquadDetail) extends Response
+
+  final case class InitWaypoints(char_id : Long, waypoints : Iterable[(SquadWaypoints.Value, WaypointInfo, Int)]) extends Response
+  final case class WaypointEvent(event_type : WaypointEventAction.Value, char_id : Long, waypoint_type : SquadWaypoints.Value, unk5 : Option[Long], waypoint_info : Option[WaypointInfo], unk : Int) extends Response
+
+  final case class SquadSearchResults() extends Response
+}
diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala
index e48f1c4d..4c1e6fc9 100644
--- a/pslogin/src/main/scala/WorldSessionActor.scala
+++ b/pslogin/src/main/scala/WorldSessionActor.scala
@@ -539,7 +539,6 @@ class WorldSessionActor extends Actor with MDCContextAware {
             SwapSquadUIElements(squad, from_index, to_index)
 
           case SquadResponse.UpdateMembers(squad, positions) =>
-            import services.teamwork.SquadAction.{Update => SAUpdate}
             val pairedEntries = positions.collect {
               case entry if squadUI.contains(entry.char_id) =>
                 (entry, squadUI(entry.char_id))
@@ -3220,6 +3219,10 @@ class WorldSessionActor extends Actor with MDCContextAware {
       val clientVersion = s"Client Version: $majorVersion.$minorVersion.$revision, $buildDate"
       log.info(s"New world login to $server with Token:$token. $clientVersion")
       //TODO begin temp player character auto-loading; remove later
+      //this is all just temporary character creation used in the dev branch, making explicit values that allow for testing
+      //the unique character identifier number for this testing character is based on the original test character,
+      //whose identifier number was 41605314
+      //all head features, faction, and sex also match that test character
       import net.psforever.objects.GlobalDefinitions._
       import net.psforever.types.CertificationType._
       val faction = PlanetSideEmpire.VS
