diff --git a/common/src/main/scala/net/psforever/objects/Avatar.scala b/common/src/main/scala/net/psforever/objects/Avatar.scala
index 1307ee58d..4b6891874 100644
--- a/common/src/main/scala/net/psforever/objects/Avatar.scala
+++ b/common/src/main/scala/net/psforever/objects/Avatar.scala
@@ -1,17 +1,17 @@
// 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._
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) {
+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 */
@@ -29,11 +29,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
- * 0-9 are Infantry loadouts
+ /** Equipment Loadouts
+ * 0-9 are Infantry loadouts
* 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 = {
@@ -42,6 +46,14 @@ class Avatar(val name : String, val faction : PlanetSideEmpire.Value, val sex :
}
private val deployables : DeployableToolbox = new DeployableToolbox
+ /**
+ * Looking For 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
+
+ def CharId : Long = char_id
def BEP : Long = bep
@@ -164,25 +176,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
@@ -194,6 +190,13 @@ class Avatar(val name : String, val faction : PlanetSideEmpire.Value, val sex :
def Deployables : DeployableToolbox = deployables
+ def LFS : Boolean = lfs
+
+ def LFS_=(looking : Boolean) : Boolean = {
+ lfs = looking
+ LFS
+ }
+
def Definition : AvatarDefinition = GlobalDefinitions.avatar
/*
@@ -228,7 +231,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/objects/Player.scala b/common/src/main/scala/net/psforever/objects/Player.scala
index a3b5e7f2b..250c81b2e 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
@@ -68,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
@@ -294,7 +298,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 000000000..44a7ab771
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/avatar/LoadoutManager.scala
@@ -0,0 +1,29 @@
+// 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) if entries.length > line =>
+ entries(line) = Some(loadout)
+ case _ => ;
+ }
+ }
+
+ def LoadLoadout(line : Int) : Option[Loadout] = entries.lift(line).flatten
+
+ def DeleteLoadout(line : Int) : Unit = {
+ 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/net/psforever/objects/definition/converter/AvatarConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala
index 163d698cd..4b7a217f2 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,
@@ -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/objects/loadouts/EquipmentLoadout.scala b/common/src/main/scala/net/psforever/objects/loadouts/EquipmentLoadout.scala
new file mode 100644
index 000000000..53925f90c
--- /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`.
+ * 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]`.
+ *
+ * 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 0f9986e29..18c2f57fe 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 12e0cf410..64668c4e7 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`.
- * 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]`.
- *
- * 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, s.Task))
+ 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,31 @@ 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,
+ 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 000000000..029c58b7f
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/loadouts/SquadLoadout.scala
@@ -0,0 +1,10 @@
+// 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(task : String,
+ zone_id : Option[Int],
+ members : List[SquadPositionLoadout]) extends Loadout(task)
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 5f7e0b63a..c4199e0ce 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 4784d8de3..9c4e34d7c 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
new file mode 100644
index 000000000..a2406a515
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/teamwork/Member.scala
@@ -0,0 +1,89 @@
+// 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 orders : String = ""
+ 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
+ private var position : Vector3 = Vector3.Zero
+
+ def Role : String = role
+
+ def Role_=(title : String) : String = {
+ role = title
+ Role
+ }
+
+ def Orders : String = orders
+
+ def Orders_=(text : String) : String = {
+ orders = text
+ Orders
+ }
+
+ def Requirements : Set[CertificationType.Value] = requirements
+
+ def Requirements_=(req : Set[CertificationType.Value]) : Set[CertificationType.Value] = {
+ requirements = req
+ Requirements
+ }
+
+ def Name : String = name
+
+ def Name_=(moniker : String) : String = {
+ name = moniker
+ Name
+ }
+
+ def CharId : Long = charId
+
+ def CharId_=(id : Long) : Long = {
+ charId = id
+ CharId
+ }
+
+ 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 : Int = zoneId
+
+ def ZoneId_=(id : Int) : Int = {
+ zoneId = id
+ ZoneId
+ }
+
+ def Position : Vector3 = position
+
+ def Position_=(pos : Vector3) : Vector3 = {
+ position = pos
+ Position
+ }
+
+ 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
new file mode 100644
index 000000000..0ecd2e20a
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/teamwork/Squad.scala
@@ -0,0 +1,85 @@
+// 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.{CertificationType, PlanetSideEmpire}
+
+class Squad(squadId : PlanetSideGUID, alignment : PlanetSideEmpire.Value) extends IdentifiableEntity {
+ super.GUID_=(squadId)
+ private val faction : PlanetSideEmpire.Value = alignment //does not change
+ 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)(elem = true)
+
+ override def GUID_=(d : PlanetSideGUID) : PlanetSideGUID = GUID
+
+ def Faction : PlanetSideEmpire.Value = faction
+
+ def CustomZoneId : Boolean = zoneId.isDefined
+
+ def ZoneId : Int = zoneId.getOrElse(membership(0).ZoneId)
+
+ 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 = {
+ task = assignment
+ Task
+ }
+
+ def Membership : Array[Member] = membership
+
+ def Availability : Array[Boolean] = availability
+
+ def Leader : Member = {
+ membership(0) match {
+ case member if !member.Name.equals("") =>
+ member
+ case _ =>
+ throw new Exception("can not find squad leader!")
+ }
+ }
+
+ 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 {
+ 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 Membership : Array[Member] = Array.empty[Member]
+ override def Availability : Array[Boolean] = Array.fill[Boolean](10)(false)
+ }
+}
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 000000000..2e1a5f056
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/teamwork/SquadFeatures.scala
@@ -0,0 +1,142 @@
+// Copyright (c) 2019 PSForever
+package net.psforever.objects.teamwork
+
+import akka.actor.{Actor, ActorContext, ActorRef, Props}
+import net.psforever.types.SquadWaypoints
+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` `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,
+ * 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
+ /**
+ * 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.
+ *
+ * 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
+ /**
+ * 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](SquadWaypoints.values.size)(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
+ }
+
+ 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
+}
diff --git a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
index b26a9834c..2d10e1529 100644
--- a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
+++ b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
@@ -446,18 +446,18 @@ object GamePacketOpcode extends Enumeration {
case 0x6b => game.TriggerSoundMessage.decode
case 0x6c => game.LootItemMessage.decode
case 0x6d => game.VehicleSubStateMessage.decode
- case 0x6e => noDecoder(SquadMembershipRequest)
- case 0x6f => noDecoder(SquadMembershipResponse)
+ case 0x6e => game.SquadMembershipRequest.decode
+ 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)
@@ -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
@@ -592,11 +592,11 @@ 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)
- 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 000000000..3b9710a3b
--- /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/PlanetsideAttributeMessage.scala b/common/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala
index eec5c3124..4c33eab11 100644
--- a/common/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala
+++ b/common/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala
@@ -118,8 +118,12 @@ import scodec.codecs._
* `27 - PA_JAMMED - plays jammed buzzing sound`
* `28 - PA_IMPLANT_ACTIVE - Plays implant sounds. Valid values seem to be up to 20.`
* `29 - PA_VAPORIZED - Visible ?! That's not the cloaked effect, Maybe for spectator mode ?. Value is 0 to visible, 1 to invisible.`
- * `31 - Info under avatar name : 0 = LFS, 1 = Looking For Squad Members`
- * `32 - Info under avatar name : 0 = Looking For Squad Members, 1 = LFS`
+ * `31 - Looking for Squad info (marquee and ui):
+ * ` - 0 is LFS`
+ * ` - 1 is LFSM (Looking for Squad Members)`
+ * ` - n is the supplemental squad identifier number; same as "LFS;" for the leader, sets "LFSM" after the first manual flagging`
+ * `32 - Maintain the squad role index, when a member of a squad;
+ * - OLD: "Info under avatar name : 0 = Looking For Squad Members, 1 = LFS`"
* `35 - BR. Value is the BR`
* `36 - CR. Value is the CR`
* `43 - Info on avatar name : 0 = Nothing, 1 = "(LD)" message`
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 0f4aafc30..c34a057ce 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,98 @@ 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)
+ )
+ }
+
+ //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)
+}
/**
- * Define three fields determining the purpose of data in this listing.
- *
- * The third field `unk3` is not always be defined and will be supplanted by the squad (definition) GUID during initialization and a full update.
- *
- * Actions:
- * `unk1 unk2 unk3`
- * `0 true 4 -- `Remove a squad from listing
- * `128 true 0 -- `Update a squad's leader
- * `128 true 1 -- `Update a squad's task or continent
- * `128 true 2 -- `Update a squad's size
- * `129 false 0 -- `Update a squad's leader or size
- * `129 false 1 -- `Update a squad's task and continent
- * `131 false 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.
- *
- * 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.
+ * Display the list of squads available to a given player.
*
- * 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.
+ * Squad list entries are typically referenced by their line index.
*
- * 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.
- *
- * 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.
@@ -106,9 +114,10 @@ final case class SquadListing(index : Int = 255,
* `5 6 `Clear squad list and initialize new squad list
* `5 6 `Clear squad list (transitions directly into 255-entry)
* `6 X `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 +130,22 @@ final case class ReplicationStreamMessage(behavior : Int,
}
object SquadInfo {
+ /**
+ * An entry where no fields are defined.
+ */
+ 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.
*
* 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 +155,13 @@ object SquadInfo {
/**
* Alternate constructor for `SquadInfo` that ignores the `Option` requirement for the fields.
*
- * 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 +169,23 @@ object SquadInfo {
}
/**
- * Alternate constructor for `SquadInfo` that ignores the `Option` requirement for the important field.
- *
- * 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.
- *
- * 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.
*
- * 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.
*
* 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 +193,7 @@ object SquadInfo {
}
/**
- * Alternate constructor for `SquadInfo` that ignores the `Option` requirement for the field.
- *
- * 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 +202,589 @@ object SquadInfo {
}
/**
- * Alternate constructor for `SquadInfo` that ignores the `Option` requirement for the important field.
- *
- * 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.
- *
- * Recommended use: `SquadInfo(size, None)`
- *
- * Exploration:
- * 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.
*
- * 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.
+ * 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.
*
- * Recommended use: `SquadInfo(None, capacity)`
- *
- * Exploration:
- * 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.
- *
- * 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.
- *
- * 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/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala b/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala
index 052c0e55e..ccdc41b5d 100644
--- a/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala
+++ b/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala
@@ -2,26 +2,375 @@
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}
+/**
+ * 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
+ */
+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)
+
+ /**
+ * Dispatched from client to server to indicate a squad detail update that has no foundation entry to update?
+ * Not dissimilar from `DisplaySquad`.
+ */
+ final case class SquadMemberInitializationIssue() extends SquadAction(1)
+
+ final case class SaveSquadFavorite() extends SquadAction(3)
+
+ final case class LoadSquadFavorite() extends SquadAction(4)
+
+ final case class DeleteSquadFavorite() extends SquadAction(5)
+
+ 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 AssociateWithSquad() extends SquadAction(16)
+
+ 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)
+
+ 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 ChangeSquadMemberRequirementsCertifications(u1 : Int, certs : Set[CertificationType.Value]) 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(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)
+
+ 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.
+ */
+ object Codecs {
+ private val everFailCondition = conditional(included = false, bool)
+
+ val displaySquadCodec = everFailCondition.xmap[DisplaySquad] (
+ _ => DisplaySquad(),
+ {
+ case DisplaySquad() => None
+ }
+ )
+
+ val squadMemberInitializationIssueCodec = everFailCondition.xmap[SquadMemberInitializationIssue] (
+ _ => SquadMemberInitializationIssue(),
+ {
+ case SquadMemberInitializationIssue() => None
+ }
+ )
+
+ val saveSquadFavoriteCodec = everFailCondition.xmap[SaveSquadFavorite] (
+ _ => SaveSquadFavorite(),
+ {
+ case SaveSquadFavorite() => None
+ }
+ )
+
+ val loadSquadFavoriteCodec = everFailCondition.xmap[LoadSquadFavorite] (
+ _ => LoadSquadFavorite(),
+ {
+ case LoadSquadFavorite() => None
+ }
+ )
+
+ val deleteSquadFavoriteCodec = everFailCondition.xmap[DeleteSquadFavorite] (
+ _ => DeleteSquadFavorite(),
+ {
+ case DeleteSquadFavorite() => None
+ }
+ )
+
+ val listSquadFavoriteCodec = PacketHelpers.encodedWideStringAligned(6).xmap[ListSquadFavorite] (
+ text => ListSquadFavorite(text),
+ {
+ case ListSquadFavorite(text) => text
+ }
+ )
+
+ val requestListSquadCodec = everFailCondition.xmap[RequestListSquad] (
+ _ => RequestListSquad(),
+ {
+ case RequestListSquad() => None
+ }
+ )
+
+ val stopListSquadCodec = everFailCondition.xmap[StopListSquad] (
+ _ => StopListSquad(),
+ {
+ case StopListSquad() => None
+ }
+ )
+
+ val selectRoleForYourselfCodec = uint4.xmap[SelectRoleForYourself] (
+ value => SelectRoleForYourself(value),
+ {
+ case SelectRoleForYourself(value) => value
+ }
+ )
+
+ val cancelSelectRoleForYourselfCodec = uint32.xmap[CancelSelectRoleForYourself] (
+ value => CancelSelectRoleForYourself(value),
+ {
+ case CancelSelectRoleForYourself(value) => value
+ }
+ )
+
+ val associateWithSquadCodec = everFailCondition.xmap[AssociateWithSquad] (
+ _ => AssociateWithSquad(),
+ {
+ case AssociateWithSquad() => None
+ }
+ )
+
+ val setListSquadCodec = everFailCondition.xmap[SetListSquad] (
+ _ => SetListSquad(),
+ {
+ case SetListSquad() => None
+ }
+ )
+
+ 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 changeSquadMemberRequirementsCertificationsCodec = (uint4 :: ulongL(46)).xmap[ChangeSquadMemberRequirementsCertifications] (
+ {
+ case u1 :: u2 :: HNil =>
+ ChangeSquadMemberRequirementsCertifications(u1, CertificationType.fromEncodedLong(u2))
+ },
+ {
+ case ChangeSquadMemberRequirementsCertifications(u1, u2) =>
+ u1 :: CertificationType.toEncodedLong(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 ::
+ SearchMode.codec).xmap[SearchForSquadsWithParticularRole] (
+ {
+ case u1 :: u2 :: u3 :: u4 :: HNil => SearchForSquadsWithParticularRole(u1, CertificationType.fromEncodedLong(u2), u3, u4)
+ },
+ {
+ case SearchForSquadsWithParticularRole(u1, u2, u3, u4) => u1 :: CertificationType.toEncodedLong(u2) :: u3 :: u4 :: HNil
+ }
+ )
+
+ val cancelSquadSearchCodec = everFailCondition.xmap[CancelSquadSearch] (
+ _ => CancelSquadSearch(),
+ {
+ case CancelSquadSearch() => None
+ }
+ )
+
+ val assignSquadMemberToRoleCodec = (uint4 :: uint32L).xmap[AssignSquadMemberToRole] (
+ {
+ case u1 :: u2 :: HNil => AssignSquadMemberToRole(u1, u2)
+ },
+ {
+ case AssignSquadMemberToRole(u1, u2) => u1 :: u2 :: HNil
+ }
+ )
+
+ val noSquadSearchResultsCodec = everFailCondition.xmap[NoSquadSearchResults] (
+ _ => NoSquadSearchResults(),
+ {
+ case NoSquadSearchResults() => 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.
*
* The `action` code indicates the format of the remainder data in the packet.
* The following formats are translated; their purposes are listed:
* `(None)`
- * `3 ` - Save Squad Definition
- * `8 ` - List Squad
- * `26` - Reset All
- * `35` - Cancel Squad Search
- * `41` - Cancel Find
+ * `0 ` - Display Squad
+ * `1 ` - Answer Squad Join Request
+ * `2 ` - UNKNOWN
+ * `3 ` - Save Squad Favorite
+ * `4 ` - Load Squad Favorite
+ * `5 ` - Delete Squad Favorite
+ * `6 ` - UNKNOWN
+ * `8 ` - Request List Squad
+ * `9 ` - Stop List Squad
+ * `16` - Associate with Squad
+ * `17` - Set List Squad (ui)
+ * `18` - UNKNOWN
+ * `26` - Reset All
+ * `32` - UNKNOWN
+ * `35` - Cancel Squad Search
+ * `39` - No Squad Search Results
+ * `41` - Cancel Find
+ * `42` - UNKNOWN
+ * `43` - UNKNOWN
* `Boolean`
* `28` - Auto-approve Requests for Invitation
* `29` - UNKNOWN
* `30` - UNKNOWN
- * `31` - Location Follows Squad Lead
+ * `31` - Location Follows Squad Leader
* `Int`
* `10` - Select this Role for Yourself
* `11` - UNKNOWN
@@ -33,49 +382,30 @@ import shapeless.{::, HNil}
* `Long`
* `13` - UNKNOWN
* `14` - UNKNOWN
- * `15` - UNKNOWN
+ * `15` - Select this Role for Yourself
* `37` - UNKNOWN
* `String`
- * `7 ` - UNKNOWN
+ * `7 ` - List Squad Favorite
* `19` - (Squad leader) Change Squad Purpose
* `Int :: Long`
* `12` - UNKNOWN
* `25` - (Squad leader) Change Squad Member Requirements - Weapons
- * `38` - UNKNOWN
+ * `38` - Assign Squad Member To Role
* `Int :: String`
* `23` - (Squad leader) Change Squad Member Requirements - Role
* `24` - (Squad leader) Change Squad Member Requirements - Detailed Orders
* `Long :: Long`
* `36` - UNKNOWN
* `String :: Long :: Int :: Int`
- * `34` - Search for Squads with a Particular Role
- *
- * Exploration:
- * 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.
+ * `34` - Search for Squads with a Particular Role
+ * @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
- * @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
*/
-final case class SquadDefinitionActionMessage(action : Int,
- 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])
+final case class SquadDefinitionActionMessage(squad_guid : PlanetSideGUID,
+ line : Int,
+ action : SquadAction)
extends PlanetSideGamePacket {
type Packet = SquadDefinitionActionMessage
def opcode = GamePacketOpcode.SquadDefinitionActionMessage
@@ -84,309 +414,78 @@ 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 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
+ case 22 => addSquadMemberPositionCodec
+ case 23 => changeSquadMemberRequirementsRoleCodec
+ 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 42 => unknownCodec(action = 42)
+ case 43 => unknownCodec(action = 43)
+ case _ => failureCodec(code)
+ }).asInstanceOf[Codec[SquadAction]]
}
implicit val codec : Codec[SquadDefinitionActionMessage] = (
- ("action" | uintL(6)) >>:~ { action =>
- ("unk1" | uint16L) ::
- ("unk2" | uint4L) ::
- selectCodec(action)
+ uintL(6) >>:~ { code =>
+ ("squad_guid" | PlanetSideGUID.codec) ::
+ ("line" | uint4L) ::
+ ("action" | selectFromActionCode(code))
}
- ).as[SquadDefinitionActionMessage]
+ ).xmap[SquadDefinitionActionMessage] (
+ {
+ case _ :: guid :: line :: action :: HNil =>
+ SquadDefinitionActionMessage(guid, line, action)
+ },
+ {
+ case SquadDefinitionActionMessage(guid, line, action) =>
+ action.code :: guid :: line :: action :: HNil
+ }
+ )
}
-
-/*
-("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/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala b/common/src/main/scala/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala
new file mode 100644
index 000000000..7c60d0483
--- /dev/null
+++ b/common/src/main/scala/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala
@@ -0,0 +1,1416 @@
+// Copyright (c) 2019 PSForever
+package net.psforever.packet.game
+
+import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
+import net.psforever.types.CertificationType
+import scodec.{Attempt, Codec, Err}
+import scodec.codecs._
+import shapeless.HNil
+
+import scala.annotation.tailrec
+
+/**
+ * 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
+
+ def Length_=(toLength : Int) : StreamLengthToken = {
+ bitLength = toLength
+ this
+ }
+
+ def Add(more : Int) : StreamLengthToken = {
+ bitLength += more
+ this
+ }
+}
+
+/**
+ * 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)
+ case _ =>
+ Some(true)
+ },
+ role.orElse(info.role),
+ detailed_orders.orElse(info.detailed_orders),
+ requirements.orElse(info.requirements),
+ char_id.orElse(info.char_id),
+ 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(""))
+ )
+}
+
+/**
+ * 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]) {
+ 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.
+ * 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.
+ *
+ * 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],
+ unk3 : Option[Long],
+ leader_name : Option[String],
+ task : Option[String],
+ 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),
+ 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)) =>
+ //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
+ }
+ }
+ )
+ }
+
+ //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(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 (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
+ (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.
+ *
+ * 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 {
+ type Packet = SquadDetailDefinitionUpdateMessage
+ def opcode = GamePacketOpcode.SquadDetailDefinitionUpdateMessage
+ def encode = SquadDetailDefinitionUpdateMessage.encode(this)
+}
+
+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)
+
+ /**
+ * 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 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 {
+ /**
+ * An overloaded constructor.
+ * @return a `SquadPositionEntry` object
+ */
+ def apply(index : Int, detail : SquadPositionDetail) : SquadPositionEntry = SquadPositionEntry(index, Some(detail))
+}
+
+object SquadDetail {
+ /**
+ * 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 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] {
+ /**
+ * 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.::
+ (
+ bool >>:~ { flag =>
+ conditional(flag, {
+ bitsOverByte.Add(4)
+ uint(3) :: vector(ItemizedPositions.codec(bitsOverByte))
+ }) ::
+ conditional(!flag, {
+ bitsOverByte.Add(3)
+ 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(ignoreTerminatingEntry(member_list.toList))))
+ case false :: None :: Some(_ :: member_list :: HNil) :: 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(false :: None :: Some(2 :: ensureTerminatingEntry(member_list).toVector :: HNil) :: HNil)
+ }
+ else {
+ Attempt.successful(true :: Some(4 :: ensureTerminatingEntry(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 =>
+ ignoreTerminatingEntry(unlinkFields(linkedMembers)).toVector
+ },
+ memberList => 10 :: 12 :: linkFields(ensureTerminatingEntry(memberList.toList).reverse) :: 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
+ )
+
+ /**
+ * 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 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;
+ * gets converted to a 0-7 string padding number based on how many bits remain befoire the next byte
+ * @return the padded string `Codec`
+ */
+ private def paddedStringMetaCodec(bitsOverByte : Int) : Codec[String] = PacketHelpers.encodedWideStringAligned({
+ val mod8 = bitsOverByte % 8
+ if(mod8 == 0) {
+ 0
+ }
+ else {
+ 8 - mod8
+ }
+ })
+
+ /**
+ * 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" | paddedStringMetaCodec(bitsOverByteLength)) ::
+ ("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(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 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)
+ }
+ )
+ }
+
+ /**
+ * 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
+ (
+ ("guid" | PlanetSideGUID.codec) ::
+ bool ::
+ (uint8 >>:~ { size =>
+ newcodecs.binary_choice(
+ size == 9,
+ FullSquad.codec,
+ ItemizedSquad.codec(size)
+ ).hlist
+ })
+ ).exmap[SquadDetailDefinitionUpdateMessage] (
+ {
+ case guid :: _ :: _ :: info :: HNil =>
+ Attempt.Successful(SquadDetailDefinitionUpdateMessage(guid, 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
+ Attempt.Successful(guid :: true :: occupiedSquadFieldCount :: info :: HNil)
+ }
+ else {
+ //full squad detail definition protocol
+ Attempt.Successful(guid :: true :: 9 :: info :: HNil)
+ }
+ }
+ )
+ }
+}
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 000000000..60de66e19
--- /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/net/psforever/packet/game/SquadMemberEvent.scala b/common/src/main/scala/net/psforever/packet/game/SquadMemberEvent.scala
new file mode 100644
index 000000000..51b749559
--- /dev/null
+++ b/common/src/main/scala/net/psforever/packet/game/SquadMemberEvent.scala
@@ -0,0 +1,81 @@
+// 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}
+
+object MemberEvent extends Enumeration {
+ type Type = Value
+
+ val
+ Add,
+ Remove,
+ Promote,
+ UpdateZone,
+ Unknown4
+ = Value
+
+ implicit val codec = PacketHelpers.createEnumerationCodec(enum = this, uint(bits = 3))
+}
+
+final case class SquadMemberEvent(action : MemberEvent.Value,
+ unk2 : Int,
+ char_id : Long,
+ position : Int,
+ player_name : Option[String],
+ zone_number : 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(action : MemberEvent.Value, unk2 : Int, char_id : Long, position : Int) : SquadMemberEvent =
+ SquadMemberEvent(action, unk2, char_id, position, None, None, None)
+
+ 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 Remove(unk2 : Int, char_id : Long, position : Int) : SquadMemberEvent =
+ SquadMemberEvent(MemberEvent.Remove, unk2, char_id, position, None, None, None)
+
+ 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] = (
+ ("action" | MemberEvent.codec) >>:~ { action =>
+ ("unk2" | uint16L) ::
+ ("char_id" | uint32L) ::
+ ("position" | uint4) ::
+ conditional(action == MemberEvent.Add, "player_name" | PacketHelpers.encodedWideStringAligned(1)) ::
+ conditional(action == MemberEvent.Add || action == MemberEvent.UpdateZone, "zone_number" | uint16L) ::
+ conditional(action == MemberEvent.Add || action == MemberEvent.Unknown4, "unk7" | uint32L)
+ }).exmap[SquadMemberEvent] (
+ {
+ case action :: unk2 :: char_id :: member_position :: player_name :: zone_number :: unk7 :: HNil =>
+ Attempt.Successful(SquadMemberEvent(action, unk2, char_id, member_position, player_name, zone_number, unk7))
+ },
+ {
+ case 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/net/psforever/packet/game/SquadMembershipRequest.scala b/common/src/main/scala/net/psforever/packet/game/SquadMembershipRequest.scala
new file mode 100644
index 000000000..c5bd3550c
--- /dev/null
+++ b/common/src/main/scala/net/psforever/packet/game/SquadMembershipRequest.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.SquadRequestType
+import scodec.Codec
+import scodec.codecs._
+
+/**
+ * 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,
+ char_id : Long,
+ unk3 : Option[Long],
+ player_name : String,
+ unk5 : Option[Option[String]])
+ extends PlanetSideGamePacket {
+ request_type match {
+ case SquadRequestType.Accept | SquadRequestType.Reject | SquadRequestType.Disband |
+ 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")
+ }
+ if(request_type == SquadRequestType.Invite) {
+ assert(unk5.nonEmpty, "an Invite request requires the unk5 field be defined")
+ }
+
+ 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.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}))
+ ).hlist
+ })
+ }).as[SquadMembershipRequest]
+}
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 000000000..a9f035183
--- /dev/null
+++ b/common/src/main/scala/net/psforever/packet/game/SquadMembershipResponse.scala
@@ -0,0 +1,96 @@
+// Copyright (c) 2019 PSForever
+package net.psforever.packet.game
+
+import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
+import net.psforever.types.SquadResponseType
+import scodec.Codec
+import scodec.codecs._
+
+/**
+ * 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
+ * @param unk2 na
+ * @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 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
+ *
+ * `request_type` (enum value) / `unk5` state (`false`/`true`)
+ * ----------------------------------------
+ * - `Invite` (0)
+ * false => [PROMPT] "`player_name` has invited you into a squad." [YES/NO]
+ * true => "You have invited `player_name` to join your squad."
+ * - `Unk01` (1)
+ * false => n/a
+ * true => n/a
+ * - `Accept` (2)
+ * false => "`player_name` has accepted your invitation to join into your squad.
+ * "You have formed a squad and are now that squad's commander." (if first time)
+ * true => "You have accepted an invitation to join a squad."
+ * "You have successfully joined a squad for the first time." (if first time)
+ * - `Reject` (3)
+ * false => "`player_name` does not want to join your squad at this time."
+ * true => "You have declined an invitation to join a squad."
+ * - `Cancel` (4)
+ * false => "`player_name` has withdrawn his invitation."
+ * true => "You have canceled your invitation to `player_name`."
+ * - `Leave` (5)
+ * false => "The Squad Leader has kicked you out of the squad."
+ * true => "You have kicked `player_name` out of the squad."
+ * - `Disband` (6)
+ * false => "The squad has been disbanded."
+ * true => "You have disbanded the squad."
+ * - `PlatoonInvite` (7)
+ * false => [PROMPT] "`player_name` has invited you into a platoon." [YES/NO]
+ * true => "You have invited `player_name`'s squad to join your platoon."
+ * - `PlatoonAccept` (8)
+ * false => "`player_name` has accepted your invitation to join into your platoon.
+ * "You have formed a platoon and are now that platoon commander." (if first time)
+ * true => "You have accepted an invitation to join a platoon."
+ * "You have successfully joined a platoon for the first time." (if first time)
+ * - `PlatoonReject` (9)
+ * false => "`player_name` does not want to join your platoon at this time."
+ * true => "You have declined an invitation to join a platoon."
+ * - `PlatoonCancel` (10)
+ * false => "`player_name` has withdrawn his invitation."
+ * true => "You have declined your invitation to `player_name`." (nonsense?)
+ * - `PlatoonLeave` (11)
+ * false => "The Platoon Leader has kicked you out of the platoon."
+ * true => "You have kicked `player_name`'s squad out of the platoon."
+ * - `PlatoonDisband` (12)
+ * false => "The platoon has been disbanded."
+ * true => "You have disbanded the platoon."
+ */
+final case class SquadMembershipResponse(request_type : SquadResponseType.Value,
+ unk1 : Int,
+ unk2 : Int,
+ char_id : Long,
+ other_id : Option[Long],
+ player_name : String,
+ unk5 : Boolean,
+ unk6 : Option[Option[String]])
+ extends PlanetSideGamePacket {
+ type Packet = SquadMembershipResponse
+ def opcode = GamePacketOpcode.SquadMembershipResponse
+ def encode = SquadMembershipResponse.encode(this)
+}
+
+object SquadMembershipResponse extends Marshallable[SquadMembershipResponse] {
+ implicit val codec : Codec[SquadMembershipResponse] = (
+ "request_type" | SquadResponseType.codec >>:~ { d =>
+ ("unk1" | uint(5)) ::
+ ("unk2" | uint2) ::
+ ("char_id" | uint32L) ::
+ ("other_id" | conditional(d != SquadResponseType.Disband && d != SquadResponseType.PlatoonDisband, uint32L)) ::
+ ("player_name" | PacketHelpers.encodedWideStringAligned(5)) ::
+ ("unk5" | bool) ::
+ conditional(d != SquadResponseType.Invite, optional(bool, "unk6" | PacketHelpers.encodedWideStringAligned(6)))
+ }
+ ).as[SquadMembershipResponse]
+}
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 000000000..dc0927494
--- /dev/null
+++ b/common/src/main/scala/net/psforever/packet/game/SquadState.scala
@@ -0,0 +1,101 @@
+// 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}
+
+/**
+ * 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,
+ health : Int,
+ armor : Int,
+ pos : Vector3,
+ unk4 : Int,
+ unk5 : Int,
+ unk6 : Boolean,
+ unk7 : Int,
+ unk8 : Option[Int],
+ unk9 : Option[Boolean])
+
+/**
+ * Dispatched by the server to update a squad member's representative icons on the continental maps and the interstellar map.
+ *
+ * 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 {
+ 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) ::
+ ("health" | uint(7)) ::
+ ("armor" | 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 :: 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, 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"))
+ }
+ )
+
+ implicit val codec : Codec[SquadState] = (
+ ("guid" | PlanetSideGUID.codec) ::
+ ("info_list" | listOfN(uint4, info_codec))
+ ).as[SquadState]
+}
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 3874b625a..ff466782e 100644
--- a/common/src/main/scala/net/psforever/packet/game/SquadWaypointEvent.scala
+++ b/common/src/main/scala/net/psforever/packet/game/SquadWaypointEvent.scala
@@ -1,22 +1,22 @@
-// 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}
-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 : SquadWaypoints.Value,
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 : SquadWaypoints.Value, 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 : SquadWaypoints.Value, 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 : SquadWaypoints.Value) : 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" | SquadWaypoints.codec) ::
+ ("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 000000000..3384f8b50
--- /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.{SquadWaypoints, 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 : SquadWaypoints.Value,
+ 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 : SquadWaypoints.Value, waypoint : WaypointInfo) : SquadWaypointRequest =
+ SquadWaypointRequest(WaypointEventAction.Add, char_id, waypoint_type, None, Some(waypoint))
+
+ 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 : SquadWaypoints.Value) : 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" | SquadWaypoints.codec) ::
+ ("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/net/psforever/packet/game/objectcreate/CharacterAppearanceData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterAppearanceData.scala
index c2b064d37..c3f179217 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/net/psforever/types/CertificationType.scala b/common/src/main/scala/net/psforever/types/CertificationType.scala
index ea4ca3ab5..99afa33c5 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.
*
@@ -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/net/psforever/types/PlanetSideEmpire.scala b/common/src/main/scala/net/psforever/types/PlanetSideEmpire.scala
index 522cd392a..6494d07d2 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/net/psforever/types/SquadRequestType.scala b/common/src/main/scala/net/psforever/types/SquadRequestType.scala
new file mode 100644
index 000000000..1947abe75
--- /dev/null
+++ b/common/src/main/scala/net/psforever/types/SquadRequestType.scala
@@ -0,0 +1,27 @@
+// 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,
+ ProximityInvite,
+ Accept,
+ Reject,
+ Cancel,
+ Leave,
+ Promote,
+ Disband,
+ PlatoonInvite,
+ PlatoonAccept,
+ PlatoonReject,
+ PlatoonCancel,
+ PlatoonLeave,
+ PlatoonDisband
+ = Value
+
+ implicit val codec = PacketHelpers.createEnumerationCodec(this, uint4L)
+}
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 000000000..84ab57ad4
--- /dev/null
+++ b/common/src/main/scala/net/psforever/types/SquadResponseType.scala
@@ -0,0 +1,26 @@
+// 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)
+}
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 000000000..5a11ea95c
--- /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/SquadService.scala b/common/src/main/scala/services/teamwork/SquadService.scala
new file mode 100644
index 000000000..ad717e598
--- /dev/null
+++ b/common/src/main/scala/services/teamwork/SquadService.scala
@@ -0,0 +1,3033 @@
+// Copyright (c) 2019 PSForever
+package services.teamwork
+
+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, SquadFeatures}
+import net.psforever.objects.zones.Zone
+import net.psforever.packet.game._
+import net.psforever.types._
+import services.{GenericEventBus, Service}
+
+import scala.collection.concurrent.TrieMap
+import scala.collection.mutable
+import scala.collection.mutable.ListBuffer
+
+class SquadService extends Actor {
+ import SquadService._
+
+ /**
+ * 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
+ /**
+ * All squads.
+ * key - squad unique number; value - the squad wrapped around its attributes object
+ */
+ 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.
+ * 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 given player has refused participation into this other player's squad.
+ * key - a unique character identifier number; value - a list of unique character identifier numbers
+ */
+ private val refused : mutable.LongMap[List[Long]] = mutable.LongMap[List[Long]]()
+ /**
+ * Players who are interested in updated details regarding a certain squad though they may not be a member of the squad.
+ * key - unique character identifier number; value - a squad identifier number
+ */
+ private val continueToMonitorDetails : mutable.LongMap[PlanetSideGUID] = mutable.LongMap[PlanetSideGUID]()
+ /**
+ * A placeholder for an absent active invite that has not (yet) been accepted or rejected,
+ * equal to the then-current active invite.
+ * Created when removing an active invite.
+ * Checked when trying to add a new invite (if found, the new invite is queued).
+ * Cleared when the next queued invite becomes active.
+ * key - unique character identifier number; value, unique character identifier number
+ */
+ private val previousInvites : mutable.LongMap[Invitation] = mutable.LongMap[Invitation]()
+
+ /**
+ * This is a formal `ActorEventBus` object that is reserved for faction-wide messages and squad-specific messages.
+ * When the user joins the `SquadService` with a `Service.Join` message
+ * that includes a confirmed faction affiliation identifier,
+ * the origin `ActorRef` is added as a subscription.
+ * Squad channels are produced when a squad is created,
+ * and are subscribed to as users join the squad,
+ * and unsubscribed from as users leave the squad.
+ * key - a `PlanetSideEmpire` value; value - `ActorRef` reference
+ * key - a consistent squad channel name; value - `ActorRef` reference
+ * @see `CloseSquad`
+ * @see `JoinSquad`
+ * @see `LeaveSquad`
+ * @see `Service.Join`
+ * @see `Service.Leave`
+ */
+ private val SquadEvents = new GenericEventBus[SquadServiceResponse]
+ /**
+ * This collection contains the message-sending contact reference for individuals.
+ * When the user joins the `SquadService` with a `Service.Join` message
+ * that includes their unique character identifier,
+ * 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`.
+ * 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
+ if(j == 65536) {
+ sid = 1
+ }
+ else {
+ sid = j
+ }
+ PlanetSideGUID(out)
+ }
+
+ /**
+ * 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(UserEvents.isEmpty && squadFeatures.isEmpty) {
+ sid = 1
+ true
+ }
+ else {
+ false
+ }
+ }
+
+ /**
+ * 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
+ }
+
+ /**
+ * 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
+ }
+
+ /**
+ * 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
+ }
+
+ /**
+ * Overloaded message-sending operation.
+ * The `Actor` version wraps around the expected `!` functionality.
+ * @param to an `ActorRef` which to send the message
+ * @param msg a message that can be stored in a `SquadServiceResponse` object
+ */
+ def Publish(to : ActorRef, msg : SquadResponse.Response) : Unit = {
+ Publish(to, msg, Nil)
+ }
+ /**
+ * Overloaded message-sending operation.
+ * The `Actor` version wraps around the expected `!` functionality.
+ * @param to an `ActorRef` which to send the message
+ * @param msg a message that can be stored in a `SquadServiceResponse` object
+ * @param excluded a group of character identifier numbers who should not receive the message
+ * (resolved at destination)
+ */
+ def Publish(to : ActorRef, msg : SquadResponse.Response, excluded : Iterable[Long]) : Unit = {
+ to ! SquadServiceResponse("", excluded, msg)
+ }
+ /**
+ * Overloaded message-sending operation.
+ * Always publishes on the `SquadEvents` object.
+ * @param to a faction affiliation used as the channel for the message
+ * @param msg a message that can be stored in a `SquadServiceResponse` object
+ */
+ def Publish(to : PlanetSideEmpire.Type, msg : SquadResponse.Response) : Unit = {
+ Publish(to, msg, Nil)
+ }
+ /**
+ * Overloaded message-sending operation.
+ * Always publishes on the `SquadEvents` object.
+ * @param to a faction affiliation used as the channel for the message
+ * @param msg a message that can be stored in a `SquadServiceResponse` object
+ * @param excluded a group of character identifier numbers who should not receive the message
+ * (resolved at destination)
+ */
+ def Publish(to : PlanetSideEmpire.Type, msg : SquadResponse.Response, excluded : Iterable[Long]) : Unit = {
+ SquadEvents.publish(SquadServiceResponse(s"/$to/Squad", excluded, msg))
+ }
+ /**
+ * Overloaded message-sending operation.
+ * Strings come in three accepted patterns.
+ * The first resolves into a faction name, as determined by `PlanetSideEmpire` when transformed into a string.
+ * The second resolves into a squad's dedicated channel, a name that is formulaic.
+ * The third resolves as a unique character identifier number.
+ * @param to a string used as the channel for the message
+ * @param msg a message that can be stored in a `SquadServiceResponse` object
+ */
+ def Publish(to : String, msg : SquadResponse.Response) : Unit = {
+ Publish(to, msg, Nil)
+ }
+ /**
+ * Overloaded message-sending operation.
+ * Strings come in three accepted patterns.
+ * The first resolves into a faction name, as determined by `PlanetSideEmpire` when transformed into a string.
+ * The second resolves into a squad's dedicated channel, a name that is formulaic.
+ * The third resolves as a unique character identifier number.
+ * @param to a string used as the channel for the message
+ * @param msg a message that can be stored in a `SquadServiceResponse` object
+ * @param excluded a group of character identifier numbers who should not receive the message
+ * (resolved at destination, usually)
+ */
+ def Publish(to : String, msg : SquadResponse.Response, excluded : Iterable[Long]) : Unit = {
+ to match {
+ case str if "TRNCVS".indexOf(str) > -1 || str.matches("(TR|NC|VS)-Squad\\d+") =>
+ SquadEvents.publish(SquadServiceResponse(s"/$str/Squad", excluded, msg))
+ case str if str.matches("//d+") =>
+ Publish(to.toLong, msg, excluded)
+ case _ =>
+ log.error(s"Publish(String): subscriber information is an unhandled format - $to")
+ }
+ }
+ /**
+ * Overloaded message-sending operation.
+ * Always publishes on the `ActorRef` objects retained by the `UserEvents` object.
+ * @param to a unique character identifier used as the channel for the message
+ * @param msg a message that can be stored in a `SquadServiceResponse` object
+ */
+ def Publish(to : Long, msg : SquadResponse.Response) : Unit = {
+ UserEvents.get(to) match {
+ case Some(user) =>
+ user ! SquadServiceResponse("", msg)
+ case None =>
+ log.error(s"Publish(Long): subscriber information can not be found - $to")
+ }
+ }
+ /**
+ * Overloaded message-sending operation.
+ * Always publishes on the `ActorRef` objects retained by the `UserEvents` object.
+ * @param to a unique character identifier used as the channel for the message
+ * @param msg a message that can be stored in a `SquadServiceResponse` object
+ * @param excluded a group of character identifier numbers who should not receive the message
+ */
+ def Publish(to : Long, msg : SquadResponse.Response, excluded : Iterable[Long]) : Unit = {
+ if(!excluded.exists(_ == to)) {
+ Publish(to, msg)
+ }
+ }
+ /**
+ * Overloaded message-sending operation.
+ * No message can be sent using this distinction.
+ * Log a warning.
+ * @param to something that was expected to be used as the channel for the message
+ * but is not handled as such
+ * @param msg a message that can be stored in a `SquadServiceResponse` object
+ */
+ def Publish[ANY >: Any](to : ANY, msg : SquadResponse.Response) : Unit = {
+ log.warn(s"Publish(Any): subscriber information is an unhandled format - $to")
+ }
+ /**
+ * Overloaded message-sending operation.
+ * No message can be sent using this distinction.
+ * Log a warning.
+ * @param to something that was expected to be used as the channel for the message
+ * but is not handled as such
+ * @param msg a message that can be stored in a `SquadServiceResponse` object
+ * @param excluded a group of character identifier numbers who should not receive the message
+ */
+ def Publish[ANY >: Any](to : ANY, msg : SquadResponse.Response, excluded : Iterable[Long]) : Unit = {
+ log.warn(s"Publish(Any): subscriber information is an unhandled format - $to")
+ }
+
+ def receive : Receive = {
+ //subscribe to a faction's channel - necessary to receive updates about listed squads
+ case Service.Join(faction) if "TRNCVS".indexOf(faction) > -1 =>
+ val path = s"/$faction/Squad"
+ val who = sender()
+ debug(s"$who has joined $path")
+ SquadEvents.subscribe(who, path)
+
+ //subscribe to the player's personal channel - necessary for future and previous squad information
+ case Service.Join(char_id) =>
+ try {
+ val longCharId = char_id.toLong
+ val path = s"/$char_id/Squad"
+ val who = sender()
+ 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)) => LeaveService(char_id, sender)
+
+ case Service.Leave(None) | Service.LeaveAll() => UserEvents find { case(_, subscription) => subscription.path.equals(sender.path)} match {
+ case Some((to, _)) =>
+ LeaveService(to, sender)
+ case _ => ;
+ }
+
+ 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.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
+ (if(invitedName.nonEmpty) {
+ //validate player with name exists
+ LivePlayerList.WorldPopulation({ case (_, a : Avatar) => a.name.equalsIgnoreCase(invitedName) && a.faction == tplayer.Faction }).headOption match {
+ case Some(player) => UserEvents.keys.find(_ == player.CharId)
+ case None => None
+ }
+ }
+ else {
+ //validate player with id exists
+ 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) if invitingPlayer != invitedPlayer =>
+ (memberToSquad.get(invitingPlayer), memberToSquad.get(invitedPlayer)) match {
+ case (Some(squad1), Some(squad2)) if squad1.GUID == squad2.GUID =>
+ //both players are in the same squad; no need to do anything
+
+ case (Some(squad1), Some(squad2)) if squad1.Leader.CharId == invitingPlayer && squad2.Leader.CharId == invitedPlayer &&
+ squad1.Size > 1 && squad2.Size > 1 =>
+ //we might do some platoon chicanery with this case later
+ //TODO platoons
+
+ case (Some(squad1), Some(squad2)) if squad2.Size == 1 =>
+ //both players belong to squads, but the invitedPlayer's squad (squad2) is underutilized by comparison
+ //treat the same as "the classic situation" using squad1
+ if(!Refused(invitedPlayer).contains(invitingPlayer)) {
+ val charId = tplayer.CharId
+ AddInviteAndRespond(
+ invitedPlayer,
+ VacancyInvite(charId, tplayer.Name, squad1.GUID),
+ charId,
+ tplayer.Name
+ )
+ }
+
+ case (Some(squad1), Some(squad2)) if squad1.Size == 1 =>
+ //both players belong to squads, but the invitingPlayer's squad is underutilized by comparison
+ //treat the same as "indirection ..." using squad2
+ val leader = squad2.Leader.CharId
+ if(Refused(invitingPlayer).contains(invitedPlayer)) {
+ debug(s"$invitedPlayer repeated a previous refusal to $invitingPlayer's invitation offer")
+ }
+ else if(Refused(invitingPlayer).contains(leader)) {
+ debug(s"$invitedPlayer repeated a previous refusal to $leader's invitation offer")
+ }
+ else {
+ AddInviteAndRespond(
+ leader,
+ IndirectInvite(tplayer, squad2.GUID),
+ invitingPlayer,
+ tplayer.Name
+ )
+ }
+
+ case (Some(squad), None) =>
+ //the classic situation
+ if(!Refused(invitedPlayer).contains(invitingPlayer)) {
+ AddInviteAndRespond(
+ invitedPlayer,
+ VacancyInvite(tplayer.CharId, tplayer.Name, squad.GUID),
+ invitingPlayer,
+ tplayer.Name
+ )
+ }
+ else {
+ debug(s"$invitedPlayer repeated a previous refusal to $invitingPlayer's invitation offer")
+ }
+
+ case (None, Some(squad)) =>
+ //indirection; we're trying to invite ourselves to someone else's squad
+ val leader = squad.Leader.CharId
+ if(Refused(invitingPlayer).contains(invitedPlayer)) {
+ debug(s"$invitedPlayer repeated a previous refusal to $invitingPlayer's invitation offer")
+ }
+ else if(Refused(invitingPlayer).contains(leader)) {
+ debug(s"$invitedPlayer repeated a previous refusal to $leader's invitation offer")
+ }
+ else {
+ AddInviteAndRespond(
+ squad.Leader.CharId,
+ IndirectInvite(tplayer, squad.GUID),
+ invitingPlayer,
+ tplayer.Name
+ )
+ }
+
+ case (None, None) =>
+ //neither the invited player nor the inviting player belong to any squad
+ if(Refused(invitingPlayer).contains(invitedPlayer)) {
+ debug(s"$invitedPlayer repeated a previous refusal to $invitingPlayer's invitation offer")
+ }
+ else if(Refused(invitedPlayer).contains(invitingPlayer)) {
+ debug(s"$invitingPlayer repeated a previous refusal to $invitingPlayer's invitation offer")
+ }
+ else {
+ AddInviteAndRespond(
+ invitedPlayer,
+ SpontaneousInvite(tplayer),
+ invitingPlayer,
+ tplayer.Name
+ )
+ }
+
+ case _ => ;
+ }
+ case _ => ;
+ }
+
+ case SquadAction.Membership(SquadRequestType.ProximityInvite, invitingPlayer, _, _, _) =>
+ GetLeadingSquad(invitingPlayer, None) match {
+ case Some(squad) =>
+ val sguid = squad.GUID
+ val features = squadFeatures(sguid)
+ features.SearchForRole match {
+ case Some(-1) =>
+ //we've already issued a proximity invitation; no need to do another
+ debug("ProximityInvite: wait for existing proximity invitations to clear")
+ case _ =>
+ val outstandingActiveInvites = features.SearchForRole match {
+ case Some(pos) =>
+ RemoveQueuedInvitesForSquadAndPosition(sguid, pos)
+ invites.collect { case(charId, LookingForSquadRoleInvite(_,_, squad_guid, role)) if squad_guid == sguid && role == pos => charId }
+ case None =>
+ List.empty[Long]
+ }
+ val faction = squad.Faction
+ val center = tplayer.Position
+ val excusedInvites = features.Refuse
+ //positions that can be recruited to
+ val positions = squad.Membership.zipWithIndex
+ .collect { case(member, index) if member.CharId == 0 && squad.Availability(index) => member }
+ /*
+ players who are:
+ - the same faction as the squad
+ - have Looking For Squad enabled
+ - do not currently belong to a squad
+ - are denied the opportunity to be invited
+ - are a certain distance from the squad leader (n < 25m)
+ */
+ (zone.LivePlayers
+ .collect { case player
+ if player.Faction == faction && player.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 =>
+ 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
+ outstandingActiveInvites foreach RemoveInvite
+ features.ProxyInvites = Nil
+ None
+ case (outstandingPlayerList, invitedPlayerList) =>
+ //players who were actively invited for the previous position and are eligible for the new position
+ features.SearchForRole = Some(-1)
+ outstandingPlayerList.foreach { charId =>
+ val bid = invites(charId).asInstanceOf[LookingForSquadRoleInvite]
+ invites(charId) = ProximityInvite(bid.char_id, bid.name, sguid)
+ }
+ //players who were actively invited for the previous position but are ineligible for the new position
+ (features.ProxyInvites filterNot (outstandingPlayerList contains)) foreach RemoveInvite
+ features.ProxyInvites = outstandingPlayerList ++ invitedPlayerList
+ Some(invitedPlayerList)
+ }) match {
+ //add invitations for position in squad
+ case Some(invitedPlayers) =>
+ val invitingPlayer = tplayer.CharId
+ val name = tplayer.Name
+ invitedPlayers.foreach { invitedPlayer =>
+ AddInviteAndRespond(
+ invitedPlayer,
+ ProximityInvite(invitingPlayer, name, sguid),
+ invitingPlayer,
+ name
+ )
+ }
+ case None => ;
+ }
+ }
+
+ case None =>
+ }
+
+ case SquadAction.Membership(SquadRequestType.Accept, invitedPlayer, _, _, _) =>
+ val acceptedInvite = RemoveInvite(invitedPlayer)
+ acceptedInvite match {
+ case Some(RequestRole(petitioner, guid, position)) if EnsureEmptySquad(petitioner.CharId) && squadFeatures.get(guid).nonEmpty =>
+ //player requested to join a squad's specific position
+ //invitedPlayer is actually the squad leader; petitioner is the actual "invitedPlayer"
+ val features = squadFeatures(guid)
+ JoinSquad(petitioner, features.Squad, position)
+ RemoveInvitesForSquadAndPosition(guid, position)
+
+ case Some(IndirectInvite(recruit, guid)) if EnsureEmptySquad(recruit.CharId) =>
+ //tplayer / invitedPlayer is actually the squad leader
+ val recruitCharId = recruit.CharId
+ HandleVacancyInvite(guid, recruitCharId, invitedPlayer, recruit) match {
+ case Some((squad, line)) =>
+ Publish(invitedPlayer, SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitedPlayer, Some(recruitCharId), recruit.Name, true, Some(None)))
+ JoinSquad(recruit, squad, line)
+ RemoveInvitesForSquadAndPosition(squad.GUID, line)
+ //since we are the squad leader, we do not want to brush off our queued squad invite tasks
+ case _ => ;
+ }
+
+ case Some(VacancyInvite(invitingPlayer, _, guid)) if EnsureEmptySquad(invitedPlayer) =>
+ //accepted an invitation to join an existing squad
+ HandleVacancyInvite(guid, invitedPlayer, invitingPlayer, tplayer) match {
+ case Some((squad, line)) =>
+ Publish(invitingPlayer, SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitingPlayer, Some(invitedPlayer), tplayer.Name, false, Some(None)))
+ Publish(invitedPlayer, SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitedPlayer, Some(invitingPlayer), "", true, Some(None)))
+ JoinSquad(tplayer, squad, line)
+ RemoveQueuedInvites(invitedPlayer) //TODO deal with these somehow
+ RemoveInvitesForSquadAndPosition(squad.GUID, line)
+ case _ => ;
+ }
+
+ case Some(SpontaneousInvite(invitingPlayer)) if EnsureEmptySquad(invitedPlayer) =>
+ //originally, we were invited by someone into a new squad they would form
+ val invitingPlayerCharId = invitingPlayer.CharId
+ (GetParticipatingSquad(invitingPlayer) match {
+ case Some(participating) =>
+ //invitingPlayer became part of a squad while invited player was answering the original summons
+ Some(participating)
+ case _ =>
+ //generate a new squad, with invitingPlayer as the leader
+ val squad = StartSquad(invitingPlayer)
+ squad.Task = s"${invitingPlayer.Name}'s Squad"
+ Publish(invitingPlayerCharId, SquadResponse.AssociateWithSquad(squad.GUID))
+ Some(squad)
+ }) match {
+ case Some(squad) =>
+ HandleVacancyInvite(squad, tplayer.CharId, invitingPlayerCharId, tplayer) match {
+ case Some((_, line)) =>
+ Publish(invitedPlayer, SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitedPlayer, Some(invitingPlayerCharId), "", true, Some(None)))
+ Publish(invitingPlayerCharId, SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitingPlayerCharId, Some(invitedPlayer), tplayer.Name, false, Some(None)))
+ JoinSquad(tplayer, squad, line)
+ RemoveQueuedInvites(tplayer.CharId) //TODO deal with these somehow
+ case _ => ;
+ }
+ case _ => ;
+ }
+
+ case Some(LookingForSquadRoleInvite(invitingPlayer, name, guid, position)) if EnsureEmptySquad(invitedPlayer) =>
+ squadFeatures.get(guid) match {
+ case Some(features) if JoinSquad(tplayer, features.Squad, position) =>
+ //join this squad
+ Publish(invitedPlayer, SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitedPlayer, Some(invitingPlayer), "", true, Some(None)))
+ Publish(invitingPlayer, SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitingPlayer, Some(invitedPlayer), tplayer.Name, false, Some(None)))
+ RemoveQueuedInvites(tplayer.CharId)
+ features.ProxyInvites = Nil
+ features.SearchForRole = None
+ RemoveInvitesForSquadAndPosition(guid, position)
+
+ case Some(features) =>
+ //can not join squad; position is unavailable or other reasons block action
+ features.ProxyInvites = features.ProxyInvites.filterNot(_ == invitedPlayer)
+
+ case _ =>
+ //squad no longer exists?
+ }
+
+ case Some(ProximityInvite(invitingPlayer, _, guid)) if EnsureEmptySquad(invitedPlayer) =>
+ squadFeatures.get(guid) match {
+ case Some(features) =>
+ val squad = features.Squad
+ if(squad.Size < squad.Capacity) {
+ val positions = (for {
+ (member, index) <- squad.Membership.zipWithIndex
+ if squad.isAvailable(index, tplayer.Certifications)
+ } yield (index, member.Requirements.size))
+ .toList
+ .sortBy({ case (_, reqs) => reqs })
+ ((positions.headOption, positions.lastOption) match {
+ case (Some((first, size1)), Some((_, size2))) if size1 == size2 => Some(first) //join the first available position
+ case (Some(_), Some((last, _))) => Some(last) //join the most demanding position
+ case _ => None
+ }) match {
+ case Some(position) if JoinSquad(tplayer, squad, position) =>
+ //join this squad
+ Publish(invitedPlayer, SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitedPlayer, Some(invitingPlayer), "", true, Some(None)))
+ Publish(invitingPlayer, SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitingPlayer, Some(invitedPlayer), tplayer.Name, false, Some(None)))
+ RemoveQueuedInvites(invitedPlayer)
+ features.ProxyInvites = features.ProxyInvites.filterNot(_ == invitedPlayer)
+ case _ =>
+ }
+ }
+ if(features.ProxyInvites.isEmpty) {
+ //all invitations exhausted; this invitation period is concluded
+ features.SearchForRole = None
+ }
+ else if(squad.Size == squad.Capacity) {
+ //all available squad positions filled; terminate all remaining invitations
+ RemoveProximityInvites(guid)
+ RemoveAllInvitesToSquad(guid)
+ //RemoveAllInvitesWithPlayer(invitingPlayer)
+ }
+
+ case _ =>
+ //squad no longer exists?
+ }
+
+ case _ =>
+ //the invite either timed-out or was withdrawn or is now invalid
+ (previousInvites.get(invitedPlayer) match {
+ case Some(SpontaneousInvite(leader)) => (leader.CharId, leader.Name)
+ case Some(VacancyInvite(charId, name, _)) => (charId, name)
+ case Some(ProximityInvite(charId, name, _)) => (charId, name)
+ case Some(LookingForSquadRoleInvite(charId, name, _, _)) => (charId, name)
+ case _ => (0L, "")
+ }) match {
+ case (0L, "") => ;
+ case (charId, name) =>
+ Publish(charId, SquadResponse.Membership(SquadResponseType.Cancel, 0, 0, charId, Some(0L), name, false, Some(None)))
+ }
+ }
+ NextInviteAndRespond(invitedPlayer)
+
+ case SquadAction.Membership(SquadRequestType.Leave, actingPlayer, _leavingPlayer, name, _) =>
+ GetParticipatingSquad(actingPlayer) match {
+ case Some(squad) =>
+ val leader = squad.Leader.CharId
+ (if(name.nonEmpty) {
+ //validate player with name
+ LivePlayerList.WorldPopulation({ case (_, a : Avatar) => a.name.equalsIgnoreCase(name) }).headOption match {
+ case Some(a) => UserEvents.keys.find(_ == a.CharId)
+ case None => None
+ }
+ }
+ else {
+ //validate player with id
+ _leavingPlayer match {
+ case Some(id) => UserEvents.keys.find(_ == id)
+ case None => None
+ }
+ }) match {
+ case out @ Some(leavingPlayer) if GetParticipatingSquad(leavingPlayer).contains(squad) => //kicked player must be in the same squad
+ if(actingPlayer == leader) {
+ if(leavingPlayer == leader || squad.Size == 2) {
+ //squad leader is leaving his own squad, so it will be disbanded
+ //OR squad is only composed of two people, so it will be closed-out when one of them leaves
+ DisbandSquad(squad)
+ }
+ else {
+ //kicked by the squad leader
+ Publish(leavingPlayer, SquadResponse.Membership(SquadResponseType.Leave, 0, 0, leavingPlayer, Some(leader), tplayer.Name, false, Some(None)))
+ Publish(leader, SquadResponse.Membership(SquadResponseType.Leave, 0, 0, leader, Some(leavingPlayer), "", true, Some(None)))
+ squadFeatures(squad.GUID).Refuse = leavingPlayer
+ LeaveSquad(leavingPlayer, squad)
+ }
+ }
+ else if(leavingPlayer == actingPlayer) {
+ if(squad.Size == 2) {
+ //squad is only composed of two people, so it will be closed-out when one of them leaves
+ DisbandSquad(squad)
+ }
+ else {
+ //leaving the squad of own accord
+ LeaveSquad(actingPlayer, squad)
+ }
+ }
+
+ case _ => ;
+ }
+ case _ => ;
+ }
+
+ case SquadAction.Membership(SquadRequestType.Reject, rejectingPlayer, _, _, _) =>
+ val rejectedBid = RemoveInvite(rejectingPlayer)
+ //(A, B) -> person who made the rejection, person who was rejected
+ (rejectedBid match {
+ case Some(SpontaneousInvite(leader)) =>
+ //rejectingPlayer is the would-be squad member; the squad leader's request was rejected
+ val invitingPlayerCharId = leader.CharId
+ Refused(rejectingPlayer, invitingPlayerCharId)
+ (Some(rejectingPlayer), Some(invitingPlayerCharId))
+
+ case Some(VacancyInvite(leader, _, guid))
+ if squadFeatures.get(guid).nonEmpty && squadFeatures(guid).Squad.Leader.CharId != rejectingPlayer =>
+ //rejectingPlayer is the would-be squad member; the squad leader's request was rejected
+ Refused(rejectingPlayer, leader)
+ (Some(rejectingPlayer), Some(leader))
+
+ case Some(ProximityInvite(_, _, guid))
+ if squadFeatures.get(guid).nonEmpty && squadFeatures(guid).Squad.Leader.CharId != rejectingPlayer =>
+ //rejectingPlayer is the would-be squad member; the squad leader's request was rejected
+ val features = squadFeatures(guid)
+ features.Refuse = rejectingPlayer //do not bother this player anymore
+ if((features.ProxyInvites = features.ProxyInvites.filterNot(_ == rejectingPlayer)).isEmpty) {
+ features.SearchForRole = None
+ }
+ (None, None)
+
+ case Some(LookingForSquadRoleInvite(leader, _, guid, _))
+ if squadFeatures.get(guid).nonEmpty && squadFeatures(guid).Squad.Leader.CharId != rejectingPlayer =>
+ //rejectingPlayer is the would-be squad member; the squad leader's request was rejected
+ Refused(rejectingPlayer, leader)
+ val features = squadFeatures(guid)
+ features.Refuse = rejectingPlayer
+ if((features.ProxyInvites = features.ProxyInvites.filterNot(_ == rejectingPlayer)).isEmpty) {
+ features.SearchForRole = None
+ }
+ (None, None)
+
+ case Some(RequestRole(_, guid, _))
+ if squadFeatures.get(guid).nonEmpty && squadFeatures(guid).Squad.Leader.CharId == rejectingPlayer =>
+ //rejectingPlayer is the squad leader; candidate is the would-be squad member who was rejected
+ val features = squadFeatures(guid)
+ features.Refuse = rejectingPlayer
+ (Some(rejectingPlayer), None)
+
+ case _ => ;
+ (None, None)
+ }) match {
+ case (Some(rejected), Some(invited)) =>
+ Publish(rejected, SquadResponse.Membership(SquadResponseType.Reject, 0, 0, rejected, Some(invited), "", true, Some(None)))
+ Publish(invited, SquadResponse.Membership(SquadResponseType.Reject, 0, 0, invited, Some(rejected), tplayer.Name, false, Some(None)))
+ case (Some(rejected), None) =>
+ Publish(rejected, SquadResponse.Membership(SquadResponseType.Reject, 0, 0, rejected, Some(rejected), "", true, Some(None)))
+ case _ => ;
+ }
+ NextInviteAndRespond(rejectingPlayer)
+
+ case SquadAction.Membership(SquadRequestType.Disband, char_id, _, _, _) =>
+ GetLeadingSquad(char_id, None) match {
+ case Some(squad) =>
+ DisbandSquad(squad)
+ case None => ;
+ }
+
+ 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 = squad.Leader
+ val (member, index) = membership.zipWithIndex.find { case (_member, _) => _member.CharId == promotedPlayer }.get
+ val features = squadFeatures(squad.GUID)
+ SwapMemberPosition(leader, member)
+ //move around invites so that the proper squad leader deals with them
+ val leaderInvite = invites.remove(promotingPlayer)
+ val leaderQueuedInvites = queuedInvites.remove(promotingPlayer).toList.flatten
+ invites.get(promotedPlayer).orElse(previousInvites.get(promotedPlayer)) match {
+ case Some(_) =>
+ //the promoted player has an active invite; queue these
+ queuedInvites += promotedPlayer -> (leaderInvite.toList ++ leaderQueuedInvites ++ queuedInvites.remove(promotedPlayer).toList.flatten)
+ case None if leaderInvite.nonEmpty =>
+ //no active invite for the promoted player, but the leader had an active invite; trade the queued invites
+ val invitation = leaderInvite.get
+ AddInviteAndRespond(promotedPlayer, invitation, invitation.InviterCharId, invitation.InviterName)
+ queuedInvites += promotedPlayer -> (leaderQueuedInvites ++ queuedInvites.remove(promotedPlayer).toList.flatten)
+ case None =>
+ //no active invites for anyone; assign the first queued invite from the promoting player, if available, and queue the rest
+ leaderQueuedInvites match {
+ case Nil => ;
+ case x :: xs =>
+ AddInviteAndRespond(promotedPlayer, x, x.InviterCharId, x.InviterName)
+ queuedInvites += promotedPlayer -> (xs ++ queuedInvites.remove(promotedPlayer).toList.flatten)
+ }
+ }
+ debug(s"Promoting player ${leader.Name} to be the leader of ${squad.Task}")
+ Publish(features.ToChannel, SquadResponse.PromoteMember(squad, promotedPlayer, index, 0))
+ if(features.Listed) {
+ Publish(promotingPlayer, SquadResponse.SetListSquad(PlanetSideGUID(0)))
+ Publish(promotedPlayer, SquadResponse.SetListSquad(squad.GUID))
+ }
+ UpdateSquadListWhenListed(
+ features,
+ SquadInfo().Leader(leader.Name)
+ )
+ UpdateSquadDetail(
+ squad.GUID,
+ SquadDetail()
+ .LeaderCharId(leader.CharId)
+ .Field3(value = 0L)
+ .LeaderName(leader.Name)
+ .Members(List(
+ SquadPositionEntry(0, SquadPositionDetail().CharId(leader.CharId).Name(leader.Name)),
+ SquadPositionEntry(index, SquadPositionDetail().CharId(member.CharId).Name(member.Name))
+ ))
+ )
+ case _ => ;
+ }
+
+ case SquadAction.Membership(event, _, _, _, _) =>
+ debug(s"SquadAction.Membership: $event is not yet supported")
+
+ case SquadAction.Waypoint(_, wtype, _, info) =>
+ val playerCharId = tplayer.CharId
+ (GetLeadingSquad(tplayer, None) match {
+ case Some(squad) =>
+ info match {
+ case Some(winfo) =>
+ (Some(squad), AddWaypoint(squad.GUID, wtype, winfo))
+ case _ =>
+ RemoveWaypoint(squad.GUID, wtype)
+ (Some(squad), None)
+ }
+ case _ => (None, None)
+ }) match {
+ case (Some(squad), Some(_)) =>
+ //waypoint added or updated
+ Publish(
+ s"${squadFeatures(squad.GUID).ToChannel}",
+ SquadResponse.WaypointEvent(WaypointEventAction.Add, playerCharId, wtype, None, info, 1),
+ Seq(tplayer.CharId)
+ )
+ case (Some(squad), None) =>
+ //waypoint removed
+ Publish(
+ s"${squadFeatures(squad.GUID).ToChannel}",
+ SquadResponse.WaypointEvent(WaypointEventAction.Remove, playerCharId, wtype, None, None, 0),
+ Seq(tplayer.CharId)
+ )
+
+ case msg =>
+ log.warn(s"Unsupported squad waypoint behavior: $msg")
+ }
+
+ case SquadAction.Definition(guid, line, action) =>
+ import net.psforever.packet.game.SquadAction._
+ val pSquadOpt = GetParticipatingSquad(tplayer)
+ val lSquadOpt = GetLeadingSquad(tplayer, pSquadOpt)
+ //the following actions can only be performed by a squad's leader
+ action match {
+ case SaveSquadFavorite() =>
+ val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
+ if(squad.Task.nonEmpty && squad.ZoneId > 0) {
+ tplayer.SquadLoadouts.SaveLoadout(squad, squad.Task, line)
+ Publish(sender, SquadResponse.ListSquadFavorite(line, squad.Task))
+ }
+
+ case LoadSquadFavorite() =>
+ if(pSquadOpt.isEmpty || pSquadOpt == lSquadOpt) {
+ val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
+ tplayer.SquadLoadouts.LoadLoadout(line) match {
+ case Some(loadout : SquadLoadout) if squad.Size == 1 =>
+ SquadService.LoadSquadDefinition(squad, loadout)
+ UpdateSquadListWhenListed(squadFeatures(squad.GUID), SquadService.SquadList.Publish(squad))
+ Publish(sender, SquadResponse.AssociateWithSquad(PlanetSideGUID(0)))
+ InitSquadDetail(PlanetSideGUID(0), Seq(tplayer.CharId), squad)
+ UpdateSquadDetail(squad)
+ Publish(sender, SquadResponse.AssociateWithSquad(squad.GUID))
+ case _ =>
+ }
+ }
+
+ case DeleteSquadFavorite() =>
+ tplayer.SquadLoadouts.DeleteLoadout(line)
+ Publish(sender, SquadResponse.ListSquadFavorite(line, ""))
+
+ case ChangeSquadPurpose(purpose) =>
+ val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
+ squad.Task = purpose
+ UpdateSquadListWhenListed(squadFeatures(squad.GUID), SquadInfo().Task(purpose))
+ UpdateSquadDetail(squad.GUID, SquadDetail().Task(purpose))
+
+ case ChangeSquadZone(zone_id) =>
+ val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
+ squad.ZoneId = zone_id.zoneId.toInt
+ UpdateSquadListWhenListed(squadFeatures(squad.GUID), SquadInfo().ZoneId(zone_id))
+ InitialAssociation(squad)
+ Publish(sender, SquadResponse.Detail(squad.GUID, SquadService.Detail.Publish(squad)))
+ UpdateSquadDetail(
+ squad.GUID,
+ squad.GUID,
+ Seq(squad.Leader.CharId),
+ SquadDetail().ZoneId(zone_id)
+ )
+
+ case CloseSquadMemberPosition(position) =>
+ val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
+ squad.Availability.lift(position) match {
+ case Some(true) if position > 0 => //do not close squad leader position; undefined behavior
+ squad.Availability.update(position, false)
+ val memberPosition = squad.Membership(position)
+ if(memberPosition.CharId > 0) {
+ LeaveSquad(memberPosition.CharId, squad)
+ }
+ UpdateSquadListWhenListed(squadFeatures(squad.GUID), SquadInfo().Capacity(squad.Capacity))
+ UpdateSquadDetail(
+ squad.GUID,
+ SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail.Closed)))
+ )
+ case Some(false) | None => ;
+ }
+
+ case AddSquadMemberPosition(position) =>
+ val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
+ squad.Availability.lift(position) match {
+ case Some(false) =>
+ squad.Availability.update(position, true)
+ UpdateSquadListWhenListed(squadFeatures(squad.GUID), SquadInfo().Capacity(squad.Capacity))
+ UpdateSquadDetail(
+ squad.GUID,
+ SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail.Open)))
+ )
+ case Some(true) | None => ;
+ }
+
+ case ChangeSquadMemberRequirementsRole(position, role) =>
+ val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
+ squad.Availability.lift(position) match {
+ case Some(true) =>
+ squad.Membership(position).Role = role
+ UpdateSquadDetail(
+ squad.GUID,
+ SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail().Role(role))))
+ )
+ case Some(false) | None => ;
+ }
+
+ case ChangeSquadMemberRequirementsDetailedOrders(position, orders) =>
+ val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
+ squad.Availability.lift(position) match {
+ case Some(true) =>
+ squad.Membership(position).Orders = orders
+ UpdateSquadDetail(
+ squad.GUID,
+ SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail().DetailedOrders(orders))))
+ )
+ case Some(false) | None => ;
+ }
+
+ case ChangeSquadMemberRequirementsCertifications(position, certs) =>
+ val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
+ squad.Availability.lift(position) match {
+ case Some(true) =>
+ squad.Membership(position).Requirements = certs
+ UpdateSquadDetail(
+ squad.GUID,
+ SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail().Requirements(certs))))
+ )
+ case Some(false) | None => ;
+ }
+
+ case LocationFollowsSquadLead(state) =>
+ val features = squadFeatures(lSquadOpt.getOrElse(StartSquad(tplayer)).GUID)
+ features.LocationFollowsSquadLead = state
+
+ case AutoApproveInvitationRequests(state) =>
+ val features = squadFeatures(lSquadOpt.getOrElse(StartSquad(tplayer)).GUID)
+ features.AutoApproveInvitationRequests = state
+ if(state) {
+ //allowed auto-approval - resolve the requests (only)
+ val charId = tplayer.CharId
+ val (requests, others) = (invites.get(charId).toList ++ queuedInvites.get(charId).toList)
+ .partition({ case _ : RequestRole => true})
+ invites.remove(charId)
+ queuedInvites.remove(charId)
+ previousInvites.remove(charId)
+ requests.foreach {
+ case request : RequestRole =>
+ JoinSquad(request.player, features.Squad, request.position)
+ case _ => ;
+ }
+ others.collect { case invite : Invitation => invite } match {
+ case Nil => ;
+ case x :: Nil =>
+ AddInviteAndRespond(charId, x, x.InviterCharId, x.InviterName)
+ case x :: xs =>
+ AddInviteAndRespond(charId, x, x.InviterCharId, x.InviterName)
+ queuedInvites += charId -> xs
+ }
+ }
+
+ case FindLfsSoldiersForRole(position) =>
+ lSquadOpt match {
+ case Some(squad) =>
+ val sguid = squad.GUID
+ val features = squadFeatures(sguid)
+ features.SearchForRole match {
+ case Some(-1) =>
+ //a proximity invitation has not yet cleared; nothing will be gained by trying to invite for a specific role
+ debug("FindLfsSoldiersForRole: waiting for proximity invitations to clear")
+ case _ =>
+ //either no role has ever been recruited, or some other role has been recruited
+ //normal LFS recruitment for the given position
+ val excusedInvites = features.Refuse
+ val faction = squad.Faction
+ val requirementsToMeet = squad.Membership(position).Requirements
+ val outstandingActiveInvites = features.SearchForRole match {
+ case Some(pos) =>
+ RemoveQueuedInvitesForSquadAndPosition(sguid, pos)
+ invites.collect { case(charId, LookingForSquadRoleInvite(_,_, squad_guid, role)) if squad_guid == sguid && role == pos => charId }
+ case None =>
+ List.empty[Long]
+ }
+ features.SearchForRole = position
+ //this will update the role entry in the GUI to visually indicate being searched for; only one will be displayed at a time
+ Publish(
+ tplayer.CharId,
+ SquadResponse.Detail(
+ sguid,
+ SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail().CharId(char_id = 0L).Name(name = ""))))
+ )
+ )
+ //collect all players that are eligible for invitation to the new position
+ //divide into players with an active invite (A) and players with a queued invite (B)
+ //further filter (A) into players whose invitation is renewed (A1) and new invitations (A2)
+ //TODO only checks the leader's current zone; should check all zones
+ (zone.LivePlayers
+ .collect { case player
+ if !excusedInvites.contains(player.CharId) &&
+ faction == player.Faction && player.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[LookingForSquadRoleInvite]
+ invites(charId) = LookingForSquadRoleInvite(bid.char_id, bid.name, sguid, position)
+ }
+ //players who were actively invited for the previous position but are ineligible for the new position
+ (features.ProxyInvites filterNot (outstandingPlayerList contains)) foreach RemoveInvite
+ features.ProxyInvites = outstandingPlayerList ++ invitedPlayerList
+ Some(invitedPlayerList)
+ }) match {
+ //add invitations for position in squad
+ case Some(invitedPlayers) =>
+ val invitingPlayer = tplayer.CharId
+ val name = tplayer.Name
+ invitedPlayers.foreach { invitedPlayer =>
+ AddInviteAndRespond(
+ invitedPlayer,
+ LookingForSquadRoleInvite(invitingPlayer, name, sguid, position),
+ invitingPlayer,
+ name
+ )
+ }
+ case None => ;
+ }
+ }
+
+ case _ => ;
+ }
+
+ case CancelFind() =>
+ lSquadOpt match {
+ case Some(squad) =>
+ val sguid = squad.GUID
+ val position = squadFeatures(sguid).SearchForRole
+ squadFeatures(sguid).SearchForRole = None
+ //remove active invites
+ invites.filter {
+ case (_, LookingForSquadRoleInvite(_, _, _guid, pos)) => _guid == sguid && position.contains(pos)
+ case _ => false
+ }
+ .keys.foreach { charId =>
+ RemoveInvite(charId)
+ }
+ //remove queued invites
+ queuedInvites.foreach { case (charId, queue) =>
+ val filtered = queue.filterNot {
+ case LookingForSquadRoleInvite(_, _, _guid, _) => _guid == sguid
+ case _ => false
+ }
+ queuedInvites += charId -> filtered
+ if(filtered.isEmpty) {
+ queuedInvites.remove(charId)
+ }
+ }
+ //remove yet-to-be invitedPlayers
+ squadFeatures(sguid).ProxyInvites = Nil
+ case _ => ;
+ }
+
+ case RequestListSquad() =>
+ val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
+ val features = squadFeatures(squad.GUID)
+ if(!features.Listed && squad.Task.nonEmpty && squad.ZoneId > 0) {
+ features.Listed = true
+ InitialAssociation(squad)
+ Publish(sender, SquadResponse.SetListSquad(squad.GUID))
+ UpdateSquadList(squad, None)
+ }
+
+ case StopListSquad() =>
+ val squad = lSquadOpt.getOrElse(StartSquad(tplayer))
+ val features = squadFeatures(squad.GUID)
+ if(features.Listed) {
+ features.Listed = false
+ Publish(sender, SquadResponse.SetListSquad(PlanetSideGUID(0)))
+ UpdateSquadList(squad, None)
+ }
+
+ case ResetAll() =>
+ lSquadOpt match {
+ case Some(squad) if squad.Size > 1 =>
+ val guid = squad.GUID
+ squad.Task = ""
+ squad.ZoneId = None
+ squad.Availability.indices.foreach { i =>
+ squad.Availability.update(i, true)
+ }
+ squad.Membership.foreach(position => {
+ position.Role = ""
+ position.Orders = ""
+ position.Requirements = Set()
+ })
+ val features = squadFeatures(squad.GUID)
+ features.LocationFollowsSquadLead = true
+ features.AutoApproveInvitationRequests = true
+ if(features.Listed) {
+ //unlist the squad
+ features.Listed = false
+ Publish(features.ToChannel, SquadResponse.SetListSquad(PlanetSideGUID(0)))
+ UpdateSquadList(squad, None)
+ }
+ UpdateSquadDetail(squad)
+ InitialAssociation(squad)
+ squadFeatures(guid).InitialAssociation = true
+ case Some(squad) =>
+ //underutilized squad; just close it out
+ CloseSquad(squad)
+ case _ => ;
+ }
+
+ case _ =>
+ (pSquadOpt, action) match {
+ //the following action can be performed by the squad leader and maybe an unaffiliated player
+ case (Some(_), SelectRoleForYourself(_)) =>
+ //TODO should be possible, but doesn't work correctly due to UI squad cards not updating as expected
+// if(squad.Leader.CharId == tplayer.CharId) {
+// //squad leader currently disallowed
+// } else
+// //the squad leader may swap to any open position; a normal member has to validate against requirements
+// if(squad.Leader.CharId == tplayer.CharId || squad.isAvailable(position, tplayer.Certifications)) {
+// squad.Membership.zipWithIndex.find { case (member, _) => member.CharId == tplayer.CharId } match {
+// case Some((fromMember, fromIndex)) =>
+// SwapMemberPosition(squad.Membership(position), fromMember)
+// Publish(squadFeatures(squad.GUID).ToChannel, SquadResponse.AssignMember(squad, fromIndex, position))
+// UpdateSquadDetail(squad)
+// case _ => ;
+// //somehow, this is not our squad; do nothing, for now
+// }
+// }
+// else {
+// //not qualified for requested position
+// }
+
+ //the following action can be performed by an unaffiliated player
+ case (None, SelectRoleForYourself(position)) =>
+ //not a member of any squad, but we might become a member of this one
+ GetSquad(guid) match {
+ case Some(squad) =>
+ if(squad.isAvailable(position, tplayer.Certifications)) {
+ //we could join but we may need permission from the squad leader first
+ AddInviteAndRespond(
+ squad.Leader.CharId,
+ RequestRole(tplayer, guid, position),
+ invitingPlayer = 0L, //we ourselves technically are ...
+ tplayer.Name
+ )
+ }
+ case None => ;
+ //squad does not exist? assume old local data; force update to correct discrepancy
+ }
+
+ //the following action can be performed by anyone who has tried to join a squad
+ case (_, CancelSelectRoleForYourself(_)) =>
+ val cancellingPlayer = tplayer.CharId
+ GetSquad(guid) match {
+ case Some(squad) =>
+ //assumption: a player who is cancelling will rarely end up with their invite queued
+ val leaderCharId = squad.Leader.CharId
+ //clean up any active RequestRole invite entry where we are the player who wants to join the leader's squad
+ ((invites.get(leaderCharId) match {
+ case out @ Some(entry) if entry.isInstanceOf[RequestRole] &&
+ entry.asInstanceOf[RequestRole].player.CharId == cancellingPlayer =>
+ out
+ case _ =>
+ None
+ }) match {
+ case Some(entry : RequestRole) =>
+ RemoveInvite(leaderCharId)
+ Publish(leaderCharId, SquadResponse.Membership(SquadResponseType.Cancel, 0, 0, cancellingPlayer, None, entry.player.Name, false, Some(None)))
+ NextInviteAndRespond(leaderCharId)
+ Some(true)
+ case _ =>
+ None
+ }).orElse(
+ //look for a queued RequestRole entry where we are the player who wants to join the leader's squad
+ (queuedInvites.get(leaderCharId) match {
+ case Some(_list) =>
+ (_list, _list.indexWhere { entry =>
+ entry.isInstanceOf[RequestRole] &&
+ entry.asInstanceOf[RequestRole].player.CharId == cancellingPlayer
+ })
+ case None =>
+ (Nil, -1)
+ }) match {
+ case (_, -1) =>
+ None //no change
+ case (list, _) if list.size == 1 =>
+ val entry = list.head.asInstanceOf[RequestRole]
+ Publish(leaderCharId, SquadResponse.Membership(SquadResponseType.Cancel, 0, 0, cancellingPlayer, None, entry.player.Name, false, Some(None)))
+ queuedInvites.remove(leaderCharId)
+ Some(true)
+ case (list, index) =>
+ val entry = list(index).asInstanceOf[RequestRole]
+ Publish(leaderCharId, SquadResponse.Membership(SquadResponseType.Cancel, 0, 0, cancellingPlayer, None, entry.player.Name, false, Some(None)))
+ queuedInvites(leaderCharId) = list.take(index) ++ list.drop(index+1)
+ Some(true)
+ }
+ )
+
+ case _ => ;
+ }
+
+ //the following action can be performed by ???
+ case (Some(squad), AssignSquadMemberToRole(position, char_id)) =>
+ val membership = squad.Membership.zipWithIndex
+ (membership.find({ case (member, _) => member.CharId == char_id}), membership(position)) match {
+ //TODO squad leader currently disallowed
+ case (Some((fromMember, fromPosition)), (toMember, _)) if fromPosition != 0 =>
+ val name = fromMember.Name
+ SwapMemberPosition(toMember, fromMember)
+ Publish(squadFeatures(guid).ToChannel, SquadResponse.AssignMember(squad, fromPosition, position))
+ UpdateSquadDetail(
+ squad.GUID,
+ SquadDetail().Members(List(
+ SquadPositionEntry(position, SquadPositionDetail().CharId(fromMember.CharId).Name(fromMember.Name)),
+ SquadPositionEntry(fromPosition, SquadPositionDetail().CharId(char_id).Name(name))
+ ))
+ )
+ case _ => ;
+ }
+
+ //the following action can be peprformed by anyone
+ case (_, SearchForSquadsWithParticularRole(_/*role*/, _/*requirements*/, _/*zone_id*/, _/*search_mode*/)) =>
+ //though we should be able correctly search squads as is intended
+ //I don't know how search results should be prioritized or even how to return search results to the user
+ Publish(sender, SquadResponse.SquadSearchResults())
+
+ //the following action can be performed by anyone
+ case (_, DisplaySquad()) =>
+ val charId = tplayer.CharId
+ GetSquad(guid) match {
+ case Some(squad) if memberToSquad.get(charId).isEmpty =>
+ continueToMonitorDetails += charId -> squad.GUID
+ Publish(sender, SquadResponse.Detail(squad.GUID, SquadService.Detail.Publish(squad)))
+ case Some(squad) =>
+ Publish(sender, SquadResponse.Detail(squad.GUID, SquadService.Detail.Publish(squad)))
+ case _ => ;
+ }
+
+ //the following message is feedback from a specific client, awaiting proper initialization
+ case (_, SquadMemberInitializationIssue()) =>
+// GetSquad(guid) match {
+// case Some(squad) =>
+// Publish(sender, SquadResponse.Detail(squad.GUID, SquadService.Detail.Publish(squad)))
+// case None => ;
+// }
+
+ case msg => ;
+ log.warn(s"Unsupported squad definition behavior: $msg")
+ }
+ }
+
+ case SquadAction.Update(char_id, health, max_health, armor, max_armor, pos, zone_number) =>
+ memberToSquad.get(char_id) match {
+ case Some(squad) =>
+ squad.Membership.find(_.CharId == char_id) match {
+ case Some(member) =>
+ member.Health = StatConverter.Health(health, max_health, min=1, max=64)
+ member.Armor = StatConverter.Health(armor, max_armor, min=1, max=64)
+ member.Position = pos
+ member.ZoneId = zone_number
+ Publish(
+ sender,
+ SquadResponse.UpdateMembers(
+ squad,
+ squad.Membership
+ .filterNot { _.CharId == 0 }
+ .map { member => SquadAction.Update(member.CharId, member.Health, 0, member.Armor, 0, member.Position, member.ZoneId) }
+ .toList
+ )
+ )
+ case _ => ;
+ }
+
+ case None => ;
+ }
+
+ case msg =>
+ debug(s"Unhandled message $msg from $sender")
+ }
+
+ case msg =>
+ debug(s"Unhandled message $msg from $sender")
+ }
+
+ /**
+ * This player has refused to join squad leader's squads or some other players's offers to form a squad.
+ * @param charId the player who refused other players
+ * @return the list of other players who have been refused
+ */
+ def Refused(charId : Long) : List[Long] = refused.getOrElse(charId, Nil)
+
+ /**
+ * This player has refused to join squad leader's squads or some other players's offers to form a squad.
+ * @param charId the player who is doing the refusal
+ * @param refusedCharId the player who is refused
+ * @return the list of other players who have been refused
+ */
+ def Refused(charId : Long, refusedCharId : Long) : List[Long] = {
+ if(charId != refusedCharId) {
+ Refused(charId, List(refusedCharId))
+ }
+ else {
+ Nil
+ }
+ }
+
+ /**
+ * This player has refused to join squad leader's squads or some other players's offers to form a squad.
+ * @param charId the player who is doing the refusal
+ * @param list the players who are refused
+ * @return the list of other players who have been refused
+ */
+ def Refused(charId : Long, list : List[Long]) : List[Long] = {
+ refused.get(charId) match {
+ case Some(refusedList) =>
+ refused(charId) = list ++ refusedList
+ Refused(charId)
+ case None =>
+ Nil
+ }
+ }
+
+ /**
+ * Assign a provided invitation object to either the active or inactive position for a player.
+ *
+ * The determination for the active position is whether or not something is currently in the active position
+ * or whether some mechanism tried to shift invitation object into the active position
+ * but found nothing to shift.
+ * If an invitation object originating from the reported player already exists,
+ * a new one is not appended to the inactive queue.
+ * This method should always be used as the entry point for the active and inactive invitation options
+ * or as a part of the entry point for the aforesaid options.
+ * @see `AddInviteAndRespond`
+ * @see `AltAddInviteAndRespond`
+ * @param invitedPlayer the unique character identifier for the player being invited;
+ * in actuality, represents the player who will address the invitation object
+ * @param invite the "new" invitation envelop object
+ * @return an optional invite;
+ * the invitation object in the active invite position;
+ * `None`, if it is not added to either the active option or inactive position
+ */
+ def AddInvite(invitedPlayer : Long, invite : Invitation) : Option[Invitation] = {
+ invites.get(invitedPlayer).orElse(previousInvites.get(invitedPlayer)) match {
+ case Some(_bid) =>
+ //the active invite does not interact with the given invite; add to queued invites
+ queuedInvites.get(invitedPlayer) match {
+ case Some(bidList) =>
+ //ensure that new invite does not interact with the queue's invites by invitingPlayer info
+ if(_bid.InviterCharId != invite.InviterCharId && !bidList.exists { eachBid => eachBid.InviterCharId == invite.InviterCharId }) {
+ queuedInvites(invitedPlayer) = invite match {
+ case _: RequestRole =>
+ val (normals, others) = bidList.partition(_.isInstanceOf[RequestRole])
+ (normals :+ invite) ++ others
+ case _ =>
+ bidList :+ invite
+ }
+ Some(_bid)
+ }
+ else {
+ None
+ }
+ case None =>
+ if(_bid.InviterCharId != invite.InviterCharId) {
+ queuedInvites(invitedPlayer) = List[Invitation](invite)
+ Some(_bid)
+ }
+ else {
+ None
+ }
+ }
+
+ case None =>
+ invites(invitedPlayer) = invite
+ Some(invite)
+ }
+ }
+
+ /**
+ * Component method used for the response behavior for processing the invitation object as an `IndirectInvite` object.
+ * @see `HandleRequestRole`
+ * @param invite the original invitation object that started this process
+ * @param player the target of the response and invitation
+ * @param invitedPlayer the unique character identifier for the player being invited;
+ * in actuality, represents the player who will address the invitation object;
+ * not useful here
+ * @param invitingPlayer the unique character identifier for the player who invited the former;
+ * not useful here
+ * @param name a name to be used in message composition;
+ * not useful here
+ * @return na
+ */
+ def indirectInviteResp(invite : IndirectInvite, player : Player, invitedPlayer : Long, invitingPlayer : Long, name : String) : Boolean = {
+ HandleRequestRole(invite, player)
+ }
+
+ /**
+ * Component method used for the response behavior for processing the invitation object as an `IndirectInvite` object.
+ * @see `HandleRequestRole`
+ * @param invite the original invitation object that started this process
+ * @param player the target of the response and invitation
+ * @param invitedPlayer the unique character identifier for the player being invited
+ * in actuality, represents the player who will address the invitation object
+ * @param invitingPlayer the unique character identifier for the player who invited the former
+ * @param name a name to be used in message composition
+ * @return na
+ */
+ def altIndirectInviteResp(invite : IndirectInvite, player : Player, invitedPlayer : Long, invitingPlayer : Long, name : String) : Boolean = {
+ Publish(invitingPlayer, SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitingPlayer, Some(invitedPlayer), player.Name, false, Some(None)))
+ HandleRequestRole(invite, player)
+ }
+
+ /**
+ * A branched response for processing (new) invitation objects that have been submitted to the system.
+ *
+ * A comparison is performed between the original invitation object and an invitation object
+ * that represents the potential modification or redirection of the current active invitation obect.
+ * Any further action is only performed when an "is equal" comparison is `true`.
+ * When passing, the system publishes up to two messages
+ * to users that would anticipate being informed of squad join activity.
+ * @param indirectVacancyFunc the method that cans the respondign behavior should an `IndirectVacancy` object being consumed
+ * @param targetInvite a comparison invitation object;
+ * represents the unmodified, unadjusted invite
+ * @param actualInvite a comparaison invitation object;
+ * proper use of this field should be the output of another process upon the following `actualInvite`
+ * @param invitedPlayer the unique character identifier for the player being invited
+ * in actuality, represents the player who will address the invitation object
+ * @param invitingPlayer the unique character identifier for the player who invited the former
+ * @param name a name to be used in message composition
+ */
+ def InviteResponseTemplate(indirectVacancyFunc : (IndirectInvite, Player, Long, Long, String) => Boolean)(targetInvite : Invitation, actualInvite : Option[Invitation], invitedPlayer : Long, invitingPlayer : Long, name : String) : Unit = {
+ if(actualInvite.contains(targetInvite)) {
+ //immediately respond
+ targetInvite match {
+ case VacancyInvite(charId, _name, _) =>
+ Publish(invitedPlayer, SquadResponse.Membership(SquadResponseType.Invite, 0, 0, charId, Some(invitedPlayer), _name, false, Some(None)))
+ Publish(charId, SquadResponse.Membership(SquadResponseType.Invite, 0, 0, invitedPlayer, Some(charId), _name, true, Some(None)))
+
+ case _bid @ IndirectInvite(player, _) =>
+ indirectVacancyFunc(_bid, player, invitedPlayer, invitingPlayer, name)
+
+ case _bid @ SpontaneousInvite(player) =>
+ val bidInvitingPlayer = _bid.InviterCharId
+ Publish(invitedPlayer, SquadResponse.Membership(SquadResponseType.Invite, 0, 0, bidInvitingPlayer, Some(invitedPlayer), player.Name, false, Some(None)))
+ Publish(bidInvitingPlayer, SquadResponse.Membership(SquadResponseType.Invite, 0, 0, invitedPlayer, Some(bidInvitingPlayer), player.Name, true, Some(None)))
+
+ case _bid @ RequestRole(player, _, _) =>
+ HandleRequestRole(_bid, player)
+
+ case LookingForSquadRoleInvite(charId, _name, _, _) =>
+ Publish(invitedPlayer, SquadResponse.Membership(SquadResponseType.Invite, 0, 0, invitedPlayer, Some(charId), _name, false, Some(None)))
+
+ case ProximityInvite(charId, _name, _) =>
+ Publish(invitedPlayer, SquadResponse.Membership(SquadResponseType.Invite, 0, 0, invitedPlayer, Some(charId), _name, false, Some(None)))
+
+ case _ =>
+ log.warn(s"AddInviteAndRespond: can not parse discovered unhandled invitation type - $targetInvite")
+ }
+ }
+ }
+
+ /**
+ * Enqueue a newly-submitted invitation object
+ * either as the active position or into the inactive positions
+ * and dispatch a response for any invitation object that is discovered.
+ * Implementation of a workflow.
+ * @see `AddInvite`
+ * @see `indirectInviteResp`
+ * @param targetInvite a comparison invitation object
+ * @param invitedPlayer the unique character identifier for the player being invited;
+ * in actuality, represents the player who will address the invitation object
+ * @param invitingPlayer the unique character identifier for the player who invited the former
+ * @param name a name to be used in message composition
+ */
+ def AddInviteAndRespond(invitedPlayer : Long, targetInvite : Invitation, invitingPlayer : Long, name : String) : Unit = {
+ InviteResponseTemplate(indirectInviteResp)(
+ targetInvite,
+ AddInvite(invitedPlayer, targetInvite),
+ invitedPlayer,
+ invitingPlayer,
+ name
+ )
+ }
+
+ /**
+ * Enqueue a newly-submitted invitation object
+ * either as the active position or into the inactive positions
+ * and dispatch a response for any invitation object that is discovered.
+ * Implementation of a workflow.
+ * @see `AddInvite`
+ * @see `altIndirectInviteResp`
+ * @param targetInvite a comparison invitation object
+ * @param invitedPlayer the unique character identifier for the player being invited
+ * @param invitingPlayer the unique character identifier for the player who invited the former
+ * @param name a name to be used in message composition
+ */
+ def AltAddInviteAndRespond(invitedPlayer : Long, targetInvite : Invitation, invitingPlayer : Long, name : String) : Unit = {
+ InviteResponseTemplate(altIndirectInviteResp)(
+ targetInvite,
+ AddInvite(invitedPlayer, targetInvite),
+ invitedPlayer,
+ invitingPlayer,
+ name
+ )
+ }
+
+ /**
+ * Select the next invitation object to be shifted into the active position.
+ *
+ * The determination for the active position is whether or not something is currently in the active position
+ * or whether some mechanism tried to shift invitation object into the active position
+ * but found nothing to shift.
+ * After handling of the previous invitation object has completed or finished,
+ * the temporary block on adding new invitations is removed
+ * and any queued inactive invitation on the head of the inactive queue is shifted into the active position.
+ * @see `NextInviteAndRespond`
+ * @param invitedPlayer the unique character identifier for the player being invited;
+ * in actuality, represents the player who will address the invitation object
+ * @return an optional invite;
+ * the invitation object in the active invite position;
+ * `None`, if not shifted into the active position
+ */
+ def NextInvite(invitedPlayer : Long) : Option[Invitation] = {
+ previousInvites.remove(invitedPlayer)
+ invites.get(invitedPlayer) match {
+ case None =>
+ queuedInvites.get(invitedPlayer) match {
+ case Some(list) =>
+ list match {
+ case Nil =>
+ None
+ case x :: Nil =>
+ invites(invitedPlayer) = x
+ queuedInvites.remove(invitedPlayer)
+ Some(x)
+ case x :: xs =>
+ invites(invitedPlayer) = x
+ queuedInvites(invitedPlayer) = xs
+ Some(x)
+ }
+
+ case None =>
+ None
+ }
+ case Some(_) =>
+ None
+ }
+ }
+
+ /**
+ * Select the next invitation object to be shifted into the active position
+ * and dispatch a response for any invitation object that is discovered.
+ * @see `InviteResponseTemplate`
+ * @see `NextInvite`
+ * @param invitedPlayer the unique character identifier for the player being invited;
+ * in actuality, represents the player who will address the invitation object
+ * @return an optional invite;
+ * the invitation object in the active invite position;
+ * `None`, if not shifted into the active position
+ */
+ def NextInviteAndRespond(invitedPlayer : Long) : Unit = {
+ NextInvite(invitedPlayer) match {
+ case Some(invite) =>
+ InviteResponseTemplate(indirectInviteResp)(
+ invite,
+ Some(invite),
+ invitedPlayer,
+ invite.InviterCharId,
+ invite.InviterName
+ )
+ case None => ;
+ }
+ }
+
+ /**
+ * Remove any invitation object from the active position.
+ * Flag the temporary field to indicate that the active position, while technically available,
+ * should not yet have a new invitation object shifted into it yet.
+ * This is the "proper" way to demote invitation objects from the active position
+ * whether or not they are to be handled.
+ * @see `NextInvite`
+ * @see `NextInviteAndRespond`
+ * @param invitedPlayer the unique character identifier for the player being invited;
+ * in actuality, represents the player who will address the invitation object
+ * @return an optional invite;
+ * the invitation object formerly in the active invite position;
+ * `None`, if no invitation was in the active position
+ */
+ def RemoveInvite(invitedPlayer : Long) : Option[Invitation] = {
+ invites.remove(invitedPlayer) match {
+ case out @ Some(invite) =>
+ previousInvites += invitedPlayer -> invite
+ out
+ case None =>
+ None
+ }
+ }
+
+ /**
+ * Remove all inactive invites.
+ * @param invitedPlayer the unique character identifier for the player being invited;
+ * in actuality, represents the player who will address the invitation object
+ * @return a list of the removed inactive invitation objects
+ */
+ def RemoveQueuedInvites(invitedPlayer : Long) : List[Invitation] = {
+ queuedInvites.remove(invitedPlayer) match {
+ case Some(_bidList) => _bidList
+ case None => Nil
+ }
+ }
+
+ /**
+ * Remove all active invitation objects that are related to the particular squad and the particular role in the squad.
+ * Specifically used to safely disarm obsolete invitation objects related to the specific criteria.
+ * Affects only certain invitation object types.
+ * @see `RequestRole`
+ * @see `LookingForSquadRoleInvite`
+ * @see `RemoveInvite`
+ * @see `RemoveQueuedInvitesForSquadAndPosition`
+ * @param guid the squad identifier
+ * @param position the role position index
+ */
+ def RemoveInvitesForSquadAndPosition(guid : PlanetSideGUID, position : Int) : Unit = {
+ //eliminate active invites for this role
+ invites.collect {
+ case(charId, LookingForSquadRoleInvite(_,_, sguid, pos)) if sguid == guid && pos == position =>
+ RemoveInvite(charId)
+ case (charId, RequestRole(_, sguid, pos)) if sguid == guid && pos == position =>
+ RemoveInvite(charId)
+ }
+ RemoveQueuedInvitesForSquadAndPosition(guid, position)
+ }
+
+ /**
+ * Remove all inactive invitation objects that are related to the particular squad and the particular role in the squad.
+ * Specifically used to safely disarm obsolete invitation objects by specific criteria.
+ * Affects only certain invitation object types.
+ * @see `RequestRole`
+ * @see `LookingForSquadRoleInvite`
+ * @see `RemoveInvitesForSquadAndPosition`
+ * @param guid the squad identifier
+ * @param position the role position index
+ */
+ def RemoveQueuedInvitesForSquadAndPosition(guid : PlanetSideGUID, position : Int) : Unit = {
+ //eliminate other invites for this role
+ queuedInvites.foreach { case(charId, queue) =>
+ val filtered = queue.filterNot {
+ case LookingForSquadRoleInvite(_,_, sguid, pos) => sguid == guid && pos == position
+ case RequestRole(_, sguid, pos) => sguid == guid && pos == position
+ case _ => false
+ }
+ if(filtered.isEmpty) {
+ queuedInvites.remove(charId)
+ }
+ else if(queue.size != filtered.size) {
+ queuedInvites += charId -> filtered
+ }
+ }
+ }
+
+ /**
+ * Remove all active and inactive invitation objects that are related to the particular squad.
+ * Specifically used to safely disarm obsolete invitation objects by specific criteria.
+ * Affects all invitation object types and all data structures that deal with the squad.
+ * @see `RequestRole`
+ * @see `IndirectInvite`
+ * @see `LookingForSquadRoleInvite`
+ * @see `ProximityInvite`
+ * @see `RemoveInvite`
+ * @see `VacancyInvite`
+ * @param sguid the squad identifier
+ */
+ def RemoveAllInvitesToSquad(sguid : PlanetSideGUID) : Unit = {
+ //clean up invites
+ invites.collect {
+ case (id, VacancyInvite(_, _, guid)) if sguid == guid =>
+ RemoveInvite(id)
+ case (id, IndirectInvite(_, guid)) if sguid == guid =>
+ RemoveInvite(id)
+ case (id, LookingForSquadRoleInvite(_, _, guid, _)) if sguid == guid =>
+ RemoveInvite(id)
+ case (id, RequestRole(_, guid, _)) if sguid == guid =>
+ RemoveInvite(id)
+ case (id, ProximityInvite(_, _, guid)) if sguid == guid =>
+ RemoveInvite(id)
+ }
+ //tidy the queued invitations
+ queuedInvites.foreach { case(id, queue) =>
+ val filteredQueue = queue.filterNot {
+ case VacancyInvite(_, _, guid) => sguid == guid
+ case IndirectInvite(_, guid) => sguid == guid
+ case LookingForSquadRoleInvite(_, _, guid, _) => sguid == guid
+ case RequestRole(_, guid, _) => sguid == guid
+ case ProximityInvite(_, _, guid) => sguid == guid
+ case _ => false
+ }
+ if(filteredQueue.isEmpty) {
+ queuedInvites.remove(id)
+ }
+ else if(filteredQueue.size != queue.size) {
+ queuedInvites.update(id, filteredQueue)
+ }
+ }
+ squadFeatures(sguid).ProxyInvites = Nil
+ squadFeatures(sguid).SearchForRole match {
+ case None => ;
+ case Some(_) =>
+ squadFeatures(sguid).SearchForRole = None
+ }
+ continueToMonitorDetails.collect {
+ case (charId, guid) if sguid == guid =>
+ continueToMonitorDetails.remove(charId)
+ }
+ }
+
+ /**
+ * Remove all active and inactive invitation objects that are related to the particular player.
+ * Specifically used to safely disarm obsolete invitation objects by specific criteria.
+ * Affects all invitation object types and all data structures that deal with the player.
+ * @see `RequestRole`
+ * @see `IndirectInvite`
+ * @see `LookingForSquadRoleInvite`
+ * @see `RemoveInvite`
+ * @see `RemoveProximityInvites`
+ * @see `VacancyInvite`
+ * @param charId the player's unique identifier number
+ */
+ def RemoveAllInvitesWithPlayer(charId : Long) : Unit = {
+ RemoveInvite(charId)
+ invites.collect {
+ case (id, SpontaneousInvite(player)) if player.CharId == charId =>
+ RemoveInvite(id)
+ case (id, VacancyInvite(_charId, _, _)) if _charId == charId =>
+ RemoveInvite(id)
+ case (id, IndirectInvite(player, _)) if player.CharId == charId =>
+ RemoveInvite(id)
+ case (id, LookingForSquadRoleInvite(_charId, _, _, _)) if _charId == charId =>
+ RemoveInvite(id)
+ case (id, RequestRole(player, _, _)) if player.CharId == charId =>
+ RemoveInvite(id)
+ }
+ //tidy the queued invitations
+ queuedInvites.remove(charId)
+ queuedInvites.foreach { case(id, queue) =>
+ val filteredQueue = queue.filterNot {
+ case SpontaneousInvite(player) => player.CharId == charId
+ case VacancyInvite(player, _, _) => player == charId
+ case IndirectInvite(player, _) => player.CharId == charId
+ case LookingForSquadRoleInvite(player, _, _, _) => player == charId
+ case RequestRole(player, _, _) => player.CharId == charId
+ case _ => false
+ }
+ if(filteredQueue.isEmpty) {
+ queuedInvites.remove(id)
+ }
+ else if(filteredQueue.size != queue.size) {
+ queuedInvites.update(id, filteredQueue)
+ }
+ }
+ continueToMonitorDetails.remove(charId)
+ RemoveProximityInvites(charId)
+ }
+
+ /**
+ * Remove all active and inactive proximity squad invites related to the recruiter.
+ * @see `RemoveProximityInvites(Iterable[(Long, PlanetSideGUID)])`
+ * @param invitingPlayer the player who did the recruiting
+ * @return a list of all players (unique character identifier number and name) who had active proximity invitations
+ */
+ def RemoveProximityInvites(invitingPlayer : Long) : Iterable[(Long, String)] = {
+ //invites
+ val (removedInvites, out) = invites.collect {
+ case (id, ProximityInvite(inviterCharId, inviterName, squadGUID)) if inviterCharId == invitingPlayer =>
+ RemoveInvite(id)
+ ((id, squadGUID), (id, inviterName))
+ }.unzip
+ RemoveProximityInvites(removedInvites)
+ //queued
+ RemoveProximityInvites(queuedInvites.flatMap { case (id : Long, inviteList) =>
+ val (outList, inList) = inviteList.partition {
+ case ProximityInvite(inviterCharId, _, _) if inviterCharId == invitingPlayer => true
+ case _ => false
+ }
+ if(inList.isEmpty) {
+ queuedInvites.remove(id)
+ }
+ else {
+ queuedInvites(id) = inList
+ }
+ outList.collect { case ProximityInvite(_, _, sguid : PlanetSideGUID) => id -> sguid }
+ })
+ out.toSeq.distinct
+ }
+
+ /**
+ * Remove all queued proximity squad invite information retained by the squad object.
+ * @see `RemoveProximityInvites(Long)`
+ * @see `SquadFeatures.ProxyInvites`
+ * @see `SquadFeatures.SearchForRole`
+ * @param list a list of players to squads with expected entry redundancy
+ */
+ def RemoveProximityInvites(list : Iterable[(Long, PlanetSideGUID)]) : Unit = {
+ val (_, squads) = list.unzip
+ squads.toSeq.distinct.foreach { squad =>
+ squadFeatures.get(squad) match {
+ case Some(features) =>
+ val out = list.collect { case (id, sguid) if sguid == squad => id } .toSeq
+ if((features.ProxyInvites = features.ProxyInvites filterNot out.contains) isEmpty) {
+ features.SearchForRole = None
+ }
+ case _ => ;
+ }
+ }
+ }
+
+ /**
+ * 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.
+ *
+ * Originally, the instigating type of invitation object was a "`VacancyInvite`"
+ * which indicated a type of undirected invitation extended from the squad leader to another player
+ * but the resolution is generalized enough to suffice for a number of invitation objects.
+ * First, an actual position is determined;
+ * then, the squad is tested for recruitment conditions,
+ * including whether the person who solicited the would-be member is still the squad leader.
+ * If the recruitment is manual and the squad leader is not the same as the recruiting player,
+ * then the real squad leader is sent an indirect query regarding the player's eligibility.
+ * These `IndirectInvite` invitation objects also are handled by calls to `HandleVacancyInvite`.
+ * @see `AltAddInviteAndRespond`
+ * @see `IndirectInvite`
+ * @see `SquadFeatures::AutoApproveInvitationRequests`
+ * @see `VacancyInvite`
+ * @param squad the squad
+ * @param invitedPlayer the unique character identifier for the player being invited
+ * @param invitingPlayer the unique character identifier for the player who invited the former
+ * @param recruit the player being invited
+ * @return the squad object and a role position index, if properly invited;
+ * `None`, otherwise
+ */
+ def HandleVacancyInvite(squad : Squad, invitedPlayer : Long, invitingPlayer : Long, recruit : Player) : Option[(Squad, Int)] = {
+ //accepted an invitation to join an existing squad
+ squad.Membership.zipWithIndex.find({ case (_, index) =>
+ squad.isAvailable(index, recruit.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.
+ *
+ * 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 {
+ Publish(leaderCharId, 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.
+ *
+ * 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.
+ *
+ * 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`
+ * @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 && squad.isAvailable(position, 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 = features.ToChannel
+ memberCharIds.foreach { charId =>
+ SquadEvents.subscribe(UserEvents(charId), s"/$toChannel/Squad")
+ Publish(charId,
+ SquadResponse.Join(
+ squad,
+ indices.filterNot(_ == position) :+ position,
+ 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 = features.ToChannel
+ //new member gets full squad UI updates
+ Publish(
+ charId,
+ SquadResponse.Join(
+ squad,
+ position +: squad.Membership
+ .zipWithIndex
+ .collect({ case (member, index) if member.CharId > 0 => index })
+ .filterNot(_ == position)
+ .toList,
+ toChannel
+ )
+ )
+ //other squad members see new member joining the squad
+ Publish(toChannel, SquadResponse.Join(squad, 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), s"/$toChannel/Squad")
+ }
+ 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.
+ *
+ * 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)), Seq(charId))
+ 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 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) =>
+ 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
+ )
+ }
+
+ /**
+ * 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.
+ *
+ * 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
+ fromMember.CharId = toMember.CharId
+ fromMember.ZoneId = toMember.ZoneId
+ fromMember.Position = toMember.Position
+ fromMember.Health = toMember.Health
+ fromMember.Armor = toMember.Armor
+ }
+ else {
+ fromMember.Name = ""
+ fromMember.CharId = 0L
+ }
+ toMember.Name = name
+ toMember.CharId = charId
+ toMember.ZoneId = zoneId
+ toMember.Position = pos
+ toMember.Health = health
+ toMember.Armor = armor
+ }
+
+ /**
+ * Display the indicated waypoint.
+ *
+ * Despite the name, no waypoints are actually "added."
+ * All of the waypoints constantly exist as long as the squad to which they are attached exists.
+ * They are merely "activated" and "deactivated."
+ * @see `SquadWaypointRequest`
+ * @see `WaypointInfo`
+ * @param guid the squad's unique identifier
+ * @param waypointType the type of the waypoint
+ * @param info information about the waypoint, as was reported by the client's packet
+ * @return the waypoint data, if the waypoint type is changed
+ */
+ def AddWaypoint(guid : PlanetSideGUID, waypointType : SquadWaypoints.Value, info : WaypointInfo) : Option[WaypointData] = {
+ squadFeatures.get(guid) match {
+ case Some(features) =>
+ features.Waypoints.lift(waypointType.id) match {
+ case Some(point) =>
+ point.zone_number = info.zone_number
+ point.pos = info.pos
+ Some(point)
+ case None =>
+ log.error(s"no squad waypoint $waypointType found")
+ None
+ }
+ case None =>
+ log.error(s"no squad waypoint $waypointType found")
+ None
+ }
+ }
+
+ /**
+ * Hide the indicated waypoint.
+ * Unused waypoints are marked by having a non-zero z-coordinate.
+ *
+ * Despite the name, no waypoints are actually "removed."
+ * All of the waypoints constantly exist as long as the squad to which they are attached exists.
+ * They are merely "activated" and "deactivated."
+ * @param guid the squad's unique identifier
+ * @param waypointType the type of the waypoint
+ */
+ def RemoveWaypoint(guid : PlanetSideGUID, waypointType : SquadWaypoints.Value) : Unit = {
+ squadFeatures.get(guid) match {
+ case Some(features) =>
+ features.Waypoints.lift(waypointType.id) match {
+ case Some(point) =>
+ point.pos = Vector3.z(1)
+ case _ =>
+ log.warn(s"no squad waypoint $waypointType found")
+ }
+ case _ =>
+ log.warn(s"no squad #$guid found")
+ }
+ }
+
+ /**
+ * Dispatch all of the information about a given squad's waypoints to a user.
+ * @param toCharId the player to whom the waypoint data will be dispatched
+ * @param guid the squad's unique identifier
+ */
+ def InitWaypoints(toCharId : Long, guid : PlanetSideGUID) : Unit = {
+ squadFeatures.get(guid) match {
+ case Some(features) =>
+ val squad = features.Squad
+ val vz1 = Vector3.z(value = 1)
+ val list = features.Waypoints
+ Publish(
+ toCharId, SquadResponse.InitWaypoints(squad.Leader.CharId,
+ list.zipWithIndex.collect { case (point, index) if point.pos != vz1 =>
+ (SquadWaypoints(index), WaypointInfo(point.zone_number, point.pos), 1)
+ }
+ )
+ )
+ case None => ;
+ }
+ }
+
+ /**
+ * 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)
+ 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
+ TryResetSquadId()
+ }
+
+ /**
+ * 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))
+ }
+
+ /**
+ * 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)
+ }
+ }
+
+ /**
+ * Dispatch a message entailing the composition of this squad
+ * and focus on any specific aspects of it, purported as being changed recently.
+ *
+ * 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.
+ *
+ * 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)))
+ }
+ }
+
+ /**
+ * Dispatch a message entailing the composition of this squad.
+ * This is considered the first time this information will be dispatched to any relevant observers
+ * so the details of the squad will be updated in full and be sent to all relevant observers,
+ * namely, all the occupants of the squad.
+ * External observers are ignored.
+ * @see `InitSquadDetail(PlanetSideGUID, Iterable[Long], Squad)`
+ * @param squad the squad
+ */
+ def InitSquadDetail(squad : Squad) : Unit = {
+ InitSquadDetail(
+ squad.GUID,
+ squad.Membership.collect { case member if member.CharId > 0 => member.CharId },
+ squad
+ )
+ }
+
+ /**
+ * Dispatch an intial message entailing the strategic information and the composition of this squad.
+ * The details of the squad will be updated in full and be sent to all indicated observers.
+ * @see `SquadService.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) }
+ }
+
+ /**
+ * 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)
+ }
+ }
+
+ /**
+ * 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))
+ }
+ /**
+ * 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
+ }
+
+ /**
+ * Utilized when one player attempts to join an existing squad in a specific role.
+ * Accessed by the joining player from the squad detail window.
+ * @param player the player who requested the role
+ * @param squad_guid the squad with the role
+ * @param position the index of the role
+ */
+ final case class RequestRole(player : Player, squad_guid : PlanetSideGUID, position : Int)
+ extends Invitation(player.CharId, player.Name)
+
+ /**
+ * Utilized when one squad member issues an invite for some other player.
+ * Accessed by an existing squad member using the "Invite" menu option on another player.
+ * @param char_id the unique character identifier of the player who sent the invite
+ * @param name the name the player who sent the invite
+ * @param squad_guid the squad
+ */
+ final case class VacancyInvite(char_id : Long, name : String, squad_guid : PlanetSideGUID)
+ extends Invitation(char_id, name)
+
+ /**
+ * Utilized to redirect an (accepted) invitation request to the proper squad leader.
+ * No direct action causes this message.
+ * @param player the player who would be joining the squad;
+ * may or may not have actually requested it in the first place
+ * @param squad_guid the squad
+ */
+ final case class IndirectInvite(player : Player, squad_guid : PlanetSideGUID)
+ extends Invitation(player.CharId, player.Name)
+
+ /**
+ * Utilized in conjunction with an external queuing data structure
+ * to search for and submit requests to other players
+ * for the purposes of fill out unoccupied squad roles.
+ * @param char_id the unique character identifier of the squad leader
+ * @param name the name of the squad leader
+ * @param squad_guid the squad
+ */
+ final case class ProximityInvite(char_id : Long, name : String, squad_guid : PlanetSideGUID)
+ extends Invitation(char_id, name)
+
+ /**
+ * Utilized in conjunction with an external queuing data structure
+ * to search for and submit requests to other players
+ * for the purposes of fill out an unoccupied squad role.
+ * @param char_id the unique character identifier of the squad leader
+ * @param name the name of the squad leader
+ * @param squad_guid the squad with the role
+ * @param position the index of the role
+ */
+ final case class LookingForSquadRoleInvite(char_id : Long, name : String, squad_guid : PlanetSideGUID, position : Int)
+ extends Invitation(char_id, name)
+
+ /**
+ * Utilized when one player issues an invite for some other player for a squad that does not yet exist.
+ * @param player na
+ */
+ final case class SpontaneousInvite(player : Player)
+ extends Invitation(player.CharId, player.Name)
+
+ object SquadList {
+ /**
+ * Produce complete squad information.
+ * @see `SquadInfo`
+ * @param squad the squad
+ * @return the squad's information to be used in the squad list
+ */
+ def Publish(squad : Squad) : SquadInfo = {
+ SquadInfo(
+ squad.Leader.Name,
+ squad.Task,
+ PlanetSideZoneID(squad.ZoneId),
+ squad.Size,
+ squad.Capacity,
+ squad.GUID
+ )
+ }
+ }
+
+ object Detail {
+ /**
+ * Produce complete squad membership details.
+ * @see `SquadDetail`
+ * @param squad the squad
+ * @return the squad's information to be used in the squad's detail window
+ */
+ def Publish(squad : Squad) : SquadDetail = {
+ SquadDetail()
+ .Field1(squad.GUID.guid)
+ .LeaderCharId(squad.Leader.CharId)
+ .LeaderName(squad.Leader.Name)
+ .Task(squad.Task)
+ .ZoneId(PlanetSideZoneID(squad.ZoneId))
+ .Members(
+ squad.Membership.zipWithIndex.map({ case (p, index) =>
+ SquadPositionEntry(index, if(squad.Availability(index)) {
+ SquadPositionDetail(p.Role, p.Orders, p.Requirements, p.CharId, p.Name)
+ }
+ else {
+ SquadPositionDetail.Closed
+ })
+ }).toList
+ )
+ .Complete
+ }
+ }
+
+ /**
+ * Clear the current detail about a squad's membership and replace it with a previously stored details.
+ * @param squad the squad
+ * @param favorite the loadout object
+ */
+ def LoadSquadDefinition(squad : Squad, favorite : SquadLoadout) : Unit = {
+ squad.Task = favorite.task
+ squad.ZoneId = favorite.zone_id.getOrElse(squad.ZoneId)
+ squad.Availability.indices.foreach { index => squad.Availability.update(index, false) }
+ squad.Membership.foreach { position =>
+ position.Role = ""
+ position.Orders = ""
+ position.Requirements = Set()
+ }
+ favorite.members.foreach { position =>
+ squad.Availability.update(position.index, true)
+ val member = squad.Membership(position.index)
+ member.Role = position.role
+ member.Orders = position.orders
+ member.Requirements = position.requirements
+ }
+ }
+}
diff --git a/common/src/main/scala/services/teamwork/SquadServiceMessage.scala b/common/src/main/scala/services/teamwork/SquadServiceMessage.scala
new file mode 100644
index 000000000..fd856ae02
--- /dev/null
+++ b/common/src/main/scala/services/teamwork/SquadServiceMessage.scala
@@ -0,0 +1,25 @@
+// Copyright (c) 2019 PSForever
+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
new file mode 100644
index 000000000..82fd13394
--- /dev/null
+++ b/common/src/main/scala/services/teamwork/SquadServiceResponse.scala
@@ -0,0 +1,45 @@
+// 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
+
+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)
+}
+
+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/common/src/main/scala/services/teamwork/SquadSwitchboard.scala b/common/src/main/scala/services/teamwork/SquadSwitchboard.scala
new file mode 100644
index 000000000..165d757c1
--- /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 000000000..37ba5ad0f
--- /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/common/src/test/scala/game/ReplicationStreamMessageTest.scala b/common/src/test/scala/game/ReplicationStreamMessageTest.scala
index d878b9374..2d39a2a63 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/common/src/test/scala/game/SquadDefinitionActionMessageTest.scala b/common/src/test/scala/game/SquadDefinitionActionMessageTest.scala
index a9657decb..3ef295770 100644
--- a/common/src/test/scala/game/SquadDefinitionActionMessageTest.scala
+++ b/common/src/test/scala/game/SquadDefinitionActionMessageTest.scala
@@ -3,12 +3,17 @@ package game
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"
@@ -30,18 +35,48 @@ 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 (00)" in {
+ PacketCoding.DecodePacket(string_00).require match {
+ case SquadDefinitionActionMessage(unk1, unk2, action) =>
+ unk1 mustEqual PlanetSideGUID(3)
+ unk2 mustEqual 0
+ action mustEqual DisplaySquad()
+ case _ =>
+ ko
+ }
+ }
+
"decode (03)" in {
PacketCoding.DecodePacket(string_03).require match {
- case SquadDefinitionActionMessage(action, unk1, unk2, str, int1, int2, long1, long2, bool) =>
- action mustEqual 3
- unk1 mustEqual 0
+ case SquadDefinitionActionMessage(unk1, unk2, action) =>
+ unk1 mustEqual PlanetSideGUID(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 SaveSquadFavorite()
+ case _ =>
+ ko
+ }
+ }
+
+ "decode (03)" in {
+ PacketCoding.DecodePacket(string_04).require match {
+ case SquadDefinitionActionMessage(unk1, unk2, action) =>
+ unk1 mustEqual PlanetSideGUID(0)
+ unk2 mustEqual 3
+ action mustEqual LoadSquadFavorite()
+ case _ =>
+ ko
+ }
+ }
+
+ "decode (07)" in {
+ PacketCoding.DecodePacket(string_07).require match {
+ case SquadDefinitionActionMessage(unk1, unk2, action) =>
+ unk1 mustEqual PlanetSideGUID(0)
+ unk2 mustEqual 3
+ action mustEqual ListSquadFavorite("Cops and Military Officers")
case _ =>
ko
}
@@ -49,16 +84,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
- unk1 mustEqual 0
+ case SquadDefinitionActionMessage(unk1, unk2, action) =>
+ unk1 mustEqual PlanetSideGUID(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 RequestListSquad()
case _ =>
ko
}
@@ -66,17 +95,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
- unk1 mustEqual 0
+ case SquadDefinitionActionMessage(unk1, unk2, action) =>
+ unk1 mustEqual PlanetSideGUID(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 +106,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
- unk1 mustEqual 0
+ case SquadDefinitionActionMessage(unk1, unk2, action) =>
+ unk1 mustEqual PlanetSideGUID(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 +117,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
- unk1 mustEqual 0
+ case SquadDefinitionActionMessage(unk1, unk2, action) =>
+ unk1 mustEqual PlanetSideGUID(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 +128,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
- unk1 mustEqual 0
+ case SquadDefinitionActionMessage(unk1, unk2, action) =>
+ unk1 mustEqual PlanetSideGUID(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 +139,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
- unk1 mustEqual 0
+ case SquadDefinitionActionMessage(unk1, unk2, action) =>
+ unk1 mustEqual PlanetSideGUID(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 +150,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
- unk1 mustEqual 0
+ case SquadDefinitionActionMessage(unk1, unk2, action) =>
+ unk1 mustEqual PlanetSideGUID(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 +161,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
- unk1 mustEqual 0
+ case SquadDefinitionActionMessage(unk1, unk2, action) =>
+ unk1 mustEqual PlanetSideGUID(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 +172,13 @@ 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
- unk1 mustEqual 0
+ case SquadDefinitionActionMessage(unk1, unk2, action) =>
+ unk1 mustEqual PlanetSideGUID(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 ChangeSquadMemberRequirementsCertifications(
+ 1,
+ Set(CertificationType.AntiVehicular, CertificationType.InfiltrationSuit)
+ )
case _ =>
ko
}
@@ -213,16 +186,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
- unk1 mustEqual 0
+ case SquadDefinitionActionMessage(unk1, unk2, action) =>
+ unk1 mustEqual PlanetSideGUID(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 +197,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
- unk1 mustEqual 0
+ case SquadDefinitionActionMessage(unk1, unk2, action) =>
+ unk1 mustEqual PlanetSideGUID(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 +208,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
- unk1 mustEqual 0
+ case SquadDefinitionActionMessage(unk1, unk2, action) =>
+ unk1 mustEqual PlanetSideGUID(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 +219,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
- unk1 mustEqual 0
+ case SquadDefinitionActionMessage(unk1, unk2, action) =>
+ unk1 mustEqual PlanetSideGUID(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", Set(), 1, SearchMode.AnyPositions)
case _ =>
ko
}
@@ -287,20 +230,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
- unk1 mustEqual 0
+ case SquadDefinitionActionMessage(unk1, unk2, action) =>
+ unk1 mustEqual PlanetSideGUID(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", Set(), 2, SearchMode.AnyPositions)
case _ =>
ko
}
@@ -308,20 +241,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
- unk1 mustEqual 0
+ case SquadDefinitionActionMessage(unk1, unk2, action) =>
+ unk1 mustEqual PlanetSideGUID(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", Set(), 2, SearchMode.AvailablePositions)
case _ =>
ko
}
@@ -329,20 +252,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
- unk1 mustEqual 0
+ case SquadDefinitionActionMessage(unk1, unk2, action) =>
+ unk1 mustEqual PlanetSideGUID(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", Set(CertificationType.InfiltrationSuit, CertificationType.AntiVehicular), 2, SearchMode.SomeCertifications)
case _ =>
ko
}
@@ -350,20 +263,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
- unk1 mustEqual 0
+ case SquadDefinitionActionMessage(unk1, unk2, action) =>
+ unk1 mustEqual PlanetSideGUID(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", Set(CertificationType.InfiltrationSuit, CertificationType.AntiVehicular), 2, SearchMode.AllCertifications)
case _ =>
ko
}
@@ -371,16 +274,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
- unk1 mustEqual 0
+ case SquadDefinitionActionMessage(unk1, unk2, action) =>
+ unk1 mustEqual PlanetSideGUID(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 +285,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
- unk1 mustEqual 0
+ case SquadDefinitionActionMessage(unk1, unk2, action) =>
+ unk1 mustEqual PlanetSideGUID(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 +296,205 @@ 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
- unk1 mustEqual 0
+ case SquadDefinitionActionMessage(unk1, unk2, action) =>
+ unk1 mustEqual PlanetSideGUID(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 PlanetSideGUID(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 (00)" in {
+ val msg = SquadDefinitionActionMessage(PlanetSideGUID(3), 0, DisplaySquad())
+ val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+ pkt mustEqual string_00
+ }
+
"encode (03)" in {
- val msg = SquadDefinitionActionMessage(3, 0, 3, None, None, None, None, None, None)
+ 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, LoadSquadFavorite())
+ val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+ pkt mustEqual string_04
+ }
+
+ "encode (07)" in {
+ 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(8, 0, 0, None, None, None, None, None, None)
+ val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, RequestListSquad())
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(PlanetSideGUID(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(PlanetSideGUID(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(PlanetSideGUID(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(PlanetSideGUID(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(PlanetSideGUID(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(PlanetSideGUID(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(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(25, 0, 0, None, Some(1), None, Some(536870928L), None, None)
+ val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, ChangeSquadMemberRequirementsCertifications(
+ 1,
+ Set(CertificationType.AntiVehicular, CertificationType.InfiltrationSuit)
+ ))
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(PlanetSideGUID(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(PlanetSideGUID(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(PlanetSideGUID(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(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(34, 0, 0, Some("Badass"), Some(2), Some(0), Some(0L), None, None)
+ 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(34, 0, 0, Some("Badass"), Some(2), Some(1), Some(0L), None, None)
+ 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(34, 0, 0, Some("Badass"), Some(2), Some(2), Some(536870928L), None, None)
+ 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(34, 0, 0, Some("Badass"), Some(2), Some(3), Some(536870928L), None, None)
+ 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
}
"encode (35)" in {
- val msg = SquadDefinitionActionMessage(35, 0, 0, None, None, None, None, None, None)
+ val msg = SquadDefinitionActionMessage(PlanetSideGUID(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(PlanetSideGUID(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(PlanetSideGUID(0), 0, CancelFind())
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_41
}
+
+ "encode (43, unknown)" in {
+ val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, Unknown(43, BitVector.empty))
+ val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+ pkt mustEqual string_43
+ }
}
diff --git a/common/src/test/scala/game/SquadDetailDefinitionUpdateMessageTest.scala b/common/src/test/scala/game/SquadDetailDefinitionUpdateMessageTest.scala
new file mode 100644
index 000000000..9e3fd2c17
--- /dev/null
+++ b/common/src/test/scala/game/SquadDetailDefinitionUpdateMessageTest.scala
@@ -0,0 +1,716 @@
+// Copyright (c) 2019 PSForever
+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_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"
+ val string_mixed = hex"e80300812cd85000530046006f007200650076006500720020005000610063006b0065007400200043006f006c006c0065006300740069006f006e00841400000181306400800000000080000000000000220c808000000000800000000000001e0c808000000000800000000000001a0c80800000000080000000000000160c80800000000080000000000000120c808000000000800000000000000e0c808000000000800000000000000a0c80800000000080000000000000060c80800000000080000000000000020c80800000000080000000000003fc"
+
+ "SquadDetailDefinitionUpdateMessage" should {
+ "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
+ }
+ }
+
+ "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 1
+ 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 1
+ 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 1
+ 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 1
+ 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 10
+ //
+ 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 10
+ 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, ""))
+ ))
+ )
+ 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, ""))
+ ))
+ )
+ 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"))
+ ))
+ )
+ 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)
+ ))
+ )
+ 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"))
+ ))
+ )
+ 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)))
+ ))
+ )
+ 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"))
+ ))
+ )
+ 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(""))
+ ))
+ )
+ 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)
+ )
+ val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+ pkt mustEqual string_mixed
+ }
+ }
+}
diff --git a/common/src/test/scala/game/SquadMemberEventTest.scala b/common/src/test/scala/game/SquadMemberEventTest.scala
new file mode 100644
index 000000000..2f431991f
--- /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 MemberEvent.Add
+ 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(MemberEvent.Add, 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/SquadMembershipRequestTest.scala b/common/src/test/scala/game/SquadMembershipRequestTest.scala
new file mode 100644
index 000000000..79cb9319f
--- /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]
+ }
+}
diff --git a/common/src/test/scala/game/SquadMembershipResponseTest.scala b/common/src/test/scala/game/SquadMembershipResponseTest.scala
new file mode 100644
index 000000000..f9ff10c2c
--- /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.SquadResponseType
+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 SquadResponseType.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 SquadResponseType.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 SquadResponseType.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 SquadResponseType.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 SquadResponseType.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 SquadResponseType.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 SquadResponseType.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 SquadResponseType.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 SquadResponseType.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 SquadResponseType.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 SquadResponseType.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 SquadResponseType.PlatoonInvite
+ 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 SquadResponseType.PlatoonInvite
+ 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 SquadResponseType.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 (9-1)" in {
+ PacketCoding.DecodePacket(string_91).require match {
+ case SquadMembershipResponse(unk1, unk2, unk3, unk4, unk5, unk6, unk7, unk8) =>
+ unk1 mustEqual SquadResponseType.PlatoonReject
+ 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 SquadResponseType.PlatoonReject
+ 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 SquadResponseType.PlatoonLeave
+ 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(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(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(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(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(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(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(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(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(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(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(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(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.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.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.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.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.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/game/SquadStateTest.scala b/common/src/test/scala/game/SquadStateTest.scala
new file mode 100644
index 000000000..5cce7ae30
--- /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/common/src/test/scala/game/SquadWaypointEventTest.scala b/common/src/test/scala/game/SquadWaypointEventTest.scala
index 3c2b8bb81..199b0bb1c 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, 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 {
@@ -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
+ unk4 mustEqual SquadWaypoints.One
+ 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
+ unk4 mustEqual SquadWaypoints.ExperienceRally
+ 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))
+ unk4 mustEqual SquadWaypoints.Two
+ 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
+ unk4 mustEqual SquadWaypoints.Two
+ 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, SquadWaypoints.One)
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, SquadWaypoints.ExperienceRally)
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, 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(1, 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/common/src/test/scala/game/objectcreate/CharacterDataTest.scala b/common/src/test/scala/game/objectcreate/CharacterDataTest.scala
index eece51214..22a1e7db4 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 e64dd8fcf..2843386de 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
@@ -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/common/src/test/scala/game/objectcreatevehicle/MountedVehiclesTest.scala b/common/src/test/scala/game/objectcreatevehicle/MountedVehiclesTest.scala
index 5b73667f0..1c831f74b 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 c2c482c06..6e1502b15 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.LoadLoadout(_) mustEqual None }
+ (0 to 9).foreach { avatar.EquipmentLoadouts.LoadLoadout(_).isEmpty mustEqual true }
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", 50)
- avatar.LoadLoadout(10) mustEqual None
+ avatar.EquipmentLoadouts.LoadLoadout(50).isEmpty mustEqual true
}
"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).isEmpty mustEqual true
}
"the fifth slot is the locker wrapped in an EquipmentSlot" in {
diff --git a/common/src/test/scala/objects/DamageModelTests.scala b/common/src/test/scala/objects/DamageModelTests.scala
index 1230b0438..5a477c881 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/common/src/test/scala/objects/PlayerTest.scala b/common/src/test/scala/objects/PlayerTest.scala
index c1471800e..f579e4fc6 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/common/src/test/scala/objects/terminal/OrderTerminalTest.scala b/common/src/test/scala/objects/terminal/OrderTerminalTest.scala
index 34b21670b..c60e1c17f 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/PsLogin.scala b/pslogin/src/main/scala/PsLogin.scala
index 30eaca8ed..cfc0c1642 100644
--- a/pslogin/src/main/scala/PsLogin.scala
+++ b/pslogin/src/main/scala/PsLogin.scala
@@ -22,6 +22,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._
@@ -258,6 +259,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 e09979db0..82fc7ec87 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}
@@ -52,7 +53,9 @@ 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.{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
@@ -76,6 +79,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
@@ -97,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)
@@ -113,6 +118,21 @@ 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 squad_supplement_id : Int = 0
+ /**
+ * 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 `WorldSessionActor`-local variable is contingent on `squadUI` being populated.
+ */
+ var lfsm : 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
@@ -144,6 +164,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()
@@ -155,6 +176,7 @@ 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
//TODO final character save before doing any of this (use equipment)
continent.Population ! Zone.Population.Release(avatar)
if(player.isAlive) {
@@ -266,6 +288,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 +314,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 +362,231 @@ class WorldSessionActor extends Actor with MDCContextAware {
case VehicleServiceResponse(toChannel, guid, reply) =>
HandleVehicleServiceResponse(toChannel, guid, reply)
+ 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) =>
+ sendResponse(ReplicationStreamMessage(infos))
+
+ case SquadResponse.UpdateList(infos) if infos.nonEmpty =>
+ 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.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.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 = positionsToUpdate map squad.Membership.zipWithIndex
+ 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(playerGuid, 53, 0))
+ avatarService ! AvatarServiceMessage(factionOnContinentChannel, AvatarAction.PlanetsideAttribute(playerGuid, 53, 0))
+ }
+ //squad colors
+ 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 = PeriodicUpdatesWhenEnrolledInSquad
+ squadChannel = Some(toChannel)
+ case _ =>
+ //other player is joining our squad
+ //load each member's entry
+ 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
+ .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) =>
+ StartBundlingPackets()
+ 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
+ val playerGuid = player.GUID
+ sendResponse(SquadMemberEvent.Remove(squad_supplement_id, ourMember, ourIndex)) //repeat of our entry
+ 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?
+ lfsm = false
+ //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 _ =>
+ //remove each member's entry
+ GiveSquadColorsInZone(
+ positionsToUpdate.map { case(member, index) =>
+ sendResponse(SquadMemberEvent.Remove(squad_supplement_id, member, index))
+ squadUI.remove(member)
+ member
+ },
+ value = 0
+ )
+ }
+ StopBundlingPackets()
+
+ 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(lfsm) {
+ sendResponse(PlanetsideAttributeMessage(guid, 53, 0))
+ avatarService ! AvatarServiceMessage(factionOnContinentChannel, AvatarAction.PlanetsideAttribute(guid, 53, 0))
+ }
+ lfsm = false
+ sendResponse(PlanetsideAttributeMessage(guid, 32, from_index)) //associate with member position in squad
+ }
+ //are we being promoted?
+ else if(charId == char_id) {
+ sendResponse(PlanetsideAttributeMessage(guid, 32, 0)) //associate with member position in squad
+ }
+ 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) =>
+ 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) =>
val vehicle_guid = obj.GUID
//TODO remove this arbitrary allowance angle when no longer helpful
@@ -484,7 +735,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
player.Stamina = stamina
player.Armor = armor
}
- sendResponse(CharacterInfoMessage(15, PlanetSideZoneID(10000), 41605313, player.GUID, false, 6404428))
+ 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))
@@ -802,7 +1053,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
@@ -810,6 +1061,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(s"${avatar.CharId}") //channel will be player.CharId (in order to work with packets)
cluster ! InterstellarCluster.GetWorld("home3")
case InterstellarCluster.GiveWorld(zoneId, zone) =>
@@ -2828,7 +3081,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)))
@@ -2840,9 +3093,14 @@ 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))
+ //looking for squad (members)
+ if(tplayer.LFS || lfsm) {
+ 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?
sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(i), 67, 0))
})
(0 to 30).foreach(i => {
@@ -2851,7 +3109,9 @@ class WorldSessionActor extends Actor with MDCContextAware {
})
//AvatarAwardMessage
//DisplayAwardMessage
- //SquadDefinitionActionMessage and SquadDetailDefinitionUpdateMessage
+ sendResponse(PlanetsideStringAttributeMessage(guid, 0, "Outfit Name"))
+ //squad stuff (loadouts, assignment)
+ squadSetup()
//MapObjectStateBlockMessage and ObjectCreateMessage?
//TacticsMessage?
//change the owner on our deployables (re-draw the icons for our deployables too)
@@ -2888,6 +3148,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 - 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 = RespawnSquadSetup
+ }
+
+ /**
+ * 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))
+ 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) =>
@@ -2908,10 +3259,14 @@ 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
- 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
@@ -2946,6 +3301,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
@@ -3043,7 +3399,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)) })
@@ -3339,6 +3695,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
case None => false
}
avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlayerState(avatar_guid, msg, spectator, wepInHand))
+ updateSquad()
}
case msg @ ChildObjectStateMessage(object_guid, pitch, yaw) =>
@@ -3380,6 +3737,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
obj.Cloaked = obj.Definition.CanCloak && is_cloaked
vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.VehicleState(player.GUID, vehicle_guid, unk1, pos, ang, vel, flight, unk6, unk7, wheels, unk9, is_cloaked))
}
+ 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
@@ -4466,7 +4824,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))
@@ -4484,6 +4842,30 @@ class WorldSessionActor extends Actor with MDCContextAware {
log.info(s"GenericObject: $player is MAX with an unexpected weapon - ${definition.Name}")
}
}
+ else if(action == 36) { //Looking For Squad ON
+ if(squadUI.nonEmpty) {
+ if(!lfsm && squadUI(player.CharId).index == 0) {
+ lfsm = true
+ avatarService ! AvatarServiceMessage(s"${continent.Id}/${player.Faction}", AvatarAction.PlanetsideAttribute(player.GUID, 53, 1))
+ }
+ }
+ else if(!avatar.LFS) {
+ avatar.LFS = true
+ 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(lfsm && squadUI(player.CharId).index == 0) {
+ lfsm = false
+ avatarService ! AvatarServiceMessage(s"${continent.Id}/${player.Faction}", AvatarAction.PlanetsideAttribute(player.GUID, 53, 0))
+ }
+ }
+ else if(avatar.LFS) {
+ avatar.LFS = false
+ avatarService ! AvatarServiceMessage(s"${continent.Id}/${player.Faction}", AvatarAction.PlanetsideAttribute(player.GUID, 53, 0))
+ }
+ }
case msg @ ItemTransactionMessage(terminal_guid, transaction_type, _, _, _, _) =>
log.info("ItemTransaction: " + msg)
@@ -4522,18 +4904,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 =>
@@ -4778,8 +5160,17 @@ 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(player, continent, SquadServiceAction.Definition(u1, u2, action))
+
+ case msg @ SquadMembershipRequest(request_type, unk2, unk3, player_name, unk5) =>
+ log.info(s"$msg")
+ 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, 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)
@@ -7520,9 +7911,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
@@ -7531,13 +7920,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 = {
- //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 killer_seated = killer match {
case obj : PlayerSource => obj.Seated
case _ => false
@@ -7547,9 +7929,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
)
}
@@ -8478,6 +8860,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)
}
@@ -8947,6 +9330,76 @@ class WorldSessionActor extends Actor with MDCContextAware {
}
}
+ def SwapSquadUIElements(squad : Squad, fromIndex : Int, toIndex : Int) : Unit = {
+ if(squadUI.nonEmpty) {
+ val fromMember = squad.Membership(toIndex) //the players have already been swapped in the backend object
+ val fromCharId = fromMember.CharId
+ val toMember = squad.Membership(fromIndex) //the players have already been swapped in the backend object
+ val toCharId = toMember.CharId
+ val id = 11
+ if(toCharId > 0) {
+ //toMember and fromMember have swapped places
+ val fromElem = squadUI(fromCharId)
+ val toElem = squadUI(toCharId)
+ squadUI(toCharId) = SquadUIElement(fromElem.name, toIndex, fromElem.zone, fromElem.health, fromElem.armor, fromElem.position)
+ squadUI(fromCharId) = SquadUIElement(toElem.name, fromIndex, toElem.zone, toElem.health, toElem.armor, toElem.position)
+ sendResponse(SquadMemberEvent.Add(id, toCharId, toIndex, fromElem.name, fromElem.zone, unk7 = 0))
+ sendResponse(SquadMemberEvent.Add(id, fromCharId, fromIndex, toElem.name, toElem.zone, unk7 = 0))
+ 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)
+ 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(fromCharId, 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 NoSquadUpdates() : Unit = { }
+
+ def SquadUpdates() : 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 PeriodicUpdatesWhenEnrolledInSquad() : Unit = {
+ queuedSquadActions(squadUpdateCounter)()
+ squadUpdateCounter = (squadUpdateCounter + 1) % queuedSquadActions.length
+ }
+
def failWithError(error : String) = {
log.error(error)
sendResponse(ConnectionClose())
@@ -9103,6 +9556,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)
diff --git a/pslogin/src/test/scala/PacketCodingActorTest.scala b/pslogin/src/test/scala/PacketCodingActorTest.scala
index 5efe312b9..e055d325b 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 {