initial work and dummy tests for SquadDetailDefinitionUpdateMessage packet; 46-bit certification encoding; indicating four previously unhandled SquadAction types; the squad leader is allowed to move around his squad

This commit is contained in:
FateJH 2019-06-03 23:45:10 -04:00
parent ceb145d94f
commit 5c433204cf
10 changed files with 582 additions and 129 deletions

View file

@ -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

View file

@ -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 =>

View file

@ -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)

View file

@ -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:<br>
* &nbsp;&nbsp;`(None)`<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`0 ` - UNKNOWN<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`0 ` - Display Squad<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`1 ` - UNKNOWN<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`2 ` - UNKNOWN<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`3 ` - Save Squad Definition<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`4 ` - UNKNOWN<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`4 ` - Load Squad Definition<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`6 ` - UNKNOWN<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`8 ` - List Squad<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`9 ` - UNKNOWN<br>
@ -251,10 +290,10 @@ object SquadAction{
* &nbsp;&nbsp;`Long`<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`13` - UNKNOWN<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`14` - UNKNOWN<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`15` - UNKNOWN<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`15` - Select this Role for Yourself<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`37` - UNKNOWN<br>
* &nbsp;&nbsp;`String`<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`7 ` - UNKNOWN<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`7 ` - List Squad Definition<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`19` - (Squad leader) Change Squad Purpose<br>
* &nbsp;&nbsp;`Int :: Long`<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`12` - UNKNOWN<br>
@ -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)

View file

@ -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)
}
)
}
}

View file

@ -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.<br>
* <br>
@ -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)))
}
}
}

View file

@ -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
}

View file

@ -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

View file

@ -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
}
}
}

View file

@ -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) = {