From f81c87ce2203eda8a5f536419e15899fb128980c Mon Sep 17 00:00:00 2001 From: FateJH Date: Mon, 22 Jul 2019 17:56:09 -0400 Subject: [PATCH] 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 --- .../psforever/objects/loadouts/Loadout.scala | 3 +- .../objects/loadouts/SquadLoadout.scala | 5 +- .../psforever/objects/teamwork/Squad.scala | 2 +- .../game/ReplicationStreamMessage.scala | 20 +- .../game/SquadDefinitionActionMessage.scala | 110 ++- .../SquadDetailDefinitionUpdateMessage.scala | 36 +- .../packet/game/SquadMembershipResponse.scala | 60 +- .../psforever/types/SquadRequestType.scala | 37 +- .../psforever/types/SquadResponseType.scala | 33 + .../services/teamwork/SquadResponse.scala | 18 +- .../services/teamwork/SquadService.scala | 708 +++++++++++++----- .../SquadDefinitionActionMessageTest.scala | 16 +- .../src/main/scala/WorldSessionActor.scala | 65 +- 13 files changed, 820 insertions(+), 293 deletions(-) create mode 100644 common/src/main/scala/net/psforever/types/SquadResponseType.scala diff --git a/common/src/main/scala/net/psforever/objects/loadouts/Loadout.scala b/common/src/main/scala/net/psforever/objects/loadouts/Loadout.scala index 58490f60e..64668c4e7 100644 --- a/common/src/main/scala/net/psforever/objects/loadouts/Loadout.scala +++ b/common/src/main/scala/net/psforever/objects/loadouts/Loadout.scala @@ -23,7 +23,7 @@ object Loadout { owner match { case p : Player => Success(Create(p, 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")) } } @@ -65,7 +65,6 @@ object Loadout { def Create(squad : Squad, label : String) : Loadout = { SquadLoadout( label, - squad.Task, if(squad.CustomZoneId) { Some(squad.ZoneId) } else { None }, squad.Membership .zipWithIndex diff --git a/common/src/main/scala/net/psforever/objects/loadouts/SquadLoadout.scala b/common/src/main/scala/net/psforever/objects/loadouts/SquadLoadout.scala index 9039c5a07..029c58b7f 100644 --- a/common/src/main/scala/net/psforever/objects/loadouts/SquadLoadout.scala +++ b/common/src/main/scala/net/psforever/objects/loadouts/SquadLoadout.scala @@ -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 SquadLoadout(label : String, - task : String, +final case class SquadLoadout(task : String, zone_id : Option[Int], - members : List[SquadPositionLoadout]) extends Loadout(label) + members : List[SquadPositionLoadout]) extends Loadout(task) diff --git a/common/src/main/scala/net/psforever/objects/teamwork/Squad.scala b/common/src/main/scala/net/psforever/objects/teamwork/Squad.scala index b5e5d81da..5f653dcdd 100644 --- a/common/src/main/scala/net/psforever/objects/teamwork/Squad.scala +++ b/common/src/main/scala/net/psforever/objects/teamwork/Squad.scala @@ -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) } diff --git a/common/src/main/scala/net/psforever/packet/game/ReplicationStreamMessage.scala b/common/src/main/scala/net/psforever/packet/game/ReplicationStreamMessage.scala index 233f5a59b..c34a057ce 100644 --- a/common/src/main/scala/net/psforever/packet/game/ReplicationStreamMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/ReplicationStreamMessage.scala @@ -51,6 +51,22 @@ final case class SquadInfo(leader : Option[String], 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. */ - 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.
diff --git a/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala b/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala index 7b9b46472..32bbc68e2 100644 --- a/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala @@ -20,18 +20,24 @@ object SquadAction{ 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 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 ChangeSquadZone(zone : PlanetSideZoneID) extends SquadAction(20) @@ -56,12 +62,21 @@ object SquadAction{ 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 CancelFind() extends SquadAction(41) 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 * and extract the field data from that stream. @@ -83,31 +98,45 @@ object SquadAction{ } ) - val saveSquadDefinitionCodec = everFailCondition.xmap[SaveSquadDefinition] ( - _ => SaveSquadDefinition(), + val saveSquadFavoriteCodec = everFailCondition.xmap[SaveSquadFavorite] ( + _ => SaveSquadFavorite(), { - case SaveSquadDefinition() => None + case SaveSquadFavorite() => None } ) - val loadSquadDefinitionCodec = everFailCondition.xmap[LoadSquadDefinition] ( - _ => LoadSquadDefinition(), + val loadSquadFavoriteCodec = everFailCondition.xmap[LoadSquadFavorite] ( + _ => LoadSquadFavorite(), { - case LoadSquadDefinition() => None + case LoadSquadFavorite() => None } ) - val listSquadDefinitionCodec = PacketHelpers.encodedWideStringAligned(6).xmap[ListSquadDefinition] ( - text => ListSquadDefinition(text), + val deleteSquadFavoriteCodec = everFailCondition.xmap[DeleteSquadFavorite] ( + _ => DeleteSquadFavorite(), { - case ListSquadDefinition(text) => text + case DeleteSquadFavorite() => None } ) - val listSquadCodec = everFailCondition.xmap[ListSquad] ( - _ => ListSquad(), + val listSquadFavoriteCodec = PacketHelpers.encodedWideStringAligned(6).xmap[ListSquadFavorite] ( + 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] ( 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] ( state => FindLfsSoldiersForRole(state), { @@ -270,13 +315,14 @@ object SquadAction{ *     `0 ` - Display Squad
*     `1 ` - Answer Squad Join Request
*     `2 ` - UNKNOWN
- *     `3 ` - Save Squad Definition
- *     `4 ` - Load Squad Definition
+ *     `3 ` - Save Squad Favorite
+ *     `4 ` - Load Squad Favorite
+ *     `5 ` - Delete Squad Favorite
*     `6 ` - UNKNOWN
- *     `8 ` - List Squad
- *     `9 ` - UNKNOWN
+ *     `8 ` - Request List Squad
+ *     `9 ` - Stop List Squad
*     `16` - UNKNOWN
- *     `17` - UNKNOWN
+ *     `17` - Set List Squad (ui)
*     `18` - UNKNOWN
*     `26` - Reset All
*     `35` - Cancel Squad Search
@@ -302,12 +348,12 @@ object SquadAction{ *     `15` - Select this Role for Yourself
*     `37` - UNKNOWN
*   `String`
- *     `7 ` - List Squad Definition
+ *     `7 ` - List Squad Favorite
*     `19` - (Squad leader) Change Squad Purpose
*   `Int :: Long`
*     `12` - UNKNOWN
*     `25` - (Squad leader) Change Squad Member Requirements - Weapons
- *     `38` - UNKNOWN
+ *     `38` - Assign Squad Member To Role
*   `Int :: String`
*     `23` - (Squad leader) Change Squad Member Requirements - Role
*     `24` - (Squad leader) Change Squad Member Requirements - Detailed Orders
@@ -343,12 +389,15 @@ object SquadDefinitionActionMessage extends Marshallable[SquadDefinitionActionMe ((code : @switch) match { case 0 => displaySquadCodec case 1 => answerSquadJoinRequestCodec - case 3 => saveSquadDefinitionCodec - case 4 => loadSquadDefinitionCodec - case 7 => listSquadDefinitionCodec - case 8 => listSquadCodec + case 3 => saveSquadFavoriteCodec + case 4 => loadSquadFavoriteCodec + case 5 => deleteSquadFavoriteCodec + case 7 => listSquadFavoriteCodec + case 8 => requestListSquadCodec + case 9 => stopListSquadCodec case 10 => selectRoleForYourselfCodec case 15 => cancelSelectRoleForYourselfCodec + case 17 => setListSquadCodec case 19 => changeSquadPurposeCodec case 20 => changeSquadZoneCodec case 21 => closeSquadMemberPositionCodec @@ -361,12 +410,13 @@ object SquadDefinitionActionMessage extends Marshallable[SquadDefinitionActionMe case 31 => locationFollowsSquadLeadCodec case 34 => searchForSquadsWithParticularRoleCodec case 35 => cancelSquadSearchCodec + case 38 => assignSquadMemberToRoleCodec case 40 => findLfsSoldiersForRoleCodec case 41 => cancelFindCodec - case 2 | 6 | 9 | 11 | - 12 | 13 | 14 | 16 | 17 | + case 2 | 6 | 11 | + 12 | 13 | 14 | 16 | 18 | 29 | 30 | 32 | 33 | - 36 | 37 | 38 | 39 | 42 | 43 => unknownCodec(code) + 36 | 37 | 39 | 42 | 43 => unknownCodec(code) case _ => failureCodec(code) }).asInstanceOf[Codec[SquadAction]] } diff --git a/common/src/main/scala/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala b/common/src/main/scala/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala index 9bda75277..225489b76 100644 --- a/common/src/main/scala/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/SquadDetailDefinitionUpdateMessage.scala @@ -161,7 +161,21 @@ final case class SquadDetail(unk1 : Option[Int], task.orElse(info.task), zone_id.orElse(info.zone_id), 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 */ 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 { @@ -318,6 +341,17 @@ object SquadDetail { SquadDetail(None, None, None, None, None, None, None, Some(unk7), None) def Members(list : List[SquadPositionEntry]) : SquadDetail = 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] { diff --git a/common/src/main/scala/net/psforever/packet/game/SquadMembershipResponse.scala b/common/src/main/scala/net/psforever/packet/game/SquadMembershipResponse.scala index ee1746ddd..a9f035183 100644 --- a/common/src/main/scala/net/psforever/packet/game/SquadMembershipResponse.scala +++ b/common/src/main/scala/net/psforever/packet/game/SquadMembershipResponse.scala @@ -2,12 +2,12 @@ package net.psforever.packet.game import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} -import net.psforever.types.SquadRequestType +import net.psforever.types.SquadResponseType import scodec.Codec 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. * @param request_type the purpose of the request * @param unk1 na @@ -17,11 +17,57 @@ import scodec.codecs._ * @param other_id another squad member's unique identifier; * may be the same as `char_id` * @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; * the internal field, the `Option[String]`, never seems to be set + *
+ * `request_type` (enum value) / `unk5` state (`false`/`true`)
+ * ----------------------------------------
+ * - `Invite` (0)
+ * false => [PROMPT] "`player_name` has invited you into a squad." [YES/NO]
+ * true => "You have invited `player_name` to join your squad."
+ * - `Unk01` (1)
+ * false => n/a
+ * true => n/a
+ * - `Accept` (2)
+ * false => "`player_name` has accepted your invitation to join into your squad.
+ * "You have formed a squad and are now that squad's commander." (if first time)
+ * true => "You have accepted an invitation to join a squad."
+ * "You have successfully joined a squad for the first time." (if first time)
+ * - `Reject` (3)
+ * false => "`player_name` does not want to join your squad at this time."
+ * true => "You have declined an invitation to join a squad."
+ * - `Cancel` (4)
+ * false => "`player_name` has withdrawn his invitation."
+ * true => "You have canceled your invitation to `player_name`."
+ * - `Leave` (5)
+ * false => "The Squad Leader has kicked you out of the squad."
+ * true => "You have kicked `player_name` out of the squad."
+ * - `Disband` (6)
+ * false => "The squad has been disbanded."
+ * true => "You have disbanded the squad."
+ * - `PlatoonInvite` (7)
+ * false => [PROMPT] "`player_name` has invited you into a platoon." [YES/NO]
+ * true => "You have invited `player_name`'s squad to join your platoon."
+ * - `PlatoonAccept` (8) + * false => "`player_name` has accepted your invitation to join into your platoon.
+ * "You have formed a platoon and are now that platoon commander." (if first time)
+ * true => "You have accepted an invitation to join a platoon."
+ * "You have successfully joined a platoon for the first time." (if first time)
+ * - `PlatoonReject` (9)
+ * false => "`player_name` does not want to join your platoon at this time."
+ * true => "You have declined an invitation to join a platoon."
+ * - `PlatoonCancel` (10)
+ * false => "`player_name` has withdrawn his invitation."
+ * true => "You have declined your invitation to `player_name`." (nonsense?)
+ * - `PlatoonLeave` (11)
+ * false => "The Platoon Leader has kicked you out of the platoon."
+ * true => "You have kicked `player_name`'s squad out of the platoon."
+ * - `PlatoonDisband` (12)
+ * false => "The platoon has been disbanded."
+ * true => "You have disbanded the platoon." */ -final case class SquadMembershipResponse(request_type : SquadRequestType.Value, +final case class SquadMembershipResponse(request_type : SquadResponseType.Value, unk1 : Int, unk2 : Int, char_id : Long, @@ -37,14 +83,14 @@ final case class SquadMembershipResponse(request_type : SquadRequestType.Value, object SquadMembershipResponse extends Marshallable[SquadMembershipResponse] { implicit val codec : Codec[SquadMembershipResponse] = ( - "request_type" | SquadRequestType.codec >>:~ { d => + "request_type" | SquadResponseType.codec >>:~ { d => ("unk1" | uint(5)) :: ("unk2" | uint2) :: ("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)) :: ("unk5" | bool) :: - conditional(d != SquadRequestType.Invite, optional(bool, "unk6" | PacketHelpers.encodedWideStringAligned(6))) + conditional(d != SquadResponseType.Invite, optional(bool, "unk6" | PacketHelpers.encodedWideStringAligned(6))) } ).as[SquadMembershipResponse] } diff --git a/common/src/main/scala/net/psforever/types/SquadRequestType.scala b/common/src/main/scala/net/psforever/types/SquadRequestType.scala index 8d7e24351..1b22bc380 100644 --- a/common/src/main/scala/net/psforever/types/SquadRequestType.scala +++ b/common/src/main/scala/net/psforever/types/SquadRequestType.scala @@ -7,23 +7,28 @@ import scodec.codecs._ object SquadRequestType extends Enumeration { type Type = Value val - Invite, //00 - Unk01, //01 - Accept, //02 - Reject, //03 - Cancel, //04 - Leave, //05 - Promote, //06 - Disband, //07 - PlatoonInvite, //08 - PlatoonAccept, //09 - PlatoonReject, //10 - PlatoonCancel, //11 - PlatoonLeave, //12 - PlatoonDisband, //13 - Unk14, //14 - Unk15 //15 + Invite, + Unk01, + Accept, + Reject, + Cancel, + Leave, + Promote, + Disband, + PlatoonInvite, + PlatoonAccept, + PlatoonReject, + PlatoonCancel, + PlatoonLeave, + PlatoonDisband = Value 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") + } } diff --git a/common/src/main/scala/net/psforever/types/SquadResponseType.scala b/common/src/main/scala/net/psforever/types/SquadResponseType.scala new file mode 100644 index 000000000..1f73ccc45 --- /dev/null +++ b/common/src/main/scala/net/psforever/types/SquadResponseType.scala @@ -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") + } +} diff --git a/common/src/main/scala/services/teamwork/SquadResponse.scala b/common/src/main/scala/services/teamwork/SquadResponse.scala index e1ab03cb1..b74f153fa 100644 --- a/common/src/main/scala/services/teamwork/SquadResponse.scala +++ b/common/src/main/scala/services/teamwork/SquadResponse.scala @@ -1,21 +1,27 @@ // Copyright (c) 2019 PSForever package services.teamwork -import net.psforever.objects.teamwork.{Member, Squad} +import net.psforever.objects.teamwork.Squad import net.psforever.packet.game._ -import net.psforever.types.SquadRequestType +import net.psforever.types.SquadResponseType object SquadResponse { trait Response - final case class Init(info : Vector[SquadInfo]) extends Response - final case class Update(infos : Iterable[(Int, SquadInfo)]) extends Response - final case class Remove(infos : Iterable[Int]) extends Response + final case class ListSquadFavorite(line : Int, task : String) 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 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 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 } diff --git a/common/src/main/scala/services/teamwork/SquadService.scala b/common/src/main/scala/services/teamwork/SquadService.scala index 9aa5954bc..0f8ae794a 100644 --- a/common/src/main/scala/services/teamwork/SquadService.scala +++ b/common/src/main/scala/services/teamwork/SquadService.scala @@ -4,9 +4,10 @@ package services.teamwork import akka.actor.Actor import net.psforever.objects.Player 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.types.{PlanetSideEmpire, SquadRequestType, Vector3} +import net.psforever.types._ import services.{GenericEventBus, Service} import scala.collection.concurrent.TrieMap @@ -16,7 +17,9 @@ import scala.collection.mutable.ListBuffer //import scala.concurrent.duration._ 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 i : Int = 1 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.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 - override def preStart = { + override def preStart : Unit = { log.info("Starting...") val testSquad = new Squad(PlanetSideGUID(3), PlanetSideEmpire.VS) testSquad.Task = "\\#66CCFF Sentinels of Auraxis\\#FFFFFF ... \\#40FF40 Squad Up!!" testSquad.ZoneId = 5 testSquad.Membership(0).Name = "Wizkid45" + testSquad.Membership(0).Role = "Fearless Leader" testSquad.Membership(0).CharId = 30910985L testSquad.Membership(0).ZoneId = 5 testSquad.Membership(0).Health = 64 testSquad.Membership(0).Armor = 34 testSquad.Membership(0).Position = Vector3(5526.5234f, 3818.7344f, 54.59375f) testSquad.Membership(1).Name = "xoBLADEox" + testSquad.Membership(1).Role = "Right Hand" testSquad.Membership(1).CharId = 42781919L testSquad.Membership(1).ZoneId = 5 testSquad.Membership(1).Health = 54 testSquad.Membership(1).Armor = 44 testSquad.Membership(1).Position = Vector3(4673.5312f, 2604.8047f, 40.015625f) testSquad.Membership(3).Name = "cabal0428" + testSquad.Membership(3).Role = "Left Hand" testSquad.Membership(3).CharId = 353380L testSquad.Membership(3).ZoneId = 5 testSquad.Membership(3).Health = 44 testSquad.Membership(3).Armor = 54 testSquad.Membership(3).Position = Vector3(4727.492f, 2613.5312f, 51.390625f) testSquad.Membership(4).Name = "xSkiku" + testSquad.Membership(4).Role = "Right Foot's Middle Toe's Nail" testSquad.Membership(4).CharId = 41588340L testSquad.Membership(4).ZoneId = 5 testSquad.Membership(4).Health = 34 @@ -60,7 +67,7 @@ class SquadService extends Actor { testSquad.Membership(4).Position = Vector3(3675.0f, 4789.8047f, 63.21875f) idToSquad(PlanetSideGUID(3)) = testSquad testSquad.Listed = true - UpdateSquadList(testSquad, List()) + UpdateSquadList(testSquad, None) } def GetNextSquadId() : PlanetSideGUID = { @@ -76,7 +83,7 @@ class SquadService extends Actor { } def GetParticipatingSquad(player : Player, zone : Int) : Option[Squad] = { - memberToSquad.get(player.Name) match { + memberToSquad.get(player.CharId) match { case opt @ Some(squad) => squad.Membership.find(_.Name == player.Name).get.ZoneId = zone opt @@ -86,10 +93,10 @@ class SquadService extends Actor { } def GetLeadingSquad(player : Player, zone : Int, opt : Option[Squad]) : Squad = { - val name = player.Name + val charId = player.CharId val squadOut = opt match { case Some(squad) => - if(squad.Leader.Name.equals(name)) { + if(squad.Leader.CharId == charId) { squad } else { @@ -97,21 +104,23 @@ class SquadService extends Actor { } case None => - memberToSquad.get(name) match { - case Some(squad) if squad.Leader.Name.equals(name) => + memberToSquad.get(charId) match { + case Some(squad) if squad.Leader.CharId.equals(charId) => squad case _ => val faction = player.Faction val id = GetNextSquadId() val squad = new Squad(id, faction) val leadPosition = squad.Membership(squad.LeaderPositionIndex) + val name = player.Name leadPosition.Name = name + leadPosition.CharId = charId leadPosition.Health = player.Health leadPosition.Armor = player.Armor leadPosition.Position = player.Position leadPosition.ZoneId = zone log.info(s"$name-$faction has started a new squad") - memberToSquad += name -> squad + memberToSquad += charId -> squad idToSquad += id -> squad squad } @@ -130,26 +139,27 @@ class SquadService extends Actor { log.info(s"$who has joined $path") SquadEvents.subscribe(who, path) //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 - case Service.Join(name) => - val path = s"$name/Squad" + case Service.Join(char_id) => + val path = s"$char_id/Squad" val who = sender() log.info(s"$who has joined $path") SquadEvents.subscribe(who, path) //TODO squad-specific switchboard //check for renewable squad information - memberToSquad.get(name) match { + memberToSquad.get(char_id.toLong) match { case None => ; case Some(_) => sender ! SquadServiceMessage.RecoverSquadMembership() //TODO? } - case Service.Leave(Some(name)) => ; + case Service.Leave(Some(char_id)) => ; SquadEvents.unsubscribe(sender()) - memberToSquad.get(name) match { + val longCharId = char_id.toLong + memberToSquad.get(longCharId) match { case Some(squad) => - if(squad.Leader.Name.equals(name)) { + if(squad.Leader.Name.equals(char_id)) { //we were the leader if(squad.Membership.count(p => p.Name.equals("")) > 1) { //other players were in the squad; publicly disband it @@ -162,19 +172,19 @@ class SquadService extends Actor { position.Armor = 0 }) } - memberToSquad.remove(name) + memberToSquad.remove(longCharId) idToSquad.remove(squad.GUID) - UpdateSquadList(squad, List()) + UpdateSquadList(squad, None) } else { //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.ZoneId = 0 position.Position = Vector3.Zero position.Health = 0 position.Armor = 0 - UpdateSquadDetail(squad) + UpdateSquadDetail(squad.GUID, squad) } case None => ; } @@ -184,67 +194,116 @@ class SquadService extends Actor { case SquadServiceMessage(tplayer, squad_action) => squad_action match { case SquadAction.Membership(request_type, char_id, optional_char_id, _, _) => request_type match { - case SquadRequestType.Accept => - bids.get(char_id) match { - case Some((squadGUID, line)) if idToSquad.get(squadGUID).nonEmpty => - //join squad - val squad = idToSquad(squadGUID) - val position = squad.Membership(line) - if(squad.Availability(line) && position.CharId == 0 && - tplayer.Certifications.intersect(position.Requirements) == position.Requirements) { - position.Name = tplayer.Name - position.CharId = char_id - 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 SquadRequestType.Invite => + (optional_char_id, memberToSquad.get(char_id)) match { + case (Some(toCharId), Some(squad)) => + bids(toCharId) = VacancyBid(char_id, squad.GUID) + SquadEvents.publish( SquadServiceResponse(s"$toCharId/Squad", SquadResponse.Invite(char_id, toCharId, tplayer.Name)) ) + case (Some(toCharId), None) => + val ourSquad = GetLeadingSquad(tplayer, 1, None) + memberToSquad.remove(char_id) + bids(toCharId) = CamraderieBid(char_id, ourSquad) + SquadEvents.publish( SquadServiceResponse(s"$toCharId/Squad", SquadResponse.Invite(char_id, toCharId, tplayer.Name)) ) case _ => ; } case SquadRequestType.Leave => + val squad = memberToSquad(char_id) if(optional_char_id.contains(char_id)) { - //we're leaving the squad on our own - val name = tplayer.Name - val squad = memberToSquad(name) - val membership = squad.Membership.zipWithIndex - val (member, index) = membership - .find { case (_member, _) => _member.Name == name } - .get - val updateList = membership.collect({ case (_member, _index) if _member.CharId > 0 => (_member.CharId, _index) }).toList - memberToSquad.remove(name) - member.Name = "" - member.CharId = 0 - //leaving the squad completely + //leaving the squad of our own accord + } + else { + val leader = squad.Leader + if(optional_char_id.contains(leader.CharId)) { + //kicked by the squad leader + } + } + val membership = squad.Membership.zipWithIndex + val (member, index) = membership + .find { case (_member, _) => _member.CharId == char_id } + .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)) - //other squad members see us leaving the squad + //other squad members see the member leaving only val leavingMember = List((char_id, index)) membership - .collect({ case (_member, _) if _member.CharId > 0 => _member.Name }) - .foreach { name => - SquadEvents.publish( SquadServiceResponse(s"$name/Squad", SquadResponse.Leave(squad, leavingMember)) ) - } + .collect({ case (_member, _) if _member.CharId > 0 => _member.CharId }) + .foreach { charId => + 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 " 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 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) => squad.Membership.find(_.CharId == char_id) match { case Some(member) => @@ -263,46 +322,64 @@ class SquadService extends Actor { )) 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 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._ val squadOpt = GetParticipatingSquad(tplayer, zone_ordinal_number) 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) => log.info(s"${tplayer.Name}-${tplayer.Faction} has changed his squad's task to $purpose") val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt) 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) => log.info(s"${tplayer.Name}-${tplayer.Faction} has changed squad's ops zone to $zone") val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt) squad.ZoneId = zone.zoneId.toInt - UpdateSquadList(squad, List(SquadInfo.Field.ZoneId)) - UpdateSquadDetail(squad) + UpdateSquadListWhenListed(squad, SquadInfo().ZoneId(zone)) + SquadDetail().ZoneId(zone) + UpdateListedSquadDetail(squad.GUID, squad) case CloseSquadMemberPosition(position) => 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") val memberPosition = squad.Membership(position) val listingChanged = if(memberPosition.Name.nonEmpty) { - List(SquadInfo.Field.Size, SquadInfo.Field.Capacity) + SquadInfo().Size(squad.Size).Capacity(squad.Capacity) } else { - List(SquadInfo.Field.Capacity) + SquadInfo().Capacity(squad.Capacity) } memberPosition.Close() - UpdateSquadList(squad, listingChanged) - UpdateSquadDetail(squad) + UpdateSquadListWhenListed(squad, listingChanged) + SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail.Closed))) + UpdateListedSquadDetail(squad.GUID, squad) case Some(false) | None => ; } @@ -329,8 +407,9 @@ class SquadService extends Actor { case Some(false) => log.info(s"${tplayer.Name}-${tplayer.Faction} has opened the #$position position in squad") squad.Availability.update(position, true) - UpdateSquadList(squad, List(SquadInfo.Field.Capacity)) - UpdateSquadDetail(squad) + UpdateSquadListWhenListed(squad, SquadInfo().Capacity(squad.Capacity)) + SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail.Open))) + UpdateListedSquadDetail(squad.GUID, squad) case Some(true) | None => ; } @@ -340,7 +419,8 @@ class SquadService extends Actor { case Some(true) => log.info(s"${tplayer.Name}-${tplayer.Faction} has changed the role of squad position #$position") squad.Membership(position).Role = role - UpdateSquadDetail(squad) + SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail().Role(role)))) + UpdateListedSquadDetail(squad.GUID, squad) case Some(false) | None => ; } @@ -350,7 +430,8 @@ class SquadService extends Actor { case Some(true) => log.info(s"${tplayer.Name}-${tplayer.Faction} has changed the orders for squad position #$position") squad.Membership(position).Orders = orders - UpdateSquadDetail(squad) + SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail().DetailedOrders(orders)))) + UpdateListedSquadDetail(squad.GUID, squad) case Some(false) | None => ; } @@ -360,7 +441,8 @@ class SquadService extends Actor { case Some(true) => log.info(s"${tplayer.Name}-${tplayer.Faction} has changed the requirements for squad position #$position") squad.Membership(position).Requirements = certs - UpdateSquadDetail(squad) + SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail().Requirements(certs)))) + UpdateListedSquadDetail(squad.GUID, squad) case Some(false) | None => ; } @@ -386,12 +468,47 @@ class SquadService extends Actor { 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 - //val name = tplayer.Name squadOpt match { - case Some(squad) if squad.GUID == guid => - //already a member of this squad; swap positions freely + case Some(squad) if squad.GUID == guid || squad.Leader.Name == tplayer.Name => + //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(_) => - //not a member of the requesting squad; do nothing + //not a member of the requesting squad; do nothing case None => //not a member of any squad; consider request of joining the target squad idToSquad.get(guid) match { @@ -399,10 +516,10 @@ class SquadService extends Actor { val member = squad.Membership(position) if(squad.Availability(position) && member.CharId == 0 && 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 //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 => @@ -411,36 +528,62 @@ class SquadService extends Actor { } } - case ListSquad() => + case RequestListSquad() => val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt) 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 + 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() => - val squad = GetLeadingSquad(tplayer, zone_ordinal_number, squadOpt) - squad.Task = "" - squad.ZoneId = None - squad.Availability.indices.foreach { i => - squad.Availability.update(i, true) + squadOpt match { + case Some(squad) if squad.Leader.CharId == tplayer.CharId => + squad.Task = "" + squad.ZoneId = None + 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() => idToSquad.get(guid) match { case Some(squad) => - sender ! SquadServiceResponse(s"${tplayer.Name}/Squad", GenSquadDetail(squad)) + sender ! SquadServiceResponse(s"${tplayer.Name}/Squad", GenSquadDetail(squad.GUID, squad)) 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 _ => ; } @@ -449,54 +592,109 @@ class SquadService extends Actor { } } - def UpdateSquadList(squad : Squad, listingChanged : List[Int]) : Unit = { - //queue updates + def JoinSquad(player : Player, squad : Squad, line : Int) : Boolean = { + 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) { - val entry = SquadService.Publish(squad) - val faction = squad.Faction - val factionListings = publishedLists(faction) - factionListings.find(info => { - info.squad_guid match { - case Some(sguid) => sguid == squad.GUID - case _ => false - } - }) match { - case Some(listedSquad) => - val index = factionListings.indexOf(listedSquad) - val changes = if(listingChanged.nonEmpty) { - SquadService.Differences(listingChanged, entry) - } - else { - SquadService.Differences(listedSquad, entry) - } - if(changes != SquadInfo.Blank) { + UpdateSquadList(squad, changes) + } + } + + def UpdateSquadList(squad : Squad, changes : Option[SquadInfo]) : Unit = { + val entry = SquadService.SquadList.Publish(squad) + val faction = squad.Faction + val factionListings = publishedLists(faction) + factionListings.find(info => { + info.squad_guid match { + case Some(sguid) => sguid == squad.GUID + case _ => false + } + }) match { + case Some(listedSquad) => + val index = factionListings.indexOf(listedSquad) + changes match { + case Some(changedFields) => //squad information update log.info(s"Squad will be updated") factionListings(index) = entry SquadEvents.publish( - SquadServiceResponse(s"$faction/Squad", SquadResponse.Update(Seq((index, changes)))) + SquadServiceResponse(s"$faction/Squad", SquadResponse.UpdateList(Seq((index, changedFields)))) ) - } - else { + case None => //remove squad from listing log.info(s"Squad will be removed") factionListings.remove(index) 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 => - //first time being published - log.info(s"Squad will be introduced") - factionListings += SquadService.Publish(squad) - SquadEvents.publish( - SquadServiceResponse(s"$faction/Squad", SquadResponse.Init(factionListings.toVector)) - ) - } + } + case None => + //first time being published + log.info(s"Squad will be introduced") + factionListings += entry + SquadEvents.publish( + SquadServiceResponse(s"$faction/Squad", SquadResponse.InitList(factionListings.toVector)) + ) } } - def GenSquadDetail(squad : Squad) : SquadResponse.Detail = { + def GenSquadDetail(guid : PlanetSideGUID, squad : Squad) : SquadResponse.Detail = { SquadResponse.Detail( squad.GUID, squad.Leader.Name, @@ -513,60 +711,164 @@ class SquadService extends Actor { ) } - def UpdateSquadDetail(squad : Squad) : Unit = { - val detail = GenSquadDetail(squad) - squad.Membership.collect { - case member if !member.Name.equals("") => - member.Name - }.foreach { name => - SquadEvents.publish( - SquadServiceResponse(s"$name/Squad", detail) - ) + def UpdateListedSquadDetail(guid : PlanetSideGUID, squad : Squad) : Unit = { + if(squad.Listed) { + UpdateSquadDetail(guid, squad) } } + + 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 { - def Publish(squad : Squad) : SquadInfo = { - SquadInfo( - squad.Leader.Name, - squad.Task, - PlanetSideZoneID(squad.ZoneId), - squad.Size, - squad.Capacity, - squad.GUID - ) - } + trait PositionBid - 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) + final case class NormalBid(char_id : Long, squad_guid : PlanetSideGUID, position : Int) extends PositionBid + + final case class VacancyBid(char_id : Long, squad_guid : PlanetSideGUID) extends PositionBid + + final case class CamraderieBid(char_id : Long, placeholder : Squad) extends PositionBid + + object SquadList { + def Publish(squad : Squad) : SquadInfo = { + 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 = { - SquadInfo( - if(!before.leader.equals(after.leader)) after.leader 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.size.equals(after.size)) after.size else None, - if(!before.capacity.equals(after.capacity)) after.capacity else None - ) + object Detail { + def Publish(squad : Squad) : SquadDetail = { + SquadDetail() + .LeaderCharId(squad.Leader.CharId) + .LeaderName(squad.Leader.Name) + .Task(squad.Task) + .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 } } diff --git a/common/src/test/scala/game/SquadDefinitionActionMessageTest.scala b/common/src/test/scala/game/SquadDefinitionActionMessageTest.scala index f36b61c54..09e2cd42a 100644 --- a/common/src/test/scala/game/SquadDefinitionActionMessageTest.scala +++ b/common/src/test/scala/game/SquadDefinitionActionMessageTest.scala @@ -54,7 +54,7 @@ class SquadDefinitionActionMessageTest extends Specification { case SquadDefinitionActionMessage(unk1, unk2, action) => unk1 mustEqual PlanetSideGUID(0) unk2 mustEqual 3 - action mustEqual SaveSquadDefinition() + action mustEqual SaveSquadFavorite() case _ => ko } @@ -65,7 +65,7 @@ class SquadDefinitionActionMessageTest extends Specification { case SquadDefinitionActionMessage(unk1, unk2, action) => unk1 mustEqual PlanetSideGUID(0) unk2 mustEqual 3 - action mustEqual LoadSquadDefinition() + action mustEqual LoadSquadFavorite() case _ => ko } @@ -76,7 +76,7 @@ class SquadDefinitionActionMessageTest extends Specification { case SquadDefinitionActionMessage(unk1, unk2, action) => unk1 mustEqual PlanetSideGUID(0) unk2 mustEqual 3 - action mustEqual ListSquadDefinition("Cops and Military Officers") + action mustEqual ListSquadFavorite("Cops and Military Officers") case _ => ko } @@ -87,7 +87,7 @@ class SquadDefinitionActionMessageTest extends Specification { case SquadDefinitionActionMessage(unk1, unk2, action) => unk1 mustEqual PlanetSideGUID(0) unk2 mustEqual 0 - action mustEqual ListSquad() + action mustEqual RequestListSquad() case _ => ko } @@ -328,28 +328,28 @@ class SquadDefinitionActionMessageTest extends Specification { } "encode (03)" in { - val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 3, SaveSquadDefinition()) + val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 3, SaveSquadFavorite()) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector pkt mustEqual string_03 } "encode (03)" in { - val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 3, LoadSquadDefinition()) + val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 3, LoadSquadFavorite()) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector pkt mustEqual string_04 } "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 pkt mustEqual string_07 } "encode (08)" in { - val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, ListSquad()) + val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, RequestListSquad()) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector pkt mustEqual string_08 diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index 6064e729a..37619aeca 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -346,10 +346,13 @@ class WorldSessionActor extends Actor with MDCContextAware { case SquadServiceResponse(toChannel, response) => 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)) - case SquadResponse.Update(infos) if infos.nonEmpty => + case SquadResponse.UpdateList(infos) if infos.nonEmpty => val o = ReplicationStreamMessage(6, None, infos.map { case (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( ReplicationStreamMessage(1, None, 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) => 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) => val leader = squad.Leader val id = 11 @@ -399,7 +414,7 @@ class WorldSessionActor extends Actor with MDCContextAware { membershipPositions.find({ case(member, _) => member.CharId == avatar.CharId }) match { case Some((ourMember, ourIndex)) => //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) membershipPositions.foreach { case(member, index) => 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, 32, ourIndex)) //associate with member position in squad? //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 _ => //other player is joining our squad //load each member's entry @@ -432,7 +447,7 @@ class WorldSessionActor extends Actor with MDCContextAware { positionsToUpdate.find({ case(member, _) => member == avatar.CharId }) match { case Some((ourMember, ourIndex)) => //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) positionsToUpdate.foreach { case(member, index) => 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, 34, 4294967295L)) //unknown, perhaps unrelated? //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 _ => //remove each member's entry positionsToUpdate.foreach { case(member, index) => @@ -455,6 +470,7 @@ class WorldSessionActor extends Actor with MDCContextAware { case SquadResponse.UpdateMembers(squad, positions) => import services.teamwork.SquadAction.{Update => SAUpdate} + val id = 11 val pairedEntries = positions.collect { case entry if squadUI.contains(entry.char_id) => (entry, squadUI(entry.char_id)) @@ -476,12 +492,31 @@ class WorldSessionActor extends Actor with MDCContextAware { if(updatedEntries.nonEmpty) { sendResponse( SquadState( - PlanetSideGUID(11), + PlanetSideGUID(id), 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 _ => ; } @@ -956,7 +991,7 @@ class WorldSessionActor extends Actor with MDCContextAware { galaxyService ! Service.Join("galaxy") //for galaxy-wide messages galaxyService ! Service.Join(s"${avatar.faction}") //for hotspots 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") case InterstellarCluster.GiveWorld(zoneId, zone) => @@ -2964,14 +2999,14 @@ class WorldSessionActor extends Actor with MDCContextAware { //AvatarAwardMessage //DisplayAwardMessage 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 => { - sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), line, SquadAction.ListSquadDefinition(""))) + sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), line, SquadAction.ListSquadFavorite(""))) }) sendResponse(SquadDetailDefinitionUpdateMessage.Init) - sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0,SquadAction.Unknown(16, hex"".toBitVector))) - sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0,SquadAction.Unknown(17, hex"".toBitVector))) - sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0,SquadAction.Unknown(18, hex"".toBitVector))) + sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0,SquadAction.Unknown(16))) + sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0,SquadAction.SetListSquad())) + sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0,SquadAction.Unknown(18))) //MapObjectStateBlockMessage and ObjectCreateMessage? //TacticsMessage? //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) => if(deadState == DeadState.Alive) { 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.Velocity = vel