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 46d9e15ff..4d3b24e88 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 071d8a8eb..4ac3cfc2c 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 3845c4a3a..d0ab02cff 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 5b714fc81..401937306 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 000000000..291ef219f --- /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 ea4ca3ab5..99afa33c5 100644 --- a/common/src/main/scala/net/psforever/types/CertificationType.scala +++ b/common/src/main/scala/net/psforever/types/CertificationType.scala @@ -3,6 +3,8 @@ package net.psforever.types import net.psforever.packet.PacketHelpers import scodec.codecs._ + +import scala.annotation.tailrec /** * An `Enumeration` of the available certifications.
*
@@ -76,4 +78,59 @@ object CertificationType extends Enumeration { = Value implicit val codec = PacketHelpers.createEnumerationCodec(this, uint8L) + + /** + * Certifications are often stored, in object form, as a 46-member collection. + * Encode a subset of certification values for packet form. + * @see `ChangeSquadMemberRequirementsCertifications` + * @see `changeSquadMemberRequirementsCertificationsCodec` + * @param certs the certifications, as a sequence of values + * @return the certifications, as a single value + */ + def toEncodedLong(certs : Set[CertificationType.Value]) : Long = { + certs + .map{ cert => math.pow(2, cert.id).toLong } + .foldLeft(0L)(_ + _) + } + + /** + * Certifications are often stored, in packet form, as an encoded little-endian `46u` value. + * Decode a representative value into a subset of certification values. + * @see `ChangeSquadMemberRequirementsCertifications` + * @see `changeSquadMemberRequirementsCertificationsCodec` + * @see `fromEncodedLong(Long, Iterable[Long], Set[CertificationType.Value])` + * @param certs the certifications, as a single value + * @return the certifications, as a sequence of values + */ + def fromEncodedLong(certs : Long) : Set[CertificationType.Value] = { + recursiveFromEncodedLong( + certs, + CertificationType.values.map{ cert => math.pow(2, cert.id).toLong }.toSeq.sorted + ) + } + + /** + * Certifications are often stored, in packet form, as an encoded little-endian `46u` value. + * Decode a representative value into a subset of certification values + * by repeatedly finding the partition point of values less than a specific one, + * providing for both the next lowest value (to subtract) and an index (of a certification). + * @see `ChangeSquadMemberRequirementsCertifications` + * @see `changeSquadMemberRequirementsCertificationsCodec` + * @see `fromEncodedLong(Long)` + * @param certs the certifications, as a single value + * @param splitList the available values to partition + * @param out the accumulating certification values; + * defaults to an empty set + * @return the certifications, as a sequence of values + */ + @tailrec + private def recursiveFromEncodedLong(certs : Long, splitList : Iterable[Long], out : Set[CertificationType.Value] = Set.empty) : Set[CertificationType.Value] = { + if(certs == 0 || splitList.isEmpty) { + out + } + else { + val (less, _) = splitList.partition(_ <= certs) + recursiveFromEncodedLong(certs - less.last, less, out ++ Set(CertificationType(less.size - 1))) + } + } } diff --git a/common/src/main/scala/services/teamwork/SquadService.scala b/common/src/main/scala/services/teamwork/SquadService.scala index 4d58a1af1..3572bd969 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 934422a79..0acd6c8f0 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 000000000..5ad56bd07 --- /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 170ede0e2..9b83272ab 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) = {