split original aphedox SquadRequestType's into two different enumerations, one for Request and the other for Response; added functionality for more squad interactions; squad favorites, excepting the ability to actually load them onto one's client; extensive comments about SquadResponseMessage events

This commit is contained in:
FateJH 2019-07-22 17:56:09 -04:00
parent 640a96ae9c
commit f81c87ce22
13 changed files with 820 additions and 293 deletions

View file

@ -23,7 +23,7 @@ object Loadout {
owner match { owner match {
case p : Player => Success(Create(p, label)) case p : Player => Success(Create(p, label))
case v : Vehicle => Success(Create(v, label)) case v : Vehicle => Success(Create(v, label))
case s : Squad => Success(Create(s, label)) case s : Squad => Success(Create(s, s.Task))
case _ => Failure(new MatchError(s"can not create a loadout based on the (current status of) $owner")) case _ => Failure(new MatchError(s"can not create a loadout based on the (current status of) $owner"))
} }
} }
@ -65,7 +65,6 @@ object Loadout {
def Create(squad : Squad, label : String) : Loadout = { def Create(squad : Squad, label : String) : Loadout = {
SquadLoadout( SquadLoadout(
label, label,
squad.Task,
if(squad.CustomZoneId) { Some(squad.ZoneId) } else { None }, if(squad.CustomZoneId) { Some(squad.ZoneId) } else { None },
squad.Membership squad.Membership
.zipWithIndex .zipWithIndex

View file

@ -5,7 +5,6 @@ import net.psforever.types.CertificationType
final case class SquadPositionLoadout(index : Int, role : String, orders : String, requirements : Set[CertificationType.Value]) final case class SquadPositionLoadout(index : Int, role : String, orders : String, requirements : Set[CertificationType.Value])
final case class SquadLoadout(label : String, final case class SquadLoadout(task : String,
task : String,
zone_id : Option[Int], zone_id : Option[Int],
members : List[SquadPositionLoadout]) extends Loadout(label) members : List[SquadPositionLoadout]) extends Loadout(task)

View file

@ -91,7 +91,7 @@ class Squad(squadId : PlanetSideGUID, alignment : PlanetSideEmpire.Value) extend
} }
} }
def Size : Int = membership.count(member => !member.Name.equals("")) def Size : Int = membership.count(member => member.CharId != 0)
def Capacity : Int = availability.count(open => open) def Capacity : Int = availability.count(open => open)
} }

View file

@ -51,6 +51,22 @@ final case class SquadInfo(leader : Option[String],
squad_guid.orElse(info.squad_guid) squad_guid.orElse(info.squad_guid)
) )
} }
//methods intended to combine the fields of itself and another object
def Leader(leader : String) : SquadInfo =
this And SquadInfo(Some(leader), None, None, None, None, None)
def Task(task : String) : SquadInfo =
this And SquadInfo(None, Some(task), None, None, None, None)
def ZoneId(zone : PlanetSideZoneID) : SquadInfo =
this And SquadInfo(None, None, Some(zone), None, None, None)
def ZoneId(zone : Option[PlanetSideZoneID]) : SquadInfo = zone match {
case Some(zoneId) => this And SquadInfo(None, None, zone, None, None, None)
case None => SquadInfo(leader, task, zone, size, capacity, squad_guid)
}
def Size(sz : Int) : SquadInfo =
this And SquadInfo(None, None, None, Some(sz), None, None)
def Capacity(cap : Int) : SquadInfo =
this And SquadInfo(None, None, None, None, Some(cap), None)
} }
/** /**
@ -117,7 +133,9 @@ object SquadInfo {
/** /**
* An entry where no fields are defined. * An entry where no fields are defined.
*/ */
final val Blank = SquadInfo(None, None, None, None, None) final val Blank = SquadInfo()
def apply() : SquadInfo = SquadInfo(None, None, None, None, None, None)
/** /**
* Alternate constructor for `SquadInfo` that ignores the `Option` requirement for the fields.<br> * Alternate constructor for `SquadInfo` that ignores the `Option` requirement for the fields.<br>

View file

@ -20,18 +20,24 @@ object SquadAction{
final case class AnswerSquadJoinRequest() extends SquadAction(1) final case class AnswerSquadJoinRequest() extends SquadAction(1)
final case class SaveSquadDefinition() extends SquadAction(3) final case class SaveSquadFavorite() extends SquadAction(3)
final case class LoadSquadDefinition() extends SquadAction(4) final case class LoadSquadFavorite() extends SquadAction(4)
final case class ListSquadDefinition(name : String) extends SquadAction(7) final case class DeleteSquadFavorite() extends SquadAction(5)
final case class ListSquad() extends SquadAction(8) final case class ListSquadFavorite(name : String) extends SquadAction(7)
final case class RequestListSquad() extends SquadAction(8)
final case class StopListSquad() extends SquadAction(9)
final case class SelectRoleForYourself(state : Int) extends SquadAction(10) final case class SelectRoleForYourself(state : Int) extends SquadAction(10)
final case class CancelSelectRoleForYourself(value: Long = 0) extends SquadAction(15) final case class CancelSelectRoleForYourself(value: Long = 0) extends SquadAction(15)
final case class SetListSquad() extends SquadAction(17)
final case class ChangeSquadPurpose(purpose : String) extends SquadAction(19) final case class ChangeSquadPurpose(purpose : String) extends SquadAction(19)
final case class ChangeSquadZone(zone : PlanetSideZoneID) extends SquadAction(20) final case class ChangeSquadZone(zone : PlanetSideZoneID) extends SquadAction(20)
@ -56,12 +62,21 @@ object SquadAction{
final case class CancelSquadSearch() extends SquadAction(35) final case class CancelSquadSearch() extends SquadAction(35)
final case class AssignSquadMemberToRole(position : Int, char_id : Long) extends SquadAction(38)
final case class FindLfsSoldiersForRole(state : Int) extends SquadAction(40) final case class FindLfsSoldiersForRole(state : Int) extends SquadAction(40)
final case class CancelFind() extends SquadAction(41) final case class CancelFind() extends SquadAction(41)
final case class Unknown(badCode : Int, data : BitVector) extends SquadAction(badCode) final case class Unknown(badCode : Int, data : BitVector) extends SquadAction(badCode)
object Unknown {
import scodec.bits._
val StandardBits : BitVector = hex"00".toBitVector.take(6)
def apply(badCode : Int) : Unknown = Unknown(badCode, StandardBits)
}
/** /**
* The `Codec`s used to transform the input stream into the context of a specific action * The `Codec`s used to transform the input stream into the context of a specific action
* and extract the field data from that stream. * and extract the field data from that stream.
@ -83,31 +98,45 @@ object SquadAction{
} }
) )
val saveSquadDefinitionCodec = everFailCondition.xmap[SaveSquadDefinition] ( val saveSquadFavoriteCodec = everFailCondition.xmap[SaveSquadFavorite] (
_ => SaveSquadDefinition(), _ => SaveSquadFavorite(),
{ {
case SaveSquadDefinition() => None case SaveSquadFavorite() => None
} }
) )
val loadSquadDefinitionCodec = everFailCondition.xmap[LoadSquadDefinition] ( val loadSquadFavoriteCodec = everFailCondition.xmap[LoadSquadFavorite] (
_ => LoadSquadDefinition(), _ => LoadSquadFavorite(),
{ {
case LoadSquadDefinition() => None case LoadSquadFavorite() => None
} }
) )
val listSquadDefinitionCodec = PacketHelpers.encodedWideStringAligned(6).xmap[ListSquadDefinition] ( val deleteSquadFavoriteCodec = everFailCondition.xmap[DeleteSquadFavorite] (
text => ListSquadDefinition(text), _ => DeleteSquadFavorite(),
{ {
case ListSquadDefinition(text) => text case DeleteSquadFavorite() => None
} }
) )
val listSquadCodec = everFailCondition.xmap[ListSquad] ( val listSquadFavoriteCodec = PacketHelpers.encodedWideStringAligned(6).xmap[ListSquadFavorite] (
_ => ListSquad(), text => ListSquadFavorite(text),
{ {
case ListSquad() => None case ListSquadFavorite(text) => text
}
)
val requestListSquadCodec = everFailCondition.xmap[RequestListSquad] (
_ => RequestListSquad(),
{
case RequestListSquad() => None
}
)
val stopListSquadCodec = everFailCondition.xmap[StopListSquad] (
_ => StopListSquad(),
{
case StopListSquad() => None
} }
) )
@ -125,6 +154,13 @@ object SquadAction{
} }
) )
val setListSquadCodec = everFailCondition.xmap[SetListSquad] (
_ => SetListSquad(),
{
case SetListSquad() => None
}
)
val changeSquadPurposeCodec = PacketHelpers.encodedWideStringAligned(6).xmap[ChangeSquadPurpose] ( val changeSquadPurposeCodec = PacketHelpers.encodedWideStringAligned(6).xmap[ChangeSquadPurpose] (
purpose => ChangeSquadPurpose(purpose), purpose => ChangeSquadPurpose(purpose),
{ {
@ -223,6 +259,15 @@ object SquadAction{
} }
) )
val assignSquadMemberToRoleCodec = (uint4 :: uint32L).xmap[AssignSquadMemberToRole] (
{
case u1 :: u2 :: HNil => AssignSquadMemberToRole(u1, u2)
},
{
case AssignSquadMemberToRole(u1, u2) => u1 :: u2 :: HNil
}
)
val findLfsSoldiersForRoleCodec = uint4.xmap[FindLfsSoldiersForRole] ( val findLfsSoldiersForRoleCodec = uint4.xmap[FindLfsSoldiersForRole] (
state => FindLfsSoldiersForRole(state), state => FindLfsSoldiersForRole(state),
{ {
@ -270,13 +315,14 @@ object SquadAction{
* &nbsp;&nbsp;&nbsp;&nbsp;`0 ` - Display Squad<br> * &nbsp;&nbsp;&nbsp;&nbsp;`0 ` - Display Squad<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`1 ` - Answer Squad Join Request<br> * &nbsp;&nbsp;&nbsp;&nbsp;`1 ` - Answer Squad Join Request<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`2 ` - UNKNOWN<br> * &nbsp;&nbsp;&nbsp;&nbsp;`2 ` - UNKNOWN<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`3 ` - Save Squad Definition<br> * &nbsp;&nbsp;&nbsp;&nbsp;`3 ` - Save Squad Favorite<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`4 ` - Load Squad Definition<br> * &nbsp;&nbsp;&nbsp;&nbsp;`4 ` - Load Squad Favorite<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`5 ` - Delete Squad Favorite<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`6 ` - UNKNOWN<br> * &nbsp;&nbsp;&nbsp;&nbsp;`6 ` - UNKNOWN<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`8 ` - List Squad<br> * &nbsp;&nbsp;&nbsp;&nbsp;`8 ` - Request List Squad<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`9 ` - UNKNOWN<br> * &nbsp;&nbsp;&nbsp;&nbsp;`9 ` - Stop List Squad<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`16` - UNKNOWN<br> * &nbsp;&nbsp;&nbsp;&nbsp;`16` - UNKNOWN<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`17` - UNKNOWN<br> * &nbsp;&nbsp;&nbsp;&nbsp;`17` - Set List Squad (ui)<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`18` - UNKNOWN<br> * &nbsp;&nbsp;&nbsp;&nbsp;`18` - UNKNOWN<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`26` - Reset All<br> * &nbsp;&nbsp;&nbsp;&nbsp;`26` - Reset All<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`35` - Cancel Squad Search<br> * &nbsp;&nbsp;&nbsp;&nbsp;`35` - Cancel Squad Search<br>
@ -302,12 +348,12 @@ object SquadAction{
* &nbsp;&nbsp;&nbsp;&nbsp;`15` - Select this Role for Yourself<br> * &nbsp;&nbsp;&nbsp;&nbsp;`15` - Select this Role for Yourself<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`37` - UNKNOWN<br> * &nbsp;&nbsp;&nbsp;&nbsp;`37` - UNKNOWN<br>
* &nbsp;&nbsp;`String`<br> * &nbsp;&nbsp;`String`<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`7 ` - List Squad Definition<br> * &nbsp;&nbsp;&nbsp;&nbsp;`7 ` - List Squad Favorite<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`19` - (Squad leader) Change Squad Purpose<br> * &nbsp;&nbsp;&nbsp;&nbsp;`19` - (Squad leader) Change Squad Purpose<br>
* &nbsp;&nbsp;`Int :: Long`<br> * &nbsp;&nbsp;`Int :: Long`<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`12` - UNKNOWN<br> * &nbsp;&nbsp;&nbsp;&nbsp;`12` - UNKNOWN<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`25` - (Squad leader) Change Squad Member Requirements - Weapons<br> * &nbsp;&nbsp;&nbsp;&nbsp;`25` - (Squad leader) Change Squad Member Requirements - Weapons<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`38` - UNKNOWN<br> * &nbsp;&nbsp;&nbsp;&nbsp;`38` - Assign Squad Member To Role<br>
* &nbsp;&nbsp;`Int :: String`<br> * &nbsp;&nbsp;`Int :: String`<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`23` - (Squad leader) Change Squad Member Requirements - Role<br> * &nbsp;&nbsp;&nbsp;&nbsp;`23` - (Squad leader) Change Squad Member Requirements - Role<br>
* &nbsp;&nbsp;&nbsp;&nbsp;`24` - (Squad leader) Change Squad Member Requirements - Detailed Orders<br> * &nbsp;&nbsp;&nbsp;&nbsp;`24` - (Squad leader) Change Squad Member Requirements - Detailed Orders<br>
@ -343,12 +389,15 @@ object SquadDefinitionActionMessage extends Marshallable[SquadDefinitionActionMe
((code : @switch) match { ((code : @switch) match {
case 0 => displaySquadCodec case 0 => displaySquadCodec
case 1 => answerSquadJoinRequestCodec case 1 => answerSquadJoinRequestCodec
case 3 => saveSquadDefinitionCodec case 3 => saveSquadFavoriteCodec
case 4 => loadSquadDefinitionCodec case 4 => loadSquadFavoriteCodec
case 7 => listSquadDefinitionCodec case 5 => deleteSquadFavoriteCodec
case 8 => listSquadCodec case 7 => listSquadFavoriteCodec
case 8 => requestListSquadCodec
case 9 => stopListSquadCodec
case 10 => selectRoleForYourselfCodec case 10 => selectRoleForYourselfCodec
case 15 => cancelSelectRoleForYourselfCodec case 15 => cancelSelectRoleForYourselfCodec
case 17 => setListSquadCodec
case 19 => changeSquadPurposeCodec case 19 => changeSquadPurposeCodec
case 20 => changeSquadZoneCodec case 20 => changeSquadZoneCodec
case 21 => closeSquadMemberPositionCodec case 21 => closeSquadMemberPositionCodec
@ -361,12 +410,13 @@ object SquadDefinitionActionMessage extends Marshallable[SquadDefinitionActionMe
case 31 => locationFollowsSquadLeadCodec case 31 => locationFollowsSquadLeadCodec
case 34 => searchForSquadsWithParticularRoleCodec case 34 => searchForSquadsWithParticularRoleCodec
case 35 => cancelSquadSearchCodec case 35 => cancelSquadSearchCodec
case 38 => assignSquadMemberToRoleCodec
case 40 => findLfsSoldiersForRoleCodec case 40 => findLfsSoldiersForRoleCodec
case 41 => cancelFindCodec case 41 => cancelFindCodec
case 2 | 6 | 9 | 11 | case 2 | 6 | 11 |
12 | 13 | 14 | 16 | 17 | 12 | 13 | 14 | 16 |
18 | 29 | 30 | 32 | 33 | 18 | 29 | 30 | 32 | 33 |
36 | 37 | 38 | 39 | 42 | 43 => unknownCodec(code) 36 | 37 | 39 | 42 | 43 => unknownCodec(code)
case _ => failureCodec(code) case _ => failureCodec(code)
}).asInstanceOf[Codec[SquadAction]] }).asInstanceOf[Codec[SquadAction]]
} }

View file

@ -161,7 +161,21 @@ final case class SquadDetail(unk1 : Option[Int],
task.orElse(info.task), task.orElse(info.task),
zone_id.orElse(info.zone_id), zone_id.orElse(info.zone_id),
unk7.orElse(info.unk7), unk7.orElse(info.unk7),
member_info.orElse(info.member_info) {
(member_info, info.member_info) match {
case (Some(info1), Some(info2)) =>
//combine the first list with the elements of the second list whose indices not found in the first list
val indices = info1.map { _.index }
Some(info1 ++ (for {
position <- info2
if !indices.contains(position.index)
} yield position).sortBy(_.index))
case (Some(info1), None) =>
Some(info1)
case (None, _) =>
info.member_info
}
}
) )
} }
@ -269,6 +283,15 @@ object SquadPositionDetail {
* @return a `SquadPositionDetail` object * @return a `SquadPositionDetail` object
*/ */
def apply(role : String, detailed_orders : String, requirements : Set[CertificationType.Value], char_id : Long, name : String) : SquadPositionDetail = SquadPositionDetail(Some(false), Some(role), Some(detailed_orders), Some(requirements), Some(char_id), Some(name)) def apply(role : String, detailed_orders : String, requirements : Set[CertificationType.Value], char_id : Long, name : String) : SquadPositionDetail = SquadPositionDetail(Some(false), Some(role), Some(detailed_orders), Some(requirements), Some(char_id), Some(name))
object Fields {
final val Closed = 0
final val Role = 1
final val Orders = 2
final val CharId = 3
final val Name = 4
final val Requirements = 5
}
} }
object SquadPositionEntry { object SquadPositionEntry {
@ -318,6 +341,17 @@ object SquadDetail {
SquadDetail(None, None, None, None, None, None, None, Some(unk7), None) SquadDetail(None, None, None, None, None, None, None, Some(unk7), None)
def Members(list : List[SquadPositionEntry]) : SquadDetail = def Members(list : List[SquadPositionEntry]) : SquadDetail =
SquadDetail(None, None, None, None, None, None, None, None, Some(list)) SquadDetail(None, None, None, None, None, None, None, None, Some(list))
object Fields {
final val Field1 = 1
final val CharId = 2
final val Field3 = 3
final val Leader = 4
final val Task = 5
final val ZoneId = 6
final val Field7 = 7
final val Members = 8
}
} }
object SquadDetailDefinitionUpdateMessage extends Marshallable[SquadDetailDefinitionUpdateMessage] { object SquadDetailDefinitionUpdateMessage extends Marshallable[SquadDetailDefinitionUpdateMessage] {

View file

@ -2,12 +2,12 @@
package net.psforever.packet.game package net.psforever.packet.game
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
import net.psforever.types.SquadRequestType import net.psforever.types.SquadResponseType
import scodec.Codec import scodec.Codec
import scodec.codecs._ import scodec.codecs._
/** /**
* Dispatched by the server as manipulation protocol for squad and platoon members. * Dispatched by the server as message generation protocol for squad and platoon members.
* Prompted by and answers for a `SquadMembershipRequest` packet. * Prompted by and answers for a `SquadMembershipRequest` packet.
* @param request_type the purpose of the request * @param request_type the purpose of the request
* @param unk1 na * @param unk1 na
@ -17,11 +17,57 @@ import scodec.codecs._
* @param other_id another squad member's unique identifier; * @param other_id another squad member's unique identifier;
* may be the same as `char_id` * may be the same as `char_id`
* @param player_name name of the player being affected, if applicable * @param player_name name of the player being affected, if applicable
* @param unk5 na * @param unk5 adjusts the nature of the request-type response based on the message recipient
* @param unk6 na; * @param unk6 na;
* the internal field, the `Option[String]`, never seems to be set * the internal field, the `Option[String]`, never seems to be set
* <br>
* `request_type` (enum value) / `unk5` state (`false`/`true`)<br>
* ----------------------------------------<br>
* - `Invite` (0)<br>
* false => [PROMPT] "`player_name` has invited you into a squad." [YES/NO]<br>
* true => "You have invited `player_name` to join your squad."<br>
* - `Unk01` (1)<br>
* false => n/a<br>
* true => n/a<br>
* - `Accept` (2)<br>
* false => "`player_name` has accepted your invitation to join into your squad.<br>
* "You have formed a squad and are now that squad's commander." (if first time)<br>
* true => "You have accepted an invitation to join a squad."<br>
* "You have successfully joined a squad for the first time." (if first time)<br>
* - `Reject` (3)<br>
* false => "`player_name` does not want to join your squad at this time."<br>
* true => "You have declined an invitation to join a squad."<br>
* - `Cancel` (4)<br>
* false => "`player_name` has withdrawn his invitation."<br>
* true => "You have canceled your invitation to `player_name`."<br>
* - `Leave` (5)<br>
* false => "The Squad Leader has kicked you out of the squad."<br>
* true => "You have kicked `player_name` out of the squad."<br>
* - `Disband` (6)<br>
* false => "The squad has been disbanded."<br>
* true => "You have disbanded the squad."<br>
* - `PlatoonInvite` (7)<br>
* false => [PROMPT] "`player_name` has invited you into a platoon." [YES/NO]<br>
* true => "You have invited `player_name`'s squad to join your platoon."<br>
* - `PlatoonAccept` (8)
* false => "`player_name` has accepted your invitation to join into your platoon.<br>
* "You have formed a platoon and are now that platoon commander." (if first time)<br>
* true => "You have accepted an invitation to join a platoon."<br>
* "You have successfully joined a platoon for the first time." (if first time)<br>
* - `PlatoonReject` (9)<br>
* false => "`player_name` does not want to join your platoon at this time."<br>
* true => "You have declined an invitation to join a platoon."<br>
* - `PlatoonCancel` (10)<br>
* false => "`player_name` has withdrawn his invitation."<br>
* true => "You have declined your invitation to `player_name`." (nonsense?)<br>
* - `PlatoonLeave` (11)<br>
* false => "The Platoon Leader has kicked you out of the platoon."<br>
* true => "You have kicked `player_name`'s squad out of the platoon."<br>
* - `PlatoonDisband` (12)<br>
* false => "The platoon has been disbanded."<br>
* true => "You have disbanded the platoon."
*/ */
final case class SquadMembershipResponse(request_type : SquadRequestType.Value, final case class SquadMembershipResponse(request_type : SquadResponseType.Value,
unk1 : Int, unk1 : Int,
unk2 : Int, unk2 : Int,
char_id : Long, char_id : Long,
@ -37,14 +83,14 @@ final case class SquadMembershipResponse(request_type : SquadRequestType.Value,
object SquadMembershipResponse extends Marshallable[SquadMembershipResponse] { object SquadMembershipResponse extends Marshallable[SquadMembershipResponse] {
implicit val codec : Codec[SquadMembershipResponse] = ( implicit val codec : Codec[SquadMembershipResponse] = (
"request_type" | SquadRequestType.codec >>:~ { d => "request_type" | SquadResponseType.codec >>:~ { d =>
("unk1" | uint(5)) :: ("unk1" | uint(5)) ::
("unk2" | uint2) :: ("unk2" | uint2) ::
("char_id" | uint32L) :: ("char_id" | uint32L) ::
("other_id" | conditional(d != SquadRequestType.Promote && d != SquadRequestType.PlatoonLeave, uint32L)) :: ("other_id" | conditional(d != SquadResponseType.Disband && d != SquadResponseType.PlatoonDisband, uint32L)) ::
("player_name" | PacketHelpers.encodedWideStringAligned(5)) :: ("player_name" | PacketHelpers.encodedWideStringAligned(5)) ::
("unk5" | bool) :: ("unk5" | bool) ::
conditional(d != SquadRequestType.Invite, optional(bool, "unk6" | PacketHelpers.encodedWideStringAligned(6))) conditional(d != SquadResponseType.Invite, optional(bool, "unk6" | PacketHelpers.encodedWideStringAligned(6)))
} }
).as[SquadMembershipResponse] ).as[SquadMembershipResponse]
} }

View file

@ -7,23 +7,28 @@ import scodec.codecs._
object SquadRequestType extends Enumeration { object SquadRequestType extends Enumeration {
type Type = Value type Type = Value
val val
Invite, //00 Invite,
Unk01, //01 Unk01,
Accept, //02 Accept,
Reject, //03 Reject,
Cancel, //04 Cancel,
Leave, //05 Leave,
Promote, //06 Promote,
Disband, //07 Disband,
PlatoonInvite, //08 PlatoonInvite,
PlatoonAccept, //09 PlatoonAccept,
PlatoonReject, //10 PlatoonReject,
PlatoonCancel, //11 PlatoonCancel,
PlatoonLeave, //12 PlatoonLeave,
PlatoonDisband, //13 PlatoonDisband
Unk14, //14
Unk15 //15
= Value = Value
implicit val codec = PacketHelpers.createEnumerationCodec(this, uint4L) implicit val codec = PacketHelpers.createEnumerationCodec(this, uint4L)
def toResponse(request : SquadRequestType.Value) : SquadResponseType.Value = {
val id = request.id
if(id < 6) SquadResponseType(id)
else if(id > 6) SquadResponseType(id - 1)
else throw new NoSuchElementException("request does not have an applicable response")
}
} }

View file

@ -0,0 +1,33 @@
// Copyright (c) 2019 PSForever
package net.psforever.types
import net.psforever.packet.PacketHelpers
import scodec.codecs._
object SquadResponseType extends Enumeration {
type Type = Value
val
Invite,
Unk01,
Accept,
Reject,
Cancel,
Leave,
Disband,
PlatoonInvite,
PlatoonAccept,
PlatoonReject,
PlatoonCancel,
PlatoonLeave,
PlatoonDisband
= Value
implicit val codec = PacketHelpers.createEnumerationCodec(this, uint4L)
def fromRequest(response : SquadResponseType.Value) : SquadRequestType.Value = {
val id = response.id
if(id < 6) SquadRequestType(id)
else if(id > 5) SquadRequestType(id + 1)
else throw new NoSuchElementException("response does not stem from an applicable request")
}
}

View file

@ -1,21 +1,27 @@
// Copyright (c) 2019 PSForever // Copyright (c) 2019 PSForever
package services.teamwork package services.teamwork
import net.psforever.objects.teamwork.{Member, Squad} import net.psforever.objects.teamwork.Squad
import net.psforever.packet.game._ import net.psforever.packet.game._
import net.psforever.types.SquadRequestType import net.psforever.types.SquadResponseType
object SquadResponse { object SquadResponse {
trait Response trait Response
final case class Init(info : Vector[SquadInfo]) extends Response final case class ListSquadFavorite(line : Int, task : String) extends Response
final case class Update(infos : Iterable[(Int, SquadInfo)]) extends Response
final case class Remove(infos : Iterable[Int]) extends Response
final case class Membership(request_type : SquadRequestType.Value, unk1 : Int, unk2 : Int, unk3 : Long, unk4 : Option[Long], player_name : String, unk5 : Boolean, unk6 : Option[Option[String]]) extends Response //see SquadMembershipResponse final case class InitList(info : Vector[SquadInfo]) extends Response
final case class UpdateList(infos : Iterable[(Int, SquadInfo)]) extends Response
final case class RemoveFromList(infos : Iterable[Int]) extends Response
final case class InitSquad(squad_guid : PlanetSideGUID) extends Response
final case class Unknown17(squad : Squad, char_id : Long) extends Response
final case class Membership(request_type : SquadResponseType.Value, unk1 : Int, unk2 : Int, unk3 : Long, unk4 : Option[Long], player_name : String, unk5 : Boolean, unk6 : Option[Option[String]]) extends Response //see SquadMembershipResponse
final case class Invite(from_char_id : Long, to_char_id : Long, name : String) extends Response
final case class Join(squad : Squad, positionsToUpdate : List[Int]) extends Response final case class Join(squad : Squad, positionsToUpdate : List[Int]) extends Response
final case class Leave(squad : Squad, positionsToUpdate : List[(Long, Int)]) extends Response final case class Leave(squad : Squad, positionsToUpdate : List[(Long, Int)]) extends Response
final case class UpdateMembers(squad : Squad, update_info : List[SquadAction.Update]) extends Response final case class UpdateMembers(squad : Squad, update_info : List[SquadAction.Update]) extends Response
final case class SwapMember(squad : Squad, from_index : Int, to_index : Int) extends Response
final case class Detail(guid : PlanetSideGUID, leader : String, task : String, zone : PlanetSideZoneID, member_info : List[SquadPositionDetail]) extends Response final case class Detail(guid : PlanetSideGUID, leader : String, task : String, zone : PlanetSideZoneID, member_info : List[SquadPositionDetail]) extends Response
} }

View file

@ -4,9 +4,10 @@ package services.teamwork
import akka.actor.Actor import akka.actor.Actor
import net.psforever.objects.Player import net.psforever.objects.Player
import net.psforever.objects.definition.converter.StatConverter import net.psforever.objects.definition.converter.StatConverter
import net.psforever.objects.teamwork.Squad import net.psforever.objects.loadouts.SquadLoadout
import net.psforever.objects.teamwork.{Member, Squad}
import net.psforever.packet.game._ import net.psforever.packet.game._
import net.psforever.types.{PlanetSideEmpire, SquadRequestType, Vector3} import net.psforever.types._
import services.{GenericEventBus, Service} import services.{GenericEventBus, Service}
import scala.collection.concurrent.TrieMap import scala.collection.concurrent.TrieMap
@ -16,7 +17,9 @@ import scala.collection.mutable.ListBuffer
//import scala.concurrent.duration._ //import scala.concurrent.duration._
class SquadService extends Actor { class SquadService extends Actor {
private var memberToSquad : TrieMap[String, Squad] = new TrieMap[String, Squad]() import SquadService._
private var memberToSquad : mutable.LongMap[Squad] = mutable.LongMap[Squad]()
private var idToSquad : TrieMap[PlanetSideGUID, Squad] = new TrieMap[PlanetSideGUID, Squad]() private var idToSquad : TrieMap[PlanetSideGUID, Squad] = new TrieMap[PlanetSideGUID, Squad]()
private var i : Int = 1 private var i : Int = 1
private val publishedLists : TrieMap[PlanetSideEmpire.Value, ListBuffer[SquadInfo]] = TrieMap[PlanetSideEmpire.Value, ListBuffer[SquadInfo]]( private val publishedLists : TrieMap[PlanetSideEmpire.Value, ListBuffer[SquadInfo]] = TrieMap[PlanetSideEmpire.Value, ListBuffer[SquadInfo]](
@ -24,35 +27,39 @@ class SquadService extends Actor {
PlanetSideEmpire.NC -> ListBuffer.empty, PlanetSideEmpire.NC -> ListBuffer.empty,
PlanetSideEmpire.VS -> ListBuffer.empty PlanetSideEmpire.VS -> ListBuffer.empty
) )
private val bids : mutable.LongMap[(PlanetSideGUID, Int)] = mutable.LongMap[(PlanetSideGUID, Int)]() private val bids : mutable.LongMap[PositionBid] = mutable.LongMap[PositionBid]()
private [this] val log = org.log4s.getLogger private [this] val log = org.log4s.getLogger
override def preStart = { override def preStart : Unit = {
log.info("Starting...") log.info("Starting...")
val testSquad = new Squad(PlanetSideGUID(3), PlanetSideEmpire.VS) val testSquad = new Squad(PlanetSideGUID(3), PlanetSideEmpire.VS)
testSquad.Task = "\\#66CCFF Sentinels of Auraxis\\#FFFFFF ... \\#40FF40 Squad Up!!" testSquad.Task = "\\#66CCFF Sentinels of Auraxis\\#FFFFFF ... \\#40FF40 Squad Up!!"
testSquad.ZoneId = 5 testSquad.ZoneId = 5
testSquad.Membership(0).Name = "Wizkid45" testSquad.Membership(0).Name = "Wizkid45"
testSquad.Membership(0).Role = "Fearless Leader"
testSquad.Membership(0).CharId = 30910985L testSquad.Membership(0).CharId = 30910985L
testSquad.Membership(0).ZoneId = 5 testSquad.Membership(0).ZoneId = 5
testSquad.Membership(0).Health = 64 testSquad.Membership(0).Health = 64
testSquad.Membership(0).Armor = 34 testSquad.Membership(0).Armor = 34
testSquad.Membership(0).Position = Vector3(5526.5234f, 3818.7344f, 54.59375f) testSquad.Membership(0).Position = Vector3(5526.5234f, 3818.7344f, 54.59375f)
testSquad.Membership(1).Name = "xoBLADEox" testSquad.Membership(1).Name = "xoBLADEox"
testSquad.Membership(1).Role = "Right Hand"
testSquad.Membership(1).CharId = 42781919L testSquad.Membership(1).CharId = 42781919L
testSquad.Membership(1).ZoneId = 5 testSquad.Membership(1).ZoneId = 5
testSquad.Membership(1).Health = 54 testSquad.Membership(1).Health = 54
testSquad.Membership(1).Armor = 44 testSquad.Membership(1).Armor = 44
testSquad.Membership(1).Position = Vector3(4673.5312f, 2604.8047f, 40.015625f) testSquad.Membership(1).Position = Vector3(4673.5312f, 2604.8047f, 40.015625f)
testSquad.Membership(3).Name = "cabal0428" testSquad.Membership(3).Name = "cabal0428"
testSquad.Membership(3).Role = "Left Hand"
testSquad.Membership(3).CharId = 353380L testSquad.Membership(3).CharId = 353380L
testSquad.Membership(3).ZoneId = 5 testSquad.Membership(3).ZoneId = 5
testSquad.Membership(3).Health = 44 testSquad.Membership(3).Health = 44
testSquad.Membership(3).Armor = 54 testSquad.Membership(3).Armor = 54
testSquad.Membership(3).Position = Vector3(4727.492f, 2613.5312f, 51.390625f) testSquad.Membership(3).Position = Vector3(4727.492f, 2613.5312f, 51.390625f)
testSquad.Membership(4).Name = "xSkiku" testSquad.Membership(4).Name = "xSkiku"
testSquad.Membership(4).Role = "Right Foot's Middle Toe's Nail"
testSquad.Membership(4).CharId = 41588340L testSquad.Membership(4).CharId = 41588340L
testSquad.Membership(4).ZoneId = 5 testSquad.Membership(4).ZoneId = 5
testSquad.Membership(4).Health = 34 testSquad.Membership(4).Health = 34
@ -60,7 +67,7 @@ class SquadService extends Actor {
testSquad.Membership(4).Position = Vector3(3675.0f, 4789.8047f, 63.21875f) testSquad.Membership(4).Position = Vector3(3675.0f, 4789.8047f, 63.21875f)
idToSquad(PlanetSideGUID(3)) = testSquad idToSquad(PlanetSideGUID(3)) = testSquad
testSquad.Listed = true testSquad.Listed = true
UpdateSquadList(testSquad, List()) UpdateSquadList(testSquad, None)
} }
def GetNextSquadId() : PlanetSideGUID = { def GetNextSquadId() : PlanetSideGUID = {
@ -76,7 +83,7 @@ class SquadService extends Actor {
} }
def GetParticipatingSquad(player : Player, zone : Int) : Option[Squad] = { def GetParticipatingSquad(player : Player, zone : Int) : Option[Squad] = {
memberToSquad.get(player.Name) match { memberToSquad.get(player.CharId) match {
case opt @ Some(squad) => case opt @ Some(squad) =>
squad.Membership.find(_.Name == player.Name).get.ZoneId = zone squad.Membership.find(_.Name == player.Name).get.ZoneId = zone
opt opt
@ -86,10 +93,10 @@ class SquadService extends Actor {
} }
def GetLeadingSquad(player : Player, zone : Int, opt : Option[Squad]) : Squad = { def GetLeadingSquad(player : Player, zone : Int, opt : Option[Squad]) : Squad = {
val name = player.Name val charId = player.CharId
val squadOut = opt match { val squadOut = opt match {
case Some(squad) => case Some(squad) =>
if(squad.Leader.Name.equals(name)) { if(squad.Leader.CharId == charId) {
squad squad
} }
else { else {
@ -97,21 +104,23 @@ class SquadService extends Actor {
} }
case None => case None =>
memberToSquad.get(name) match { memberToSquad.get(charId) match {
case Some(squad) if squad.Leader.Name.equals(name) => case Some(squad) if squad.Leader.CharId.equals(charId) =>
squad squad
case _ => case _ =>
val faction = player.Faction val faction = player.Faction
val id = GetNextSquadId() val id = GetNextSquadId()
val squad = new Squad(id, faction) val squad = new Squad(id, faction)
val leadPosition = squad.Membership(squad.LeaderPositionIndex) val leadPosition = squad.Membership(squad.LeaderPositionIndex)
val name = player.Name
leadPosition.Name = name leadPosition.Name = name
leadPosition.CharId = charId
leadPosition.Health = player.Health leadPosition.Health = player.Health
leadPosition.Armor = player.Armor leadPosition.Armor = player.Armor
leadPosition.Position = player.Position leadPosition.Position = player.Position
leadPosition.ZoneId = zone leadPosition.ZoneId = zone
log.info(s"$name-$faction has started a new squad") log.info(s"$name-$faction has started a new squad")
memberToSquad += name -> squad memberToSquad += charId -> squad
idToSquad += id -> squad idToSquad += id -> squad
squad squad
} }
@ -130,26 +139,27 @@ class SquadService extends Actor {
log.info(s"$who has joined $path") log.info(s"$who has joined $path")
SquadEvents.subscribe(who, path) SquadEvents.subscribe(who, path)
//send initial squad catalog //send initial squad catalog
sender ! SquadServiceResponse(s"$faction/Squad", SquadResponse.Init(publishedLists(PlanetSideEmpire(faction)).toVector)) sender ! SquadServiceResponse(s"$faction/Squad", SquadResponse.InitList(publishedLists(PlanetSideEmpire(faction)).toVector))
//subscribe to the player's personal channel - necessary only to inform about any previous squad association //subscribe to the player's personal channel - necessary only to inform about any previous squad association
case Service.Join(name) => case Service.Join(char_id) =>
val path = s"$name/Squad" val path = s"$char_id/Squad"
val who = sender() val who = sender()
log.info(s"$who has joined $path") log.info(s"$who has joined $path")
SquadEvents.subscribe(who, path) //TODO squad-specific switchboard SquadEvents.subscribe(who, path) //TODO squad-specific switchboard
//check for renewable squad information //check for renewable squad information
memberToSquad.get(name) match { memberToSquad.get(char_id.toLong) match {
case None => ; case None => ;
case Some(_) => case Some(_) =>
sender ! SquadServiceMessage.RecoverSquadMembership() //TODO? sender ! SquadServiceMessage.RecoverSquadMembership() //TODO?
} }
case Service.Leave(Some(name)) => ; case Service.Leave(Some(char_id)) => ;
SquadEvents.unsubscribe(sender()) SquadEvents.unsubscribe(sender())
memberToSquad.get(name) match { val longCharId = char_id.toLong
memberToSquad.get(longCharId) match {
case Some(squad) => case Some(squad) =>
if(squad.Leader.Name.equals(name)) { if(squad.Leader.Name.equals(char_id)) {
//we were the leader //we were the leader
if(squad.Membership.count(p => p.Name.equals("")) > 1) { if(squad.Membership.count(p => p.Name.equals("")) > 1) {
//other players were in the squad; publicly disband it //other players were in the squad; publicly disband it
@ -162,19 +172,19 @@ class SquadService extends Actor {
position.Armor = 0 position.Armor = 0
}) })
} }
memberToSquad.remove(name) memberToSquad.remove(longCharId)
idToSquad.remove(squad.GUID) idToSquad.remove(squad.GUID)
UpdateSquadList(squad, List()) UpdateSquadList(squad, None)
} }
else { else {
//we were just a grunt in the squad //we were just a grunt in the squad
val position = squad.Membership.find(_.Name == name).get val position = squad.Membership.find(_.CharId == longCharId).get
position.Name = "" position.Name = ""
position.ZoneId = 0 position.ZoneId = 0
position.Position = Vector3.Zero position.Position = Vector3.Zero
position.Health = 0 position.Health = 0
position.Armor = 0 position.Armor = 0
UpdateSquadDetail(squad) UpdateSquadDetail(squad.GUID, squad)
} }
case None => ; case None => ;
} }
@ -184,67 +194,116 @@ class SquadService extends Actor {
case SquadServiceMessage(tplayer, squad_action) => squad_action match { case SquadServiceMessage(tplayer, squad_action) => squad_action match {
case SquadAction.Membership(request_type, char_id, optional_char_id, _, _) => request_type match { case SquadAction.Membership(request_type, char_id, optional_char_id, _, _) => request_type match {
case SquadRequestType.Accept => case SquadRequestType.Invite =>
bids.get(char_id) match { (optional_char_id, memberToSquad.get(char_id)) match {
case Some((squadGUID, line)) if idToSquad.get(squadGUID).nonEmpty => case (Some(toCharId), Some(squad)) =>
//join squad bids(toCharId) = VacancyBid(char_id, squad.GUID)
val squad = idToSquad(squadGUID) SquadEvents.publish( SquadServiceResponse(s"$toCharId/Squad", SquadResponse.Invite(char_id, toCharId, tplayer.Name)) )
val position = squad.Membership(line) case (Some(toCharId), None) =>
if(squad.Availability(line) && position.CharId == 0 && val ourSquad = GetLeadingSquad(tplayer, 1, None)
tplayer.Certifications.intersect(position.Requirements) == position.Requirements) { memberToSquad.remove(char_id)
position.Name = tplayer.Name bids(toCharId) = CamraderieBid(char_id, ourSquad)
position.CharId = char_id SquadEvents.publish( SquadServiceResponse(s"$toCharId/Squad", SquadResponse.Invite(char_id, toCharId, tplayer.Name)) )
position.Health = tplayer.Health
position.Armor = tplayer.Armor
position.Position = tplayer.Position
position.ZoneId = 13
memberToSquad(tplayer.Name) = squad
//joining the squad
sender ! SquadServiceResponse("", SquadResponse.Join(
squad,
squad.Membership.zipWithIndex.collect({ case (member, index) if member.CharId != 0 => index }).toList
))
//other squad members see us joining the squad
val updatedIndex = List(line)
squad.Membership
.collect({ case member if member.CharId != 0 && member.CharId != char_id => member.Name })
.foreach { name =>
SquadEvents.publish( SquadServiceResponse(s"$name/Squad", SquadResponse.Join(squad, updatedIndex)) )
}
}
bids.remove(char_id)
case _ => ; case _ => ;
} }
case SquadRequestType.Leave => case SquadRequestType.Leave =>
val squad = memberToSquad(char_id)
if(optional_char_id.contains(char_id)) { if(optional_char_id.contains(char_id)) {
//we're leaving the squad on our own //leaving the squad of our own accord
val name = tplayer.Name }
val squad = memberToSquad(name) else {
val membership = squad.Membership.zipWithIndex val leader = squad.Leader
val (member, index) = membership if(optional_char_id.contains(leader.CharId)) {
.find { case (_member, _) => _member.Name == name } //kicked by the squad leader
.get }
val updateList = membership.collect({ case (_member, _index) if _member.CharId > 0 => (_member.CharId, _index) }).toList }
memberToSquad.remove(name) val membership = squad.Membership.zipWithIndex
member.Name = "" val (member, index) = membership
member.CharId = 0 .find { case (_member, _) => _member.CharId == char_id }
//leaving the squad completely .get
val updateList = membership.collect({ case (_member, _index) if _member.CharId > 0 => (_member.CharId, _index) }).toList
memberToSquad.remove(char_id)
member.Name = ""
member.CharId = 0
val size = squad.Size
if(size == 1) {
//squad is rendered to just one person; collapse it
(membership.collect({ case (_member, _) if _member.CharId > 0 => _member.CharId }) :+ char_id)
.foreach { charId =>
SquadEvents.publish( SquadServiceResponse(s"$charId/Squad", SquadResponse.Leave(squad, updateList)) )
memberToSquad.remove(charId)
}
idToSquad.remove(squad.GUID)
}
else {
//squad continues, despite player's parting
//member leaves the squad completely
sender ! SquadServiceResponse("", SquadResponse.Leave(squad, updateList)) sender ! SquadServiceResponse("", SquadResponse.Leave(squad, updateList))
//other squad members see us leaving the squad //other squad members see the member leaving only
val leavingMember = List((char_id, index)) val leavingMember = List((char_id, index))
membership membership
.collect({ case (_member, _) if _member.CharId > 0 => _member.Name }) .collect({ case (_member, _) if _member.CharId > 0 => _member.CharId })
.foreach { name => .foreach { charId =>
SquadEvents.publish( SquadServiceResponse(s"$name/Squad", SquadResponse.Leave(squad, leavingMember)) ) SquadEvents.publish( SquadServiceResponse(s"$charId/Squad", SquadResponse.Leave(squad, leavingMember)) )
} }
}
case SquadRequestType.Accept =>
bids.remove(char_id) match {
case Some(NormalBid(inviterCharId, squadGUID, line)) if idToSquad.get(squadGUID).nonEmpty =>
JoinSquad(tplayer, idToSquad(squadGUID), line)
case Some(VacancyBid(inviterCharId, squadGUID)) if idToSquad.get(squadGUID).nonEmpty =>
val squad = idToSquad(squadGUID)
squad.Membership.zipWithIndex.find({ case (member, index) =>
ValidOpenSquadPosition(squad, index, squad.Membership(index), tplayer.Certifications)
}) match {
case Some((_, line)) =>
JoinSquad(tplayer, squad, line)
case _ => ;
}
case Some(CamraderieBid(inviterCharId, placeholderSquad)) =>
(GetParticipatingSquad(tplayer, 1) match {
case Some(participating) =>
if(participating.Leader.CharId == inviterCharId) {
Some(participating)
}
else {
//inviter is not leader of squad; bounce this request off of the squad leader
val leaderCharId = participating.Leader.CharId
bids(char_id) = VacancyBid(leaderCharId, participating.GUID) //reframed request
//TODO squad leader receives "<player> wants to join squad" prompt
//SquadEvents.publish(SquadServiceResponse(s"$leaderCharId/Squad", SquadResponse.Invite(char_id, leaderCharId, tplayer.Name)))
None
}
case None =>
placeholderSquad.Task = s"${tplayer.Name}'s Squad"
memberToSquad(inviterCharId) = placeholderSquad
SquadEvents.publish(SquadServiceResponse(s"$inviterCharId/Squad", SquadResponse.InitSquad(placeholderSquad.GUID)))
Some(placeholderSquad)
}) match {
case Some(squad) =>
squad.Membership.zipWithIndex.find({ case (member, index) =>
ValidOpenSquadPosition(squad, index, squad.Membership(index), tplayer.Certifications)
}) match {
case Some((_, line)) =>
JoinSquad(tplayer, squad, line)
case _ => ;
}
case None => ;
}
case _ => ;
} }
case _ => ; case _ => ;
} }
case SquadAction.Update(char_id, health, max_health, armor, max_armor, pos, zone_number) => case SquadAction.Update(char_id, health, max_health, armor, max_armor, pos, zone_number) =>
memberToSquad.get(tplayer.Name) match { memberToSquad.get(char_id) match {
case Some(squad) => case Some(squad) =>
squad.Membership.find(_.CharId == char_id) match { squad.Membership.find(_.CharId == char_id) match {
case Some(member) => case Some(member) =>
@ -263,46 +322,64 @@ class SquadService extends Actor {
)) ))
case _ => ; case _ => ;
} }
// val (self, others) = squad.Membership.partition(_.CharId == char_id)
// self match {
// case Array(member) =>
// val newHealth = StatConverter.Health(health, max_health, min=1, max=64)
// val newArmor = StatConverter.Health(armor, max_armor, min=1, max=64)
// member.Health = newHealth
// member.Armor = newArmor
// member.Position = pos
// member.ZoneId = zone_number
// sender ! SquadServiceResponse("", SquadResponse.UpdateMembers(
// squad,
// others
// .map { member => SquadAction.Update(member.CharId, member.Health, 0, member.Armor, 0, member.Position, member.ZoneId) }
// .toList
// ))
// case _ => ;
// }
case None => ; case None => ;
} }
case SquadAction.Definition(tplayer : Player, zone_ordinal_number : Int, guid : PlanetSideGUID, _ : Int, action : SquadAction) => case SquadAction.Definition(tplayer : Player, zone_ordinal_number : Int, guid : PlanetSideGUID, line : Int, action : SquadAction) =>
import net.psforever.packet.game.SquadAction._ import net.psforever.packet.game.SquadAction._
val squadOpt = GetParticipatingSquad(tplayer, zone_ordinal_number) val squadOpt = GetParticipatingSquad(tplayer, zone_ordinal_number)
action match { action match {
case SaveSquadDefinition() => case SaveSquadFavorite() =>
squadOpt match {
case Some(squad) if squad.Leader.CharId == tplayer.CharId && squad.Task.nonEmpty && squad.ZoneId > 0 =>
tplayer.SquadLoadouts.SaveLoadout(squad, squad.Task, line)
sender ! SquadServiceResponse("", SquadResponse.ListSquadFavorite(line, squad.Task))
case _ => ;
}
case LoadSquadFavorite() =>
(squadOpt match {
case Some(squad) if squad.Size == 1 =>
//we are the leader of our own squad, but no one else has joined yet
Some(squad)
case None => ;
//we are not yet member of a squad; start a squad to support our favorite definition
Some(GetLeadingSquad(tplayer, zone_ordinal_number, None))
case _ => ;
//player is member of populated squad; should not overwrite squad definition with favorite
None
}, tplayer.SquadLoadouts.LoadLoadout(line)) match {
case (Some(squad : Squad), Some(loadout : SquadLoadout)) =>
log.info(s"${tplayer.Name} is loading a squad composition: $loadout")
SquadService.LoadSquadDefinition(squad, loadout)
sender ! SquadServiceResponse("", SquadResponse.InitSquad(squad.GUID))
UpdateSquadList(squad, SquadInfo().Task(squad.Task).ZoneId(PlanetSideZoneID(squad.ZoneId)).Capacity(squad.Capacity))
UpdateSquadDetail(PlanetSideGUID(0), squad)
case _ => ;
}
case DeleteSquadFavorite() =>
tplayer.SquadLoadouts.DeleteLoadout(line)
sender ! SquadServiceResponse("", SquadResponse.ListSquadFavorite(line, ""))
case ChangeSquadPurpose(purpose) => case ChangeSquadPurpose(purpose) =>
log.info(s"${tplayer.Name}-${tplayer.Faction} has changed his squad's task to $purpose") log.info(s"${tplayer.Name}-${tplayer.Faction} has changed his squad's task to $purpose")
val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt) val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt)
squad.Task = purpose squad.Task = purpose
UpdateSquadList(squad, List(SquadInfo.Field.Task))
UpdateSquadDetail(squad) UpdateSquadListWhenListed(squad, SquadInfo().Task(purpose))
SquadDetail().Task(purpose)
UpdateListedSquadDetail(squad.GUID, squad)
case ChangeSquadZone(zone) => case ChangeSquadZone(zone) =>
log.info(s"${tplayer.Name}-${tplayer.Faction} has changed squad's ops zone to $zone") log.info(s"${tplayer.Name}-${tplayer.Faction} has changed squad's ops zone to $zone")
val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt) val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt)
squad.ZoneId = zone.zoneId.toInt squad.ZoneId = zone.zoneId.toInt
UpdateSquadList(squad, List(SquadInfo.Field.ZoneId)) UpdateSquadListWhenListed(squad, SquadInfo().ZoneId(zone))
UpdateSquadDetail(squad) SquadDetail().ZoneId(zone)
UpdateListedSquadDetail(squad.GUID, squad)
case CloseSquadMemberPosition(position) => case CloseSquadMemberPosition(position) =>
val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt) val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt)
@ -312,14 +389,15 @@ class SquadService extends Actor {
log.info(s"${tplayer.Name}-${tplayer.Faction} has closed the #$position position in squad") log.info(s"${tplayer.Name}-${tplayer.Faction} has closed the #$position position in squad")
val memberPosition = squad.Membership(position) val memberPosition = squad.Membership(position)
val listingChanged = if(memberPosition.Name.nonEmpty) { val listingChanged = if(memberPosition.Name.nonEmpty) {
List(SquadInfo.Field.Size, SquadInfo.Field.Capacity) SquadInfo().Size(squad.Size).Capacity(squad.Capacity)
} }
else { else {
List(SquadInfo.Field.Capacity) SquadInfo().Capacity(squad.Capacity)
} }
memberPosition.Close() memberPosition.Close()
UpdateSquadList(squad, listingChanged) UpdateSquadListWhenListed(squad, listingChanged)
UpdateSquadDetail(squad) SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail.Closed)))
UpdateListedSquadDetail(squad.GUID, squad)
case Some(false) | None => ; case Some(false) | None => ;
} }
@ -329,8 +407,9 @@ class SquadService extends Actor {
case Some(false) => case Some(false) =>
log.info(s"${tplayer.Name}-${tplayer.Faction} has opened the #$position position in squad") log.info(s"${tplayer.Name}-${tplayer.Faction} has opened the #$position position in squad")
squad.Availability.update(position, true) squad.Availability.update(position, true)
UpdateSquadList(squad, List(SquadInfo.Field.Capacity)) UpdateSquadListWhenListed(squad, SquadInfo().Capacity(squad.Capacity))
UpdateSquadDetail(squad) SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail.Open)))
UpdateListedSquadDetail(squad.GUID, squad)
case Some(true) | None => ; case Some(true) | None => ;
} }
@ -340,7 +419,8 @@ class SquadService extends Actor {
case Some(true) => case Some(true) =>
log.info(s"${tplayer.Name}-${tplayer.Faction} has changed the role of squad position #$position") log.info(s"${tplayer.Name}-${tplayer.Faction} has changed the role of squad position #$position")
squad.Membership(position).Role = role squad.Membership(position).Role = role
UpdateSquadDetail(squad) SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail().Role(role))))
UpdateListedSquadDetail(squad.GUID, squad)
case Some(false) | None => ; case Some(false) | None => ;
} }
@ -350,7 +430,8 @@ class SquadService extends Actor {
case Some(true) => case Some(true) =>
log.info(s"${tplayer.Name}-${tplayer.Faction} has changed the orders for squad position #$position") log.info(s"${tplayer.Name}-${tplayer.Faction} has changed the orders for squad position #$position")
squad.Membership(position).Orders = orders squad.Membership(position).Orders = orders
UpdateSquadDetail(squad) SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail().DetailedOrders(orders))))
UpdateListedSquadDetail(squad.GUID, squad)
case Some(false) | None => ; case Some(false) | None => ;
} }
@ -360,7 +441,8 @@ class SquadService extends Actor {
case Some(true) => case Some(true) =>
log.info(s"${tplayer.Name}-${tplayer.Faction} has changed the requirements for squad position #$position") log.info(s"${tplayer.Name}-${tplayer.Faction} has changed the requirements for squad position #$position")
squad.Membership(position).Requirements = certs squad.Membership(position).Requirements = certs
UpdateSquadDetail(squad) SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail().Requirements(certs))))
UpdateListedSquadDetail(squad.GUID, squad)
case Some(false) | None => ; case Some(false) | None => ;
} }
@ -386,12 +468,47 @@ class SquadService extends Actor {
case SelectRoleForYourself(position) => case SelectRoleForYourself(position) =>
//TODO need to ask permission from the squad leader, unless our character is the squad leader or already currently in the squad //TODO need to ask permission from the squad leader, unless our character is the squad leader or already currently in the squad
//val name = tplayer.Name
squadOpt match { squadOpt match {
case Some(squad) if squad.GUID == guid => case Some(squad) if squad.GUID == guid || squad.Leader.Name == tplayer.Name =>
//already a member of this squad; swap positions freely //already a member of this squad, or the leader; swap positions freely
val membership = squad.Membership.zipWithIndex
val toMember = squad.Membership(position)
if(ValidOpenSquadPosition(squad, position, toMember, tplayer.Certifications)) {
//acquire this membership position
membership.find { case (member, _) => member.Name == tplayer.Name } match {
case Some((fromMember, fromIndex)) =>
//copy member details
toMember.Name = fromMember.Name
toMember.CharId = fromMember.CharId
toMember.Health = fromMember.Health
toMember.Armor = fromMember.Armor
toMember.Position = fromMember.Position
toMember.ZoneId = fromMember.ZoneId
//old membership no longer valid
fromMember.Name = ""
fromMember.CharId = 0L
if(fromIndex == squad.LeaderPositionIndex) {
squad.LeaderPositionIndex = position
}
membership
.collect({ case (_member, _) if _member.CharId > 0 => _member.Name })
.foreach { name =>
SquadEvents.publish(SquadServiceResponse(s"$name/Squad", SquadResponse.SwapMember(squad, fromIndex, position)))
}
case None =>
toMember.Name = tplayer.Name
toMember.CharId = tplayer.CharId
toMember.Health = StatConverter.Health(tplayer.Health, tplayer.MaxHealth, min = 1, max = 64)
toMember.Armor = StatConverter.Health(tplayer.Armor, tplayer.MaxArmor, min = 1, max = 64)
toMember.Position = tplayer.Position
toMember.ZoneId = 13
memberToSquad(tplayer.CharId) = squad
}
}
UpdateSquadDetail(squad.GUID, squad)
case Some(_) => case Some(_) =>
//not a member of the requesting squad; do nothing //not a member of the requesting squad; do nothing
case None => case None =>
//not a member of any squad; consider request of joining the target squad //not a member of any squad; consider request of joining the target squad
idToSquad.get(guid) match { idToSquad.get(guid) match {
@ -399,10 +516,10 @@ class SquadService extends Actor {
val member = squad.Membership(position) val member = squad.Membership(position)
if(squad.Availability(position) && member.CharId == 0 && if(squad.Availability(position) && member.CharId == 0 &&
tplayer.Certifications.intersect(member.Requirements) == member.Requirements) { tplayer.Certifications.intersect(member.Requirements) == member.Requirements) {
bids(tplayer.CharId) = (guid, position) bids(tplayer.CharId) = NormalBid(squad.Leader.CharId, guid, position)
val leader = squad.Leader val leader = squad.Leader
//TODO need to ask permission from the squad leader, unless auto-approve is in effect //TODO need to ask permission from the squad leader, unless auto-approve is in effect
sender ! SquadServiceResponse("", SquadResponse.Membership(SquadRequestType.Invite, 0, 0, leader.CharId, Some(tplayer.CharId), leader.Name, false, None)) sender ! SquadServiceResponse("", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, leader.CharId, Some(tplayer.CharId), leader.Name, false, None))
} }
case None => case None =>
@ -411,36 +528,62 @@ class SquadService extends Actor {
} }
} }
case ListSquad() => case RequestListSquad() =>
val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt) val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt)
if(!squad.Listed) { if(!squad.Listed) {
log.info(s"${tplayer.Name}-${tplayer.Faction} has opened recruitment for this squad") log.info(s"${tplayer.Name}-${tplayer.Faction} has opened public recruitment for squad ${squad.Task}")
squad.Listed = true squad.Listed = true
sender ! SquadServiceResponse("", SquadResponse.InitSquad(squad.GUID))
UpdateSquadList(squad, None)
sender ! SquadServiceResponse("", SquadResponse.Unknown17(squad, tplayer.CharId))
}
case StopListSquad() =>
val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt)
if(!squad.Listed) {
log.info(s"${tplayer.Name}-${tplayer.Faction} has closed public recruitment for squad ${squad.Task}")
squad.Listed = false
sender ! SquadServiceResponse("", SquadResponse.InitSquad(PlanetSideGUID(0)))
UpdateSquadList(squad, None)
} }
UpdateSquadList(squad, List())
case ResetAll() => case ResetAll() =>
val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt) squadOpt match {
squad.Task = "" case Some(squad) if squad.Leader.CharId == tplayer.CharId =>
squad.ZoneId = None squad.Task = ""
squad.Availability.indices.foreach { i => squad.ZoneId = None
squad.Availability.update(i, true) squad.Availability.indices.foreach { i =>
squad.Availability.update(i, true)
}
squad.Membership.foreach(position => {
position.Role = ""
position.Orders = ""
position.Requirements = Set()
})
squad.LocationFollowsSquadLead = false
squad.AutoApproveInvitationRequests = false
UpdateSquadList(squad, SquadInfo().Task("").ZoneId(None).Capacity(squad.Capacity))
UpdateSquadDetail(squad.GUID, squad)
case None => ;
} }
squad.Membership.foreach(position => {
position.Role = ""
position.Orders = ""
position.Requirements = Set()
})
UpdateSquadList(squad, List(SquadInfo.Field.Task, SquadInfo.Field.ZoneId, SquadInfo.Field.Size, SquadInfo.Field.Capacity))
UpdateSquadDetail(squad)
case DisplaySquad() => case DisplaySquad() =>
idToSquad.get(guid) match { idToSquad.get(guid) match {
case Some(squad) => case Some(squad) =>
sender ! SquadServiceResponse(s"${tplayer.Name}/Squad", GenSquadDetail(squad)) sender ! SquadServiceResponse(s"${tplayer.Name}/Squad", GenSquadDetail(squad.GUID, squad))
case None => ; case None => ;
} }
// case AnswerSquadJoinRequest() =>
// idToSquad.get(guid) match {
// case Some(squad) =>
// if(squad.Leader.Name == tplayer.Name && squad.Listed) {
// //squad was just listed but we have not yet declared ourselves as the leader
// UpdateSquadDetail(squad)
// }
// case None => ;
// }
case _ => ; case _ => ;
} }
@ -449,54 +592,109 @@ class SquadService extends Actor {
} }
} }
def UpdateSquadList(squad : Squad, listingChanged : List[Int]) : Unit = { def JoinSquad(player : Player, squad : Squad, line : Int) : Boolean = {
//queue updates val charId = player.CharId
val position = squad.Membership(line)
if(squad.Availability(line) && position.CharId == 0 &&
player.Certifications.intersect(position.Requirements) == position.Requirements) {
position.Name = player.Name
position.CharId = charId
position.Health = StatConverter.Health(player.Health, player.MaxHealth, min=1, max=64)
position.Armor = StatConverter.Health(player.Armor, player.MaxArmor, min=1, max=64)
position.Position = player.Position
position.ZoneId = 13
memberToSquad(player.CharId) = squad
val size = squad.Size
if(size == 1) {
//leader joins the squad? do nothing?
squad.LeaderPositionIndex = line
}
else if(size == 2) {
//first squad member after leader; both members fully initialize
val indices = squad.Membership.zipWithIndex.collect({ case (member, index) if member.CharId != 0 => index }).toList
squad.Membership
.collect({ case member if member.CharId != 0 => member.CharId })
.foreach { charId =>
SquadEvents.publish(SquadServiceResponse(s"$charId/Squad", SquadResponse.Join(squad, indices)))
}
}
else {
//joining an active squad; everybody updates differently
//new member gets full UI updates
sender ! SquadServiceResponse("", SquadResponse.Join(
squad,
squad.Membership.zipWithIndex.collect({ case (member, index) if member.CharId != 0 => index }).toList
))
//other squad members see us joining the squad
val updatedIndex = List(line)
squad.Membership
.collect({ case member if member.CharId != 0 && member.CharId != charId => member.CharId })
.foreach { charId =>
SquadEvents.publish(SquadServiceResponse(s"$charId/Squad", SquadResponse.Join(squad, updatedIndex)))
}
}
true
}
else {
false
}
}
def UpdateSquadList(squad : Squad, changes : SquadInfo) : Unit = {
UpdateSquadList(squad, Some(changes))
}
def UpdateSquadListWhenListed(squad : Squad, changes : SquadInfo) : Unit = {
UpdateSquadListWhenListed(squad, Some(changes))
}
def UpdateSquadListWhenListed(squad : Squad, changes : Option[SquadInfo]) : Unit = {
if(squad.Listed) { if(squad.Listed) {
val entry = SquadService.Publish(squad) UpdateSquadList(squad, changes)
val faction = squad.Faction }
val factionListings = publishedLists(faction) }
factionListings.find(info => {
info.squad_guid match { def UpdateSquadList(squad : Squad, changes : Option[SquadInfo]) : Unit = {
case Some(sguid) => sguid == squad.GUID val entry = SquadService.SquadList.Publish(squad)
case _ => false val faction = squad.Faction
} val factionListings = publishedLists(faction)
}) match { factionListings.find(info => {
case Some(listedSquad) => info.squad_guid match {
val index = factionListings.indexOf(listedSquad) case Some(sguid) => sguid == squad.GUID
val changes = if(listingChanged.nonEmpty) { case _ => false
SquadService.Differences(listingChanged, entry) }
} }) match {
else { case Some(listedSquad) =>
SquadService.Differences(listedSquad, entry) val index = factionListings.indexOf(listedSquad)
} changes match {
if(changes != SquadInfo.Blank) { case Some(changedFields) =>
//squad information update //squad information update
log.info(s"Squad will be updated") log.info(s"Squad will be updated")
factionListings(index) = entry factionListings(index) = entry
SquadEvents.publish( SquadEvents.publish(
SquadServiceResponse(s"$faction/Squad", SquadResponse.Update(Seq((index, changes)))) SquadServiceResponse(s"$faction/Squad", SquadResponse.UpdateList(Seq((index, changedFields))))
) )
} case None =>
else {
//remove squad from listing //remove squad from listing
log.info(s"Squad will be removed") log.info(s"Squad will be removed")
factionListings.remove(index) factionListings.remove(index)
SquadEvents.publish( SquadEvents.publish(
SquadServiceResponse(s"$faction/Squad", SquadResponse.Remove(Seq(index))) //SquadServiceResponse(s"$faction/Squad", SquadResponse.RemoveFromList(Seq(index)))
SquadServiceResponse(s"$faction/Squad", SquadResponse.InitList(factionListings.toVector))
) )
} }
case None => case None =>
//first time being published //first time being published
log.info(s"Squad will be introduced") log.info(s"Squad will be introduced")
factionListings += SquadService.Publish(squad) factionListings += entry
SquadEvents.publish( SquadEvents.publish(
SquadServiceResponse(s"$faction/Squad", SquadResponse.Init(factionListings.toVector)) SquadServiceResponse(s"$faction/Squad", SquadResponse.InitList(factionListings.toVector))
) )
}
} }
} }
def GenSquadDetail(squad : Squad) : SquadResponse.Detail = { def GenSquadDetail(guid : PlanetSideGUID, squad : Squad) : SquadResponse.Detail = {
SquadResponse.Detail( SquadResponse.Detail(
squad.GUID, squad.GUID,
squad.Leader.Name, squad.Leader.Name,
@ -513,60 +711,164 @@ class SquadService extends Actor {
) )
} }
def UpdateSquadDetail(squad : Squad) : Unit = { def UpdateListedSquadDetail(guid : PlanetSideGUID, squad : Squad) : Unit = {
val detail = GenSquadDetail(squad) if(squad.Listed) {
squad.Membership.collect { UpdateSquadDetail(guid, squad)
case member if !member.Name.equals("") =>
member.Name
}.foreach { name =>
SquadEvents.publish(
SquadServiceResponse(s"$name/Squad", detail)
)
} }
} }
def UpdateSquadDetail(guid : PlanetSideGUID, squad : Squad) : Unit = {
val detail = GenSquadDetail(guid, squad)
squad.Membership
.collect { case member if member.CharId > 0L => member.CharId }
.foreach { charId =>
SquadEvents.publish(SquadServiceResponse(s"$charId/Squad", detail))
}
}
} }
object SquadService { object SquadService {
def Publish(squad : Squad) : SquadInfo = { trait PositionBid
SquadInfo(
squad.Leader.Name,
squad.Task,
PlanetSideZoneID(squad.ZoneId),
squad.Size,
squad.Capacity,
squad.GUID
)
}
def Differences(updates : List[Int], info : SquadInfo) : SquadInfo = { final case class NormalBid(char_id : Long, squad_guid : PlanetSideGUID, position : Int) extends PositionBid
if(updates.nonEmpty) {
val list = Seq( final case class VacancyBid(char_id : Long, squad_guid : PlanetSideGUID) extends PositionBid
SquadInfo.Blank, //must be index-0
SquadInfo(info.leader, None, None, None, None), final case class CamraderieBid(char_id : Long, placeholder : Squad) extends PositionBid
SquadInfo(None, info.task, None, None, None),
SquadInfo(None, None, info.zone_id, None, None), object SquadList {
SquadInfo(None, None, None, info.size, None), def Publish(squad : Squad) : SquadInfo = {
SquadInfo(None, None, None, None, info.capacity) SquadInfo(
squad.Leader.Name,
squad.Task,
PlanetSideZoneID(squad.ZoneId),
squad.Size,
squad.Capacity,
squad.GUID
) )
var out = SquadInfo.Blank
updates
.map(i => list(i))
.filterNot { _ == SquadInfo.Blank }
.foreach(sinfo => out = out And sinfo )
out
}
else {
SquadInfo.Blank
} }
} }
def Differences(before : SquadInfo, after : SquadInfo) : SquadInfo = { object Detail {
SquadInfo( def Publish(squad : Squad) : SquadDetail = {
if(!before.leader.equals(after.leader)) after.leader else None, SquadDetail()
if(!before.task.equals(after.task)) after.task else None, .LeaderCharId(squad.Leader.CharId)
if(!before.zone_id.equals(after.zone_id)) after.zone_id else None, .LeaderName(squad.Leader.Name)
if(!before.size.equals(after.size)) after.size else None, .Task(squad.Task)
if(!before.capacity.equals(after.capacity)) after.capacity else None .ZoneId(PlanetSideZoneID(squad.ZoneId))
) .Members(
squad.Membership.zipWithIndex.map({ case (p, index) =>
SquadPositionEntry(index, if(squad.Availability(index)) {
SquadPositionDetail(p.Role, p.Orders, p.Requirements, p.CharId, p.Name)
}
else {
SquadPositionDetail.Closed
})
}).toList
)
.Complete
}
def Differences(updates : List[Int], info : SquadDetail) : SquadDetail = {
if(updates.nonEmpty) {
val list = Seq(
SquadDetail.Blank, //must be index-0
SquadDetail(info.unk1, None, None, None, None, None, None, None, None),
SquadDetail(None, None, info.leader_char_id, None, None, None, None, None, None),
SquadDetail(None, None, None, info.unk3, None, None, None, None, None),
SquadDetail(None, None, None, None, info.leader_name, None, None, None, None),
SquadDetail(None, None, None, None, None, info.task, None, None, None),
SquadDetail(None, None, None, None, None, None, info.zone_id, None, None),
SquadDetail(None, None, None, None, None, None, None, info.unk7, None),
SquadDetail(None, None, None, None, None, None, None, None, info.member_info)
)
var out = SquadDetail.Blank
updates
.map(i => list(i))
.filterNot { _ == SquadDetail.Blank }
.foreach(sinfo => out = out And sinfo )
out
}
else {
SquadDetail.Blank
}
}
def Differences(before : SquadDetail, after : SquadDetail) : SquadDetail = {
SquadDetail(
if(!before.unk1.equals(after.unk1)) after.unk1 else None,
None,
if(!before.leader_char_id.equals(after.leader_char_id)) after.leader_char_id else None,
if(!before.unk3.equals(after.unk3)) after.unk3 else None,
if(!before.leader_name.equals(after.leader_name)) after.leader_name else None,
if(!before.task.equals(after.task)) after.task else None,
if(!before.zone_id.equals(after.zone_id)) after.zone_id else None,
if(!before.unk7.equals(after.unk7)) after.unk7 else None,
{
(before.member_info, after.member_info) match {
case (Some(beforeEntry), Some(afterEntry)) =>
val out = beforeEntry.zip(afterEntry)
.map { case (b, a) => PositionEquality(b, a) }
.collect { case Some(entry) => entry }
if(out.nonEmpty) {
Some(out)
}
else {
None
}
case _ =>
None
}
}
)
}
private def PositionEquality(before : SquadPositionEntry, after : SquadPositionEntry) : Option[SquadPositionEntry] = {
(before.info, after.info) match {
case (Some(binfo), Some(ainfo)) =>
val out = MemberEquality(binfo, ainfo)
if(out == SquadPositionDetail.Blank) {
None
}
else {
Some(SquadPositionEntry(before.index, out))
}
case _ =>
None
}
}
private def MemberEquality(before : SquadPositionDetail, after : SquadPositionDetail) : SquadPositionDetail = {
SquadPositionDetail(
if(!before.is_closed.equals(after.is_closed)) after.is_closed else None,
if(!before.role.equals(after.role)) after.role else None,
if(!before.detailed_orders.equals(after.detailed_orders)) after.detailed_orders else None,
if(!before.requirements.equals(after.requirements)) after.requirements else None,
if(!before.char_id.equals(after.char_id)) after.char_id else None,
if(!before.name.equals(after.name)) after.name else None
)
}
}
def LoadSquadDefinition(squad : Squad, favorite : SquadLoadout) : Unit = {
squad.Task = favorite.task
squad.ZoneId = favorite.zone_id.getOrElse(squad.ZoneId)
squad.Availability.indices.foreach { index => squad.Availability.update(index, false) }
squad.Membership.foreach { position =>
position.Role = ""
position.Orders = ""
position.Requirements = Set()
}
favorite.members.foreach { position =>
squad.Availability.update(position.index, true)
val member = squad.Membership(position.index)
member.Role = position.role
member.Orders = position.orders
member.Requirements = position.requirements
}
}
def ValidOpenSquadPosition(squad : Squad, position : Int, member : Member, reqs : Set[CertificationType.Value]) : Boolean = {
squad.Availability(position) && member.CharId == 0 && reqs.intersect(member.Requirements) == member.Requirements
} }
} }

View file

@ -54,7 +54,7 @@ class SquadDefinitionActionMessageTest extends Specification {
case SquadDefinitionActionMessage(unk1, unk2, action) => case SquadDefinitionActionMessage(unk1, unk2, action) =>
unk1 mustEqual PlanetSideGUID(0) unk1 mustEqual PlanetSideGUID(0)
unk2 mustEqual 3 unk2 mustEqual 3
action mustEqual SaveSquadDefinition() action mustEqual SaveSquadFavorite()
case _ => case _ =>
ko ko
} }
@ -65,7 +65,7 @@ class SquadDefinitionActionMessageTest extends Specification {
case SquadDefinitionActionMessage(unk1, unk2, action) => case SquadDefinitionActionMessage(unk1, unk2, action) =>
unk1 mustEqual PlanetSideGUID(0) unk1 mustEqual PlanetSideGUID(0)
unk2 mustEqual 3 unk2 mustEqual 3
action mustEqual LoadSquadDefinition() action mustEqual LoadSquadFavorite()
case _ => case _ =>
ko ko
} }
@ -76,7 +76,7 @@ class SquadDefinitionActionMessageTest extends Specification {
case SquadDefinitionActionMessage(unk1, unk2, action) => case SquadDefinitionActionMessage(unk1, unk2, action) =>
unk1 mustEqual PlanetSideGUID(0) unk1 mustEqual PlanetSideGUID(0)
unk2 mustEqual 3 unk2 mustEqual 3
action mustEqual ListSquadDefinition("Cops and Military Officers") action mustEqual ListSquadFavorite("Cops and Military Officers")
case _ => case _ =>
ko ko
} }
@ -87,7 +87,7 @@ class SquadDefinitionActionMessageTest extends Specification {
case SquadDefinitionActionMessage(unk1, unk2, action) => case SquadDefinitionActionMessage(unk1, unk2, action) =>
unk1 mustEqual PlanetSideGUID(0) unk1 mustEqual PlanetSideGUID(0)
unk2 mustEqual 0 unk2 mustEqual 0
action mustEqual ListSquad() action mustEqual RequestListSquad()
case _ => case _ =>
ko ko
} }
@ -328,28 +328,28 @@ class SquadDefinitionActionMessageTest extends Specification {
} }
"encode (03)" in { "encode (03)" in {
val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 3, SaveSquadDefinition()) val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 3, SaveSquadFavorite())
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_03 pkt mustEqual string_03
} }
"encode (03)" in { "encode (03)" in {
val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 3, LoadSquadDefinition()) val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 3, LoadSquadFavorite())
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_04 pkt mustEqual string_04
} }
"encode (07)" in { "encode (07)" in {
val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 3, ListSquadDefinition("Cops and Military Officers")) val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 3, ListSquadFavorite("Cops and Military Officers"))
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_07 pkt mustEqual string_07
} }
"encode (08)" in { "encode (08)" in {
val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, ListSquad()) val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, RequestListSquad())
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string_08 pkt mustEqual string_08

View file

@ -346,10 +346,13 @@ class WorldSessionActor extends Actor with MDCContextAware {
case SquadServiceResponse(toChannel, response) => case SquadServiceResponse(toChannel, response) =>
response match { response match {
case SquadResponse.Init(infos) if infos.nonEmpty => case SquadResponse.ListSquadFavorite(line, task) =>
sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), line, SquadAction.ListSquadFavorite(task)))
case SquadResponse.InitList(infos) if infos.nonEmpty =>
sendResponse(ReplicationStreamMessage(infos)) sendResponse(ReplicationStreamMessage(infos))
case SquadResponse.Update(infos) if infos.nonEmpty => case SquadResponse.UpdateList(infos) if infos.nonEmpty =>
val o = ReplicationStreamMessage(6, None, val o = ReplicationStreamMessage(6, None,
infos.map { case (index, squadInfo) => infos.map { case (index, squadInfo) =>
SquadListing(index, squadInfo) SquadListing(index, squadInfo)
@ -364,7 +367,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
) )
) )
case SquadResponse.Remove(infos) if infos.nonEmpty => case SquadResponse.RemoveFromList(infos) if infos.nonEmpty =>
sendResponse( sendResponse(
ReplicationStreamMessage(1, None, ReplicationStreamMessage(1, None,
infos.map { index => infos.map { index =>
@ -387,9 +390,21 @@ class WorldSessionActor extends Actor with MDCContextAware {
) )
) )
case SquadResponse.InitSquad(squad_guid) =>
sendResponse(SquadDefinitionActionMessage(squad_guid, 0, SquadAction.Unknown(16)))
sendResponse(SquadDefinitionActionMessage(squad_guid, 0, SquadAction.SetListSquad()))
case SquadResponse.Unknown17(squad, char_id) =>
sendResponse(
SquadDefinitionActionMessage(squad.GUID, 0, SquadAction.Unknown(33))
)
case SquadResponse.Membership(request_type, unk1, unk2, unk3, unk4, player_name, unk5, unk6) => case SquadResponse.Membership(request_type, unk1, unk2, unk3, unk4, player_name, unk5, unk6) =>
sendResponse(SquadMembershipResponse(request_type, unk1, unk2, unk3, unk4, player_name, unk5, unk6)) sendResponse(SquadMembershipResponse(request_type, unk1, unk2, unk3, unk4, player_name, unk5, unk6))
case SquadResponse.Invite(from_char_id, to_char_id, name) =>
sendResponse(SquadMembershipResponse(SquadResponseType.Invite, 0, 0, from_char_id, Some(to_char_id), s"$name", false, Some(None)))
case SquadResponse.Join(squad, positionsToUpdate) => case SquadResponse.Join(squad, positionsToUpdate) =>
val leader = squad.Leader val leader = squad.Leader
val id = 11 val id = 11
@ -399,7 +414,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
membershipPositions.find({ case(member, _) => member.CharId == avatar.CharId }) match { membershipPositions.find({ case(member, _) => member.CharId == avatar.CharId }) match {
case Some((ourMember, ourIndex)) => case Some((ourMember, ourIndex)) =>
//we are joining the squad //we are joining the squad
sendResponse(SquadMembershipResponse(SquadRequestType.Accept, 0, 0, player.CharId, Some(leader.CharId), player.Name, true, Some(None))) sendResponse(SquadMembershipResponse(SquadResponseType.Accept, 0, 0, player.CharId, Some(leader.CharId), player.Name, true, Some(None)))
//load each member's entry (our own too) //load each member's entry (our own too)
membershipPositions.foreach { case(member, index) => membershipPositions.foreach { case(member, index) =>
sendResponse(SquadMemberEvent(0, id, member.CharId, index, Some(member.Name), Some(member.ZoneId), Some(0))) sendResponse(SquadMemberEvent(0, id, member.CharId, index, Some(member.Name), Some(member.ZoneId), Some(0)))
@ -410,7 +425,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
sendResponse(PlanetsideAttributeMessage(player.GUID, 31, id)) //associate with squad? sendResponse(PlanetsideAttributeMessage(player.GUID, 31, id)) //associate with squad?
sendResponse(PlanetsideAttributeMessage(player.GUID, 32, ourIndex)) //associate with member position in squad? sendResponse(PlanetsideAttributeMessage(player.GUID, 32, ourIndex)) //associate with member position in squad?
//a finalization? what does this do? //a finalization? what does this do?
sendResponse(SquadDefinitionActionMessage(squad.GUID, 0, SquadAction.Unknown(18, hex"00".toBitVector.take(6)))) sendResponse(SquadDefinitionActionMessage(squad.GUID, 0, SquadAction.Unknown(18)))
case _ => case _ =>
//other player is joining our squad //other player is joining our squad
//load each member's entry //load each member's entry
@ -432,7 +447,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
positionsToUpdate.find({ case(member, _) => member == avatar.CharId }) match { positionsToUpdate.find({ case(member, _) => member == avatar.CharId }) match {
case Some((ourMember, ourIndex)) => case Some((ourMember, ourIndex)) =>
//we are leaving the squad //we are leaving the squad
sendResponse(SquadMembershipResponse(SquadRequestType.Leave, 0,1, avatar.CharId, Some(avatar.CharId), avatar.name, true, Some(None))) sendResponse(SquadMembershipResponse(SquadResponseType.Leave, 0,1, avatar.CharId, Some(avatar.CharId), avatar.name, true, Some(None)))
//remove each member's entry (our own too) //remove each member's entry (our own too)
positionsToUpdate.foreach { case(member, index) => positionsToUpdate.foreach { case(member, index) =>
sendResponse(SquadMemberEvent(1, id, member, index, None, None, None)) sendResponse(SquadMemberEvent(1, id, member, index, None, None, None))
@ -444,7 +459,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
sendResponse(PlanetsideAttributeMessage(player.GUID, 32, 0)) //disassociate with member position in squad? sendResponse(PlanetsideAttributeMessage(player.GUID, 32, 0)) //disassociate with member position in squad?
sendResponse(PlanetsideAttributeMessage(player.GUID, 34, 4294967295L)) //unknown, perhaps unrelated? sendResponse(PlanetsideAttributeMessage(player.GUID, 34, 4294967295L)) //unknown, perhaps unrelated?
//a finalization? what does this do? //a finalization? what does this do?
sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(18, hex"00".toBitVector.take(6)))) sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(18)))
case _ => case _ =>
//remove each member's entry //remove each member's entry
positionsToUpdate.foreach { case(member, index) => positionsToUpdate.foreach { case(member, index) =>
@ -455,6 +470,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
case SquadResponse.UpdateMembers(squad, positions) => case SquadResponse.UpdateMembers(squad, positions) =>
import services.teamwork.SquadAction.{Update => SAUpdate} import services.teamwork.SquadAction.{Update => SAUpdate}
val id = 11
val pairedEntries = positions.collect { val pairedEntries = positions.collect {
case entry if squadUI.contains(entry.char_id) => case entry if squadUI.contains(entry.char_id) =>
(entry, squadUI(entry.char_id)) (entry, squadUI(entry.char_id))
@ -476,12 +492,31 @@ class WorldSessionActor extends Actor with MDCContextAware {
if(updatedEntries.nonEmpty) { if(updatedEntries.nonEmpty) {
sendResponse( sendResponse(
SquadState( SquadState(
PlanetSideGUID(11), PlanetSideGUID(id),
updatedEntries.map { entry => SquadStateInfo(entry.char_id, entry.health, entry.armor, entry.pos, 2,2, false, 429, None,None)} updatedEntries.map { entry => SquadStateInfo(entry.char_id, entry.health, entry.armor, entry.pos, 2,2, false, 429, None,None)}
) )
) )
} }
case SquadResponse.SwapMember(squad, to_index, from_index) =>
//this failsafe is not supported by normal squad member operations
val member = squad.Membership(to_index)
val charId = member.CharId
val elem = squadUI(charId)
val id = 11
squadUI(charId) = SquadUIElement(elem.name, to_index, elem.zone, elem.health, elem.armor, elem.position)
sendResponse(SquadMemberEvent(1, id, charId, from_index, None, None, None))
sendResponse(SquadMemberEvent(0, id, charId, to_index, Some(elem.name), Some(elem.zone), Some(0)))
sendResponse(
SquadState(
PlanetSideGUID(id),
List(SquadStateInfo(charId, elem.health, elem.armor, elem.position, 2,2, false, 429, None,None))
)
)
if(charId == avatar.CharId) {
sendResponse(PlanetsideAttributeMessage(player.GUID, 32, to_index))
}
case _ => ; case _ => ;
} }
@ -956,7 +991,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
galaxyService ! Service.Join("galaxy") //for galaxy-wide messages galaxyService ! Service.Join("galaxy") //for galaxy-wide messages
galaxyService ! Service.Join(s"${avatar.faction}") //for hotspots galaxyService ! Service.Join(s"${avatar.faction}") //for hotspots
squadService ! Service.Join(s"${avatar.faction}") //channel will be player.Faction squadService ! Service.Join(s"${avatar.faction}") //channel will be player.Faction
squadService ! Service.Join(avatar.name) //management of any lingering squad information connected to this player squadService ! Service.Join(s"${avatar.CharId}") //channel will be player.CharId (in order to work with packets)
cluster ! InterstellarCluster.GetWorld("home3") cluster ! InterstellarCluster.GetWorld("home3")
case InterstellarCluster.GiveWorld(zoneId, zone) => case InterstellarCluster.GiveWorld(zoneId, zone) =>
@ -2964,14 +2999,14 @@ class WorldSessionActor extends Actor with MDCContextAware {
//AvatarAwardMessage //AvatarAwardMessage
//DisplayAwardMessage //DisplayAwardMessage
sendResponse(PlanetsideStringAttributeMessage(guid, 0, "Outfit Name")) sendResponse(PlanetsideStringAttributeMessage(guid, 0, "Outfit Name"))
sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(6, hex"".toBitVector))) sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(6)))
(0 to 9).foreach(line => { (0 to 9).foreach(line => {
sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), line, SquadAction.ListSquadDefinition(""))) sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), line, SquadAction.ListSquadFavorite("")))
}) })
sendResponse(SquadDetailDefinitionUpdateMessage.Init) sendResponse(SquadDetailDefinitionUpdateMessage.Init)
sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0,SquadAction.Unknown(16, hex"".toBitVector))) sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0,SquadAction.Unknown(16)))
sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0,SquadAction.Unknown(17, hex"".toBitVector))) sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0,SquadAction.SetListSquad()))
sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0,SquadAction.Unknown(18, hex"".toBitVector))) sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0,SquadAction.Unknown(18)))
//MapObjectStateBlockMessage and ObjectCreateMessage? //MapObjectStateBlockMessage and ObjectCreateMessage?
//TacticsMessage? //TacticsMessage?
//change the owner on our deployables (re-draw the icons for our deployables too) //change the owner on our deployables (re-draw the icons for our deployables too)
@ -3419,7 +3454,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
case msg @ PlayerStateMessageUpstream(avatar_guid, pos, vel, yaw, pitch, yaw_upper, seq_time, unk3, is_crouching, is_jumping, unk4, is_cloaking, unk5, unk6) => case msg @ PlayerStateMessageUpstream(avatar_guid, pos, vel, yaw, pitch, yaw_upper, seq_time, unk3, is_crouching, is_jumping, unk4, is_cloaking, unk5, unk6) =>
if(deadState == DeadState.Alive) { if(deadState == DeadState.Alive) {
if(!player.Crouching && is_crouching) { //SQUAD TESTING CODE if(!player.Crouching && is_crouching) { //SQUAD TESTING CODE
sendResponse(SquadMemberEvent(3, 11, 353380L, 3, None, Some(13), None)) sendResponse(SquadMembershipResponse(SquadResponseType.Unk01, 0, 0, player.CharId, None, "Dummy", false, Some(None)))
} }
player.Position = pos player.Position = pos
player.Velocity = vel player.Velocity = vel