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