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