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 32bbc68e..11a56ee2 100644 --- a/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/SquadDefinitionActionMessage.scala @@ -16,9 +16,26 @@ import shapeless.{::, HNil} abstract class SquadAction(val code : Int) object SquadAction{ + object SearchMode extends Enumeration { + type Type = Value + + val + AnyPositions, + AvailablePositions, + SomeCertifications, + AllCertifications + = Value + + implicit val codec : Codec[SearchMode.Value] = PacketHelpers.createEnumerationCodec(enum = this, uint(bits = 3)) + } + final case class DisplaySquad() extends SquadAction(0) - final case class AnswerSquadJoinRequest() extends SquadAction(1) + /** + * Dispatched from client to server to indicate a squad detail update that has no foundation entry to update? + * Not dissimilar from `DisplaySquad`. + */ + final case class DisplayFullSquad() extends SquadAction(1) final case class SaveSquadFavorite() extends SquadAction(3) @@ -58,12 +75,14 @@ object SquadAction{ final case class LocationFollowsSquadLead(state : Boolean) extends SquadAction(31) - final case class SearchForSquadsWithParticularRole(u1: String, u2 : Long, u3: Int, u4 : Int) extends SquadAction(34) + final case class SearchForSquadsWithParticularRole(role: String, requirements : Set[CertificationType.Value], zone_id: Int, mode : SearchMode.Value) extends SquadAction(34) final case class CancelSquadSearch() extends SquadAction(35) final case class AssignSquadMemberToRole(position : Int, char_id : Long) extends SquadAction(38) + final case class NoSquadSearchResults() extends SquadAction(39) + final case class FindLfsSoldiersForRole(state : Int) extends SquadAction(40) final case class CancelFind() extends SquadAction(41) @@ -91,10 +110,10 @@ object SquadAction{ } ) - val answerSquadJoinRequestCodec = everFailCondition.xmap[AnswerSquadJoinRequest] ( - _ => AnswerSquadJoinRequest(), + val displayFullSquadCodec = everFailCondition.xmap[DisplayFullSquad] ( + _ => DisplayFullSquad(), { - case AnswerSquadJoinRequest() => None + case DisplayFullSquad() => None } ) @@ -243,12 +262,12 @@ object SquadAction{ PacketHelpers.encodedWideStringAligned(6) :: ulongL(46) :: uint16L :: - uintL(3)).xmap[SearchForSquadsWithParticularRole] ( + SearchMode.codec).xmap[SearchForSquadsWithParticularRole] ( { - case u1 :: u2 :: u3 :: u4 :: HNil => SearchForSquadsWithParticularRole(u1, u2, u3, u4) + case u1 :: u2 :: u3 :: u4 :: HNil => SearchForSquadsWithParticularRole(u1, CertificationType.fromEncodedLong(u2), u3, u4) }, { - case SearchForSquadsWithParticularRole(u1, u2, u3, u4) => u1 :: u2 :: u3 :: u4 :: HNil + case SearchForSquadsWithParticularRole(u1, u2, u3, u4) => u1 :: CertificationType.toEncodedLong(u2) :: u3 :: u4 :: HNil } ) @@ -268,6 +287,13 @@ object SquadAction{ } ) + val noSquadSearchResultsCodec = everFailCondition.xmap[NoSquadSearchResults] ( + _ => NoSquadSearchResults(), + { + case NoSquadSearchResults() => None + } + ) + val findLfsSoldiersForRoleCodec = uint4.xmap[FindLfsSoldiersForRole] ( state => FindLfsSoldiersForRole(state), { @@ -326,6 +352,7 @@ object SquadAction{ *     `18` - UNKNOWN
*     `26` - Reset All
*     `35` - Cancel Squad Search
+ *     `39` - No Squad Search Results
*     `41` - Cancel Find
*     `42` - UNKNOWN
*     `43` - UNKNOWN
@@ -388,7 +415,7 @@ object SquadDefinitionActionMessage extends Marshallable[SquadDefinitionActionMe import scala.annotation.switch ((code : @switch) match { case 0 => displaySquadCodec - case 1 => answerSquadJoinRequestCodec + case 1 => displayFullSquadCodec case 3 => saveSquadFavoriteCodec case 4 => loadSquadFavoriteCodec case 5 => deleteSquadFavoriteCodec @@ -411,12 +438,13 @@ object SquadDefinitionActionMessage extends Marshallable[SquadDefinitionActionMe case 34 => searchForSquadsWithParticularRoleCodec case 35 => cancelSquadSearchCodec case 38 => assignSquadMemberToRoleCodec + case 39 => noSquadSearchResultsCodec case 40 => findLfsSoldiersForRoleCodec case 41 => cancelFindCodec case 2 | 6 | 11 | 12 | 13 | 14 | 16 | 18 | 29 | 30 | 32 | 33 | - 36 | 37 | 39 | 42 | 43 => unknownCodec(code) + 36 | 37 | 42 | 43 => unknownCodec(code) case _ => failureCodec(code) }).asInstanceOf[Codec[SquadAction]] } diff --git a/common/src/main/scala/net/psforever/packet/game/SquadMemberEvent.scala b/common/src/main/scala/net/psforever/packet/game/SquadMemberEvent.scala index 503254a5..51b74955 100644 --- a/common/src/main/scala/net/psforever/packet/game/SquadMemberEvent.scala +++ b/common/src/main/scala/net/psforever/packet/game/SquadMemberEvent.scala @@ -12,18 +12,18 @@ object MemberEvent extends Enumeration { val Add, Remove, - Unknown2, + Promote, UpdateZone, Unknown4 = Value - implicit val codec = PacketHelpers.createEnumerationCodec(this, uint(3)) + implicit val codec = PacketHelpers.createEnumerationCodec(enum = this, uint(bits = 3)) } -final case class SquadMemberEvent(unk1 : Int, +final case class SquadMemberEvent(action : MemberEvent.Value, unk2 : Int, char_id : Long, - member_position : Int, + position : Int, player_name : Option[String], zone_number : Option[Int], unk7 : Option[Long]) @@ -34,40 +34,46 @@ final case class SquadMemberEvent(unk1 : Int, } object SquadMemberEvent extends Marshallable[SquadMemberEvent] { - def apply(unk1 : Int, unk2 : Int, char_id : Long, member_position : Int) : SquadMemberEvent = - SquadMemberEvent(unk1, unk2, char_id, member_position, None, None, None) + def apply(action : MemberEvent.Value, unk2 : Int, char_id : Long, position : Int) : SquadMemberEvent = + SquadMemberEvent(action, unk2, char_id, position, None, None, None) - def apply(unk2 : Int, char_id : Long, member_position : Int, player_name : String, zone_number : Int, unk7 : Long) : SquadMemberEvent = - SquadMemberEvent(0, unk2, char_id, member_position, Some(player_name), Some(zone_number), Some(unk7)) + def Add(unk2 : Int, char_id : Long, position : Int, player_name : String, zone_number : Int, unk7 : Long) : SquadMemberEvent = + SquadMemberEvent(MemberEvent.Add, unk2, char_id, position, Some(player_name), Some(zone_number), Some(unk7)) - def apply(unk2 : Int, char_id : Long, member_position : Int, zone_number : Int) : SquadMemberEvent = - SquadMemberEvent(3, unk2, char_id, member_position, None, Some(zone_number), None) + def Remove(unk2 : Int, char_id : Long, position : Int) : SquadMemberEvent = + SquadMemberEvent(MemberEvent.Remove, unk2, char_id, position, None, None, None) - def apply(unk2 : Int, char_id : Long, member_position : Int, unk7 : Long) : SquadMemberEvent = - SquadMemberEvent(4, unk2, char_id, member_position, None, None, Some(unk7)) + def Promote(unk2 : Int, char_id : Long) : SquadMemberEvent = + SquadMemberEvent(MemberEvent.Promote, unk2, char_id, 0, None, None, None) + + def UpdateZone(unk2 : Int, char_id : Long, position : Int, zone_number : Int) : SquadMemberEvent = + SquadMemberEvent(MemberEvent.UpdateZone, unk2, char_id, position, None, Some(zone_number), None) + + def Unknown4(unk2 : Int, char_id : Long, position : Int, unk7 : Long) : SquadMemberEvent = + SquadMemberEvent(MemberEvent.Unknown4, unk2, char_id, position, None, None, Some(unk7)) implicit val codec : Codec[SquadMemberEvent] = ( - ("unk1" | uint(3)) >>:~ { unk1 => + ("action" | MemberEvent.codec) >>:~ { action => ("unk2" | uint16L) :: ("char_id" | uint32L) :: - ("member_position" | uint4) :: - conditional(unk1 == 0, "player_name" | PacketHelpers.encodedWideStringAligned(1)) :: - conditional(unk1 == 0 || unk1 == 3, "zone_number" | uint16L) :: - conditional(unk1 == 0 || unk1 == 4, "unk7" | uint32L) + ("position" | uint4) :: + conditional(action == MemberEvent.Add, "player_name" | PacketHelpers.encodedWideStringAligned(1)) :: + conditional(action == MemberEvent.Add || action == MemberEvent.UpdateZone, "zone_number" | uint16L) :: + conditional(action == MemberEvent.Add || action == MemberEvent.Unknown4, "unk7" | uint32L) }).exmap[SquadMemberEvent] ( { - case unk1 :: unk2 :: char_id :: member_position :: player_name :: zone_number :: unk7 :: HNil => - Attempt.Successful(SquadMemberEvent(unk1, unk2, char_id, member_position, player_name, zone_number, unk7)) + case action :: unk2 :: char_id :: member_position :: player_name :: zone_number :: unk7 :: HNil => + Attempt.Successful(SquadMemberEvent(action, unk2, char_id, member_position, player_name, zone_number, unk7)) }, { - case data @ SquadMemberEvent(0, unk2, char_id, member_position, Some(player_name), Some(zone_number), Some(unk7)) => - Attempt.Successful(0 :: unk2 :: char_id :: member_position :: Some(player_name) :: Some(zone_number) :: Some(unk7) :: HNil) - case data @ SquadMemberEvent(3, unk2, char_id, member_position, None, Some(zone_number), None) => - Attempt.Successful(3 :: unk2 :: char_id :: member_position :: None :: Some(zone_number) :: None :: HNil) - case data @ SquadMemberEvent(4, unk2, char_id, member_position, None, None, Some(unk7)) => - Attempt.Successful(4 :: unk2 :: char_id :: member_position :: None :: None :: Some(unk7) :: HNil) - case data @ SquadMemberEvent(unk1, unk2, char_id, member_position, None, None, None) => - Attempt.Successful(unk1 :: unk2 :: char_id :: member_position :: None :: None :: None :: HNil) + case SquadMemberEvent(MemberEvent.Add, unk2, char_id, member_position, Some(player_name), Some(zone_number), Some(unk7)) => + Attempt.Successful(MemberEvent.Add :: unk2 :: char_id :: member_position :: Some(player_name) :: Some(zone_number) :: Some(unk7) :: HNil) + case SquadMemberEvent(MemberEvent.UpdateZone, unk2, char_id, member_position, None, Some(zone_number), None) => + Attempt.Successful(MemberEvent.UpdateZone :: unk2 :: char_id :: member_position :: None :: Some(zone_number) :: None :: HNil) + case SquadMemberEvent(MemberEvent.Unknown4, unk2, char_id, member_position, None, None, Some(unk7)) => + Attempt.Successful(MemberEvent.Unknown4 :: unk2 :: char_id :: member_position :: None :: None :: Some(unk7) :: HNil) + case SquadMemberEvent(action, unk2, char_id, member_position, None, None, None) => + Attempt.Successful(action :: unk2 :: char_id :: member_position :: None :: None :: None :: HNil) case data => Attempt.Failure(Err(s"SquadMemberEvent can not encode with this pattern - $data")) } diff --git a/common/src/main/scala/services/teamwork/SquadResponse.scala b/common/src/main/scala/services/teamwork/SquadResponse.scala index 26215f02..562082ae 100644 --- a/common/src/main/scala/services/teamwork/SquadResponse.scala +++ b/common/src/main/scala/services/teamwork/SquadResponse.scala @@ -18,10 +18,14 @@ object SquadResponse { 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 WantsSquadPosition(bid_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 AssignMember(squad : Squad, from_index : Int, to_index : Int) extends Response + final case class PromoteMember(squad : Squad, char_id : Long, from_index : Int, to_index : Int) extends Response final case class Detail(guid : PlanetSideGUID, squad_detail : SquadDetail) extends Response + + final case class SquadSearchResults() extends Response } diff --git a/common/src/main/scala/services/teamwork/SquadService.scala b/common/src/main/scala/services/teamwork/SquadService.scala index e3dbe592..8f3b0de2 100644 --- a/common/src/main/scala/services/teamwork/SquadService.scala +++ b/common/src/main/scala/services/teamwork/SquadService.scala @@ -22,13 +22,14 @@ class SquadService extends Actor { private var memberToSquad : mutable.LongMap[Squad] = mutable.LongMap[Squad]() private var idToSquad : TrieMap[PlanetSideGUID, Squad] = new TrieMap[PlanetSideGUID, Squad]() private var idToSwitchboard : TrieMap[PlanetSideGUID, ActorRef] = new TrieMap[PlanetSideGUID, ActorRef]() - private var i : Int = 1 + private var sid : Int = 1 private val publishedLists : TrieMap[PlanetSideEmpire.Value, ListBuffer[SquadInfo]] = TrieMap[PlanetSideEmpire.Value, ListBuffer[SquadInfo]]( PlanetSideEmpire.TR -> ListBuffer.empty, PlanetSideEmpire.NC -> ListBuffer.empty, PlanetSideEmpire.VS -> ListBuffer.empty ) - private val bids : mutable.LongMap[PositionBid] = mutable.LongMap[PositionBid]() + private val invites : mutable.LongMap[Invitation] = mutable.LongMap[Invitation]() + private val queuedInvites : mutable.LongMap[List[Invitation]] = mutable.LongMap[List[Invitation]]() private val viewDetails : mutable.LongMap[PlanetSideGUID] = mutable.LongMap[PlanetSideGUID]() private [this] val log = org.log4s.getLogger @@ -36,56 +37,70 @@ class SquadService extends Actor { 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 - testSquad.Membership(4).Armor = 64 - testSquad.Membership(4).Position = Vector3(3675.0f, 4789.8047f, 63.21875f) - idToSquad(PlanetSideGUID(3)) = testSquad - testSquad.Listed = true - UpdateSquadList(testSquad, None) +// 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 +// testSquad.Membership(4).Armor = 64 +// testSquad.Membership(4).Position = Vector3(3675.0f, 4789.8047f, 63.21875f) +// idToSquad(PlanetSideGUID(3)) = testSquad +// testSquad.Listed = true +// UpdateSquadList(testSquad, None) } def GetNextSquadId() : PlanetSideGUID = { - val out = i - val j = i + 1 + val out = sid + val j = sid + 1 if(j == 65536) { - i = 1 + sid = 1 } else { - i = j + sid = j } PlanetSideGUID(out) } + def TryResetSquadId() : Boolean = { + if(idToSquad.isEmpty) { + sid = 1 + true + } + else { + false + } + } + def GetParticipatingSquad(player : Player) : Option[Squad] = { - memberToSquad.get(player.CharId) match { + GetParticipatingSquad(player.CharId) + } + + def GetParticipatingSquad(charId : Long) : Option[Squad] = { + memberToSquad.get(charId) match { case opt @ Some(_) => opt case None => @@ -114,24 +129,49 @@ class SquadService extends Actor { } } - def StartSquad(player : Player) : Squad = { + def GetLeadingSquad(charId : Long, opt : Option[Squad]) : Option[Squad] = { + opt.orElse(memberToSquad.get(charId)) match { + case Some(squad) => + if(squad.Leader.CharId == charId) { + Some(squad) + } + else { + None + } + case _ => + None + } + } + + def CreateSquad(player : Player) : Squad = { val charId = player.CharId val faction = player.Faction - val id = GetNextSquadId() val name = player.Name - val squad = new Squad(id, faction) + val squad = new Squad(GetNextSquadId(), faction) val leadPosition = squad.Membership(squad.LeaderPositionIndex) leadPosition.Name = name - leadPosition.CharId = charId + leadPosition.CharId = player.CharId leadPosition.Health = player.Health leadPosition.Armor = player.Armor leadPosition.Position = player.Position leadPosition.ZoneId = 1 + log.info(s"$name-$faction has started a new squad") + squad + } + + def StartSquad(squad : Squad) : Squad = { + val charId = squad.Leader.CharId + val id = squad.GUID val switchboard = context.actorOf(Props[SquadSwitchboard], s"squad${id.guid}") memberToSquad += charId -> squad idToSquad += id -> squad idToSwitchboard += id -> switchboard - log.info(s"$name-$faction has started a new squad") + squad + } + + def StartSquad(player : Player) : Squad = { + val squad = CreateSquad(player) + StartSquad(squad) squad } @@ -199,137 +239,253 @@ class SquadService extends Actor { case Service.Leave(None) | Service.LeaveAll() => ; case SquadServiceMessage(tplayer, squad_action) => squad_action match { - case SquadAction.Membership(request_type, char_id, optional_char_id, _, _) => request_type match { - case SquadRequestType.Invite => - //char_id is the inviter, e.g., the (prospective) squad leader - //this is just busy work; for actual joining operations, see SquadRequestType.Accept - (optional_char_id, memberToSquad.get(char_id)) match { - case (Some(invitee), Some(squad)) => - bids(invitee) = VacancyBid(char_id, squad.GUID) - log.info(s"$invitee has been invited to squad ${squad.Task} by $char_id") - SquadEvents.publish( SquadServiceResponse(s"/$invitee/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, char_id, Some(invitee), tplayer.Name, false, Some(None))) ) - SquadEvents.publish( SquadServiceResponse(s"/$char_id/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, invitee, Some(char_id), tplayer.Name, true, Some(None))) ) - case (Some(invitee), None) => - //the inviter does not currently belong to a squad; check for an existing placeholder, or create a new one - val ourSquad = { - bids.find { case (inviter, _) => inviter == char_id } match { - case Some((_, SpontaneousBid(_, _squad))) => - _squad //borrow - case _ => - val _squad = StartSquad(tplayer) - memberToSquad.remove(char_id) //completely unlist until assured the squad is necessary - _squad - } - } - bids(invitee) = SpontaneousBid(char_id, ourSquad) - SquadEvents.publish( SquadServiceResponse(s"/$invitee/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, char_id, Some(invitee), tplayer.Name, false, Some(None))) ) - SquadEvents.publish( SquadServiceResponse(s"/$char_id/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, invitee, Some(char_id), tplayer.Name, true, Some(None))) ) - case _ => ; - } + case SquadAction.Membership(SquadRequestType.Invite, invitingPlayer, Some(invitedPlayer), _, _) => + //this is just busy work; for actual joining operations, see SquadRequestType.Accept +// FindBid(invitingPlayer, invitedPlayer) match { +// case Some(bid) => +// //invitingPlayer and invitedPlayer have both tried to join each others's squads +// //treat this junction as consent +// self ! SquadServiceMessage(tplayer, SquadAction.Membership(SquadRequestType.Accept, invitingPlayer, Some(invitedPlayer), "", None)) +// case _ => ; +// } + (memberToSquad.get(invitingPlayer), memberToSquad.get(invitedPlayer)) match { + case (Some(squad1), Some(squad2)) => + //both players are in squads + if(squad1.GUID == squad2.GUID) { + //both players are in the same squad; no need to do anything + } + else { + //we might do some platoon chicanery with this case later + //TODO platoons + } - case SquadRequestType.Accept => - //char_id is the invitee, e.g., the person joining the squad - bids.remove(char_id) match { - case Some(NormalBid(_/*inviterCharId*/, squadGUID, line)) if idToSquad.get(squadGUID).nonEmpty => - //player requested to join a squad's specific position - JoinSquad(tplayer, idToSquad(squadGUID), line) + case (Some(squad), None) => + //the classic situation + log.info(s"$invitedPlayer has been invited to squad ${squad.Task} by $invitingPlayer") + val leader = squad.Leader + val leaderCharId = leader.CharId + val bid = VacancyInvite(leaderCharId, leader.Name, squad.GUID) + AddInvite(invitedPlayer, bid) match { + case out @ Some(_) if out.contains(bid) => + SquadEvents.publish(SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, leaderCharId, Some(invitedPlayer), tplayer.Name, false, Some(None)))) + SquadEvents.publish(SquadServiceResponse(s"/$leaderCharId/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, invitedPlayer, Some(leaderCharId), tplayer.Name, true, Some(None)))) + case Some(_) => + SquadEvents.publish(SquadServiceResponse(s"/$leaderCharId/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, invitedPlayer, Some(leaderCharId), tplayer.Name, true, Some(None)))) + case _ => ; + } - case Some(VacancyBid(inviterCharId, squadGUID)) if idToSquad.get(squadGUID).nonEmpty => - //we were invited by the squad leader into an existing squad - val squad = idToSquad(squadGUID) + case (None, Some(squad)) => + //flip around the roles - the inviting becomes the invited + //TODO needs work + log.info(s"$invitedPlayer has asked $invitingPlayer for an invition to squad ${squad.Task}") + val bid = VacancyInvite(invitedPlayer, "", squad.GUID) + AddInvite(invitingPlayer, bid) match { + case out @ Some(_) if out.contains(bid) => + SquadEvents.publish(SquadServiceResponse(s"/$invitingPlayer/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, invitedPlayer, Some(invitingPlayer), tplayer.Name, false, Some(None)))) + SquadEvents.publish(SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, invitingPlayer, Some(invitedPlayer), tplayer.Name, true, Some(None)))) + case Some(_) => + SquadEvents.publish(SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, invitingPlayer, Some(invitedPlayer), tplayer.Name, true, Some(None)))) + case _ => ; + } + + case (None, None) => + //neither the invited player nor the inviting player belong to any squad + log.info(s"$invitedPlayer has been invited to join $invitingPlayer's spontaneous squad") + val bid = SpontaneousInvite(tplayer) + AddInvite(invitedPlayer, bid) match { + case out @ Some(_) if out.contains(bid) => + SquadEvents.publish(SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, invitingPlayer, Some(invitedPlayer), tplayer.Name, false, Some(None)))) + SquadEvents.publish(SquadServiceResponse(s"/$invitingPlayer/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, invitedPlayer, Some(invitingPlayer), tplayer.Name, true, Some(None)))) + case Some(_) => + SquadEvents.publish(SquadServiceResponse(s"/$invitingPlayer/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, invitedPlayer, Some(invitingPlayer), tplayer.Name, true, Some(None)))) + case _ => ; + } + + case _ => ; + } + + case SquadAction.Membership(SquadRequestType.Accept, invitedPlayer, _, _, _) => + val acceptedInvite = RemoveInvite(invitedPlayer) + acceptedInvite match { + case Some(BidForPosition(petitioner, guid, position)) if idToSquad.get(guid).nonEmpty => + //player requested to join a squad's specific position + //invitedPlayer is actually the squad leader; petitioner is the actual "invitedPlayer" + if(memberToSquad.get(petitioner.CharId).isEmpty) { + JoinSquad(petitioner, idToSquad(guid), position) + } + else { + log.warn("Accept -> Bid: the invited player is already a member of a squad and can not join a second one") + } + + case Some(VacancyInvite(invitingPlayer, _, guid)) + if idToSquad.get(guid).nonEmpty => + //we were invited by the squad leader into an existing squad + if(memberToSquad.get(invitedPlayer).isEmpty) { + val squad = idToSquad(guid) squad.Membership.zipWithIndex.find({ case (member, index) => ValidOpenSquadPosition(squad, index, member, tplayer.Certifications) }) match { case Some((_, line)) => - SquadEvents.publish( SquadServiceResponse(s"/$inviterCharId/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, inviterCharId, Some(char_id), tplayer.Name, false, Some(None))) ) - SquadEvents.publish( SquadServiceResponse(s"/$char_id/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, char_id, Some(inviterCharId), "", true, Some(None))) ) + SquadEvents.publish(SquadServiceResponse(s"/$invitingPlayer/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitingPlayer, Some(invitedPlayer), tplayer.Name, false, Some(None)))) + SquadEvents.publish(SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitedPlayer, Some(invitingPlayer), "", true, Some(None)))) JoinSquad(tplayer, squad, line) + RemoveQueuedInvites(invitedPlayer) //TODO deal with these somehow case _ => ; } + } + else { + log.warn("Accept -> Invite: the invited player is already a member of a squad and can not join a second one") + } - case Some(SpontaneousBid(inviterCharId, placeholderSquad)) => - //we were invited by someone into a new squad they would form - (GetParticipatingSquad(tplayer) match { - case Some(participating) => - if(participating.Leader.CharId == inviterCharId) { - Some(participating) + case Some(SpontaneousInvite(invitingPlayer)) => + //we were invited by someone into a new squad they would form + val invitingPlayerCharId = invitingPlayer.CharId + (GetParticipatingSquad(invitingPlayer) match { + case Some(participating) => + if(participating.Leader.CharId == invitingPlayerCharId) { + Some(participating) + } + else { + //inviter joined a squad and is not its leader; bounce this request off of the squad leader + participating.Membership.zipWithIndex.find({ case (member, index) => + ValidOpenSquadPosition(participating, index, member, tplayer.Certifications) + }) match { + case Some((_, line)) => + val bid = BidForPosition(tplayer, participating.GUID, line) + AddInvite(participating.Leader.CharId, bid) match { + case out @ Some(_) if out.contains(bid) => + HandleBidForPosition(bid, tplayer) + case _ => ; + } + case _ => ; } - else { - //inviter joined a squad and is not its leader; bounce this request off of the squad leader - //TODO squad leader receives " wants to join squad" prompt - val leaderCharId = participating.Leader.CharId - bids(char_id) = VacancyBid(leaderCharId, participating.GUID) //reframed request - 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 { + None + } + case None => + val squad = StartSquad(invitingPlayer) + squad.Task = s"${tplayer.Name}'s Squad" + SquadEvents.publish(SquadServiceResponse(s"/$invitingPlayer/Squad", SquadResponse.InitSquad(squad.GUID))) + Some(squad) + }) match { case Some(squad) => squad.Membership.zipWithIndex.find({ case (member, index) => ValidOpenSquadPosition(squad, index, member, tplayer.Certifications) }) match { case Some((_, line)) => - SquadEvents.publish( SquadServiceResponse(s"/$inviterCharId/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, inviterCharId, Some(char_id), tplayer.Name, false, Some(None))) ) - SquadEvents.publish( SquadServiceResponse(s"/$char_id/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, char_id, Some(inviterCharId), "", true, Some(None))) ) + SquadEvents.publish( SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitedPlayer, Some(invitingPlayerCharId), "", true, Some(None))) ) JoinSquad(tplayer, squad, line) + SquadEvents.publish( SquadServiceResponse(s"/$invitingPlayerCharId/Squad", SquadResponse.Membership(SquadResponseType.Accept, 0, 0, invitingPlayerCharId, Some(invitedPlayer), tplayer.Name, false, Some(None))) ) + RemoveQueuedInvites(invitedPlayer) //TODO deal with these somehow case _ => ; } case None => ; - } - - case _ => ; - } - - case SquadRequestType.Leave => - //char_id is the player leaving - val squad = memberToSquad(char_id) - val leader = squad.Leader.CharId - if(char_id == leader) { - //squad leader is leaving his own squad, so it will be disbanded - squad.Membership - .filterNot { _.CharId == leader } - .foreach { member => - val charId = member.CharId - SquadEvents.publish(SquadServiceResponse(s"/$charId/Squad", SquadResponse.Membership(SquadResponseType.Disband, 0, 0, charId, None, "", false, Some(None)))) - } - CloseOutSquad(squad) - SquadEvents.publish(SquadServiceResponse(s"/$leader/Squad", SquadResponse.Membership(SquadResponseType.Disband, 0, 0, leader, None, "", true, Some(None)))) - } - else { - if(optional_char_id.contains(char_id)) { - //leaving the squad of our own accord - LeaveSquad(tplayer, squad) } - else if(optional_char_id.contains(leader)) { - //kicked by the squad leader - SquadEvents.publish( SquadServiceResponse(s"/$char_id/Squad", SquadResponse.Membership(SquadResponseType.Leave, 0, 0, char_id, Some(leader), tplayer.Name, false, Some(None))) ) - SquadEvents.publish( SquadServiceResponse(s"/$leader/Squad", SquadResponse.Membership(SquadResponseType.Leave, 0, 0, leader, Some(char_id), "", true, Some(None))) ) - LeaveSquad(tplayer, squad) + + case None => + //the invite either timed-out or was withdrawn; select a new one? + NextInvite(invitedPlayer) match { + case Some(bid : BidForPosition) if !acceptedInvite.contains(bid) => + HandleBidForPosition(bid, tplayer) + case Some(bid) if !acceptedInvite.contains(bid) => + SquadEvents.publish(SquadServiceResponse(s"/$invitedPlayer/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, bid.InviterCharId, Some(invitedPlayer), bid.InviterName, false, Some(None)))) + case None => ; } - } + } - case SquadRequestType.Reject => - //a player has opted out of joining a squad - (bids.remove(char_id) match { - case Some(SpontaneousBid(inviterCharId, squad)) if squad.Leader.CharId != char_id => - (inviterCharId, squad) - case Some(VacancyBid(inviterCharId, guid)) if idToSquad(guid).Leader.CharId != char_id => - (inviterCharId, idToSquad.get(guid)) - case _ => ; - (0L, None) - }) match { - case (inviterCharId, Some(squad)) => - SquadEvents.publish( SquadServiceResponse(s"/$inviterCharId/Squad", SquadResponse.Membership(SquadResponseType.Reject, 0, 0, inviterCharId, Some(char_id), tplayer.Name, false, Some(None))) ) - SquadEvents.publish( SquadServiceResponse(s"/$char_id/Squad", SquadResponse.Membership(SquadResponseType.Reject, 0, 0, char_id, Some(inviterCharId), "", true, Some(None))) ) - case _ => ; + case SquadAction.Membership(SquadRequestType.Leave, leavingPlayer, optionalPlayer, _, _) => + val squad = memberToSquad(leavingPlayer) + val leader = squad.Leader.CharId + if(leavingPlayer == leader || squad.Size == 2) { + //squad leader is leaving his own squad, so it will be disbanded + //alternately, squad is only composed of two people, so it will be closed-out when one of them leaves + val membership = squad.Membership.collect { case member if member.CharId > 0 => member.CharId } + membership.foreach { charId => + SquadEvents.publish(SquadServiceResponse(s"/$charId/Squad", SquadResponse.Membership(SquadResponseType.Leave, 0, 0, charId, None, "", false, Some(None)))) } + CloseOutSquad(squad) + SquadEvents.publish(SquadServiceResponse(s"/$leader/Squad", SquadResponse.Membership(SquadResponseType.Disband, 0, 0, leader, None, "", true, Some(None)))) + membership + .filterNot(_ == leader) + .foreach { charId => + SquadEvents.publish(SquadServiceResponse(s"/$charId/Squad", SquadResponse.Membership(SquadResponseType.Disband, 0, 0, charId, None, "", false, Some(None)))) + } + SquadEvents.publish( SquadServiceResponse(s"/$leader/Squad", SquadResponse.InitSquad(PlanetSideGUID(0))) ) + SquadEvents.publish( SquadServiceResponse(s"/$leader/Squad", SquadResponse.Detail(PlanetSideGUID(0), SquadDetail().Complete)) ) + } + else { + if(optionalPlayer.contains(leavingPlayer)) { + //leaving the squad of own accord + LeaveSquad(tplayer, squad) + } + else if(optionalPlayer.contains(leader)) { + //kicked by the squad leader + SquadEvents.publish( SquadServiceResponse(s"/$leavingPlayer/Squad", SquadResponse.Membership(SquadResponseType.Leave, 0, 0, leavingPlayer, Some(leader), tplayer.Name, false, Some(None))) ) + SquadEvents.publish( SquadServiceResponse(s"/$leader/Squad", SquadResponse.Membership(SquadResponseType.Leave, 0, 0, leader, Some(leavingPlayer), "", true, Some(None))) ) + LeaveSquad(tplayer, squad) + } + } - case _ => ; - } + case SquadAction.Membership(SquadRequestType.Reject, rejectingPlayer, optionalPlayer, _, _) => + val rejectedBid = RemoveInvite(rejectingPlayer) + //(A, B) -> person who made the rejection, person who was rejected + (rejectedBid match { + case Some(SpontaneousInvite(invitingPlayer)) => + //rejectingPlayer is the would-be squad member + (Some(rejectingPlayer), Some(invitingPlayer.CharId)) + case Some(VacancyInvite(invitingPlayer, _, guid)) if idToSquad(guid).Leader.CharId != rejectingPlayer => + //rejectingPlayer is the would-be squad member + (Some(rejectingPlayer), Some(invitingPlayer)) + case Some(BidForPosition(_, guid, _)) if idToSquad(guid).Leader.CharId != rejectingPlayer => + //rejectingPlayer is the squad leader + (Some(rejectingPlayer), None) + case _ => ; + (None, None) + }) match { + case (Some(rejected), Some(invited)) => + SquadEvents.publish( SquadServiceResponse(s"/$rejected/Squad", SquadResponse.Membership(SquadResponseType.Reject, 0, 0, rejected, Some(invited), "", true, Some(None))) ) + SquadEvents.publish( SquadServiceResponse(s"/$invited/Squad", SquadResponse.Membership(SquadResponseType.Reject, 0, 0, invited, Some(rejected), tplayer.Name, false, Some(None))) ) + case (Some(rejected), None) => + SquadEvents.publish( SquadServiceResponse(s"/$rejected/Squad", SquadResponse.Membership(SquadResponseType.Reject, 0, 0, rejected, Some(rejected), "", true, Some(None))) ) + case _ => ; + } + NextInvite(rejectingPlayer) match { + case Some(bid : BidForPosition) if rejectedBid.isEmpty || !rejectedBid.contains(bid) => + HandleBidForPosition(bid, tplayer) + case Some(bid) if rejectedBid.isEmpty || !rejectedBid.contains(bid) => + SquadEvents.publish(SquadServiceResponse(s"/$rejectingPlayer/Squad", SquadResponse.Membership(SquadResponseType.Invite, 0, 0, bid.InviterCharId, Some(rejectingPlayer), bid.InviterName, false, Some(None)))) + case None => ; + } + + case SquadAction.Membership(SquadRequestType.Promote, promotingPlayer, Some(promotedPlayer), _, _) => + (memberToSquad.get(promotingPlayer), memberToSquad.get(promotedPlayer)) match { + case (Some(squad), Some(squad2)) if squad.GUID == squad2.GUID && squad.Leader.CharId == promotingPlayer => + val membership = squad.Membership.filter { _member => _member.CharId > 0 } + val (leader, position) = (squad.Leader, 0) + val (member, index) = membership.zipWithIndex.find { case (_member, _) => _member.CharId == promotedPlayer }.get + SwapMemberPosition(squad, leader, member) + log.info(s"Promoting player ${leader.Name} to be the leader of ${squad.Task}") + membership.foreach { _member => + SquadEvents.publish(SquadServiceResponse(s"/${_member.CharId}/Squad", SquadResponse.PromoteMember(squad, promotedPlayer, index, position))) + } + SquadEvents.publish(SquadServiceResponse(s"/$promotingPlayer/Squad", SquadResponse.InitSquad(PlanetSideGUID(0)))) + SquadEvents.publish(SquadServiceResponse(s"/$promotedPlayer/Squad", SquadResponse.InitSquad(squad.GUID))) + UpdateSquadListWhenListed( + squad, + SquadInfo().Leader(leader.Name) + ) + UpdateSquadDetail(squad.GUID, squad, + SquadDetail() + .LeaderCharId(leader.CharId) + .Field3(value = 0L) + .LeaderName(leader.Name) + .Members(List( + SquadPositionEntry(position, SquadPositionDetail().CharId(member.CharId).Name(member.Name)), + SquadPositionEntry(index, SquadPositionDetail().CharId(leader.CharId).Name(leader.Name)) + )) + ) + + case _ => ; + } case SquadAction.Update(char_id, health, max_health, armor, max_armor, pos, zone_number) => memberToSquad.get(char_id) match { @@ -387,14 +543,14 @@ class SquadService extends Actor { val squad = lSquadOpt.getOrElse(StartSquad(tplayer)) squad.Task = purpose UpdateSquadListWhenListed(squad, SquadInfo().Task(purpose)) - UpdateSquadDetailWhenListed(squad.GUID, squad, SquadDetail().Task(purpose)) + UpdateSquadDetail(squad.GUID, squad, SquadDetail().Task(purpose)) case ChangeSquadZone(zone) => log.info(s"${tplayer.Name}-${tplayer.Faction} has changed squad's ops zone to $zone") val squad = lSquadOpt.getOrElse(StartSquad(tplayer)) squad.ZoneId = zone.zoneId.toInt UpdateSquadListWhenListed(squad, SquadInfo().ZoneId(zone)) - UpdateSquadDetailWhenListed(squad.GUID, squad, SquadDetail().ZoneId(zone)) + UpdateSquadDetail(squad.GUID, squad, SquadDetail().ZoneId(zone)) case CloseSquadMemberPosition(position) => val squad = lSquadOpt.getOrElse(StartSquad(tplayer)) @@ -411,7 +567,7 @@ class SquadService extends Actor { } memberPosition.Close() UpdateSquadListWhenListed(squad, listingChanged) - UpdateSquadDetailWhenListed(squad.GUID, squad, + UpdateSquadDetail(squad.GUID, squad, SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail.Closed))) ) case Some(false) | None => ; @@ -424,7 +580,7 @@ class SquadService extends Actor { log.info(s"${tplayer.Name}-${tplayer.Faction} has opened the #$position position in squad") squad.Availability.update(position, true) UpdateSquadListWhenListed(squad, SquadInfo().Capacity(squad.Capacity)) - UpdateSquadDetailWhenListed(squad.GUID, squad, + UpdateSquadDetail(squad.GUID, squad, SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail.Open))) ) case Some(true) | None => ; @@ -436,7 +592,7 @@ 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 - UpdateSquadDetailWhenListed(squad.GUID, squad, + UpdateSquadDetail(squad.GUID, squad, SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail().Role(role)))) ) case Some(false) | None => ; @@ -448,7 +604,7 @@ 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 - UpdateSquadDetailWhenListed(squad.GUID, squad, + UpdateSquadDetail(squad.GUID, squad, SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail().DetailedOrders(orders)))) ) case Some(false) | None => ; @@ -460,7 +616,7 @@ 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 - UpdateSquadDetailWhenListed(squad.GUID, squad, + UpdateSquadDetail(squad.GUID, squad, SquadDetail().Members(List(SquadPositionEntry(position, SquadPositionDetail().Requirements(certs)))) ) case Some(false) | None => ; @@ -519,7 +675,7 @@ class SquadService extends Actor { squad.LocationFollowsSquadLead = false squad.AutoApproveInvitationRequests = false UpdateSquadListWhenListed(squad, SquadInfo().Task("").ZoneId(None).Capacity(squad.Capacity)) - UpdateSquadDetailWhenListed(squad.GUID, squad) + UpdateSquadDetail(squad.GUID, squad) case _ => } @@ -530,6 +686,9 @@ class SquadService extends Actor { log.info(s"${tplayer.Name} would like the #${position+1} spot in this squad") val membership = squad.Membership.zipWithIndex val toMember = squad.Membership(position) + if(squad.Leader.CharId == tplayer.CharId) { + //TODO squad leader currently disallowed + } else //the squad leader may swap to any open position; a normal member has to validate against requirements if((squad.Leader.CharId == tplayer.CharId && toMember.CharId == 0) || ValidOpenSquadPosition(squad, position, toMember, tplayer.Certifications)) { membership.find { case (member, _) => member.CharId == tplayer.CharId } match { @@ -538,12 +697,15 @@ class SquadService extends Actor { if(fromIndex == squad.LeaderPositionIndex) { squad.LeaderPositionIndex = position } + //RemoveInvite(tplayer.CharId).foreach { _ => + //close the old bids out + //} membership .filter { case (_member, _) => _member.CharId > 0 } .foreach { case (_member, _) => SquadEvents.publish(SquadServiceResponse(s"/${_member.CharId}/Squad", SquadResponse.AssignMember(squad, fromIndex, position))) } - UpdateSquadDetailWhenListed(squad.GUID, squad) + UpdateSquadDetail(squad.GUID, squad) case _ => ; //somehow, this is not our squad; do nothing, for now } @@ -561,10 +723,14 @@ class SquadService extends Actor { if(ValidOpenSquadPosition(squad, position, toMember, tplayer.Certifications)) { //we could join but we may need permission from the squad leader first log.info(s"Player ${tplayer.Name} would like to join the squad ${squad.Task}.") - bids(tplayer.CharId) = NormalBid(squad.Leader.CharId, guid, position) - //val leader = squad.Leader - //TODO ask permission from the squad leader, unless auto-approve is in effect - self ! SquadServiceMessage(tplayer, SquadAction.Membership(SquadRequestType.Accept, tplayer.CharId, None, "", None)) + val leader = squad.Leader + val bid = BidForPosition(tplayer, guid, position) + val leaderCharId = leader.CharId + AddInvite(leaderCharId, bid) match { + case out @ Some(_) if out.contains(bid) => + HandleBidForPosition(bid, tplayer) + case _ => ; + } } case None => ; //squad does not exist? assume old local data; force update to correct discrepancy @@ -574,16 +740,19 @@ class SquadService extends Actor { case (Some(squad), AssignSquadMemberToRole(position, char_id)) => val membership = squad.Membership.zipWithIndex (membership.find({ case (member, _) => member.CharId == char_id}), membership(position)) match { - case (Some((fromMember, fromPosition)), (toMember, _)) => + //TODO squad leader currently disallowed + case (Some((fromMember, fromPosition)), (toMember, _)) if fromPosition != 0 => val name = fromMember.Name SwapMemberPosition(squad, toMember, fromMember) - // + if(fromPosition == squad.LeaderPositionIndex) { + squad.LeaderPositionIndex = position + } membership .filter({ case (_member, _) => _member.CharId > 0 }) .foreach { case (_member, _) => SquadEvents.publish(SquadServiceResponse(s"/${_member.CharId}/Squad", SquadResponse.AssignMember(squad, fromPosition, position))) } - UpdateSquadDetailWhenListed(squad.GUID, squad, + UpdateSquadDetail(squad.GUID, squad, SquadDetail().Members(List( SquadPositionEntry(position, SquadPositionDetail().CharId(fromMember.CharId).Name(fromMember.Name)), SquadPositionEntry(fromPosition, SquadPositionDetail().CharId(char_id).Name(name)) @@ -597,7 +766,15 @@ class SquadService extends Actor { idToSquad.get(guid) match { case Some(squad) => viewDetails(tplayer.CharId) = guid - sender ! SquadServiceResponse(s"/${tplayer.CharId}/Squad", SquadResponse.Detail(squad.GUID, SquadService.Detail.Publish(squad))) + sender ! SquadServiceResponse("", SquadResponse.Detail(squad.GUID, SquadService.Detail.Publish(squad))) + case None => ; + } + + //the following message is feedback from a specific client, awaiting proper initialization + case (_, DisplayFullSquad()) => + idToSquad.get(guid) match { + case Some(squad) => + sender ! SquadServiceResponse("", SquadResponse.InitSquad(squad.GUID)) case None => ; } @@ -609,6 +786,122 @@ class SquadService extends Actor { } } + /** + * + * @param invitedPlayer + * @param bid + * @return an optional invite; + * if added to the active invite position, return the parameter bid; + * if added to the queued invite, return the invite in the active position; + * if not added, return `None` + */ + def AddInvite(invitedPlayer : Long, bid : Invitation) : Option[Invitation] = { + invites.get(invitedPlayer) match { + case Some(_bid) => + queuedInvites.get(invitedPlayer) match { + case Some(bidList) => + if(_bid.InviterCharId != bid.InviterCharId && !bidList.exists { eachBid => eachBid.InviterCharId == bid.InviterCharId }) { + log.debug(s"Invite from ${bid.InviterCharId} to $invitedPlayer stored in queue while active invite request pending") + queuedInvites(invitedPlayer) = bid match { + case _: BidForPosition => + val (normals, others) = bidList.partition(_.isInstanceOf[BidForPosition]) + (normals :+ bid) ++ others + case _ => + bidList :+ bid + } + None + } + else { + Some(_bid) + } + case None => + if(_bid.InviterCharId != bid.InviterCharId) { + log.debug(s"Invite from ${bid.InviterCharId} to $invitedPlayer stored while active invite request pending") + queuedInvites(invitedPlayer) = List[Invitation](bid) + } + Some(_bid) + } + + case None => + invites(invitedPlayer) = bid + Some(bid) + } + } + + def RemoveInvite(invitedPlayer : Long) : Option[Invitation] = { + invites.remove(invitedPlayer) + } + + def RemoveQueuedInvites(invitedPlayer : Long) : List[Invitation] = { + queuedInvites.remove(invitedPlayer) match { + case Some(_bidList) => _bidList.toList + case None => Nil + } + } + + def RemoveInvites(invitedPlayer : Long, invitingPlayer : Long) : Unit = { + queuedInvites.get(invitedPlayer) match { + case Some(bidList) => + val list = bidList.filterNot { _.InviterCharId == invitingPlayer } + if(list.nonEmpty) { + queuedInvites(invitedPlayer) = list + } + else { + queuedInvites.remove(invitedPlayer) + } + case None => ; + } + invites.get(invitedPlayer) match { + case Some(_bid) => + if(_bid.InviterCharId == invitingPlayer) { + //drop bid, try reload new bid + } + case None => ; + } + } + + def NextInvite(invitedPlayer : Long) : Option[Invitation] = { + invites.get(invitedPlayer) match { + case None => + queuedInvites.get(invitedPlayer) match { + case Some(list) => + list match { + case Nil => + None + case x :: Nil => + invites(invitedPlayer) = x + queuedInvites.remove(invitedPlayer) + Some(x) + case x :: xs => + invites(invitedPlayer) = x + queuedInvites(invitedPlayer) = xs + Some(x) + } + + case None => + None + } + case Some(_) => + None + } + } + + def HandleBidForPosition(bid : BidForPosition, player : Player) : Unit = { + idToSquad.get(bid.squad_guid) match { + case Some(squad) => + val leaderCharId = squad.Leader.CharId + if(squad.AutoApproveInvitationRequests) { + self ! SquadServiceMessage(player, SquadAction.Membership(SquadRequestType.Accept, leaderCharId, None, "", None)) + } + else { + SquadEvents.publish(SquadServiceResponse(s"/$leaderCharId/Squad", SquadResponse.WantsSquadPosition(bid.player.Name))) + } + case _ => + //squad is missing; will this properly short-circuit? + log.error(s"Attempted to process ${bid.InviterName}'s bid for a position in a squad (id:${bid.squad_guid.guid}) that does not exist") + } + } + def JoinSquad(player : Player, squad : Squad, line : Int) : Boolean = { val charId = player.CharId val position = squad.Membership(line) @@ -652,7 +945,7 @@ class SquadService extends Actor { .foreach { member => SquadEvents.publish(SquadServiceResponse(s"/${member.CharId}/Squad", SquadResponse.Join(squad, updatedIndex))) } - UpdateSquadDetailWhenListed(squad.GUID, squad, + UpdateSquadDetail(squad.GUID, squad, SquadDetail().Members(List(SquadPositionEntry(line, SquadPositionDetail().CharId(charId).Name(player.Name)))) ) } @@ -670,30 +963,18 @@ class SquadService extends Actor { membership.find { case (_member, _) => _member.CharId == charId } match { case Some((member, index)) => val updateList = membership.collect({ case (_member, _index) if _member.CharId > 0 => (_member.CharId, _index) }).toList + //member leaves the squad completely memberToSquad.remove(charId) member.Name = "" member.CharId = 0 - - val size = squad.Size - if(size < 2) { - //squad is rendered to just one person or less; collapse it - squad.Membership.foreach { _member => - SquadEvents.publish(SquadServiceResponse(s"/${_member.CharId}/Squad", SquadResponse.Membership(SquadResponseType.Disband, 0, 0, charId, None, "", false, Some(None)))) + sender ! SquadServiceResponse("", SquadResponse.Leave(squad, updateList)) + //other squad members see the member leaving + val leavingMember = List((charId, index)) + membership + .filter { case (_member, _) => _member.CharId > 0 } + .foreach { case (_member, _) => + SquadEvents.publish( SquadServiceResponse(s"/${_member.CharId}/Squad", SquadResponse.Leave(squad, leavingMember)) ) } - CloseOutSquad(squad, membership, updateList) - } - else { - //squad continues, despite player's parting - //member leaves the squad completely - sender ! SquadServiceResponse("", SquadResponse.Leave(squad, updateList)) - //other squad members see the member leaving - val leavingMember = List((charId, index)) - membership - .filter { case (_member, _) => _member.CharId > 0 } - .foreach { case (_member, _) => - SquadEvents.publish( SquadServiceResponse(s"/${_member.CharId}/Squad", SquadResponse.Leave(squad, leavingMember)) ) - } - } true case None => false @@ -720,6 +1001,7 @@ class SquadService extends Actor { SquadEvents.publish( SquadServiceResponse(s"/$charId/Squad", SquadResponse.Leave(squad, updateList)) ) } idToSquad.remove(squad.GUID) + UpdateSquadList(squad, None) } def SwapMemberPosition(squad : Squad, toMember : Member, fromMember : Member) : Unit = { @@ -744,6 +1026,13 @@ class SquadService extends Actor { toMember.Armor = armor } + def UpdateSquadList(faction : PlanetSideEmpire.Value): Unit = { + val factionListings = publishedLists(faction) + SquadEvents.publish( + SquadServiceResponse(s"/$faction/Squad", SquadResponse.InitList(factionListings.toVector)) + ) + } + def UpdateSquadList(squad : Squad, changes : SquadInfo) : Unit = { UpdateSquadList(squad, Some(changes)) } @@ -759,7 +1048,6 @@ class SquadService extends Actor { } 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 => { @@ -774,7 +1062,7 @@ class SquadService extends Actor { case Some(changedFields) => //squad information update log.info(s"Squad will be updated") - factionListings(index) = entry + factionListings(index) = SquadService.SquadList.Publish(squad) SquadEvents.publish( SquadServiceResponse(s"/$faction/Squad", SquadResponse.UpdateList(Seq((index, changedFields)))) ) @@ -790,7 +1078,7 @@ class SquadService extends Actor { case None => //first time being published log.info(s"Squad will be introduced") - factionListings += entry + factionListings += SquadService.SquadList.Publish(squad) SquadEvents.publish( SquadServiceResponse(s"/$faction/Squad", SquadResponse.InitList(factionListings.toVector)) ) @@ -798,19 +1086,7 @@ class SquadService extends Actor { } def UpdateSquadDetail(guid : PlanetSideGUID, squad : Squad) : Unit = { - UpdateSquadDetailWhenListed(guid, squad, SquadService.Detail.Publish(squad)) - } - - def UpdateSquadDetailWhenListed(guid : PlanetSideGUID, squad : Squad) : Unit = { - if(squad.Listed || squad.Size > 1) { - UpdateSquadDetail(guid, squad, SquadService.Detail.Publish(squad)) - } - } - - def UpdateSquadDetailWhenListed(guid : PlanetSideGUID, squad : Squad, detail : SquadDetail) : Unit = { - if(squad.Listed || squad.Size > 1) { - UpdateSquadDetail(guid, squad, detail) - } + UpdateSquadDetail(guid, squad, SquadService.Detail.Publish(squad)) } def UpdateSquadDetail(guid : PlanetSideGUID, squad : Squad, detail : SquadDetail) : Unit = { @@ -824,13 +1100,19 @@ class SquadService extends Actor { } object SquadService { - trait PositionBid + abstract class Invitation(char_id : Long, name : String) { + def InviterCharId : Long = char_id + def InviterName : String = name + } - final case class NormalBid(char_id : Long, squad_guid : PlanetSideGUID, position : Int) extends PositionBid + final case class BidForPosition(player : Player, squad_guid : PlanetSideGUID, position : Int) + extends Invitation(player.CharId, player.Name) - final case class VacancyBid(char_id : Long, squad_guid : PlanetSideGUID) extends PositionBid + final case class VacancyInvite(char_id : Long, name : String, squad_guid : PlanetSideGUID) + extends Invitation(char_id, name) - final case class SpontaneousBid(char_id : Long, placeholder : Squad) extends PositionBid + final case class SpontaneousInvite(player : Player) + extends Invitation(player.CharId, player.Name) object SquadList { def Publish(squad : Squad) : SquadInfo = { diff --git a/common/src/test/scala/game/SquadDefinitionActionMessageTest.scala b/common/src/test/scala/game/SquadDefinitionActionMessageTest.scala index 09e2cd42..3ef29577 100644 --- a/common/src/test/scala/game/SquadDefinitionActionMessageTest.scala +++ b/common/src/test/scala/game/SquadDefinitionActionMessageTest.scala @@ -222,7 +222,7 @@ class SquadDefinitionActionMessageTest extends Specification { case SquadDefinitionActionMessage(unk1, unk2, action) => unk1 mustEqual PlanetSideGUID(0) unk2 mustEqual 0 - action mustEqual SearchForSquadsWithParticularRole("Badass", 0L, 1, 0) + action mustEqual SearchForSquadsWithParticularRole("Badass", Set(), 1, SearchMode.AnyPositions) case _ => ko } @@ -233,7 +233,7 @@ class SquadDefinitionActionMessageTest extends Specification { case SquadDefinitionActionMessage(unk1, unk2, action) => unk1 mustEqual PlanetSideGUID(0) unk2 mustEqual 0 - action mustEqual SearchForSquadsWithParticularRole("Badass", 0L, 2, 0) + action mustEqual SearchForSquadsWithParticularRole("Badass", Set(), 2, SearchMode.AnyPositions) case _ => ko } @@ -244,7 +244,7 @@ class SquadDefinitionActionMessageTest extends Specification { case SquadDefinitionActionMessage(unk1, unk2, action) => unk1 mustEqual PlanetSideGUID(0) unk2 mustEqual 0 - action mustEqual SearchForSquadsWithParticularRole("Badass", 0L, 2, 1) + action mustEqual SearchForSquadsWithParticularRole("Badass", Set(), 2, SearchMode.AvailablePositions) case _ => ko } @@ -255,7 +255,7 @@ class SquadDefinitionActionMessageTest extends Specification { case SquadDefinitionActionMessage(unk1, unk2, action) => unk1 mustEqual PlanetSideGUID(0) unk2 mustEqual 0 - action mustEqual SearchForSquadsWithParticularRole("Badass", 536870928L, 2, 2) + action mustEqual SearchForSquadsWithParticularRole("Badass", Set(CertificationType.InfiltrationSuit, CertificationType.AntiVehicular), 2, SearchMode.SomeCertifications) case _ => ko } @@ -266,7 +266,7 @@ class SquadDefinitionActionMessageTest extends Specification { case SquadDefinitionActionMessage(unk1, unk2, action) => unk1 mustEqual PlanetSideGUID(0) unk2 mustEqual 0 - action mustEqual SearchForSquadsWithParticularRole("Badass", 536870928L, 2, 3) + action mustEqual SearchForSquadsWithParticularRole("Badass", Set(CertificationType.InfiltrationSuit, CertificationType.AntiVehicular), 2, SearchMode.AllCertifications) case _ => ko } @@ -436,35 +436,35 @@ class SquadDefinitionActionMessageTest extends Specification { } "encode (34a)" in { - val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SearchForSquadsWithParticularRole("Badass", 0L, 1, 0)) + val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SearchForSquadsWithParticularRole("Badass", Set(), 1, SearchMode.AnyPositions)) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector pkt mustEqual string_34a } "encode (34b)" in { - val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SearchForSquadsWithParticularRole("Badass", 0L, 2, 0)) + val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SearchForSquadsWithParticularRole("Badass", Set(), 2, SearchMode.AnyPositions)) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector pkt mustEqual string_34b } "encode (34c)" in { - val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SearchForSquadsWithParticularRole("Badass", 0L, 2, 1)) + val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SearchForSquadsWithParticularRole("Badass", Set(), 2, SearchMode.AvailablePositions)) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector pkt mustEqual string_34c } "encode (34d)" in { - val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SearchForSquadsWithParticularRole("Badass", 536870928L, 2, 2)) + val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SearchForSquadsWithParticularRole("Badass", Set(CertificationType.InfiltrationSuit, CertificationType.AntiVehicular), 2, SearchMode.SomeCertifications)) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector pkt mustEqual string_34d } "encode (34e)" in { - val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SearchForSquadsWithParticularRole("Badass", 536870928L, 2, 3)) + val msg = SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SearchForSquadsWithParticularRole("Badass", Set(CertificationType.InfiltrationSuit, CertificationType.AntiVehicular), 2, SearchMode.AllCertifications)) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector pkt mustEqual string_34e diff --git a/common/src/test/scala/game/SquadMemberEventTest.scala b/common/src/test/scala/game/SquadMemberEventTest.scala index 5ecc1cef..2f431991 100644 --- a/common/src/test/scala/game/SquadMemberEventTest.scala +++ b/common/src/test/scala/game/SquadMemberEventTest.scala @@ -12,7 +12,7 @@ class SquadMemberEventTest extends Specification { "decode" in { PacketCoding.DecodePacket(string).require match { case SquadMemberEvent(u1, u2, u3, u4, u5, u6, u7) => - u1 mustEqual 0 + u1 mustEqual MemberEvent.Add u2 mustEqual 7 u3 mustEqual 42771010L u4 mustEqual 0 @@ -25,7 +25,7 @@ class SquadMemberEventTest extends Specification { } "encode" in { - val msg = SquadMemberEvent(0, 7, 42771010L, 0, Some("HofD"), Some(7), Some(529745L)) + val msg = SquadMemberEvent(MemberEvent.Add, 7, 42771010L, 0, Some("HofD"), Some(7), Some(529745L)) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector pkt mustEqual string } diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index 49fdca7e..a7bbe9ef 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -40,6 +40,7 @@ import net.psforever.objects.serverobject.terminals._ import net.psforever.objects.serverobject.terminals.Terminal.TerminalMessage import net.psforever.objects.serverobject.tube.SpawnTube import net.psforever.objects.serverobject.turret.{FacilityTurret, TurretUpgrade, WeaponTurret} +import net.psforever.objects.teamwork.Squad import net.psforever.objects.vehicles.{AccessPermissionGroup, Cargo, Utility, VehicleLockState, _} import net.psforever.objects.vital._ import net.psforever.objects.zones.{InterstellarCluster, Zone, ZoneHotSpotProjector} @@ -358,7 +359,6 @@ class WorldSessionActor extends Actor with MDCContextAware { SquadListing(index, squadInfo) }.toVector ) - log.info(s"updating squad with msg $o") sendResponse( ReplicationStreamMessage(6, None, infos.map { case (index, squadInfo) => @@ -388,12 +388,33 @@ class WorldSessionActor extends Actor with MDCContextAware { 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.Membership(request_type, unk1, unk2, char_id, opt_char_id, player_name, unk5, unk6) => + val name = request_type match { + case SquadResponseType.Invite if unk5 => + //player_name is our name; the name of the player indicated by unk3 is needed + LivePlayerList.WorldPopulation({ case (_, a : Avatar) => char_id == a.CharId }).headOption match { + case Some(player) => + player.name + case None => + player_name + } + case _ => + player_name + } + sendResponse(SquadMembershipResponse(request_type, unk1, unk2, char_id, opt_char_id, 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.WantsSquadPosition(name : String) => + sendResponse( + ChatMsg( + ChatMessageType.CMT_TELL, true, "", + s"\\#6[SQUAD] \\#3$name\\#6 would like to join your squad. (respond with \\#3/accept\\#6 or \\#3/reject\\#6)", + None + ) + ) + case SquadResponse.Join(squad, positionsToUpdate) => val leader = squad.Leader val id = 11 @@ -405,20 +426,20 @@ class WorldSessionActor extends Actor with MDCContextAware { //we are joining the squad //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))) + sendResponse(SquadMemberEvent.Add(id, member.CharId, index, member.Name, member.ZoneId, unk7 = 0)) squadUI(member.CharId) = SquadUIElement(member.Name, index, member.ZoneId, member.Health, member.Armor, member.Position) } //initialization - sendResponse(SquadMemberEvent(0, id, ourMember.CharId, ourIndex, Some(ourMember.Name), Some(ourMember.ZoneId), Some(0))) //repeat of our entry + sendResponse(SquadMemberEvent.Add(id, ourMember.CharId, ourIndex, ourMember.Name, ourMember.ZoneId, unk7 = 0)) //repeat of our entry 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))) + sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(18))) case _ => //other player is joining our squad //load each member's entry membershipPositions.foreach { case(member, index) => - sendResponse(SquadMemberEvent(0, id, member.CharId, index, Some(member.Name), Some(member.ZoneId), Some(0))) + sendResponse(SquadMemberEvent.Add(id, member.CharId, index, member.Name, member.ZoneId, unk7 = 0)) squadUI(member.CharId) = SquadUIElement(member.Name, index, member.ZoneId, member.Health, member.Armor, member.Position) } } @@ -429,6 +450,7 @@ class WorldSessionActor extends Actor with MDCContextAware { .map{ case (member, _) => SquadStateInfo(member.CharId, member.Health, member.Armor, member.Position, 2,2, false, 429, None,None) } .toList )) + log.info(s"SquadCards: ${squadUI.map { case(id, card) => s"[${card.index}:${card.name}/$id]" }.mkString}") case SquadResponse.Leave(_, positionsToUpdate) => val id = 11 @@ -437,11 +459,11 @@ class WorldSessionActor extends Actor with MDCContextAware { //we are leaving the squad //remove each member's entry (our own too) positionsToUpdate.foreach { case(member, index) => - sendResponse(SquadMemberEvent(1, id, member, index, None, None, None)) + sendResponse(SquadMemberEvent.Remove(id, member, index)) squadUI.remove(member) } //uninitialize - sendResponse(SquadMemberEvent(1, id, ourMember, ourIndex, None, None, None)) //repeat of our entry + sendResponse(SquadMemberEvent.Remove(id, ourMember, ourIndex)) //repeat of our entry sendResponse(PlanetsideAttributeMessage(player.GUID, 31, 0)) //disassociate with squad? sendResponse(PlanetsideAttributeMessage(player.GUID, 32, 0)) //disassociate with member position in squad? sendResponse(PlanetsideAttributeMessage(player.GUID, 34, 4294967295L)) //unknown, perhaps unrelated? @@ -450,9 +472,10 @@ class WorldSessionActor extends Actor with MDCContextAware { case _ => //remove each member's entry positionsToUpdate.foreach { case(member, index) => - sendResponse(SquadMemberEvent(1, id, member, index, None, None, None)) + sendResponse(SquadMemberEvent.Remove(id, member, index)) squadUI.remove(member) } + log.info(s"SquadCards: ${squadUI.map { case(id, card) => s"[${card.index}:${card.name}/$id]" }.mkString}") } case SquadResponse.UpdateMembers(squad, positions) => @@ -467,7 +490,7 @@ class WorldSessionActor extends Actor with MDCContextAware { .collect({ case (entry, element) if entry.zone_number != element.zone => //zone gets updated for these entries - sendResponse(SquadMemberEvent(3, 11, entry.char_id, element.index, None, Some(entry.zone_number), None)) + sendResponse(SquadMemberEvent.UpdateZone(11, entry.char_id, element.index, entry.zone_number)) squadUI(entry.char_id) = SquadUIElement(element.name, element.index, entry.zone_number, entry.health, entry.armor, entry.pos) entry case (entry, element) if entry.health != element.health || entry.armor != element.armor || entry.pos != element.position => @@ -486,53 +509,19 @@ class WorldSessionActor extends Actor with MDCContextAware { } case SquadResponse.AssignMember(squad, from_index, to_index) => - if(squadUI.nonEmpty) { - val toMember = squad.Membership(to_index) - val toCharId = toMember.CharId - val fromMember = squad.Membership(from_index) - val fromCharId = fromMember.CharId - val id = 11 - if(fromCharId > 0) { - //toMember and fromMember have swapped places - val fromElem = squadUI(fromCharId) - val toElem = squadUI(toCharId) - sendResponse(SquadMemberEvent(1, id, toCharId, from_index, None, None, None)) - sendResponse(SquadMemberEvent(1, id, fromCharId, to_index, None, None, None)) - squadUI(toCharId) = SquadUIElement(fromElem.name, to_index, fromElem.zone, fromElem.health, fromElem.armor, fromElem.position) - squadUI(fromCharId) = SquadUIElement(toElem.name, from_index, toElem.zone, toElem.health, toElem.armor, toElem.position) - sendResponse(SquadMemberEvent(0, id, toCharId, to_index, Some(fromElem.name), Some(fromElem.zone), Some(0))) - sendResponse(SquadMemberEvent(0, id, fromCharId, from_index, Some(toElem.name), Some(toElem.zone), Some(0))) - sendResponse( - SquadState( - PlanetSideGUID(id), - List( - SquadStateInfo(fromCharId, toElem.health, toElem.armor, toElem.position, 2, 2, false, 429, None, None), - SquadStateInfo(toCharId, fromElem.health, fromElem.armor, fromElem.position, 2, 2, false, 429, None, None) - ) - ) - ) - } - else { - //previous fromMember has moved toMember - val elem = squadUI(fromCharId) - sendResponse(SquadMemberEvent(1, id, toCharId, from_index, None, None, None)) - squadUI(toCharId) = SquadUIElement(elem.name, to_index, elem.zone, elem.health, elem.armor, elem.position) - sendResponse(SquadMemberEvent(0, id, toCharId, to_index, Some(elem.name), Some(elem.zone), Some(0))) - sendResponse( - SquadState( - PlanetSideGUID(id), - List(SquadStateInfo(toCharId, elem.health, elem.armor, elem.position, 2, 2, false, 429, None, None)) - ) - ) - } - val charId = avatar.CharId - if(toCharId == charId) { - sendResponse(PlanetsideAttributeMessage(player.GUID, 32, to_index)) - } - else if(fromCharId == charId) { - sendResponse(PlanetsideAttributeMessage(player.GUID, 32, from_index)) - } - } + //we've already swapped position internally; now we swap the cards + SwapSquadUIElements(squad, from_index, to_index) + log.info(s"SquadCards: ${squadUI.map { case(id, card) => s"[${card.index}:${card.name}/$id]" }.mkString}") + + case SquadResponse.PromoteMember(squad, char_id, from_index, to_index) => + //promotion will swap visual cards, but we must fix the backend + val id = 11 + sendResponse(SquadMemberEvent.Promote(id, char_id)) + SwapSquadUIElements(squad, from_index, to_index) + log.info(s"SquadCards: ${squadUI.map { case(id, card) => s"[${card.index}:${card.name}/$id]" }.mkString}") + + case SquadResponse.SquadSearchResults() => + sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.NoSquadSearchResults())) case _ => ; } @@ -3478,25 +3467,16 @@ class WorldSessionActor extends Actor with MDCContextAware { "Degrado", Set( CertificationType.StandardAssault, - CertificationType.ArmoredAssault1, - CertificationType.MediumAssault, - CertificationType.ReinforcedExoSuit, - CertificationType.Harasser, - CertificationType.Engineering, - CertificationType.GroundSupport, CertificationType.AgileExoSuit, - CertificationType.AIMAX, - CertificationType.StandardExoSuit, - CertificationType.AAMAX, - CertificationType.ArmoredAssault2 + CertificationType.StandardExoSuit ), - 9, - 0, - PlanetSideGUID(1) + 37, + 5, + PlanetSideGUID(7) ) ) ) - sendResponse(SquadInvitationRequestMessage(PlanetSideGUID(1), 9, 41577140L, "Degrado")) + sendResponse(SquadInvitationRequestMessage(PlanetSideGUID(1), 4, 41577140L, "Degrado")) } player.Position = pos player.Velocity = vel @@ -9136,6 +9116,56 @@ class WorldSessionActor extends Actor with MDCContextAware { } } + def SwapSquadUIElements(squad : Squad, fromIndex : Int, toIndex : Int) : Unit = { + if(squadUI.nonEmpty) { + val fromMember = squad.Membership(fromIndex) + val fromCharId = fromMember.CharId + val toMember = squad.Membership(toIndex) + val toCharId = toMember.CharId + val id = 11 + if(fromCharId > 0) { + //toMember and fromMember have swapped places + val fromElem = squadUI(fromCharId) + val toElem = squadUI(toCharId) + sendResponse(SquadMemberEvent.Remove(id, toCharId, fromIndex)) + sendResponse(SquadMemberEvent.Remove(id, fromCharId, toIndex)) + squadUI(toCharId) = SquadUIElement(fromElem.name, toIndex, fromElem.zone, fromElem.health, fromElem.armor, fromElem.position) + squadUI(fromCharId) = SquadUIElement(toElem.name, fromIndex, toElem.zone, toElem.health, toElem.armor, toElem.position) + sendResponse(SquadMemberEvent.Add(id, toCharId, toIndex, fromElem.name, fromElem.zone, unk7 = 0)) + sendResponse(SquadMemberEvent.Add(id, fromCharId, fromIndex, toElem.name, toElem.zone, unk7 = 0)) + sendResponse( + SquadState( + PlanetSideGUID(id), + List( + SquadStateInfo(fromCharId, toElem.health, toElem.armor, toElem.position, 2, 2, false, 429, None, None), + SquadStateInfo(toCharId, fromElem.health, fromElem.armor, fromElem.position, 2, 2, false, 429, None, None) + ) + ) + ) + } + else { + //previous fromMember has moved toMember + val elem = squadUI(toCharId) + sendResponse(SquadMemberEvent.Remove(id, toCharId, fromIndex)) + squadUI(toCharId) = SquadUIElement(elem.name, toIndex, elem.zone, elem.health, elem.armor, elem.position) + sendResponse(SquadMemberEvent.Add(id, toCharId, toIndex, elem.name, elem.zone, unk7 = 0)) + sendResponse( + SquadState( + PlanetSideGUID(id), + List(SquadStateInfo(toCharId, elem.health, elem.armor, elem.position, 2, 2, false, 429, None, None)) + ) + ) + } + val charId = avatar.CharId + if(toCharId == charId) { + sendResponse(PlanetsideAttributeMessage(player.GUID, 32, toIndex)) + } + else if(fromCharId == charId) { + sendResponse(PlanetsideAttributeMessage(player.GUID, 32, fromIndex)) + } + } + } + def failWithError(error : String) = { log.error(error) sendResponse(ConnectionClose())