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