From 2a1b05171074a815310414a379a2f7ddcc2b5cd7 Mon Sep 17 00:00:00 2001 From: FateJH Date: Wed, 3 Jul 2019 01:55:42 -0400 Subject: [PATCH] completed form of SquadDetailDefinitionUpdate --- .../SquadDetailDefinitionUpdateMessage.scala | 1927 ++++++++++------- .../services/teamwork/SquadResponse.scala | 2 +- ...uadDetailDefinitionUpdateMessageTest.scala | 1225 ++++++----- .../src/main/scala/WorldSessionActor.scala | 35 +- 4 files changed, 1901 insertions(+), 1288 deletions(-) diff --git a/common/src/main/scala/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala b/common/src/main/scala/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala index 62a6702e..ad080bcf 100644 --- a/common/src/main/scala/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala @@ -3,55 +3,18 @@ package net.psforever.packet.game import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} import net.psforever.types.CertificationType -import scodec.bits.BitVector import scodec.{Attempt, Codec, Err} import scodec.codecs._ import shapeless.HNil import scala.annotation.tailrec -final case class SquadPositionDetail(is_closed : Boolean, - role : String, - detailed_orders : String, - requirements : Set[CertificationType.Value], - char_id : Long, - name : String) - -final case class OldSquadDetailDefinitionUpdateMessage(guid : PlanetSideGUID, - unk1 : Int, - leader_char_id : Long, - unk2 : BitVector, - leader_name : String, - task : String, - zone_id : PlanetSideZoneID, - member_info : List[SquadPositionDetail]) - -object SquadPositionDetail { - final val Closed : SquadPositionDetail = SquadPositionDetail(is_closed = true, "", "", Set.empty, 0L, "") - - def apply() : SquadPositionDetail = SquadPositionDetail(is_closed = false, "", "", Set.empty, 0L, "") - - def apply(char_id : Long, name : String) : SquadPositionDetail = SquadPositionDetail(is_closed = false, "", "", Set.empty, char_id, name) - - def apply(role : String, detailed_orders : String) : SquadPositionDetail = SquadPositionDetail(is_closed = false, role, detailed_orders, Set.empty, 0L, "") - - def apply(role : String, detailed_orders : String, requirements : Set[CertificationType.Value]) : SquadPositionDetail = SquadPositionDetail(is_closed = false, role, detailed_orders, requirements, 0L, "") - - def apply(role : String, detailed_orders : String, char_id : Long, name : String) : SquadPositionDetail = SquadPositionDetail(is_closed = false, role, detailed_orders, Set.empty, char_id, name) - - def apply(role : String, detailed_orders : String, requirements : Set[CertificationType.Value], char_id : Long, name : String) : SquadPositionDetail = SquadPositionDetail(is_closed = false, role, detailed_orders, requirements, char_id, name) -} - -object OldSquadDetailDefinitionUpdateMessage { - def apply(guid : PlanetSideGUID, char_id : Long, leader_name : String, task : String, zone_id : PlanetSideZoneID, member_info : List[SquadPositionDetail]) : OldSquadDetailDefinitionUpdateMessage = { - import scodec.bits._ - OldSquadDetailDefinitionUpdateMessage(guid, 1, char_id, hex"000000".toBitVector, leader_name, task, zone_id, member_info) - } -} - -//NEW FORM SquadDetailDefinitionUpdateMessage - -private class StreamLengthToken(init : Int = 0) { +/** + * A container that should be used to keep track of the current length of a stream of bits. + * @param init the starting pad value; + * defaults to 0 + */ +class StreamLengthToken(init : Int = 0) { private var bitLength : Int = init def Length : Int = bitLength @@ -67,14 +30,36 @@ private class StreamLengthToken(init : Int = 0) { } } -final case class SquadPositionDetail2(is_closed : Option[Boolean], - role : Option[String], - detailed_orders : Option[String], - requirements : Option[Set[CertificationType.Value]], - char_id : Option[Long], - name : Option[String]) { - def And(info : SquadPositionDetail2) : SquadPositionDetail2 = { - SquadPositionDetail2( +/** + * Information regarding a squad's position as a series of common fields. + * When parsed in an itemized way, only the important fields are represented. + * When parsed in a continuous manner, all of the fields are populated. + * All fields are optional for that reason. + * @param is_closed availability, whether the position can be occupied by a player; + * an unavailable position is referenced as "Closed" and no other position detail is displayed; + * an available unoccupied position is "Available" + * @param role the title of the position + * @param detailed_orders the suggested responsibilities of the position + * @param requirements the actual responsibilities of the position + * @param char_id the unique character identification number for the player that is occupying this position + * @param name the name of the player who is occupying this position + */ +final case class SquadPositionDetail(is_closed : Option[Boolean], + role : Option[String], + detailed_orders : Option[String], + requirements : Option[Set[CertificationType.Value]], + char_id : Option[Long], + name : Option[String]) { + /** + * Combine two `SquadPositionDetail` objects, with priority given to `this` one. + * Most fields that are not empty are assigned. + * Even if the current object reports the squad position being open - `is_closed = Some(false)` - + * just one instance of the squad position being closed overwrites all future updates. + * @param info the object being combined + * @return the combined `SquadDetail` object + */ + def And(info : SquadPositionDetail) : SquadPositionDetail = { + SquadPositionDetail( is_closed match { case Some(false) | None => info.is_closed.orElse(is_closed) @@ -88,10 +73,69 @@ final case class SquadPositionDetail2(is_closed : Option[Boolean], name.orElse(info.name) ) } + + //methods intended to combine the fields of itself and another object + def Open : SquadPositionDetail = + this And SquadPositionDetail(Some(false), None, None, None, None, None) + def Close : SquadPositionDetail = + this And SquadPositionDetail(Some(true), None, None, None, None, None) + def Role(role : String) : SquadPositionDetail = + this And SquadPositionDetail(None, Some(role), None, None, None, None) + def DetailedOrders(orders : String) : SquadPositionDetail = + this And SquadPositionDetail(None, None, Some(orders), None, None, None) + def Requirements(req : Set[CertificationType.Value]) : SquadPositionDetail = + this And SquadPositionDetail(None, None, None, Some(req), None, None) + def CharId(char_id : Long) : SquadPositionDetail = + this And SquadPositionDetail(None, None, None, None, Some(char_id), None) + def Name(name : String) : SquadPositionDetail = + this And SquadPositionDetail(None, None, None, None, None, Some(name)) + def Player(char_id : Long, name : String) : SquadPositionDetail = + this And SquadPositionDetail(None, None, None, None, Some(char_id), Some(name)) + + /** + * Complete the object by providing placeholder values for all fields. + * @return a `SquadPositionDetail` object with all of its field populated + */ + def Complete : SquadPositionDetail = SquadPositionDetail( + is_closed.orElse(Some(false)), + role.orElse(Some("")), + detailed_orders.orElse(Some("")), + requirements.orElse(Some(Set.empty)), + char_id.orElse(Some(0L)), + name.orElse(Some("")) + ) } -final case class SquadPositionEntry(index : Int, info : Option[SquadPositionDetail2]) +/** + * A container for squad position field data + * associating what would be the ordinal position of that field data in full squad data. + * @param index the index for this squad position; + * expected to be a number 0-9 or 255; + * when 255, this indicated the end of enumerated squad position data and the data for that position is absent + * @param info the squad position field data + */ +final case class SquadPositionEntry(index : Int, info : Option[SquadPositionDetail]) +/** + * Information regarding a squad's position as a series of common fields. + * When parsed in an itemized way, only the important fields are represented. + * When parsed in a continuous manner, all of the fields are populated. + * All fields are optional for that reason.
+ *
+ * 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, "")) ) ) )