From 014e0e88edecf4c564508946cc29fb1c13a42b7f Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Tue, 17 Sep 2024 02:30:58 -0400 Subject: [PATCH] completely retooled login messages system to support a series of tasks queued up for when the ui has finished loading and the player has control of their game; attempt to clean up old squad cards during proper log-outs and before relog; ability to pass indices to squad invitations for targeted acceptance or rejection --- .../session/normal/AvatarHandlerLogic.scala | 1 + .../actors/session/normal/GeneralLogic.scala | 1 + .../session/normal/SquadHandlerLogic.scala | 124 ++++++++------- .../actors/session/normal/VehicleLogic.scala | 3 + .../spectator/AvatarHandlerLogic.scala | 1 + .../session/spectator/GeneralLogic.scala | 1 + .../session/spectator/SquadHandlerLogic.scala | 102 +++++++----- .../session/spectator/VehicleLogic.scala | 3 + .../session/support/ChatOperations.scala | 39 ++++- .../actors/session/support/SessionData.scala | 1 + .../support/SessionSquadHandlers.scala | 14 ++ .../session/support/ZoningOperations.scala | 148 +++++++++++++++--- .../teamwork/SquadInvitationManager.scala | 1 - 13 files changed, 313 insertions(+), 126 deletions(-) diff --git a/src/main/scala/net/psforever/actors/session/normal/AvatarHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/normal/AvatarHandlerLogic.scala index 890cd27be..951fee255 100644 --- a/src/main/scala/net/psforever/actors/session/normal/AvatarHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/AvatarHandlerLogic.scala @@ -491,6 +491,7 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A sessionLogic.general.renewCharSavedTimer(fixedLen = 1800L, varLen = 0L) //player state changes + sessionLogic.zoning.spawn.avatarActive = false AvatarActor.updateToolDischargeFor(avatar) player.FreeHand.Equipment.foreach { item => DropEquipmentFromInventory(player)(item) diff --git a/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala b/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala index ab40f6222..a5eca4bf4 100644 --- a/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala @@ -105,6 +105,7 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex _, _ )= pkt + sessionLogic.zoning.spawn.tryQueuedActivity(vel) sessionLogic.persist() sessionLogic.turnCounterFunc(avatarGuid) sessionLogic.updateBlockMap(player, pos) diff --git a/src/main/scala/net/psforever/actors/session/normal/SquadHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/normal/SquadHandlerLogic.scala index b95f5e129..7039525b0 100644 --- a/src/main/scala/net/psforever/actors/session/normal/SquadHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/SquadHandlerLogic.scala @@ -2,20 +2,24 @@ package net.psforever.actors.session.normal import akka.actor.{ActorContext, ActorRef, typed} -import net.psforever.actors.session.support.SessionSquadHandlers.SquadUIElement import net.psforever.actors.session.AvatarActor -import net.psforever.actors.session.support.{SessionData, SessionSquadHandlers, SquadHandlerFunctions} +import net.psforever.actors.session.support.SessionSquadHandlers.{rethrowSquadServiceResponse, SquadUIElement} +import net.psforever.actors.session.support.{SessionData, SessionSquadHandlers, SpawnOperations, SquadHandlerFunctions} import net.psforever.objects.{Default, LivePlayerList} import net.psforever.objects.avatar.Avatar import net.psforever.packet.game.{CharacterKnowledgeInfo, CharacterKnowledgeMessage, ChatMsg, MemberEvent, PlanetsideAttributeMessage, ReplicationStreamMessage, SquadAction, SquadDefinitionActionMessage, SquadDetailDefinitionUpdateMessage, SquadListing, SquadMemberEvent, SquadMembershipRequest, SquadMembershipResponse, SquadState, SquadStateInfo, SquadWaypointEvent, SquadWaypointRequest, WaypointEvent, WaypointEventAction} import net.psforever.services.chat.SquadChannel -import net.psforever.services.teamwork.{SquadResponse, SquadServiceMessage, SquadAction => SquadServiceAction} +import net.psforever.services.teamwork.{SquadResponse, SquadServiceMessage, SquadServiceResponse, SquadAction => SquadServiceAction} import net.psforever.types.{ChatMessageType, PlanetSideGUID, SquadListDecoration, SquadResponseType, WaypointSubtype} object SquadHandlerLogic { def apply(ops: SessionSquadHandlers): SquadHandlerLogic = { new SquadHandlerLogic(ops, ops.context) } + + def rethrowSquadServiceResponse(response: SquadResponse.Response)(sessionLogic: SessionData): Unit = { + sessionLogic.context.self ! SquadServiceResponse("", response) + } } class SquadHandlerLogic(val ops: SessionSquadHandlers, implicit val context: ActorContext) extends SquadHandlerFunctions { @@ -61,6 +65,7 @@ class SquadHandlerLogic(val ops: SessionSquadHandlers, implicit val context: Act def handle(response: SquadResponse.Response, excluded: Iterable[Long]): Unit = { if (!excluded.exists(_ == avatar.id)) { response match { + /* these messages will never be queued for later */ case SquadResponse.ListSquadFavorite(line, task) => sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), line, SquadAction.ListSquadFavorite(task))) @@ -108,12 +113,69 @@ class SquadHandlerLogic(val ops: SessionSquadHandlers, implicit val context: Act case SquadResponse.Detail(guid, detail) => sendResponse(SquadDetailDefinitionUpdateMessage(guid, detail)) - case SquadResponse.IdentifyAsSquadLeader(squad_guid) => - sendResponse(SquadDefinitionActionMessage(squad_guid, 0, SquadAction.IdentifyAsSquadLeader())) - case SquadResponse.SetListSquad(squad_guid) => sendResponse(SquadDefinitionActionMessage(squad_guid, 0, SquadAction.SetListSquad())) + case SquadResponse.SquadSearchResults(results) => + //TODO positive squad search results message? + if(results.nonEmpty) { + results.foreach { guid => + sendResponse(SquadDefinitionActionMessage( + guid, + 0, + SquadAction.SquadListDecorator(SquadListDecoration.SearchResult)) + ) + } + } else { + sendResponse(SquadDefinitionActionMessage(player.GUID, 0, SquadAction.NoSquadSearchResults())) + } + sendResponse(SquadDefinitionActionMessage(player.GUID, 0, SquadAction.CancelSquadSearch())) + + case SquadResponse.UpdateMembers(_, positions) => + val pairedEntries = positions.collect { + case entry if ops.squadUI.contains(entry.char_id) => + (entry, ops.squadUI(entry.char_id)) + } + //prune entries + val updatedEntries = pairedEntries + .collect({ + case (entry, element) if entry.zone_number != element.zone => + //zone gets updated for these entries + sendResponse( + SquadMemberEvent.UpdateZone(ops.squad_supplement_id, entry.char_id, element.index, entry.zone_number) + ) + ops.squadUI(entry.char_id) = + SquadUIElement(element.name, element.outfit, 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 => + //other elements that need to be updated + ops.squadUI(entry.char_id) = + SquadUIElement(element.name, element.outfit, element.index, entry.zone_number, entry.health, entry.armor, entry.pos) + entry + }) + .filterNot(_.char_id == avatar.id) //we want to update our backend, but not our frontend + if (updatedEntries.nonEmpty) { + sendResponse( + SquadState( + PlanetSideGUID(ops.squad_supplement_id), + updatedEntries.map { entry => + SquadStateInfo(entry.char_id, entry.health, entry.armor, entry.pos) + } + ) + ) + } + + /* queue below messages for later if the initial conditions are inappropriate */ + case msg if !sessionLogic.zoning.spawn.startEnqueueSquadMessages => + sessionLogic.zoning.spawn.enqueueNewActivity( + SpawnOperations.ActivityQueuedTask(rethrowSquadServiceResponse(msg), 1) + ) + + /* these messages will be queued for later if initial conditions are inappropriate */ + case SquadResponse.IdentifyAsSquadLeader(squad_guid) => + sendResponse(SquadDefinitionActionMessage(squad_guid, 0, SquadAction.IdentifyAsSquadLeader())) + case SquadResponse.Membership(request_type, unk1, unk2, charId, opt_char_id, player_name, unk5, unk6) => val name = request_type match { case SquadResponseType.Invite if unk5 => @@ -272,59 +334,9 @@ class SquadHandlerLogic(val ops: SessionSquadHandlers, implicit val context: Act //the players have already been swapped in the backend object ops.PromoteSquadUIElements(squad, from_index) - case SquadResponse.UpdateMembers(_, positions) => - val pairedEntries = positions.collect { - case entry if ops.squadUI.contains(entry.char_id) => - (entry, ops.squadUI(entry.char_id)) - } - //prune entries - val updatedEntries = pairedEntries - .collect({ - case (entry, element) if entry.zone_number != element.zone => - //zone gets updated for these entries - sendResponse( - SquadMemberEvent.UpdateZone(ops.squad_supplement_id, entry.char_id, element.index, entry.zone_number) - ) - ops.squadUI(entry.char_id) = - SquadUIElement(element.name, element.outfit, 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 => - //other elements that need to be updated - ops.squadUI(entry.char_id) = - SquadUIElement(element.name, element.outfit, element.index, entry.zone_number, entry.health, entry.armor, entry.pos) - entry - }) - .filterNot(_.char_id == avatar.id) //we want to update our backend, but not our frontend - if (updatedEntries.nonEmpty) { - sendResponse( - SquadState( - PlanetSideGUID(ops.squad_supplement_id), - updatedEntries.map { entry => - SquadStateInfo(entry.char_id, entry.health, entry.armor, entry.pos) - } - ) - ) - } - case SquadResponse.CharacterKnowledge(charId, name, certs, u1, u2, zone) => sendResponse(CharacterKnowledgeMessage(charId, Some(CharacterKnowledgeInfo(name, certs, u1, u2, zone)))) - case SquadResponse.SquadSearchResults(_/*results*/) => - //TODO positive squad search results message? - // if(results.nonEmpty) { - // results.foreach { guid => - // sendResponse(SquadDefinitionActionMessage( - // guid, - // 0, - // SquadAction.SquadListDecorator(SquadListDecoration.SearchResult)) - // ) - // } - // } else { - // sendResponse(SquadDefinitionActionMessage(player.GUID, 0, SquadAction.NoSquadSearchResults())) - // } - // sendResponse(SquadDefinitionActionMessage(player.GUID, 0, SquadAction.CancelSquadSearch())) - case SquadResponse.InitWaypoints(char_id, waypoints) => waypoints.foreach { case (waypoint_type, info, unk) => diff --git a/src/main/scala/net/psforever/actors/session/normal/VehicleLogic.scala b/src/main/scala/net/psforever/actors/session/normal/VehicleLogic.scala index 5b8ab62d1..eaae6bf58 100644 --- a/src/main/scala/net/psforever/actors/session/normal/VehicleLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/VehicleLogic.scala @@ -44,6 +44,7 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex ops.GetVehicleAndSeat() match { case (Some(obj), Some(0)) => //we're driving the vehicle + sessionLogic.zoning.spawn.tryQueuedActivity(vel) sessionLogic.persist() sessionLogic.turnCounterFunc(player.GUID) sessionLogic.general.fallHeightTracker(pos.z) @@ -128,6 +129,7 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex ops.GetVehicleAndSeat() match { case (Some(obj), Some(0)) => //we're driving the vehicle + sessionLogic.zoning.spawn.tryQueuedActivity(vel) sessionLogic.persist() sessionLogic.turnCounterFunc(player.GUID) val (position, angle, velocity, notMountedState) = continent.GUID(obj.MountedIn) match { @@ -216,6 +218,7 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex }) match { case (None, None) | (_, None) | (Some(_: Vehicle), Some(0)) => () case _ => + sessionLogic.zoning.spawn.tryQueuedActivity() //todo conditionals? sessionLogic.persist() sessionLogic.turnCounterFunc(player.GUID) } diff --git a/src/main/scala/net/psforever/actors/session/spectator/AvatarHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/spectator/AvatarHandlerLogic.scala index e6ef12b6d..debc41644 100644 --- a/src/main/scala/net/psforever/actors/session/spectator/AvatarHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/spectator/AvatarHandlerLogic.scala @@ -455,6 +455,7 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A sessionLogic.general.renewCharSavedTimer(fixedLen = 1800L, varLen = 0L) //player state changes + sessionLogic.zoning.spawn.avatarActive = false AvatarActor.updateToolDischargeFor(avatar) player.FreeHand.Equipment.foreach { item => DropEquipmentFromInventory(player)(item) diff --git a/src/main/scala/net/psforever/actors/session/spectator/GeneralLogic.scala b/src/main/scala/net/psforever/actors/session/spectator/GeneralLogic.scala index 806747a5a..01e1ebdb9 100644 --- a/src/main/scala/net/psforever/actors/session/spectator/GeneralLogic.scala +++ b/src/main/scala/net/psforever/actors/session/spectator/GeneralLogic.scala @@ -61,6 +61,7 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex _, _ )= pkt + sessionLogic.zoning.spawn.tryQueuedActivity(vel) sessionLogic.persist() sessionLogic.turnCounterFunc(avatarGuid) ops.fallHeightTracker(pos.z) diff --git a/src/main/scala/net/psforever/actors/session/spectator/SquadHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/spectator/SquadHandlerLogic.scala index fae3a6817..9db5f6d45 100644 --- a/src/main/scala/net/psforever/actors/session/spectator/SquadHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/spectator/SquadHandlerLogic.scala @@ -2,15 +2,15 @@ package net.psforever.actors.session.spectator import akka.actor.{ActorContext, typed} -import net.psforever.actors.session.support.SessionSquadHandlers.SquadUIElement import net.psforever.actors.session.AvatarActor -import net.psforever.actors.session.support.{SessionData, SessionSquadHandlers, SquadHandlerFunctions} +import net.psforever.actors.session.support.SessionSquadHandlers.{rethrowSquadServiceResponse, SquadUIElement} +import net.psforever.actors.session.support.{SessionData, SessionSquadHandlers, SpawnOperations, SquadHandlerFunctions} import net.psforever.objects.{Default, LivePlayerList} import net.psforever.objects.avatar.Avatar -import net.psforever.packet.game.{CharacterKnowledgeInfo, CharacterKnowledgeMessage, PlanetsideAttributeMessage, ReplicationStreamMessage, SquadAction, SquadDefinitionActionMessage, SquadDetailDefinitionUpdateMessage, SquadListing, SquadMemberEvent, SquadMembershipRequest, SquadMembershipResponse, SquadState, SquadStateInfo, SquadWaypointEvent, SquadWaypointRequest, WaypointEventAction} +import net.psforever.packet.game.{CharacterKnowledgeInfo, CharacterKnowledgeMessage, ChatMsg, PlanetsideAttributeMessage, ReplicationStreamMessage, SquadAction, SquadDefinitionActionMessage, SquadDetailDefinitionUpdateMessage, SquadListing, SquadMemberEvent, SquadMembershipRequest, SquadMembershipResponse, SquadState, SquadStateInfo, SquadWaypointEvent, SquadWaypointRequest, WaypointEventAction} import net.psforever.services.chat.SquadChannel import net.psforever.services.teamwork.SquadResponse -import net.psforever.types.{PlanetSideGUID, SquadListDecoration, SquadResponseType} +import net.psforever.types.{ChatMessageType, PlanetSideGUID, SquadListDecoration, SquadResponseType} object SquadHandlerLogic { def apply(ops: SessionSquadHandlers): SquadHandlerLogic = { @@ -77,6 +77,62 @@ class SquadHandlerLogic(val ops: SessionSquadHandlers, implicit val context: Act } sendResponse(SquadDefinitionActionMessage(guid, 0, SquadAction.SquadListDecorator(decoration))) + case SquadResponse.SquadSearchResults(results) => + //TODO positive squad search results message? + if(results.nonEmpty) { + results.foreach { guid => + sendResponse(SquadDefinitionActionMessage( + guid, + 0, + SquadAction.SquadListDecorator(SquadListDecoration.SearchResult)) + ) + } + } else { + sendResponse(SquadDefinitionActionMessage(player.GUID, 0, SquadAction.NoSquadSearchResults())) + } + sendResponse(SquadDefinitionActionMessage(player.GUID, 0, SquadAction.CancelSquadSearch())) + + case SquadResponse.UpdateMembers(_, positions) => + val pairedEntries = positions.collect { + case entry if ops.squadUI.contains(entry.char_id) => + (entry, ops.squadUI(entry.char_id)) + } + //prune entries + val updatedEntries = pairedEntries + .collect({ + case (entry, element) if entry.zone_number != element.zone => + //zone gets updated for these entries + sendResponse( + SquadMemberEvent.UpdateZone(ops.squad_supplement_id, entry.char_id, element.index, entry.zone_number) + ) + ops.squadUI(entry.char_id) = + SquadUIElement(element.name, element.outfit, 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 => + //other elements that need to be updated + ops.squadUI(entry.char_id) = + SquadUIElement(element.name, element.outfit, element.index, entry.zone_number, entry.health, entry.armor, entry.pos) + entry + }) + .filterNot(_.char_id == avatar.id) //we want to update our backend, but not our frontend + if (updatedEntries.nonEmpty) { + sendResponse( + SquadState( + PlanetSideGUID(ops.squad_supplement_id), + updatedEntries.map { entry => + SquadStateInfo(entry.char_id, entry.health, entry.armor, entry.pos) + } + ) + ) + } + + /* queue below messages for later if the initial conditions are inappropriate */ + case msg if !sessionLogic.zoning.spawn.startEnqueueSquadMessages => + sessionLogic.zoning.spawn.enqueueNewActivity( + SpawnOperations.ActivityQueuedTask(rethrowSquadServiceResponse(msg), 1) + ) + case SquadResponse.Detail(guid, detail) => sendResponse(SquadDetailDefinitionUpdateMessage(guid, detail)) @@ -132,47 +188,15 @@ class SquadHandlerLogic(val ops: SessionSquadHandlers, implicit val context: Act ) } - case SquadResponse.UpdateMembers(_, positions) => - val pairedEntries = positions.collect { - case entry if ops.squadUI.contains(entry.char_id) => - (entry, ops.squadUI(entry.char_id)) - } - //prune entries - val updatedEntries = pairedEntries - .collect({ - case (entry, element) if entry.zone_number != element.zone => - //zone gets updated for these entries - sendResponse( - SquadMemberEvent.UpdateZone(ops.squad_supplement_id, entry.char_id, element.index, entry.zone_number) - ) - ops.squadUI(entry.char_id) = - SquadUIElement(element.name, element.outfit, 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 => - //other elements that need to be updated - ops.squadUI(entry.char_id) = - SquadUIElement(element.name, element.outfit, element.index, entry.zone_number, entry.health, entry.armor, entry.pos) - entry - }) - .filterNot(_.char_id == avatar.id) //we want to update our backend, but not our frontend - if (updatedEntries.nonEmpty) { - sendResponse( - SquadState( - PlanetSideGUID(ops.squad_supplement_id), - updatedEntries.map { entry => - SquadStateInfo(entry.char_id, entry.health, entry.armor, entry.pos) - } - ) - ) - } - case SquadResponse.CharacterKnowledge(charId, name, certs, u1, u2, zone) => sendResponse(CharacterKnowledgeMessage(charId, Some(CharacterKnowledgeInfo(name, certs, u1, u2, zone)))) case SquadResponse.WaypointEvent(WaypointEventAction.Remove, char_id, waypoint_type, _, _, _) => sendResponse(SquadWaypointEvent.Remove(ops.squad_supplement_id, char_id, waypoint_type)) + case SquadResponse.SquadRelatedComment(comment) => + sendResponse(ChatMsg(ChatMessageType.UNK_227, comment)) + case _ => () } } diff --git a/src/main/scala/net/psforever/actors/session/spectator/VehicleLogic.scala b/src/main/scala/net/psforever/actors/session/spectator/VehicleLogic.scala index 7ba9cca0b..4273c4b48 100644 --- a/src/main/scala/net/psforever/actors/session/spectator/VehicleLogic.scala +++ b/src/main/scala/net/psforever/actors/session/spectator/VehicleLogic.scala @@ -43,6 +43,7 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex ops.GetVehicleAndSeat() match { case (Some(obj), Some(0)) => //we're driving the vehicle + sessionLogic.zoning.spawn.tryQueuedActivity(vel) sessionLogic.persist() sessionLogic.turnCounterFunc(player.GUID) sessionLogic.general.fallHeightTracker(pos.z) @@ -126,6 +127,7 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex ops.GetVehicleAndSeat() match { case (Some(obj), Some(0)) => //we're driving the vehicle + sessionLogic.zoning.spawn.tryQueuedActivity(vel) sessionLogic.persist() sessionLogic.turnCounterFunc(player.GUID) val (position, angle, velocity, notMountedState) = continent.GUID(obj.MountedIn) match { @@ -214,6 +216,7 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex }) match { case (None, None) | (_, None) | (Some(_: Vehicle), Some(0)) => () case _ => + sessionLogic.zoning.spawn.tryQueuedActivity() //todo conditionals? sessionLogic.persist() sessionLogic.turnCounterFunc(player.GUID) } diff --git a/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala b/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala index fe00874aa..c91d04e2a 100644 --- a/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala @@ -2,26 +2,30 @@ package net.psforever.actors.session.support import akka.actor.Cancellable -import net.psforever.objects.LivePlayerList import akka.actor.{ActorRef => ClassicActorRef} import akka.actor.typed.ActorRef import akka.actor.{ActorContext, typed} +import akka.pattern.ask +import akka.util.Timeout import net.psforever.actors.session.{AvatarActor, SessionActor} import net.psforever.actors.session.normal.{NormalMode => SessionNormalMode} import net.psforever.actors.session.spectator.{SpectatorMode => SessionSpectatorMode} import net.psforever.actors.zone.ZoneActor +import net.psforever.objects.LivePlayerList import net.psforever.objects.sourcing.PlayerSource import net.psforever.objects.zones.ZoneInfo import net.psforever.packet.game.SetChatFilterMessage import net.psforever.services.chat.{DefaultChannel, SquadChannel} import net.psforever.services.local.{LocalAction, LocalServiceMessage} -import net.psforever.services.teamwork.SquadService +import net.psforever.services.teamwork.{SquadResponse, SquadService, SquadServiceResponse} import net.psforever.types.ChatMessageType.CMT_QUIT import org.log4s.Logger import scala.annotation.unused import scala.collection.{Seq, mutable} +import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration._ +import scala.util.Success // import net.psforever.actors.zone.BuildingActor import net.psforever.login.WorldSession @@ -74,6 +78,10 @@ class ChatOperations( import akka.actor.typed.scaladsl.adapter._ private val chatServiceAdapter: ActorRef[ChatService.MessageResponse] = context.self.toTyped[ChatService.MessageResponse] + private implicit lazy val timeout: Timeout = Timeout(2.seconds) + + private var invitationList: Array[String] = Array() + def JoinChannel(channel: ChatChannel): Unit = { chatService ! ChatService.JoinChannel(chatServiceAdapter, sessionLogic, channel) channels ++= List(channel) @@ -1260,22 +1268,37 @@ class ChatOperations( def customCommandSquad(params: Seq[String]): Boolean = { params match { case "invites" :: _ => - squadService ! SquadService.ListAllCurrentInvites + invitationList = Array() + ask(squadService, SquadService.ListAllCurrentInvites) + .onComplete { + case Success(msg @ SquadServiceResponse(_, _, SquadResponse.WantsSquadPosition(_, str))) => + invitationList = str.replaceAll("/s","").split(",") + context.self.forward(msg) + case _ => () + } case "accept" :: names if names.contains("all") => squadService ! SquadService.ChainAcceptance(player, player.CharId, Nil) case "accept" :: names if names.nonEmpty => - val results = names.flatMap { name => - LivePlayerList.WorldPopulation { case (_, p) => p.name.equals(name) }.map(_.id.toLong) + //when passing indices to existing invite list, the indices are 1-based + val results = (names.flatMap(_.toIntOption.flatMap(i => invitationList.lift(i-1))) ++ names) + .distinct + .flatMap { name => + LivePlayerList.WorldPopulation { case (_, p) => p.name.equalsIgnoreCase(name) } + .map(_.id.toLong) } squadService ! SquadService.ChainAcceptance(player, player.CharId, results) case "reject" :: names if names.contains("all") => squadService ! SquadService.ChainRejection(player, player.CharId, Nil) case "reject" :: names if names.nonEmpty => - val results = names.flatMap { name => - LivePlayerList.WorldPopulation { case (_, p) => p.name.equals(name) }.map(_.id.toLong) - } + //when passing indices to existing invite list, the indices are 1-based + val results = (names.flatMap(_.toIntOption.flatMap(i => invitationList.lift(i-1))) ++ names) + .distinct + .flatMap { name => + LivePlayerList.WorldPopulation { case (_, p) => p.name.equalsIgnoreCase(name) } + .map(_.id.toLong) + } squadService ! SquadService.ChainRejection(player, player.CharId, results) case _ => () diff --git a/src/main/scala/net/psforever/actors/session/support/SessionData.scala b/src/main/scala/net/psforever/actors/session/support/SessionData.scala index 6041cfa2f..9d10e37cf 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionData.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionData.scala @@ -553,6 +553,7 @@ class SessionData( if (avatar != null) { accountPersistence ! AccountPersistenceService.Logout(avatar.name) } + squad.cleanUpSquadCards() middlewareActor ! MiddlewareActor.Teardown() } diff --git a/src/main/scala/net/psforever/actors/session/support/SessionSquadHandlers.scala b/src/main/scala/net/psforever/actors/session/support/SessionSquadHandlers.scala index 1ccb6bf3f..152233388 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionSquadHandlers.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionSquadHandlers.scala @@ -2,6 +2,8 @@ package net.psforever.actors.session.support import akka.actor.{ActorContext, ActorRef, typed} +import net.psforever.services.teamwork.SquadServiceResponse + import scala.collection.mutable // import net.psforever.actors.session.AvatarActor @@ -34,6 +36,9 @@ object SessionSquadHandlers { armor: Int, position: Vector3 ) + def rethrowSquadServiceResponse(response: SquadResponse.Response)(sessionLogic: SessionData): Unit = { + sessionLogic.context.self ! SquadServiceResponse("", response) + } } class SessionSquadHandlers( @@ -82,6 +87,7 @@ class SessionSquadHandlers( sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(18))) squadService ! SquadServiceMessage(player, continent, SquadServiceAction.InitSquadList()) squadService ! SquadServiceMessage(player, continent, SquadServiceAction.InitCharId()) + cleanUpSquadCards() squadSetup = RespawnSquadSetup } @@ -343,4 +349,12 @@ class SessionSquadHandlers( ) } } + + def cleanUpSquadCards(): Unit = { + squadUI.foreach { case (id, card) => + sendResponse(SquadMemberEvent.Remove(squad_supplement_id, id, card.index)) + } + squadUI.clear() + squad_supplement_id = 0 + } } diff --git a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala index 528f34bf6..b8c395bcb 100644 --- a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala @@ -7,10 +7,12 @@ import akka.actor.{ActorContext, ActorRef, Cancellable, typed} import akka.pattern.ask import akka.util.Timeout import net.psforever.actors.session.spectator.SpectatorMode +import net.psforever.actors.session.support.SpawnOperations.ActivityQueuedTask import net.psforever.login.WorldSession import net.psforever.objects.avatar.{BattleRank, DeployableToolbox} import net.psforever.objects.avatar.scoring.{CampaignStatistics, ScoreCard, SessionStatistics} import net.psforever.objects.definition.converter.OCM +import net.psforever.objects.entity.WorldEntity import net.psforever.objects.inventory.InventoryItem import net.psforever.objects.serverobject.interior.Sidedness import net.psforever.objects.serverobject.mount.Seat @@ -21,7 +23,6 @@ import net.psforever.objects.vital.{InGameHistory, IncarnationActivity, Reconstr import net.psforever.packet.game.{CampaignStatistic, ChangeFireStateMessage_Start, HackState7, MailMessage, ObjectDetectedMessage, SessionStatistic, TriggeredSound} import net.psforever.services.chat.DefaultChannel -import scala.collection.mutable import scala.concurrent.duration._ import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future @@ -83,8 +84,8 @@ object ZoningOperations { private final val zoningCountdownMessages: Seq[Int] = Seq(5, 10, 20) - def reportProgressionSystem(sessionActor: ActorRef): Unit = { - sessionActor ! SessionActor.SendResponse( + def reportProgressionSystem(logic: SessionData): Unit = { + logic.context.self ! SessionActor.SendResponse( MailMessage( "High Command", "Progress versus Promotion", @@ -161,6 +162,14 @@ object ZoningOperations { } } +object SpawnOperations { + final case class ActivityQueuedTask(task: SessionData => Unit, delayBeforeNext: Int, repeat: Int = 0) + + def sendEventMessage(msg: ChatMsg)(sessionLogic: SessionData): Unit = { + sessionLogic.sendResponse(msg) + } +} + class ZoningOperations( val sessionLogic: SessionData, avatarActor: typed.ActorRef[AvatarActor.Command], @@ -642,11 +651,15 @@ class ZoningOperations( zoningType = Zoning.Method.Login response match { case Some((zone, spawnPoint)) => - spawn.loginChatMessage.addOne("@login_reposition_to_friendly_facility") //Your previous location was held by the enemy. You have been moved to the nearest friendly facility. + spawn.enqueueNewActivity(ActivityQueuedTask( + SpawnOperations.sendEventMessage(ChatMsg(ChatMessageType.CMT_QUIT, "@login_reposition_to_friendly_facility")), 20) + ) val (pos, ori) = spawnPoint.SpecificPoint(player) spawn.LoadZonePhysicalSpawnPoint(zone.id, pos, ori, respawnTime = 0 seconds, Some(spawnPoint)) case _ => - spawn.loginChatMessage.addOne("@login_reposition_to_sanctuary") //Your previous location was held by the enemy. As there were no operational friendly facilities on that continent, you have been brought back to your Sanctuary. + spawn.enqueueNewActivity(ActivityQueuedTask( + SpawnOperations.sendEventMessage(ChatMsg(ChatMessageType.CMT_QUIT, "@login_reposition_to_sanctuary")), 20) + ) RequestSanctuaryZoneSpawn(player, player.Zone.Number) } @@ -1839,7 +1852,6 @@ class ZoningOperations( class SpawnOperations() { private[session] var deadState: DeadState.Value = DeadState.Dead - private[session] var loginChatMessage: mutable.ListBuffer[String] = new mutable.ListBuffer[String]() private[session] var amsSpawnPoints: List[SpawnPoint] = Nil private[session] var noSpawnPointHere: Boolean = false private[session] var setupAvatarFunc: () => Unit = AvatarCreate @@ -1862,9 +1874,14 @@ class ZoningOperations( private[session] var drawDeloyableIcon: PlanetSideGameObject with Deployable => Unit = RedrawDeployableIcons private[session] var populateAvatarAwardRibbonsFunc: (Int, Long) => Unit = setupAvatarAwardMessageDelivery private[session] var setAvatar: Boolean = false + private[session] var avatarActive: Boolean = false private[session] var reviveTimer: Cancellable = Default.Cancellable private[session] var respawnTimer: Cancellable = Default.Cancellable + private var queuedActivities: Seq[SpawnOperations.ActivityQueuedTask] = Seq() + private var initialActivityDelay: Int = 4 + private var nextActivityDelay: Int = 0 + private var statisticsPacketFunc: () => Unit = loginAvatarStatisticsFields /* packets */ @@ -1960,16 +1977,19 @@ class ZoningOperations( } val noFriendlyPlayersInZone = friendlyPlayersInZone == 0 if (inZone.map.cavern) { - loginChatMessage.addOne("@reset_sanctuary_locked") - //You have been returned to the sanctuary because the location you logged out is not available. + enqueueNewActivity(ActivityQueuedTask( + SpawnOperations.sendEventMessage(ChatMsg(ChatMessageType.CMT_QUIT, "@reset_sanctuary_locked")), 20) + ) //You have been returned to the sanctuary because the location you logged out is not available. player.Zone = Zone.Nowhere } else if (ourBuildings.isEmpty && (amsSpawnPoints.isEmpty || noFriendlyPlayersInZone)) { - loginChatMessage.addOne("@reset_sanctuary_locked") - //You have been returned to the sanctuary because the location you logged out is not available. + enqueueNewActivity(ActivityQueuedTask( + SpawnOperations.sendEventMessage(ChatMsg(ChatMessageType.CMT_QUIT, "@reset_sanctuary_locked")), 20) + ) //You have been returned to the sanctuary because the location you logged out is not available. player.Zone = Zone.Nowhere } else if (friendlyPlayersInZone > 137 || playersInZone.size > 413) { - loginChatMessage.addOne("@reset_sanctuary_full") - //You have been returned to the sanctuary because the zone you logged out on is full. + enqueueNewActivity(ActivityQueuedTask( + SpawnOperations.sendEventMessage(ChatMsg(ChatMessageType.CMT_QUIT, "@reset_sanctuary_full")), 20) + ) //You have been returned to the sanctuary because the zone you logged out on is full. player.Zone = Zone.Nowhere } else { val inBuildingSOI = buildings.filter { b => @@ -1986,8 +2006,9 @@ class ZoningOperations( } } else { if (noFriendlyPlayersInZone) { - loginChatMessage.addOne("@reset_sanctuary_inactive") - //You have been returned to the sanctuary because the location you logged out is not available. + enqueueNewActivity(ActivityQueuedTask( + SpawnOperations.sendEventMessage(ChatMsg(ChatMessageType.CMT_QUIT, "@reset_sanctuary_inactive")), 20) + ) //You have been returned to the sanctuary because the location you logged out is not available. player.Zone = Zone.Nowhere } } @@ -1995,8 +2016,9 @@ class ZoningOperations( } } else { //player is dead; go back to sanctuary - loginChatMessage.addOne("@reset_sanctuary_inactive") - //You have been returned to the sanctuary because the location you logged out is not available. + enqueueNewActivity(ActivityQueuedTask( + SpawnOperations.sendEventMessage(ChatMsg(ChatMessageType.CMT_QUIT, "@reset_sanctuary_inactive")), 20) + ) //You have been returned to the sanctuary because the location you logged out is not available. player.Zone = Zone.Nowhere } @@ -2094,11 +2116,15 @@ class ZoningOperations( zoningType = Zoning.Method.Login response match { case Some((zone, spawnPoint)) => - loginChatMessage.addOne("@login_reposition_to_friendly_facility") //Your previous location was held by the enemy. You have been moved to the nearest friendly facility. + enqueueNewActivity(ActivityQueuedTask( + SpawnOperations.sendEventMessage(ChatMsg(ChatMessageType.CMT_QUIT, "@login_reposition_to_friendly_facility")), 20) + ) //Your previous location was held by the enemy. You have been moved to the nearest friendly facility. val (pos, ori) = spawnPoint.SpecificPoint(player) LoadZonePhysicalSpawnPoint(zone.id, pos, ori, respawnTime = 0 seconds, Some(spawnPoint)) case _ => - loginChatMessage.addOne("@login_reposition_to_sanctuary") //Your previous location was held by the enemy. As there were no operational friendly facilities on that continent, you have been brought back to your Sanctuary. + enqueueNewActivity(ActivityQueuedTask( + SpawnOperations.sendEventMessage(ChatMsg(ChatMessageType.CMT_QUIT, "@login_reposition_to_sanctuary")), 20) + ) //Your previous location was held by the enemy. As there were no operational friendly facilities on that continent, you have been brought back to your Sanctuary. RequestSanctuaryZoneSpawn(player, player.Zone.Number) } @@ -2855,6 +2881,7 @@ class ZoningOperations( respawnTimer.cancel() reviveTimer.cancel() deadState = DeadState.RespawnTime + avatarActive = false sendResponse( AvatarDeadStateMessage( DeadState.RespawnTime, @@ -3224,7 +3251,7 @@ class ZoningOperations( tavatar.scorecard.CurrentLife.prior.isEmpty && /* no revives */ tplayer.History.size == 1 /* did nothing but come into existence */ ) { - ZoningOperations.reportProgressionSystem(context.self) + enqueueNewActivity(ActivityQueuedTask(ZoningOperations.reportProgressionSystem, 2)) } } } @@ -3485,8 +3512,6 @@ class ZoningOperations( */ def TurnCounterLogin(guid: PlanetSideGUID): Unit = { NormalTurnCounter(guid) - loginChatMessage.foreach { msg => sendResponse(ChatMsg(zoningChatMessageType, wideContents=false, "", msg, None)) } - loginChatMessage.clear() CancelZoningProcess() sessionLogic.turnCounterFunc = NormalTurnCounter } @@ -3697,6 +3722,7 @@ class ZoningOperations( nextSpawnPoint = Some(obj) //set fallback zoningStatus = Zoning.Status.Deconstructing player.allowInteraction = false + avatarActive = false if (player.death_by == 0) { player.death_by = 1 } @@ -3731,7 +3757,7 @@ class ZoningOperations( private def usingSpawnTubeAnimation(): Unit = { getSpawnTubeOwner - .collect { case (sp, owner @ Some(_)) => (sp, owner) } + .collect { case (sp, owner@Some(_)) => (sp, owner) } .collect { case (sp, Some(_: Vehicle)) => ZoningOperations.usingVehicleSpawnTubeAnimation(sp.Zone, sp.WhichSide, sp.Faction, player.Position, sp.Orientation, List(player.Name)) @@ -3739,6 +3765,84 @@ class ZoningOperations( ZoningOperations.usingFacilitySpawnTubeAnimation(sp.Zone, sp.WhichSide, sp.Faction, player.Position, sp.Orientation, List(player.Name)) } } + + def startEnqueueSquadMessages: Boolean = { + sessionLogic.zoning.zoneReload && sessionLogic.zoning.spawn.setAvatar && player.isAlive + } + + def enqueueNewActivity(newTasking: SpawnOperations.ActivityQueuedTask): Unit = { + if (avatarActive && queuedActivities.isEmpty) { + nextActivityDelay = initialActivityDelay + } + queuedActivities = queuedActivities :+ newTasking + } + + def tryQueuedActivity(pos1: Vector3, pos2: Vector3, distanceSquared: Float = 1f) : Unit = { + if (!avatarActive) { + if (Vector3.DistanceSquared(pos1, pos2) > distanceSquared) { + startExecutingQueuedActivity() + } + } else { + countDownUntilQueuedActivity() + } + } + + def tryQueuedActivity(vel: Option[Vector3]) : Unit = { + if (!avatarActive) { + if (WorldEntity.isMoving(vel)) { + startExecutingQueuedActivity() + } + } else { + countDownUntilQueuedActivity() + } + } + + def tryQueuedActivity() : Unit = { + if (!avatarActive) { + startExecutingQueuedActivity() + } else { + countDownUntilQueuedActivity() + } + } + + def addDelayBeforeNextQueuedActivity(delay: Int): Unit = { + if (nextActivityDelay == 0) { + nextActivityDelay = initialActivityDelay + } else if (queuedActivities.nonEmpty) { + val lastActivity = queuedActivities.last + if (lastActivity.delayBeforeNext < delay) { + queuedActivities = queuedActivities.dropRight(1) :+ lastActivity.copy(delayBeforeNext = delay) + } + } + } + + private def startExecutingQueuedActivity(): Unit = { + avatarActive = startEnqueueSquadMessages + if (nextActivityDelay == 0) { + nextActivityDelay = initialActivityDelay + } + } + + private def countDownUntilQueuedActivity(): Unit = { + if (nextActivityDelay > 0) { + nextActivityDelay -= 1 + } else if (queuedActivities.nonEmpty) { + val task :: rest = queuedActivities + queuedActivities = if (task.repeat > 0) { + task.copy(repeat = task.repeat - 1) +: rest //positive: repeat immediately + } else if (task.repeat < 0) { + rest :+ task.copy(repeat = task.repeat + 1) //negative: repeat after all other tasks have been completed + } else { + rest + } + nextActivityDelay = task.delayBeforeNext + task.task(sessionLogic) + } + } + + def clearAllQueuedActivity(): Unit = { + queuedActivities = Seq() + } } def doorsThatShouldBeClosedOrBeOpenedByRange( diff --git a/src/main/scala/net/psforever/services/teamwork/SquadInvitationManager.scala b/src/main/scala/net/psforever/services/teamwork/SquadInvitationManager.scala index d03d0739c..ad9b6cb98 100644 --- a/src/main/scala/net/psforever/services/teamwork/SquadInvitationManager.scala +++ b/src/main/scala/net/psforever/services/teamwork/SquadInvitationManager.scala @@ -6,7 +6,6 @@ import akka.pattern.ask import akka.util.Timeout import scala.collection.mutable import scala.concurrent.duration._ -import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future // import net.psforever.objects.{LivePlayerList, Player}