From 60d65e22d3461b56aff09b6c4254d7161f2bc9ff Mon Sep 17 00:00:00 2001 From: FateJH Date: Mon, 19 Aug 2019 19:20:42 -0400 Subject: [PATCH] added a refactored squad joining entrypoint; initial work and test on SquadWaypointEvent packet; initial work on SquadWaypointRequest packet; squad waypoints up to #5 (squad experience) should be working --- .../psforever/packet/GamePacketOpcode.scala | 2 +- .../packet/game/SquadWaypointEvent.scala | 74 +++--- .../packet/game/SquadWaypointRequest.scala | 115 +++++++++ .../scala/services/teamwork/SquadAction.scala | 4 +- .../services/teamwork/SquadResponse.scala | 3 + .../services/teamwork/SquadService.scala | 233 +++++++++++------- .../scala/game/SquadWaypointEventTest.scala | 34 +-- .../src/main/scala/WorldSessionActor.scala | 25 +- 8 files changed, 343 insertions(+), 147 deletions(-) create mode 100644 common/src/main/scala/net/psforever/packet/game/SquadWaypointRequest.scala diff --git a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala index 54452c55..2d10e152 100644 --- a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala +++ b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala @@ -472,7 +472,7 @@ object GamePacketOpcode extends Enumeration { case 0x80 => noDecoder(GenericObjectAction2Message) case 0x81 => game.DestroyDisplayMessage.decode case 0x82 => noDecoder(TriggerBotAction) - case 0x83 => noDecoder(SquadWaypointRequest) + case 0x83 => game.SquadWaypointRequest.decode case 0x84 => game.SquadWaypointEvent.decode case 0x85 => noDecoder(OffshoreVehicleMessage) case 0x86 => game.ObjectDeployedMessage.decode diff --git a/common/src/main/scala/net/psforever/packet/game/SquadWaypointEvent.scala b/common/src/main/scala/net/psforever/packet/game/SquadWaypointEvent.scala index 3874b625..4fe9f649 100644 --- a/common/src/main/scala/net/psforever/packet/game/SquadWaypointEvent.scala +++ b/common/src/main/scala/net/psforever/packet/game/SquadWaypointEvent.scala @@ -7,16 +7,16 @@ import scodec.{Attempt, Codec, Err} import scodec.codecs._ import shapeless.{::, HNil} -final case class WaypointEvent(unk1 : Int, +final case class WaypointEvent(zone_number : Int, pos : Vector3, - unk2 : Int) + unk : Int) -final case class SquadWaypointEvent(unk1 : Int, - unk2 : Int, - unk3 : Long, - unk4 : Int, +final case class SquadWaypointEvent(event_type : WaypointEventAction.Value, + unk : Int, + char_id : Long, + waypoint_type : Int, unk5 : Option[Long], - unk6 : Option[WaypointEvent]) + waypoint_info : Option[WaypointEvent]) extends PlanetSideGamePacket { type Packet = SquadWaypointEvent def opcode = GamePacketOpcode.SquadWaypointEvent @@ -24,55 +24,55 @@ final case class SquadWaypointEvent(unk1 : Int, } object SquadWaypointEvent extends Marshallable[SquadWaypointEvent] { - def apply(unk1 : Int, unk2 : Int, unk3 : Long, unk4 : Int, unk_a : Long) : SquadWaypointEvent = - SquadWaypointEvent(unk1, unk2, unk3, unk4, Some(unk_a), None) + def Add(unk : Int, char_id : Long, waypoint_type : Int, waypoint : WaypointEvent) : SquadWaypointEvent = + SquadWaypointEvent(WaypointEventAction.Add, unk, char_id, waypoint_type, None, Some(waypoint)) - def apply(unk1 : Int, unk2 : Int, unk3 : Long, unk4 : Int, unk_a : Int, pos : Vector3, unk_b : Int) : SquadWaypointEvent = - SquadWaypointEvent(unk1, unk2, unk3, unk4, None, Some(WaypointEvent(unk_a, pos, unk_b))) + def Unknown1(unk : Int, char_id : Long, waypoint_type : Int, unk_a : Long) : SquadWaypointEvent = + SquadWaypointEvent(WaypointEventAction.Unknown1, unk, char_id, waypoint_type, Some(unk_a), None) - def apply(unk1 : Int, unk2 : Int, unk3 : Long, unk4 : Int) : SquadWaypointEvent = - SquadWaypointEvent(unk1, unk2, unk3, unk4, None, None) + def Remove(unk : Int, char_id : Long, waypoint_type : Int) : SquadWaypointEvent = + SquadWaypointEvent(WaypointEventAction.Remove, unk, char_id, waypoint_type, None, None) private val waypoint_codec : Codec[WaypointEvent] = ( - ("unk1" | uint16L) :: + ("zone_number" | uint16L) :: ("pos" | Vector3.codec_pos) :: - ("unk2" | uint(3)) + ("unk" | uint(3)) ).as[WaypointEvent] implicit val codec : Codec[SquadWaypointEvent] = ( - ("unk1" | uint2) >>:~ { unk1 => - ("unk2" | uint16L) :: - ("unk3" | uint32L) :: - ("unk4" | uint8L) :: - ("unk5" | conditional(unk1 == 1, uint32L)) :: - ("unk6" | conditional(unk1 == 0, waypoint_codec)) + ("event_type" | WaypointEventAction.codec) >>:~ { event_type => + ("unk" | uint16L) :: + ("char_id" | uint32L) :: + ("waypoint_type" | uint8L) :: + ("unk5" | conditional(event_type == WaypointEventAction.Unknown1, uint32L)) :: + ("waypoint_info" | conditional(event_type == WaypointEventAction.Add, waypoint_codec)) } ).exmap[SquadWaypointEvent] ( { - case 0 :: a :: b :: c :: None :: Some(d) :: HNil => - Attempt.Successful(SquadWaypointEvent(0, a, b, c, None, Some(d))) + case WaypointEventAction.Add :: a :: char_id :: waypoint_type :: None :: Some(waypoint) :: HNil => + Attempt.Successful(SquadWaypointEvent(WaypointEventAction.Add, a, char_id, waypoint_type, None, Some(waypoint))) - case 1 :: a :: b :: c :: Some(d) :: None :: HNil => - Attempt.Successful(SquadWaypointEvent(1, a, b, c, Some(d), None)) + case WaypointEventAction.Unknown1 :: a :: char_id :: waypoint_type :: Some(d) :: None :: HNil => + Attempt.Successful(SquadWaypointEvent(WaypointEventAction.Unknown1, a, char_id, waypoint_type, Some(d), None)) - case a :: b :: c :: d :: None :: None :: HNil => - Attempt.Successful(SquadWaypointEvent(a, b, c, d, None, None)) + case event_type :: b :: char_id :: waypoint_type :: None :: None :: HNil => + Attempt.Successful(SquadWaypointEvent(event_type, b, char_id, waypoint_type, None, None)) - case n :: _ :: _ :: _ :: _ :: _ :: HNil => - Attempt.Failure(Err(s"unexpected format for unk1 - $n")) + case data => + Attempt.Failure(Err(s"unexpected format for $data")) }, { - case SquadWaypointEvent(0, a, b, c, None, Some(d)) => - Attempt.Successful(0 :: a :: b :: c :: None :: Some(d) :: HNil) + case SquadWaypointEvent(WaypointEventAction.Add, a, char_id, waypoint_type, None, Some(waypoint)) => + Attempt.Successful(WaypointEventAction.Add :: a :: char_id :: waypoint_type :: None :: Some(waypoint) :: HNil) - case SquadWaypointEvent(1, a, b, c, Some(d), None) => - Attempt.Successful(1 :: a :: b :: c :: Some(d) :: None :: HNil) + case SquadWaypointEvent(WaypointEventAction.Unknown1, a, char_id, waypoint_type, Some(d), None) => + Attempt.Successful(WaypointEventAction.Unknown1 :: a :: char_id :: waypoint_type :: Some(d) :: None :: HNil) - case SquadWaypointEvent(a, b, c, d, None, None) => - Attempt.Successful(a :: b :: c :: d :: None :: None :: HNil) + case SquadWaypointEvent(event_type, b, char_id, waypoint_type, None, None) => + Attempt.Successful(event_type :: b :: char_id :: waypoint_type :: None :: None :: HNil) - case SquadWaypointEvent(n, _, _, _, _, _) => - Attempt.Failure(Err(s"unexpected format for unk1 - $n")) + case data => + Attempt.Failure(Err(s"unexpected format for $data")) } ) } diff --git a/common/src/main/scala/net/psforever/packet/game/SquadWaypointRequest.scala b/common/src/main/scala/net/psforever/packet/game/SquadWaypointRequest.scala new file mode 100644 index 00000000..5b7ebd57 --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/SquadWaypointRequest.scala @@ -0,0 +1,115 @@ +// Copyright (c) 2019 PSForever +package net.psforever.packet.game + +import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} +import net.psforever.types.Vector3 +import scodec.{Attempt, Codec, Err} +import scodec.codecs._ +import shapeless.{::, HNil} + +/** + * Actions that can be requested of the specific waypoint. + */ +object WaypointEventAction extends Enumeration { + type Type = Value + + val + Add, + Unknown1, + Remove, + Unknown3 //unconfirmed + = Value + + implicit val codec : Codec[WaypointEventAction.Value] = PacketHelpers.createEnumerationCodec(enum = this, uint2) +} + +/** + * na + * @param zone_number the zone + * @param pos the continental map coordinate location of the waypoint; + * the z-coordinate is almost always 0.0 + */ +final case class WaypointInfo(zone_number : Int, + pos : Vector3) + +/** + * na + * @param request_type the action to be performed + * @param char_id the unique id of player setting the waypoint + * @param waypoint_type the waypoint being updated; + * 0-3 for the standard squad waypoints numbered "1-4"; + * 4 for the squad leader experience waypoint; + * cycles through 0-3 continuously + * + * @param unk4 na + * @param waypoint_info essential data about the waypoint + */ +final case class SquadWaypointRequest(request_type : WaypointEventAction.Value, + char_id : Long, + waypoint_type : Int, + unk4 : Option[Long], + waypoint_info : Option[WaypointInfo]) + extends PlanetSideGamePacket { + type Packet = SquadWaypointRequest + def opcode = GamePacketOpcode.SquadWaypointRequest + def encode = SquadWaypointRequest.encode(this) +} + +object SquadWaypointRequest extends Marshallable[SquadWaypointRequest] { + def Add(char_id : Long, waypoint_type : Int, waypoint : WaypointInfo) : SquadWaypointRequest = + SquadWaypointRequest(WaypointEventAction.Add, char_id, waypoint_type, None, Some(waypoint)) + + def Unknown1(char_id : Long, waypoint_type : Int, unk_a : Long) : SquadWaypointRequest = + SquadWaypointRequest(WaypointEventAction.Unknown1, char_id, waypoint_type, Some(unk_a), None) + + def Remove(char_id : Long, waypoint_type : Int) : SquadWaypointRequest = + SquadWaypointRequest(WaypointEventAction.Remove, char_id, waypoint_type, None, None) + + private val waypoint_codec : Codec[WaypointInfo] = ( + ("zone_number" | uint16L) :: + ("pos" | Vector3.codec_pos) + ).xmap[WaypointInfo] ( + { + case zone_number :: pos :: HNil => WaypointInfo(zone_number, pos) + }, + { + case WaypointInfo(zone_number, pos) => zone_number :: pos.xy :: HNil + } + ) + + implicit val codec : Codec[SquadWaypointRequest] = ( + ("request_type" | WaypointEventAction.codec) >>:~ { request_type => + ("char_id" | uint32L) :: + ("waypoint_type" | uint8L) :: + ("unk4" | conditional(request_type == WaypointEventAction.Unknown1, uint32L)) :: + ("waypoint" | conditional(request_type == WaypointEventAction.Add, waypoint_codec)) + } + ).exmap[SquadWaypointRequest] ( + { + case WaypointEventAction.Add :: char_id :: waypoint_type :: None :: Some(waypoint) :: HNil => + Attempt.Successful(SquadWaypointRequest(WaypointEventAction.Add, char_id, waypoint_type, None, Some(waypoint))) + + case WaypointEventAction.Unknown1 :: char_id :: waypoint_type :: Some(d) :: None :: HNil => + Attempt.Successful(SquadWaypointRequest(WaypointEventAction.Unknown1, char_id, waypoint_type, Some(d), None)) + + case request_type :: char_id :: waypoint_type :: None :: None :: HNil => + Attempt.Successful(SquadWaypointRequest(request_type, char_id, waypoint_type, None, None)) + + case data => + Attempt.Failure(Err(s"unexpected format while decoding - $data")) + }, + { + case SquadWaypointRequest(WaypointEventAction.Add, char_id, waypoint_type, None, Some(waypoint)) => + Attempt.Successful(WaypointEventAction.Add :: char_id :: waypoint_type :: None :: Some(waypoint) :: HNil) + + case SquadWaypointRequest(WaypointEventAction.Unknown1, char_id, waypoint_type, Some(d), None) => + Attempt.Successful(WaypointEventAction.Unknown1 :: char_id :: waypoint_type :: Some(d) :: None :: HNil) + + case SquadWaypointRequest(request_type, char_id, waypoint_type, None, None) => + Attempt.Successful(request_type :: char_id :: waypoint_type :: None :: None :: HNil) + + case data : SquadWaypointRequest => + Attempt.Failure(Err(s"unexpected format while encoding - $data")) + } + ) +} diff --git a/common/src/main/scala/services/teamwork/SquadAction.scala b/common/src/main/scala/services/teamwork/SquadAction.scala index 62e3f2fe..d5da028e 100644 --- a/common/src/main/scala/services/teamwork/SquadAction.scala +++ b/common/src/main/scala/services/teamwork/SquadAction.scala @@ -1,7 +1,6 @@ // Copyright (c) 2019 PSForever package services.teamwork -import net.psforever.objects.Player import net.psforever.objects.zones.Zone import net.psforever.packet.game._ import net.psforever.types.{SquadRequestType, Vector3} @@ -9,7 +8,8 @@ import net.psforever.types.{SquadRequestType, Vector3} object SquadAction { trait Action - final case class Definition(player : Player, zone : Zone, guid : PlanetSideGUID, line : Int, action : SquadAction) extends Action + final case class Definition(zone : Zone, guid : PlanetSideGUID, line : Int, action : SquadAction) extends Action final case class Membership(request_type : SquadRequestType.Value, unk2 : Long, unk3 : Option[Long], player_name : String, unk5 : Option[Option[String]]) extends Action + final case class Waypoint(event_type : WaypointEventAction.Value, waypoint_type : Int, unk : Option[Long], waypoint_info : Option[WaypointInfo]) extends Action final case class Update(char_id : Long, health : Int, max_health : Int, armor : Int, max_armor : Int, pos : Vector3, zone_number : Int) extends Action } diff --git a/common/src/main/scala/services/teamwork/SquadResponse.scala b/common/src/main/scala/services/teamwork/SquadResponse.scala index fa06d866..817d27e5 100644 --- a/common/src/main/scala/services/teamwork/SquadResponse.scala +++ b/common/src/main/scala/services/teamwork/SquadResponse.scala @@ -29,5 +29,8 @@ object SquadResponse { final case class Detail(guid : PlanetSideGUID, squad_detail : SquadDetail) extends Response + final case class InitWaypoints(char_id : Long, waypoints : Iterable[(Int, WaypointInfo, Int)]) extends Response + final case class WaypointEvent(event_type : WaypointEventAction.Value, char_id : Long, waypoint_type : Int, unk5 : Option[Long], waypoint_info : Option[WaypointInfo], unk : Int) 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 17569777..5a439432 100644 --- a/common/src/main/scala/services/teamwork/SquadService.scala +++ b/common/src/main/scala/services/teamwork/SquadService.scala @@ -6,7 +6,6 @@ import net.psforever.objects.Player import net.psforever.objects.definition.converter.StatConverter import net.psforever.objects.loadouts.SquadLoadout import net.psforever.objects.teamwork.{Member, Squad} -import net.psforever.objects.zones.Zone import net.psforever.packet.game._ import net.psforever.types._ import services.{GenericEventBus, Service} @@ -44,6 +43,8 @@ class SquadService extends Actor { */ private val initialAssociation : ListBuffer[PlanetSideGUID] = new ListBuffer[PlanetSideGUID]() private val queuedInvites : mutable.LongMap[List[Invitation]] = mutable.LongMap[List[Invitation]]() + private val waypoints : TrieMap[PlanetSideGUID, Array[WaypointData]] = + new TrieMap[PlanetSideGUID, Array[WaypointData]]() private val viewDetails : mutable.LongMap[PlanetSideGUID] = mutable.LongMap[PlanetSideGUID]() private [this] val log = org.log4s.getLogger @@ -122,7 +123,7 @@ class SquadService extends Actor { } } - def GetLeadingSquad(player : Player, zone : Int, opt : Option[Squad]) : Option[Squad] = { + def GetLeadingSquad(player : Player, opt : Option[Squad]) : Option[Squad] = { val charId = player.CharId opt match { case Some(squad) => @@ -350,80 +351,31 @@ class SquadService extends Actor { RemoveQueuedInvites(invitedPlayer) //TODO deal with these somehow case _ => ; } -// if(idToSquad.get(guid).isEmpty) { -// log.warn("Accept->Invite: the squad no longer exists") -// } -// else if(memberToSquad.get(invitedPlayer).nonEmpty) { -// log.warn("Accept->Invite: player is already a member of a squad and can not join a second one") -// } -// else { -// val squad = idToSquad(guid) -// if(!squad.AutoApproveInvitationRequests && squad.Leader.CharId != invitingPlayer) { -// //the inviting player was not the squad leader and this decision should be bounced off the squad leader -// val bid = IndirectVacancy(tplayer, guid) -// AddInvite(squad.Leader.CharId, bid) match { -// case out @ Some(_) if out.contains(bid) => -// HandleBidForPosition(bid, tplayer) -// case _ => ; -// } -// } -// else { -// //if a suitable position in the squad can be found, player may occupy it -// squad.Membership.zipWithIndex.find({ case (member, index) => -// ValidOpenSquadPosition(squad, index, member, tplayer.Certifications) -// }) match { -// case Some((_, line)) => -// 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 _ => ; -// } -// } -// } case Some(SpontaneousInvite(invitingPlayer)) => - //we were invited by someone into a new squad they would form + //originally, 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 _ => ; - } - None - } - case None => + //invitingPlayer became part of a squad while invited player was answering the original summons + Some(participating) + case _ => + //generate a new squad, with invitingPlayer as the leader val squad = StartSquad(invitingPlayer) - squad.Task = s"${tplayer.Name}'s Squad" - SquadEvents.publish(SquadServiceResponse(s"/$invitingPlayer/Squad", SquadResponse.AssociateWithSquad(squad.GUID))) + squad.Task = s"${invitingPlayer.Name}'s Squad" + SquadEvents.publish( SquadServiceResponse(s"/$invitingPlayerCharId/Squad", SquadResponse.AssociateWithSquad(squad.GUID)) ) Some(squad) }) match { case Some(squad) => - squad.Membership.zipWithIndex.find({ case (member, index) => - ValidOpenSquadPosition(squad, index, member, tplayer.Certifications) - }) match { + HandleVacancyInvite(squad.GUID, tplayer.CharId, invitingPlayerCharId, tplayer) match { case Some((_, line)) => 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 + RemoveQueuedInvites(tplayer.CharId) //TODO deal with these somehow case _ => ; } - case None => ; + case _ => ; } case None => @@ -566,32 +518,46 @@ class SquadService extends Actor { case _ => ; } - case SquadAction.Update(char_id, health, max_health, armor, max_armor, pos, zone_number) => - memberToSquad.get(char_id) match { + case SquadAction.Waypoint(_, wtype, _, info) => + val playerCharId = tplayer.CharId + (GetLeadingSquad(tplayer, None) match { case Some(squad) => - squad.Membership.find(_.CharId == char_id) match { - case Some(member) => - member.Health = StatConverter.Health(health, max_health, min=1, max=64) - member.Armor = StatConverter.Health(armor, max_armor, min=1, max=64) - member.Position = pos - member.ZoneId = zone_number - sender ! SquadServiceResponse("", SquadResponse.UpdateMembers( - squad, - squad.Membership - .filterNot { _.CharId == 0 } - .map { member => SquadAction.Update(member.CharId, member.Health, 0, member.Armor, 0, member.Position, member.ZoneId) } - .toList - )) - case _ => ; + info match { + case Some(winfo) => + (Some(squad), AddWaypoint(squad.GUID, wtype, winfo)) + case _ => + RemoveWaypoint(squad.GUID, wtype) + (Some(squad), None) } - - case None => ; + case _ => (None, None) + }) match { + case (Some(squad), Some(waypoint)) => + //waypoint added or updated + squad.Membership + .filterNot { member => member.CharId == tplayer.CharId } + .foreach { member => + val charId = member.CharId + SquadEvents.publish( + SquadServiceResponse(s"/$charId/Squad", SquadResponse.WaypointEvent(WaypointEventAction.Add, playerCharId, wtype, None, info, 1)) + ) + } + case (Some(squad), None) => + //waypoint removed? + squad.Membership + .filterNot { member => member.CharId == tplayer.CharId } + .foreach { member => + val charId = member.CharId + SquadEvents.publish( + SquadServiceResponse(s"/$charId/Squad", SquadResponse.WaypointEvent(WaypointEventAction.Remove, playerCharId, wtype, None, None, 0)) + ) + } + case _ => ; } - case SquadAction.Definition(tplayer : Player, zone : Zone, guid : PlanetSideGUID, line : Int, action : SquadAction) => + case SquadAction.Definition(zone, guid, line, action) => import net.psforever.packet.game.SquadAction._ val pSquadOpt = GetParticipatingSquad(tplayer) - val lSquadOpt = GetLeadingSquad(tplayer, zone.Number, pSquadOpt) + val lSquadOpt = GetLeadingSquad(tplayer, pSquadOpt) //the following actions can only be performed by a squad's leader action match { case SaveSquadFavorite() => @@ -624,17 +590,17 @@ class SquadService extends Actor { UpdateSquadListWhenListed(squad, SquadInfo().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") + case ChangeSquadZone(zone_id) => + log.info(s"${tplayer.Name}-${tplayer.Faction} has changed squad's ops zone to $zone_id") val squad = lSquadOpt.getOrElse(StartSquad(tplayer)) - squad.ZoneId = zone.zoneId.toInt - UpdateSquadListWhenListed(squad, SquadInfo().ZoneId(zone)) + squad.ZoneId = zone_id.zoneId.toInt + UpdateSquadListWhenListed(squad, SquadInfo().ZoneId(zone_id)) InitialAssociation(squad) sender ! SquadServiceResponse("", SquadResponse.Detail( squad.GUID, SquadService.Detail.Publish(squad)) ) - UpdateSquadDetail(squad.GUID, squad.Membership.map { _m => _m.CharId }.filterNot { _ == squad.Leader.CharId }, SquadDetail().ZoneId(zone)) + UpdateSquadDetail(squad.GUID, squad.Membership.map { _m => _m.CharId }.filterNot { _ == squad.Leader.CharId }, SquadDetail().ZoneId(zone_id)) case CloseSquadMemberPosition(position) => val squad = lSquadOpt.getOrElse(StartSquad(tplayer)) @@ -906,6 +872,12 @@ class SquadService extends Actor { case _ => ; } + //the following action can be peprformed by anyone + case (_, SearchForSquadsWithParticularRole(role, requirements, zone_id, search_mode)) => + //though we should be able correctly search squads as is intended + //I don't know how search results should be prioritized or even how to return search results to the user + sender ! SquadServiceResponse("", SquadResponse.SquadSearchResults()) + //the following action can be performed by anyone case (_, DisplaySquad()) => idToSquad.get(guid) match { @@ -926,6 +898,28 @@ class SquadService extends Actor { case _ => ; } + case SquadAction.Update(char_id, health, max_health, armor, max_armor, pos, zone_number) => + memberToSquad.get(char_id) match { + case Some(squad) => + squad.Membership.find(_.CharId == char_id) match { + case Some(member) => + member.Health = StatConverter.Health(health, max_health, min=1, max=64) + member.Armor = StatConverter.Health(armor, max_armor, min=1, max=64) + member.Position = pos + member.ZoneId = zone_number + sender ! SquadServiceResponse("", SquadResponse.UpdateMembers( + squad, + squad.Membership + .filterNot { _.CharId == 0 } + .map { member => SquadAction.Update(member.CharId, member.Health, 0, member.Armor, 0, member.Position, member.ZoneId) } + .toList + )) + case _ => ; + } + + case None => ; + } + case msg => log.info(s"Unhandled message $msg from $sender") } @@ -1146,6 +1140,7 @@ class SquadService extends Actor { .filterNot { _.CharId == 0 } .foreach { member => SquadEvents.publish(SquadServiceResponse(s"/${member.CharId}/Squad", SquadResponse.Join(squad, indices))) + InitWaypoints(member.CharId, squad.GUID) } //fully update for all users UpdateSquadDetail(squad.GUID, squad) @@ -1156,6 +1151,7 @@ class SquadService extends Actor { val indices = squad.Membership.zipWithIndex.collect({ case (member, index) if member.CharId != 0 => index }).toList SquadEvents.publish(SquadServiceResponse(s"/$charId/Squad", SquadResponse.Join(squad, indices))) InitSquadDetail(squad.GUID, Seq(charId), squad) + InitWaypoints(charId, squad.GUID) //other squad members see new member joining the squad val updatedIndex = List(line) val otherMembers = squad.Membership.filterNot { member => member.CharId == 0 || member.CharId == charId }.map{ _.CharId } @@ -1163,7 +1159,7 @@ class SquadService extends Actor { SquadEvents.publish(SquadServiceResponse(s"/$member/Squad", SquadResponse.Join(squad, updatedIndex))) } val details = SquadDetail().Members(List(SquadPositionEntry(line, SquadPositionDetail().CharId(charId).Name(player.Name)))) - UpdateSquadDetail(squad.GUID, Seq(charId), details) + UpdateSquadDetail(squad.GUID, otherMembers, details) } UpdateSquadListWhenListed(squad, SquadInfo().Size(size)) true @@ -1207,7 +1203,6 @@ class SquadService extends Actor { } def CloseOutSquad(squad : Squad, membership : Iterable[(Member, Int)], updateList : List[(Long, Int)]) : Unit = { - val leaderCharId = squad.Leader.CharId membership.foreach { case (member, _) => val charId = member.CharId @@ -1217,6 +1212,7 @@ class SquadService extends Actor { SquadEvents.publish( SquadServiceResponse(s"/$charId/Squad", SquadResponse.Leave(squad, updateList)) ) } idToSquad.remove(squad.GUID) + waypoints.remove(squad.GUID) UpdateSquadList(squad, None) } @@ -1332,9 +1328,70 @@ class SquadService extends Actor { toMembers.foreach { charId => SquadEvents.publish(SquadServiceResponse(s"/$charId/Squad", output)) } } } + + def AddWaypoint(guid : PlanetSideGUID, waypointType : Int, info : WaypointInfo) : Option[WaypointData] = { + (waypoints.get(guid) match { + case Some(array) => + array + case None if idToSquad.get(guid).nonEmpty => + log.debug(s"initializing squad waypoint system for squad #${guid.guid}") + val array = Array.fill[WaypointData](5)(new WaypointData()) + waypoints(guid) = array + array + case _ => + log.warn(s"squad #${guid.guid} does not currently exist so it can not render waypoints") + Array.empty[WaypointData] + }).lift(waypointType) match { + case Some(point) => + //update the waypoint + log.debug(s"rendering squad waypoint $waypointType for squad #${guid.guid}") + point.zone_number = info.zone_number + point.pos = info.pos + Some(point) + case _ => + log.warn(s"no squad waypoint $waypointType found") + None + } + } + + def RemoveWaypoint(guid : PlanetSideGUID, waypointType : Int) : Unit = { + (waypoints.get(guid) match { + case Some(array) => + array + case None => + Array.empty[WaypointData] + }).lift(waypointType) match { + case Some(point) => + //update the waypoint + log.debug(s"removing squad waypoint $waypointType for squad #${guid.guid}") + point.zone_number = 1 + point.pos = Vector3.z(1) + case _ => ; + } + } + + def InitWaypoints(toCharId : Long, guid : PlanetSideGUID) : Unit = { + (idToSquad.get(guid), waypoints.get(guid)) match { + case (Some(squad), Some(list)) => + val vz1 = Vector3.z(1) + SquadEvents.publish( + SquadServiceResponse(s"/$toCharId/Squad", SquadResponse.InitWaypoints(squad.Leader.CharId, + list.zipWithIndex.collect { case (point, index) if point.pos != vz1 => + (index, WaypointInfo(point.zone_number, point.pos), 1) + } + )) + ) + case _ => ; + } + } } object SquadService { + class WaypointData() { + var zone_number : Int = 1 + var pos : Vector3 = Vector3.z(1) //a waypoint with a non-zero z-coordinate will flag as not getting drawn + } + abstract class Invitation(char_id : Long, name : String) { def InviterCharId : Long = char_id def InviterName : String = name diff --git a/common/src/test/scala/game/SquadWaypointEventTest.scala b/common/src/test/scala/game/SquadWaypointEventTest.scala index 3c2b8bb8..5903a997 100644 --- a/common/src/test/scala/game/SquadWaypointEventTest.scala +++ b/common/src/test/scala/game/SquadWaypointEventTest.scala @@ -3,7 +3,7 @@ package game import org.specs2.mutable._ import net.psforever.packet._ -import net.psforever.packet.game.{SquadWaypointEvent, WaypointEvent} +import net.psforever.packet.game.{SquadWaypointEvent, WaypointEventAction, WaypointEvent} import net.psforever.types.Vector3 import scodec.bits._ @@ -16,12 +16,12 @@ class SquadWaypointEventTest extends Specification { "decode (1)" in { PacketCoding.DecodePacket(string_1).require match { case SquadWaypointEvent(unk1, unk2, unk3, unk4, unk5, unk6) => - unk1 mustEqual 2 + unk1 mustEqual WaypointEventAction.Remove unk2 mustEqual 11 unk3 mustEqual 31155863L unk4 mustEqual 0 - unk5 mustEqual None - unk6 mustEqual None + unk5.isEmpty mustEqual true + unk6.isEmpty mustEqual true case _ => ko } @@ -30,12 +30,12 @@ class SquadWaypointEventTest extends Specification { "decode (2)" in { PacketCoding.DecodePacket(string_2).require match { case SquadWaypointEvent(unk1, unk2, unk3, unk4, unk5, unk6) => - unk1 mustEqual 2 + unk1 mustEqual WaypointEventAction.Remove unk2 mustEqual 10 unk3 mustEqual 0L unk4 mustEqual 4 - unk5 mustEqual None - unk6 mustEqual None + unk5.isEmpty mustEqual true + unk6.isEmpty mustEqual true case _ => ko } @@ -44,12 +44,12 @@ class SquadWaypointEventTest extends Specification { "decode (3)" in { PacketCoding.DecodePacket(string_3).require match { case SquadWaypointEvent(unk1, unk2, unk3, unk4, unk5, unk6) => - unk1 mustEqual 0 + unk1 mustEqual WaypointEventAction.Add unk2 mustEqual 3 unk3 mustEqual 41581052L unk4 mustEqual 1 - unk5 mustEqual None - unk6 mustEqual Some(WaypointEvent(10, Vector3(3457.9688f, 5514.4688f, 0.0f), 1)) + unk5.isEmpty mustEqual true + unk6.contains( WaypointEvent(10, Vector3(3457.9688f, 5514.4688f, 0.0f), 1) ) mustEqual true case _ => ko } @@ -58,40 +58,40 @@ class SquadWaypointEventTest extends Specification { "decode (4)" in { PacketCoding.DecodePacket(string_4).require match { case SquadWaypointEvent(unk1, unk2, unk3, unk4, unk5, unk6) => - unk1 mustEqual 1 + unk1 mustEqual WaypointEventAction.Unknown1 unk2 mustEqual 3 unk3 mustEqual 41581052L unk4 mustEqual 1 - unk5 mustEqual Some(4L) - unk6 mustEqual None + unk5.contains( 4L ) mustEqual true + unk6.isEmpty mustEqual true case _ => ko } } "encode (1)" in { - val msg = SquadWaypointEvent(2, 11, 31155863L, 0) + val msg = SquadWaypointEvent.Remove(11, 31155863L, 0) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector pkt mustEqual string_1 } "encode (2)" in { - val msg = SquadWaypointEvent(2, 10, 0L, 4) + val msg = SquadWaypointEvent.Remove(10, 0L, 4) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector pkt mustEqual string_2 } "encode (3)" in { - val msg = SquadWaypointEvent(0, 3, 41581052L, 1, 10, Vector3(3457.9688f, 5514.4688f, 0.0f), 1) + val msg = SquadWaypointEvent.Add(3, 41581052L, 1, WaypointEvent(10, Vector3(3457.9688f, 5514.4688f, 0.0f), 1)) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector pkt mustEqual string_3 } "encode (4)" in { - val msg = SquadWaypointEvent(1, 3, 41581052L, 1, 4L) + val msg = SquadWaypointEvent.Unknown1(3, 41581052L, 1, 4L) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector pkt mustEqual string_4 diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index 8eadcead..bde61079 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -516,15 +516,32 @@ class WorldSessionActor extends Actor with MDCContextAware { 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 + //promotion will swap cards visually, 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() => + //I don't actually know how to return search results sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.NoSquadSearchResults())) + case SquadResponse.InitWaypoints(char_id, waypoints) => + val id = 11 + StartBundlingPackets() + waypoints.foreach { case (waypoint_type, info, unk) => + sendResponse(SquadWaypointEvent.Add(id, char_id, waypoint_type, WaypointEvent(info.zone_number, info.pos, unk))) + } + StopBundlingPackets() + + case SquadResponse.WaypointEvent(WaypointEventAction.Add, char_id, waypoint_type, _, Some(info), unk) => + val id = 11 + sendResponse(SquadWaypointEvent.Add(id, char_id, waypoint_type, WaypointEvent(info.zone_number, info.pos, unk))) + + case SquadResponse.WaypointEvent(WaypointEventAction.Remove, char_id, waypoint_type, _, _, _) => + val id = 11 + sendResponse(SquadWaypointEvent.Remove(id, char_id, waypoint_type)) + case _ => ; } @@ -4960,12 +4977,16 @@ class WorldSessionActor extends Actor with MDCContextAware { case msg @ SquadDefinitionActionMessage(u1, u2, action) => log.info(s"SquadDefinitionAction: $msg") - squadService ! SquadServiceMessage(player, SquadServiceAction.Definition(player, continent, u1, u2, action)) + squadService ! SquadServiceMessage(player, SquadServiceAction.Definition(continent, u1, u2, action)) case msg @ SquadMembershipRequest(request_type, unk2, unk3, player_name, unk5) => log.info(s"$msg") squadService ! SquadServiceMessage(player, SquadServiceAction.Membership(request_type, unk2, unk3, player_name, unk5)) + case msg @ SquadWaypointRequest(request, _, wtype, unk, info) => + log.info(s"Waypoint Request: $msg") + squadService ! SquadServiceMessage(player, SquadServiceAction.Waypoint(request, wtype, unk, info)) + case msg @ GenericCollisionMsg(u1, p, t, php, thp, pv, tv, ppos, tpos, u2, u3, u4) => log.info("Ouch! " + msg)