From d15e916f46fa279718ce3ba34a7f52850cc96590 Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Tue, 3 Sep 2024 12:48:56 -0400 Subject: [PATCH 01/25] format changes; fixing one known source of exceptions at the start of the search algorithm --- .../services/teamwork/SquadService.scala | 119 +++++++++--------- 1 file changed, 61 insertions(+), 58 deletions(-) diff --git a/src/main/scala/net/psforever/services/teamwork/SquadService.scala b/src/main/scala/net/psforever/services/teamwork/SquadService.scala index 5a1edb56..2dc80fd1 100644 --- a/src/main/scala/net/psforever/services/teamwork/SquadService.scala +++ b/src/main/scala/net/psforever/services/teamwork/SquadService.scala @@ -2,7 +2,9 @@ package net.psforever.services.teamwork import akka.actor.{Actor, ActorRef, Terminated} + import java.io.{PrintWriter, StringWriter} +import scala.annotation.unused import scala.collection.concurrent.TrieMap import scala.collection.mutable // @@ -294,7 +296,7 @@ class SquadService extends Actor { subs.UserEvents find { case (_, subscription) => subscription.path.equals(sender.path) } match { case Some((to, _)) => LeaveService(to, sender) - case _ => ; + case _ => () } } @@ -303,7 +305,7 @@ class SquadService extends Actor { subs.UserEvents find { case (_, subscription) => subscription eq requestee } match { case Some((to, _)) => LeaveService(to, requestee) - case _ => ; + case _ => () } } @@ -343,7 +345,7 @@ class SquadService extends Actor { def SquadActionInitCharId(tplayer: Player): Unit = { val charId = tplayer.CharId GetParticipatingSquad(charId) match { - case None => ; + case None => () case Some(features) => features.Switchboard ! SquadSwitchboard.Join(tplayer, 0, sender()) } @@ -376,13 +378,15 @@ class SquadService extends Actor { case SquadAction.Membership(SquadRequestType.Cancel, cancellingPlayer, _, _, _) => SquadActionMembershipCancel(cancellingPlayer) - case SquadAction.Membership(SquadRequestType.Promote, promotingPlayer, Some(_promotedPlayer), promotedName, _) => - //SquadActionMembershipPromote(promotingPlayer, _promotedPlayer, promotedName, SquadServiceMessage(tplayer, zone, action), sender()) + case SquadAction.Membership(SquadRequestType.Promote, _, _, _, _) => + () +// case SquadAction.Membership(SquadRequestType.Promote, promotingPlayer, Some(_promotedPlayer), promotedName, _) => +// SquadActionMembershipPromote(promotingPlayer, _promotedPlayer, promotedName, SquadServiceMessage(tplayer, zone, action), sender()) case SquadAction.Membership(event, _, _, _, _) => debug(s"SquadAction.Membership: $event is not yet supported") - case _ => ; + case _ => () } } @@ -397,19 +401,16 @@ class SquadService extends Actor { //validate player with name exists LivePlayerList .WorldPopulation({ case (_, a: Avatar) => a.name.equalsIgnoreCase(invitedName) && a.faction == tplayer.Faction }) - .headOption match { - case Some(a) => subs.UserEvents.keys.find(_ == a.id) - case None => None - } } else { //validate player with id exists LivePlayerList .WorldPopulation({ case (_, a: Avatar) => a.id == _invitedPlayer && a.faction == tplayer.Faction }) - .headOption match { - case Some(_) => Some(_invitedPlayer) - case None => None - } - }) match { + }) + .headOption + .collectFirst { + //important: squads must know about the person too + a => subs.UserEvents.keys.find(_ == a.id) + }.flatten match { case Some(invitedPlayer) if invitingPlayer != invitedPlayer => (GetParticipatingSquad(invitingPlayer), GetParticipatingSquad(invitedPlayer)) match { case (Some(features1), Some(features2)) @@ -448,9 +449,9 @@ class SquadService extends Actor { //neither the invited player nor the inviting player belong to any squad invitations.createSpontaneousInvite(tplayer, invitedPlayer) - case _ => ; + case _ => () } - case _ => ; + case _ => () } } @@ -458,7 +459,7 @@ class SquadService extends Actor { GetLeadingSquad(invitingPlayer, None) match { case Some(features) => invitations.handleProximityInvite(zone, invitingPlayer, features) - case _ => ; + case _ => () } } @@ -533,9 +534,9 @@ class SquadService extends Actor { } } - case _ => ; + case _ => () } - case _ => ; + case _ => () } } @@ -551,7 +552,7 @@ class SquadService extends Actor { GetLeadingSquad(charId, None) match { case Some(features) => DisbandSquad(features) - case None => ; + case None => () } } @@ -582,7 +583,7 @@ class SquadService extends Actor { (GetLeadingSquad(sponsoringPlayer, None), GetParticipatingSquad(promotedPlayer)) match { case (Some(features), Some(features2)) if features.Squad.GUID == features2.Squad.GUID => SquadActionMembershipPromote(sponsoringPlayer, promotedPlayer, features, msg, ref) - case _ => ; + case _ => () } } @@ -649,14 +650,14 @@ class SquadService extends Actor { GetLeadingSquad(tplayer, None) match { case Some(features) => invitations.handleDefinitionAction(tplayer, action, features) - case _ => ; + case _ => () } None case CancelFind() => GetLeadingSquad(tplayer, None) match { case Some(features) => invitations.handleDefinitionAction(tplayer, action, features) - case _ => ; + case _ => () } None case SelectRoleForYourself(_) => @@ -674,7 +675,7 @@ class SquadService extends Actor { GetSquad(guid) match { case Some(features) => invitations.handleDefinitionAction(tplayer, action, features) - case _ => ; + case _ => () } None } @@ -682,10 +683,10 @@ class SquadService extends Actor { GetSquad(guid) match { case Some(features) => invitations.handleDefinitionAction(tplayer, action, features) - case _ => ; + case _ => () } None - case search: SearchForSquadsWithParticularRole => + case _/*search*/: SearchForSquadsWithParticularRole => // SquadActionDefinitionSearchForSquadsWithParticularRole(tplayer, search) None case _: CancelSquadSearch => @@ -706,7 +707,7 @@ class SquadService extends Actor { GetSquad(guid) }) match { case Some(features) => features.Switchboard.tell(message, sender()) - case None => ; + case None => () } } @@ -717,7 +718,7 @@ class SquadService extends Actor { ): Unit = { GetParticipatingSquad(char_id) match { case Some(features) => features.Switchboard.tell(message, replyTo) - case None => ; + case None => () } } @@ -739,7 +740,7 @@ class SquadService extends Actor { ): Unit = { val charId = tplayer.CharId searchData.get(charId) match { - case Some(_) => ; + case Some(_) => () //already searching, so do nothing(?) case None => val data = SquadService.SearchCriteria(tplayer.Faction, criteria) @@ -769,7 +770,7 @@ class SquadService extends Actor { def SquadActionDefinitionCancelSquadSearch(charId: Long): Unit = { searchData.remove(charId) match { - case None => ; + case None => () case Some(data) => SearchForSquadsResults(data).foreach { guid => subs.Publish(charId, SquadResponse.SquadDecoration(guid, squadFeatures(guid).Squad)) @@ -781,30 +782,32 @@ class SquadService extends Actor { criteria: SearchCriteria, guid: PlanetSideGUID ): Option[PlanetSideGUID] = { - val squad = squadFeatures(guid).Squad - val positions = if (criteria.mode == SquadRequestAction.SearchMode.AnyPositions) { - //includes occupied positions and closed positions that retain assignment information - squad.Membership - } else { - squad.Membership.zipWithIndex.filter { case (_, b) => squad.Availability(b) }.map { _._1 } - } - if ( - positions.nonEmpty && - (criteria.zoneId == 0 || criteria.zoneId == squad.ZoneId) && - (criteria.role.isEmpty || positions.exists(_.Role.equalsIgnoreCase(criteria.role))) && - (criteria.requirements.isEmpty || positions.exists { p => - val results = p.Requirements.intersect(criteria.requirements) - if (criteria.mode == SquadRequestAction.SearchMode.SomeCertifications) { - results.size > 1 - } else { - results == criteria.requirements - } - }) - ) { - Some(guid) - } else { - None - } + squadFeatures + .get(guid) + .map { features => + val squad = features.Squad + val positions = if (criteria.mode == SquadRequestAction.SearchMode.AnyPositions) { + //includes occupied positions and closed positions that retain assignment information + squad.Membership + } else { + squad.Membership.zipWithIndex.filter { case (_, b) => squad.Availability(b) }.map { _._1 } + } + (squad, positions) + } + .collect { + case (squad, positions) if positions.nonEmpty && + (criteria.zoneId == 0 || criteria.zoneId == squad.ZoneId) && + (criteria.role.isEmpty || positions.exists(_.Role.equalsIgnoreCase(criteria.role))) && + (criteria.requirements.isEmpty || positions.exists { p => + val results = p.Requirements.intersect(criteria.requirements) + if (criteria.mode == SquadRequestAction.SearchMode.SomeCertifications) { + results.size > 1 + } else { + results == criteria.requirements + } + }) => + guid + } } /** the following action can be performed by anyone */ @@ -831,14 +834,14 @@ class SquadService extends Actor { log.warn(s"${tplayer.Name} has a potential squad issue; might be exchanging information $reason") } - def CleanUpSquadFeatures(removed: List[Long], guid: PlanetSideGUID, position: Int): Unit = { + def CleanUpSquadFeatures(removed: List[Long], guid: PlanetSideGUID, @unused position: Int): Unit = { GetSquad(guid) match { case Some(features) => features.ProxyInvites = features.ProxyInvites.filterNot(removed.contains) if (features.ProxyInvites.isEmpty) { features.SearchForRole = None } - case None => ; + case None => () } } @@ -1098,7 +1101,7 @@ class SquadService extends Actor { subs.UserEvents.remove(charId) match { case Some(events) => subs.SquadEvents.unsubscribe(events, s"/${features.ToChannel}/Squad") - case _ => ; + case _ => () } if (size > 2) { GetLeadingSquad(charId, pSquadOpt) match { From 2372a95040332135fa5bcbe29df042c3446baa18 Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Tue, 10 Sep 2024 20:38:30 -0400 Subject: [PATCH 02/25] experimental invitation management commands for squad leaders, mostly untested atm; messages for being denied squad admission --- .../actors/session/normal/ChatLogic.scala | 1 + .../session/normal/SquadHandlerLogic.scala | 3 + .../session/support/ChatOperations.scala | 30 ++ .../actors/session/support/SessionData.scala | 17 +- .../teamwork/SquadInvitationManager.scala | 333 +++++++++++++++--- .../services/teamwork/SquadService.scala | 123 ++++++- .../teamwork/SquadServiceResponse.scala | 2 + .../teamwork/SquadSubscriptionEntity.scala | 10 +- 8 files changed, 436 insertions(+), 83 deletions(-) diff --git a/src/main/scala/net/psforever/actors/session/normal/ChatLogic.scala b/src/main/scala/net/psforever/actors/session/normal/ChatLogic.scala index 35c09b7f..052a1ebb 100644 --- a/src/main/scala/net/psforever/actors/session/normal/ChatLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/ChatLogic.scala @@ -138,6 +138,7 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext case "grenade" => ops.customCommandGrenade(session, log) case "macro" => ops.customCommandMacro(session, params) case "progress" => ops.customCommandProgress(session, params) + case "squad" => ops.customCommandSquad(params) case _ => // command was not handled sendResponse( 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 b80eb01c..7ebfdde2 100644 --- a/src/main/scala/net/psforever/actors/session/normal/SquadHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/SquadHandlerLogic.scala @@ -349,6 +349,9 @@ class SquadHandlerLogic(val ops: SessionSquadHandlers, implicit val context: Act 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/support/ChatOperations.scala b/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala index 399fede6..5bfe20f7 100644 --- a/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala @@ -2,6 +2,8 @@ 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 net.psforever.actors.session.spectator.SpectatorMode @@ -12,6 +14,7 @@ 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.types.ChatMessageType.CMT_QUIT import org.log4s.Logger @@ -54,6 +57,7 @@ class ChatOperations( val sessionLogic: SessionData, val avatarActor: typed.ActorRef[AvatarActor.Command], val chatService: typed.ActorRef[ChatService.Command], + val squadService: ClassicActorRef, val cluster: typed.ActorRef[InterstellarClusterService.Command], implicit val context: ActorContext ) extends CommonSessionInterfacingFunctionality { @@ -1284,6 +1288,32 @@ class ChatOperations( true } + def customCommandSquad(params: Seq[String]): Boolean = { + params match { + case "invites" :: _ => + squadService ! SquadService.ListAllCurrentInvites + + 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) + } + 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) + } + squadService ! SquadService.ChainRejection(player, player.CharId, results) + + case _ => () + } + true + } + def firstParam[T]( session: Session, buffer: Iterable[String], 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 68fcf9f0..44c7ed18 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionData.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionData.scala @@ -165,15 +165,15 @@ class SessionData( case LookupResult("squad", endpoint) => squadService = endpoint buildDependentOperationsForSquad(endpoint) + buildDependentOperationsForChat(chatService, endpoint, cluster) true case ICS.InterstellarClusterServiceKey.Listing(listings) => cluster = listings.head buildDependentOperationsForZoning(galaxyService, cluster) - buildDependentOperationsForChat(chatService, cluster) true case ChatService.ChatServiceKey.Listing(listings) => chatService = listings.head - buildDependentOperationsForChat(chatService, cluster) + buildDependentOperationsForChat(chatService, squadService, cluster) true case _ => @@ -200,9 +200,16 @@ class SessionData( } } - def buildDependentOperationsForChat(chatService: typed.ActorRef[ChatService.Command], clusterActor: typed.ActorRef[ICS.Command]): Unit = { - if (chatOpt.isEmpty && chatService != Default.typed.Actor && clusterActor != Default.typed.Actor) { - chatOpt = Some(new ChatOperations(sessionLogic=this, avatarActor, chatService, clusterActor, context)) + def buildDependentOperationsForChat( + chatService: typed.ActorRef[ChatService.Command], + squadService: ActorRef, + clusterActor: typed.ActorRef[ICS.Command] + ): Unit = { + if (chatOpt.isEmpty && + chatService != Default.typed.Actor && + squadService !=Default.Actor && + clusterActor != Default.typed.Actor) { + chatOpt = Some(new ChatOperations(sessionLogic=this, avatarActor, chatService, squadService, clusterActor, context)) } } diff --git a/src/main/scala/net/psforever/services/teamwork/SquadInvitationManager.scala b/src/main/scala/net/psforever/services/teamwork/SquadInvitationManager.scala index 9e58d2fa..6622e1b8 100644 --- a/src/main/scala/net/psforever/services/teamwork/SquadInvitationManager.scala +++ b/src/main/scala/net/psforever/services/teamwork/SquadInvitationManager.scala @@ -4,6 +4,8 @@ package net.psforever.services.teamwork import akka.actor.ActorRef import akka.pattern.ask import akka.util.Timeout + +import scala.annotation.unused import scala.collection.mutable import scala.concurrent.duration._ import scala.concurrent.ExecutionContext.Implicits.global @@ -119,14 +121,15 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { val leader = squad2.Leader.CharId Allowed(invitedPlayer, invitingPlayer) Allowed(leader, invitingPlayer) + lazy val preface = s"$invitingPlayer's invitation got reversed to $invitedPlayer's squad, but" if (squad2.Size == squad2.Capacity) { - log.debug(s"$invitingPlayer's invitation got reversed to $invitedPlayer's squad, but the squad has no available positions") + log.debug(s"$preface the squad has no available positions") } else if (Refused(invitingPlayer).contains(invitedPlayer)) { - log.debug(s"$invitingPlayer's invitation got reversed to $invitedPlayer's squad, but $invitedPlayer repeated a previous refusal to $invitingPlayer's invitation offer") + log.debug(s"$preface $invitedPlayer repeated a previous refusal to $invitingPlayer's invitation offer") } else if (Refused(invitingPlayer).contains(leader)) { - log.debug(s"$invitingPlayer's invitation got reversed to $invitedPlayer's squad, but $leader repeated a previous refusal to $invitingPlayer's invitation offer") + log.debug(s"$preface $leader repeated a previous refusal to $invitingPlayer's invitation offer") } else if (features.DeniedPlayers().contains(invitingPlayer)) { - log.debug(s"$invitingPlayer's invitation got reversed to $invitedPlayer's squad, but $invitingPlayer is denied the invitation") + log.debug(s"$preface $invitingPlayer is denied the invitation") } else { features.AllowedPlayers(invitedPlayer) AddInviteAndRespond( @@ -165,7 +168,7 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { val availableForJoiningSquad = notLimitedByEnrollmentInSquad(invitedPlayerSquadOpt, invitedPlayer) acceptedInvite match { case Some(RequestRole(petitioner, features, position)) - if canEnrollInSquad(features, petitioner.CharId) => + if SquadInvitationManager.canEnrollInSquad(features, petitioner.CharId) => //player requested to join a squad's specific position //invitedPlayer is actually the squad leader; petitioner is the actual "invitedPlayer" if (JoinSquad(petitioner, features, position)) { @@ -174,7 +177,7 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { } case Some(IndirectInvite(recruit, features)) - if canEnrollInSquad(features, recruit.CharId) => + if SquadInvitationManager.canEnrollInSquad(features, recruit.CharId) => //tplayer / invitedPlayer is actually the squad leader val recruitCharId = recruit.CharId HandleVacancyInvite(features, recruitCharId, invitedPlayer, recruit) match { @@ -188,7 +191,7 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { } case Some(VacancyInvite(invitingPlayer, _, features)) - if availableForJoiningSquad && canEnrollInSquad(features, invitedPlayer) => + if availableForJoiningSquad && SquadInvitationManager.canEnrollInSquad(features, invitedPlayer) => //accepted an invitation to join an existing squad HandleVacancyInvite(features, invitedPlayer, invitingPlayer, tplayer) match { case Some((_, line)) => @@ -204,7 +207,7 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { SquadMembershipAcceptInviteAction(invitingPlayer, tplayer, invitedPlayer) case Some(LookingForSquadRoleInvite(member, features, position)) - if availableForJoiningSquad && canEnrollInSquad(features, invitedPlayer) => + if availableForJoiningSquad && SquadInvitationManager.canEnrollInSquad(features, invitedPlayer) => val invitingPlayer = member.CharId features.ProxyInvites = features.ProxyInvites.filterNot { _ == invitedPlayer } if (JoinSquad(tplayer, features, position)) { @@ -215,7 +218,7 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { } case Some(ProximityInvite(member, features, position)) - if availableForJoiningSquad && canEnrollInSquad(features, invitedPlayer) => + if availableForJoiningSquad && SquadInvitationManager.canEnrollInSquad(features, invitedPlayer) => val invitingPlayer = member.CharId features.ProxyInvites = features.ProxyInvites.filterNot { _ == invitedPlayer } if (JoinSquad(tplayer, features, position)) { @@ -280,10 +283,6 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { } } - def canEnrollInSquad(features: SquadFeatures, charId: Long): Boolean = { - !features.Squad.Membership.exists { _.CharId == charId } - } - def SquadMembershipAcceptInviteAction(invitingPlayer: Player, player: Player, invitedPlayer: Long): Unit = { //originally, we were invited by someone into a new squad they would form val invitingPlayerCharId = invitingPlayer.CharId @@ -416,24 +415,75 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { def handleRejection( tplayer: Player, rejectingPlayer: Long, - squadsToLeaders: List[(PlanetSideGUID, Long)] + squadsToLeaders: List[(PlanetSideGUID, SquadFeatures)] ): Unit = { val rejectedBid = RemoveInvite(rejectingPlayer) - (rejectedBid match { + FormatRejection( + DoRejection(rejectedBid, tplayer, rejectingPlayer, squadsToLeaders), + tplayer + ) + NextInviteAndRespond(rejectingPlayer) + } + + def ParseRejection( + rejectedBid: Option[Invitation], + @unused tplayer: Player, + rejectingPlayer: Long, + squadsToLeaders: List[(PlanetSideGUID, SquadFeatures)] + ): (Option[Long], Option[Long]) = { + rejectedBid match { + case Some(SpontaneousInvite(leader)) => + //rejectingPlayer is the would-be squad member; the would-be squad leader sent the request and was rejected + val invitingPlayerCharId = leader.CharId + (Some(rejectingPlayer), Some(invitingPlayerCharId)) + + case Some(VacancyInvite(leader, _, _)) + /*if SquadInvitationManager.notLeaderOfThisSquad(squadsToLeaders, features.Squad.GUID, rejectingPlayer)*/ => + //rejectingPlayer is the would-be squad member; the squad leader sent the request and was rejected + (Some(rejectingPlayer), Some(leader)) + + case Some(ProximityInvite(_, _, _)) + /*if SquadInvitationManager.notLeaderOfThisSquad(squadsToLeaders, features.Squad.GUID, rejectingPlayer)*/ => + //rejectingPlayer is the would-be squad member; the squad leader sent the request and was rejected + (Some(rejectingPlayer), None) + + case Some(LookingForSquadRoleInvite(member, _, _)) + if member.CharId != rejectingPlayer => + val leaderCharId = member.CharId + //rejectingPlayer is the would-be squad member; the squad leader sent the request and was rejected + (Some(rejectingPlayer), Some(leaderCharId)) + + case Some(RequestRole(rejected, features, _)) + if SquadInvitationManager.notLeaderOfThisSquad(squadsToLeaders, features.Squad.GUID, rejected.CharId) => + //rejected is the would-be squad member; rejectingPlayer is the squad leader who rejected the request + (Some(rejectingPlayer), None) + + case _ => //TODO IndirectInvite, etc., but how to handle them? + (None, None) + } + } + + def DoRejection( + rejectedBid: Option[Invitation], + tplayer: Player, + rejectingPlayer: Long, + squadsToLeaders: List[(PlanetSideGUID, SquadFeatures)] + ): (Option[Long], Option[Long], String) = { + rejectedBid match { case Some(SpontaneousInvite(leader)) => //rejectingPlayer is the would-be squad member; the would-be squad leader sent the request and was rejected val invitingPlayerCharId = leader.CharId Refused(rejectingPlayer, invitingPlayerCharId) - (Some(rejectingPlayer), Some(invitingPlayerCharId)) + (Some(rejectingPlayer), Some(invitingPlayerCharId), "anonymous") - case Some(VacancyInvite(leader, _, _)) - /*if notLeaderOfThisSquad(squadsToLeaders, features.Squad.GUID, rejectingPlayer)*/ => + case Some(VacancyInvite(leader, _, features)) + /*if SquadInvitationManager.notLeaderOfThisSquad(squadsToLeaders, features.Squad.GUID, rejectingPlayer)*/ => //rejectingPlayer is the would-be squad member; the squad leader sent the request and was rejected Refused(rejectingPlayer, leader) - (Some(rejectingPlayer), Some(leader)) + (Some(rejectingPlayer), Some(leader), features.Squad.Task) case Some(ProximityInvite(_, features, position)) - /*if notLeaderOfThisSquad(squadsToLeaders, features.Squad.GUID, rejectingPlayer)*/ => + /*if SquadInvitationManager.notLeaderOfThisSquad(squadsToLeaders, features.Squad.GUID, rejectingPlayer)*/ => //rejectingPlayer is the would-be squad member; the squad leader sent the request and was rejected ReloadProximityInvite( tplayer.Zone.Players, @@ -441,52 +491,53 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { features, position ) - (Some(rejectingPlayer), None) + (Some(rejectingPlayer), None, features.Squad.Task) - case Some(LookingForSquadRoleInvite(member, guid, position)) + case Some(LookingForSquadRoleInvite(member, features, position)) if member.CharId != rejectingPlayer => val leaderCharId = member.CharId //rejectingPlayer is the would-be squad member; the squad leader sent the request and was rejected ReloadSearchForRoleInvite( LivePlayerList.WorldPopulation { _ => true }, rejectingPlayer, - guid, + features, position ) - (Some(rejectingPlayer), Some(leaderCharId)) + (Some(rejectingPlayer), Some(leaderCharId), features.Squad.Task) case Some(RequestRole(rejected, features, _)) - if notLeaderOfThisSquad(squadsToLeaders, features.Squad.GUID, rejected.CharId) => + if SquadInvitationManager.notLeaderOfThisSquad(squadsToLeaders, features.Squad.GUID, rejected.CharId) => //rejected is the would-be squad member; rejectingPlayer is the squad leader who rejected the request features.DeniedPlayers(rejected.CharId) - (Some(rejectingPlayer), None) + (Some(rejectingPlayer), None, features.Squad.Task) - case _ => ; //TODO IndirectInvite, etc., but how to handle them? - (None, None) - }) match { - case (Some(rejected), Some(invited)) => + case _ => //TODO IndirectInvite, etc., but how to handle them? + (None, None, "n|a") + } + } + + def FormatRejection( + rejectedPair: (Option[Long], Option[Long], String), + tplayer: Player + ): Unit = { + rejectedPair match { + case (Some(rejected), Some(inviter), squadName) => subs.Publish( rejected, - SquadResponse.Membership(SquadResponseType.Reject, 0, 0, rejected, Some(invited), "", unk5=true, Some(None)) + SquadResponse.Membership(SquadResponseType.Reject, 0, 0, rejected, Some(inviter), "", unk5=true, Some(None)) ) subs.Publish( - invited, - SquadResponse.Membership(SquadResponseType.Reject, 0, 0, invited, Some(rejected), tplayer.Name, unk5=false, Some(None)) + inviter, + SquadResponse.Membership(SquadResponseType.Reject, 0, 0, inviter, Some(rejected), tplayer.Name, unk5=false, Some(None)) ) - case (Some(rejected), None) => + subs.Publish(rejected, SquadResponse.SquadRelatedComment(s"Your request to join squad '$squadName' has been refused.")) + case (Some(rejected), None, squadName) => subs.Publish( rejected, SquadResponse.Membership(SquadResponseType.Reject, 0, 0, rejected, Some(rejected), "", unk5=true, Some(None)) ) - case _ => ; - } - NextInviteAndRespond(rejectingPlayer) - } - - def notLeaderOfThisSquad(squadsToLeaders: List[(PlanetSideGUID, Long)], guid: PlanetSideGUID, charId: Long): Boolean = { - squadsToLeaders.find { case (squadGuid, _) => squadGuid == guid } match { - case Some((_, leaderId)) => leaderId != charId - case None => false + subs.Publish(rejected, SquadResponse.SquadRelatedComment(s"Your request to join squad '$squadName' has been refused.")) + case _ => () } } @@ -611,8 +662,14 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { tplayer: Player, features: SquadFeatures ): Unit = { + SquadActionDefinitionAutoApproveInvitationRequests(tplayer.CharId, features) + } + + def SquadActionDefinitionAutoApproveInvitationRequests( + charId: Long, + features: SquadFeatures + ): Unit = { //allowed auto-approval - resolve the requests (only) - val charId = tplayer.CharId val (requests, others) = (invites.get(charId) match { case Some(invite) => invite +: queuedInvites.getOrElse(charId, Nil) @@ -911,6 +968,155 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { } } + def listCurrentInvitations(charId: Long): List[String] = { + ((invites.get(charId), queuedInvites.get(charId)) match { + case (Some(invite), Some(invites)) => + invite +: invites + case (Some(invite), None) => + List(invite) + case (None, Some(invites)) => + invites + case _ => + List() + }).collect { + case RequestRole(player, _, _) => player.Name + case IndirectInvite(player, features) if !features.Squad.Leader.Name.equals(player.Name) => player.Name + } + } + + def tryChainAcceptance( + inviter: Player, + charId: Long, + list: List[Long], + features: SquadFeatures + ): Unit = { + //filter queued invites + lazy val squadToLeader = List((features.Squad.GUID, features)) + lazy val squadName = features.Squad.Task + var foundPairs: List[(Player, Invitation)] = List() + val unmatchedInvites = queuedInvites + .getOrElse(charId, Nil) + .filter { + case invite @ RequestRole(invitee, _, _) + if list.contains(invitee.CharId) && !features.Squad.Leader.Name.equals(invitee.Name) => + foundPairs = foundPairs :+ (invitee, invite) + false + case invite @ IndirectInvite(invitee, _) + if list.contains(invitee.CharId) && !features.Squad.Leader.Name.equals(invitee.Name) => + foundPairs = foundPairs :+ (invitee, invite) + false + case _ => + true + } + //handle active invite + val clearedActiveInvite = invites + .get(charId) + .collect { + case invite @ RequestRole(invitee, _, _) + if list.contains(invitee.CharId) && !features.Squad.Leader.Name.equals(invitee.Name) => + SquadActionMembershipAcceptInvite(inviter, invitee.CharId, Some(invite), Some(features)) + invites.remove(charId) + true + case invite @ IndirectInvite(invitee, _) + if list.contains(invitee.CharId) && !features.Squad.Leader.Name.equals(invitee.Name) => + SquadActionMembershipAcceptInvite(inviter, invitee.CharId, Some(invite), Some(features)) + invites.remove(charId) + true + case _ => + false + } + //handle selected queued invites + val pairIterator = foundPairs.iterator + while (pairIterator.hasNext && features.Squad.Capacity < features.Squad.Size) { + val (player, invite) = pairIterator.next() + SquadActionMembershipAcceptInvite(inviter, player.CharId, Some(invite), Some(features)) + } + //evaluate final squad composition + if (features.Squad.Capacity < features.Squad.Size) { + //replace unfiltered invites + if (unmatchedInvites.isEmpty) { + queuedInvites.remove(charId) + } else { + queuedInvites.put(charId, unmatchedInvites) + } + //manage next invitation + clearedActiveInvite.collect { + case true => NextInviteAndRespond(charId) + } + } else { + //squad is full + previousInvites.remove(charId) + queuedInvites.remove(charId) + clearedActiveInvite.collect { + case true => invites.remove(charId) + } + unmatchedInvites.foreach { invite => + val (refusedId, _) = ParseRejection(Some(invite), inviter, inviter.CharId, squadToLeader) + subs.Publish(refusedId, SquadResponse.SquadRelatedComment(s"Your request to join squad '$squadName' has been refused.")) + } + NextInviteAndRespond(charId) + } + //clean up any incomplete selected invites + pairIterator.foreach { case (_, invite) => + val (refusedId, _) = ParseRejection(Some(invite), inviter, inviter.CharId, squadToLeader) + subs.Publish(refusedId, SquadResponse.SquadRelatedComment(s"Your request to join squad '$squadName' has been refused.")) + } + } + + def tryChainRejection( + inviter: Player, + charId: Long, + list: List[Long], + features: SquadFeatures): Unit = { + //handle queued invites + lazy val squadToLeader = List((features.Squad.GUID, features)) + lazy val squadName = features.Squad.Task + val unmatchedInvites = queuedInvites + .getOrElse(charId, Nil) + .filter { + case invite @ RequestRole(invitee, _, _) + if list.contains(invitee.CharId) && !features.Squad.Leader.Name.equals(invitee.Name) => + val (refusedId, _, _) = DoRejection(Some(invite), inviter, charId, squadToLeader) + subs.Publish(refusedId, SquadResponse.SquadRelatedComment(s"Your request to join squad '$squadName' has been refused.")) + false + case invite @ IndirectInvite(invitee, _) + if list.contains(invitee.CharId) && !features.Squad.Leader.Name.equals(invitee.Name) => + val (refusedId, _, _) = DoRejection(Some(invite), inviter, charId, squadToLeader) + subs.Publish(refusedId, SquadResponse.SquadRelatedComment(s"Your request to join squad '$squadName' has been refused.")) + false + case _ => + true + } + queuedInvites.put(charId, unmatchedInvites) + //handle active invite + invites + .get(charId) + .collect { + case invite @ RequestRole(player, features, _) + if list.contains(player.CharId) && !features.Squad.Leader.Name.equals(player.Name) => + val (refusedId, _, _) = DoRejection(Some(invite), inviter, charId, squadToLeader) + subs.Publish(refusedId, SquadResponse.SquadRelatedComment(s"Your request to join squad '$squadName' has been refused.")) + invites.remove(charId) + NextInviteAndRespond(charId) + case invite @ IndirectInvite(player, features) + if list.contains(player.CharId) && !features.Squad.Leader.Name.equals(player.Name) => + val (refusedId, _, _) = DoRejection(Some(invite), inviter, charId, squadToLeader) + subs.Publish(refusedId, SquadResponse.SquadRelatedComment(s"Your request to join squad '$squadName' has been refused.")) + invites.remove(charId) + NextInviteAndRespond(charId) + case _ => () + } + } + + def tryChainRejectionAll(charId: Long, features: SquadFeatures): Unit = { + val squadName = features.Squad.Task + CleanUpAllInvitesToSquad(features) + .filterNot(_ == charId) + .foreach { refusedId => + subs.Publish(refusedId, SquadResponse.SquadRelatedComment(s"Your request to join squad '$squadName' has been refused.")) + } + } + def ShiftInvitesToPromotedSquadLeader( sponsoringPlayer: Long, promotedPlayer: Long @@ -1695,18 +1901,18 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { * @see `VacancyInvite` * @param features the squad identifier */ - def CleanUpAllInvitesToSquad(features: SquadFeatures): Unit = { + def CleanUpAllInvitesToSquad(features: SquadFeatures): List[Long] = { val guid = features.Squad.GUID //clean up invites val activeInviteIds = { val keys = invites.keys.toSeq invites.values.zipWithIndex .collect { - case (VacancyInvite(_, _, _squad), index) if _squad.Squad.GUID == guid => index - case (IndirectInvite(_, _squad), index) if _squad.Squad.GUID == guid => index - case (LookingForSquadRoleInvite(_, _squad, _), index) if _squad.Squad.GUID == guid => index - case (ProximityInvite(_, _squad, _), index) if _squad.Squad.GUID == guid => index - case (RequestRole(_, _squad, _), index) if _squad.Squad.GUID == guid => index + case (VacancyInvite(_, _, f), index) if f.Squad.GUID == guid => index + case (IndirectInvite(_, f), index) if f.Squad.GUID == guid => index + case (LookingForSquadRoleInvite(_, f, _), index) if f.Squad.GUID == guid => index + case (ProximityInvite(_, f, _), index) if f.Squad.GUID == guid => index + case (RequestRole(_, f, _), index) if f.Squad.GUID == guid => index } .map { index => val key = keys(index) @@ -1723,12 +1929,12 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { case (queue, index) => val key = keys(index) val (targets, retained) = queue.partition { - case VacancyInvite(_, _, _squad) => _squad.Squad.GUID == guid - case IndirectInvite(_, _squad) => _squad.Squad.GUID == guid - case LookingForSquadRoleInvite(_, _squad, _) => _squad.Squad.GUID == guid - case ProximityInvite(_, _squad, _) => _squad.Squad.GUID == guid - case RequestRole(_, _squad, _) => _squad.Squad.GUID == guid - case _ => false + case VacancyInvite(_, _, f) => f.Squad.GUID == guid + case IndirectInvite(_, f) => f.Squad.GUID == guid + case LookingForSquadRoleInvite(_, f, _) => f.Squad.GUID == guid + case ProximityInvite(_, f, _) => f.Squad.GUID == guid + case RequestRole(_, f, _) => f.Squad.GUID == guid + case _ => false } if (retained.isEmpty) { queuedInvites.remove(key) @@ -1744,7 +1950,9 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { .flatten .toList } - CleanUpSquadFeatures(activeInviteIds ++ queuedInviteIds, features, position = -1) + val allInviteIds = (activeInviteIds ++ queuedInviteIds).distinct + CleanUpSquadFeatures(allInviteIds, features, position = -1) + allInviteIds } /** @@ -2151,4 +2359,15 @@ object SquadInvitationManager { } final case class FinishStartSquad(features: SquadFeatures) + + def canEnrollInSquad(features: SquadFeatures, charId: Long): Boolean = { + !features.Squad.Membership.exists { _.CharId == charId } + } + + def notLeaderOfThisSquad(squadsToLeaders: List[(PlanetSideGUID, SquadFeatures)], guid: PlanetSideGUID, charId: Long): Boolean = { + squadsToLeaders.find { case (squadGuid, _) => squadGuid == guid } match { + case Some((_, features)) => features.Squad.Leader.CharId != charId + case None => false + } + } } diff --git a/src/main/scala/net/psforever/services/teamwork/SquadService.scala b/src/main/scala/net/psforever/services/teamwork/SquadService.scala index 2dc80fd1..317e1aae 100644 --- a/src/main/scala/net/psforever/services/teamwork/SquadService.scala +++ b/src/main/scala/net/psforever/services/teamwork/SquadService.scala @@ -2,6 +2,9 @@ package net.psforever.services.teamwork import akka.actor.{Actor, ActorRef, Terminated} +import net.psforever.actors.session.SessionActor +import net.psforever.packet.game.ChatMsg +import net.psforever.types.ChatMessageType import java.io.{PrintWriter, StringWriter} import scala.annotation.unused @@ -111,7 +114,7 @@ class SquadService extends Actor { * @return `true`, if the identifier is reset; `false`, otherwise */ def TryResetSquadId(): Boolean = { - if (squadFeatures.isEmpty) { + if (squadFeatures.isEmpty && LivePlayerList.WorldPopulation(_ => true).isEmpty) { sid = 1 true } else { @@ -199,7 +202,7 @@ class SquadService extends Actor { LeaveInGeneral(sender()) case Terminated(actorRef) => - TerminatedBy(actorRef) + LeaveInGeneral(actorRef) case message @ SquadServiceMessage(tplayer, zone, squad_action) => squad_action match { @@ -243,16 +246,39 @@ class SquadService extends Actor { case SquadService.ResendActiveInvite(charId) => invitations.resendActiveInvite(charId) + case SquadService.ListAllCurrentInvites(charId) => + ListCurrentInvitations(charId) + + case SquadService.ChainAcceptance(player, charId, list) => + ChainAcceptanceIntoSquad(player, charId, list) + + case SquadService.ChainRejection(player, charId, list) => + ChainRejectionFromSquad(player, charId, list) + case msg => log.warn(s"Unhandled message $msg from ${sender()}") } + /** + * Subscribe to a faction-wide channel. + * @param faction sub-channel name + * @param sender subscriber + */ def JoinByFaction(faction: String, sender: ActorRef): Unit = { val path = s"/$faction/Squad" log.trace(s"$sender has joined $path") subs.SquadEvents.subscribe(sender, path) } + /** + * Subscribe to a client-specific channel. + * The channel name is expected to be a `Long` number but is passed as a `String`. + * As is the case, the actual channel name may not parse into a proper long integer after being passed and + * there are failure cases. + * @param charId sub-channel name + * @param sender subscriber + * @see `SquadInvitationManager.handleJoin` + */ def JoinByCharacterId(charId: String, sender: ActorRef): Unit = { try { val longCharId = charId.toLong @@ -272,12 +298,23 @@ class SquadService extends Actor { } } + /** + * Unsubscribe from a faction-wide channel. + * @param faction sub-channel name + * @param sender subscriber + */ def LeaveByFaction(faction: String, sender: ActorRef): Unit = { val path = s"/$faction/Squad" log.trace(s"$sender has left $path") subs.SquadEvents.unsubscribe(sender, path) } + /** + * Unsubscribe from a client-specific channel. + * @param charId sub-channel name + * @param sender subscriber + * @see `LeaveService` + */ def LeaveByCharacterId(charId: String, sender: ActorRef): Unit = { try { LeaveService(charId.toLong, sender) @@ -292,23 +329,23 @@ class SquadService extends Actor { } } + /** + * Assuming a subscriber that matches previously subscribed data, + * completely unsubscribe and forget this entry. + * @param sender subscriber + * @see `LeaveService` + */ def LeaveInGeneral(sender: ActorRef): Unit = { - subs.UserEvents find { case (_, subscription) => subscription.path.equals(sender.path) } match { + context.unwatch(sender) + subs.UserEvents.find { + case (_, subscription) => (subscription eq sender) || subscription.path.equals(sender.path) + } match { case Some((to, _)) => LeaveService(to, sender) case _ => () } } - def TerminatedBy(requestee: ActorRef): Unit = { - context.unwatch(requestee) - subs.UserEvents find { case (_, subscription) => subscription eq requestee } match { - case Some((to, _)) => - LeaveService(to, requestee) - case _ => () - } - } - def performStartSquad(sender: ActorRef, player: Player): Unit = { val invitingPlayerCharId = player.CharId if (EnsureEmptySquad(invitingPlayerCharId)) { @@ -544,7 +581,7 @@ class SquadService extends Actor { invitations.handleRejection( tplayer, rejectingPlayer, - squadFeatures.map { case (guid, features) => (guid, features.Squad.Leader.CharId) }.toList + squadFeatures.map { case (guid, features) => (guid, features) }.toList ) } @@ -1084,9 +1121,14 @@ class SquadService extends Actor { } /** - * na + * Completely remove information about a former subscriber from the squad management service. * @param charId the player's unique character identifier number * @param sender the `ActorRef` associated with this character + * @see `DisbandSquad` + * @see `LeaveSquad` + * @see `SquadInvitationManager.handleLeave` + * @see `SquadSwitchboard.PanicLeaveSquad` + * @see `TryResetSquadId` */ def LeaveService(charId: Long, sender: ActorRef): Unit = { subs.MonitorSquadDetails.subtractOne(charId) @@ -1129,8 +1171,7 @@ class SquadService extends Actor { } subs.SquadEvents.unsubscribe(sender) //just to make certain searchData.remove(charId) - //todo turn this back on. See PR 1157 for why it was commented out. - //TryResetSquadId() + TryResetSquadId() } /** @@ -1285,6 +1326,50 @@ class SquadService extends Actor { } } } + + def ListCurrentInvitations(charId: Long) : Unit = { + GetLeadingSquad(charId, None) + .map { _ => + invitations.listCurrentInvitations(charId) + } + .collect { + case listOfInvites if listOfInvites.nonEmpty => + listOfInvites match { + case active :: queued if queued.nonEmpty => + subs.Publish(charId, SquadResponse.WantsSquadPosition(charId, s"$active, ${queued.mkString(", ")}")) + listOfInvites + case active :: _ => + subs.Publish(charId, SquadResponse.WantsSquadPosition(charId, active)) + listOfInvites + } + } + .orElse { + context.self ! SessionActor.SendResponse(ChatMsg(ChatMessageType.UNK_227, "You do not have any current invites to manage.")) + None + } + } + + def ChainAcceptanceIntoSquad(player: Player, charId: Long, listOfCharIds: List[Long]): Unit = { + GetLeadingSquad(charId, None) + .foreach { features => + if (listOfCharIds.nonEmpty) { + invitations.tryChainAcceptance(player, charId, listOfCharIds, features) + } else { + invitations.SquadActionDefinitionAutoApproveInvitationRequests(charId, features) + } + } + } + + def ChainRejectionFromSquad(player: Player, charId: Long, listOfCharIds: List[Long]): Unit = { + GetLeadingSquad(charId, None) + .foreach { features => + if (listOfCharIds.nonEmpty) { + invitations.tryChainRejection(player, charId, listOfCharIds, features) + } else { + invitations.tryChainRejectionAll(charId, features) + } + } + } } object SquadService { @@ -1294,6 +1379,12 @@ object SquadService { final case class ResendActiveInvite(charId: Long) + final case class ListAllCurrentInvites(charId: Long) + + final case class ChainAcceptance(player: Player, charId: Long, listOfCharIds: List[Long]) + + final case class ChainRejection(player: Player, charId: Long, listOfCharIds: List[Long]) + /** * A message to indicate that the squad list needs to update for the clients. * @param features the squad diff --git a/src/main/scala/net/psforever/services/teamwork/SquadServiceResponse.scala b/src/main/scala/net/psforever/services/teamwork/SquadServiceResponse.scala index 367127f4..8935627e 100644 --- a/src/main/scala/net/psforever/services/teamwork/SquadServiceResponse.scala +++ b/src/main/scala/net/psforever/services/teamwork/SquadServiceResponse.scala @@ -73,4 +73,6 @@ object SquadResponse { unk2: Int, zoneNumber: Int ) extends Response + + final case class SquadRelatedComment(str: String) extends Response } diff --git a/src/main/scala/net/psforever/services/teamwork/SquadSubscriptionEntity.scala b/src/main/scala/net/psforever/services/teamwork/SquadSubscriptionEntity.scala index 427d6c85..104bd532 100644 --- a/src/main/scala/net/psforever/services/teamwork/SquadSubscriptionEntity.scala +++ b/src/main/scala/net/psforever/services/teamwork/SquadSubscriptionEntity.scala @@ -133,7 +133,7 @@ class SquadSubscriptionEntity { case str if str.matches("\\d+") => Publish(to.toLong, msg, excluded) case _ => - log.warn(s"Publish(String): subscriber information is an unhandled format - $to") + log.warn(s"Publish(String): subscriber information is an unhandled format - $to; message $msg dropped") } } @@ -148,7 +148,7 @@ class SquadSubscriptionEntity { case Some(user) => user ! SquadServiceResponse("", msg) case None => - log.warn(s"Publish(Long): subscriber information can not be found - $to") + log.warn(s"Publish(Long): subscriber information can not be found - $to; message $msg dropped") } } @@ -174,7 +174,7 @@ class SquadSubscriptionEntity { * @param msg a message that can be stored in a `SquadServiceResponse` object */ def Publish[ANY >: Any](to: ANY, msg: SquadResponse.Response): Unit = { - log.warn(s"Publish(Any): subscriber information is an unhandled format - $to") + log.warn(s"Publish(Any): subscriber information is an unhandled format - $to; message $msg dropped") } /** @@ -187,7 +187,7 @@ class SquadSubscriptionEntity { * @param excluded a group of character identifier numbers who should not receive the message */ def Publish[ANY >: Any](to: ANY, msg: SquadResponse.Response, excluded: Iterable[Long]): Unit = { - log.warn(s"Publish(Any): subscriber information is an unhandled format - $to") + log.warn(s"Publish(Any): subscriber information is an unhandled format - $to; message $msg dropped") } /* The following functions are related to common communications of squad information, mainly detail. */ @@ -211,7 +211,7 @@ class SquadSubscriptionEntity { } /** - * Dispatch an intial message entailing the strategic information and the composition of this squad. + * Dispatch an initial message entailing the strategic information and the composition of this squad. * The details of the squad will be updated in full and be sent to all indicated observers. * @see `SquadService.PublishFullDetails` * @param guid the unique squad identifier to be used when composing the details for this message From 1968377d054c5de2a38ac29edb38458ad505dd9a Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Mon, 16 Sep 2024 01:48:35 -0400 Subject: [PATCH 03/25] retooled invitation case classes into much more complicated invitation entities that manage their own behaviors for messaging invites, acceptances, rejection, and generalize queries; this removes a ton of match casting as a branch mechanic --- .../teamwork/SquadInvitationManager.scala | 2378 +++++++---------- .../services/teamwork/SquadService.scala | 660 +++-- .../teamwork/SquadServiceResponse.scala | 10 + .../teamwork/invitations/IndirectInvite.scala | 80 + .../teamwork/invitations/Invitation.scala | 75 + .../InvitationToCreateASquad.scala | 112 + .../invitations/InvitationToJoinSquad.scala | 95 + .../LookingForSquadRoleInvite.scala | 96 + .../invitations/ProximityInvite.scala | 104 + .../invitations/RequestToJoinSquadRole.scala | 84 + 10 files changed, 1867 insertions(+), 1827 deletions(-) create mode 100644 src/main/scala/net/psforever/services/teamwork/invitations/IndirectInvite.scala create mode 100644 src/main/scala/net/psforever/services/teamwork/invitations/Invitation.scala create mode 100644 src/main/scala/net/psforever/services/teamwork/invitations/InvitationToCreateASquad.scala create mode 100644 src/main/scala/net/psforever/services/teamwork/invitations/InvitationToJoinSquad.scala create mode 100644 src/main/scala/net/psforever/services/teamwork/invitations/LookingForSquadRoleInvite.scala create mode 100644 src/main/scala/net/psforever/services/teamwork/invitations/ProximityInvite.scala create mode 100644 src/main/scala/net/psforever/services/teamwork/invitations/RequestToJoinSquadRole.scala diff --git a/src/main/scala/net/psforever/services/teamwork/SquadInvitationManager.scala b/src/main/scala/net/psforever/services/teamwork/SquadInvitationManager.scala index 6622e1b8..d03d0739 100644 --- a/src/main/scala/net/psforever/services/teamwork/SquadInvitationManager.scala +++ b/src/main/scala/net/psforever/services/teamwork/SquadInvitationManager.scala @@ -4,18 +4,25 @@ package net.psforever.services.teamwork import akka.actor.ActorRef import akka.pattern.ask import akka.util.Timeout - -import scala.annotation.unused import scala.collection.mutable import scala.concurrent.duration._ import scala.concurrent.ExecutionContext.Implicits.global -import scala.util.Success +import scala.concurrent.Future // import net.psforever.objects.{LivePlayerList, Player} import net.psforever.objects.avatar.Avatar import net.psforever.objects.teamwork.{Member, SquadFeatures} import net.psforever.objects.zones.Zone -import net.psforever.packet.game.{SquadDetail, SquadPositionDetail, SquadPositionEntry, SquadAction => SquadRequestAction} +import net.psforever.packet.game.{SquadDetail, SquadPositionDetail, SquadPositionEntry} +import net.psforever.services.teamwork.invitations.{ + IndirectInvite, + Invitation, + InvitationToCreateASquad, + InvitationToJoinSquad, + LookingForSquadRoleInvite, + ProximityInvite, + RequestToJoinSquadRole +} import net.psforever.types.{PlanetSideGUID, SquadResponseType} class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { @@ -49,7 +56,7 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { If the player submits an invitation request for that squad, the current squad leader is cleared from the blocked list. When a squad leader refuses an invitation by a player, - that player will not be able to send further invitations (field on sqaud's features). + that player will not be able to send further invitations (field on squad's features). If the squad leader sends an invitation request for that player, the current player is cleared from the blocked list. */ @@ -58,7 +65,7 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { * key - a unique character identifier number; * value - a list of unique character identifier numbers; squad leaders or once-squad leaders */ - private val refused: mutable.LongMap[List[Long]] = mutable.LongMap[List[Long]]() + private val refusedPlayers: mutable.LongMap[List[Long]] = mutable.LongMap[List[Long]]() private[this] val log = org.log4s.getLogger @@ -66,49 +73,47 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { invites.clear() queuedInvites.clear() previousInvites.clear() - refused.clear() + refusedPlayers.clear() } + /* whenever a new player joins */ + def handleJoin(charId: Long): Unit = { - refused.put(charId, List[Long]()) + refusedPlayers.put(charId, List[Long]()) } - def createRequestRole(player: Player, features: SquadFeatures, position: Int): Unit = { + /* create invitations */ + + def createRequestToJoinSquadRole(player: Player, features: SquadFeatures, position: Int): Unit = { //we could join directly but we need permission from the squad leader first val charId = features.Squad.Leader.CharId - val requestRole = RequestRole(player, features, position) + val requestRole = RequestToJoinSquadRole(player, features, position) if (features.AutoApproveInvitationRequests) { - SquadActionMembershipAcceptInvite( - player, - charId, - Some(requestRole), - None - ) - } else { + requestRole.handleAcceptance(manager = this, player, charId, None) + } else if (addInvite(charId, requestRole).contains(requestRole)) { //circumvent tests in AddInviteAndRespond - InviteResponseTemplate(indirectInviteResp)( - requestRole, - AddInvite(charId, requestRole), + requestRole.handleInvitation(altIndirectInviteResp)( + manager = this, charId, - invitingPlayer = 0L, //we ourselves technically are ... + invitingPlayer = 0L, /* we ourselves technically are ... */ player.Name ) } } - def createVacancyInvite(player: Player, invitedPlayer: Long, features: SquadFeatures): Unit = { + def createInvitationToJoinSquad(player: Player, invitedPlayer: Long, features: SquadFeatures): Unit = { val invitingPlayer = player.CharId val squad = features.Squad - Allowed(invitedPlayer, invitingPlayer) + allowed(invitedPlayer, invitingPlayer) if (squad.Size == squad.Capacity) { log.debug(s"$invitingPlayer tried to invite $invitedPlayer to a squad without available positions") - } else if (Refused(invitingPlayer).contains(invitedPlayer)) { + } else if (refused(invitingPlayer).contains(invitedPlayer)) { log.debug(s"$invitedPlayer repeated a previous refusal to $invitingPlayer's invitation offer") } else { features.AllowedPlayers(invitingPlayer) - AddInviteAndRespond( + addInviteAndRespond( invitedPlayer, - VacancyInvite(invitingPlayer, player.Name, features), + InvitationToJoinSquad(invitingPlayer, player.Name, features), invitingPlayer, player.Name ) @@ -119,20 +124,20 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { val invitedPlayer = player.CharId val squad2 = features.Squad val leader = squad2.Leader.CharId - Allowed(invitedPlayer, invitingPlayer) - Allowed(leader, invitingPlayer) + allowed(invitedPlayer, invitingPlayer) + allowed(leader, invitingPlayer) lazy val preface = s"$invitingPlayer's invitation got reversed to $invitedPlayer's squad, but" if (squad2.Size == squad2.Capacity) { log.debug(s"$preface the squad has no available positions") - } else if (Refused(invitingPlayer).contains(invitedPlayer)) { + } else if (refused(invitingPlayer).contains(invitedPlayer)) { log.debug(s"$preface $invitedPlayer repeated a previous refusal to $invitingPlayer's invitation offer") - } else if (Refused(invitingPlayer).contains(leader)) { + } else if (refused(invitingPlayer).contains(leader)) { log.debug(s"$preface $leader repeated a previous refusal to $invitingPlayer's invitation offer") } else if (features.DeniedPlayers().contains(invitingPlayer)) { log.debug(s"$preface $invitingPlayer is denied the invitation") } else { features.AllowedPlayers(invitedPlayer) - AddInviteAndRespond( + addInviteAndRespond( leader, IndirectInvite(player, features), invitingPlayer, @@ -141,194 +146,25 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { } } - def createSpontaneousInvite(player: Player, invitedPlayer: Long): Unit = { + def createInvitationToCreateASquad(player: Player, invitedPlayer: Long): Unit = { //neither the invited player nor the inviting player belong to any squad val invitingPlayer = player.CharId - Allowed(invitedPlayer, invitingPlayer) - if (Refused(invitingPlayer).contains(invitedPlayer)) { + allowed(invitedPlayer, invitingPlayer) + if (refused(invitingPlayer).contains(invitedPlayer)) { log.debug(s"$invitedPlayer repeated a previous refusal to $invitingPlayer's invitation offer") - } else if (Refused(invitedPlayer).contains(invitingPlayer)) { + } else if (refused(invitedPlayer).contains(invitingPlayer)) { log.debug(s"$invitingPlayer repeated a previous refusal to $invitedPlayer's invitation offer") } else { - AddInviteAndRespond( + addInviteAndRespond( invitedPlayer, - SpontaneousInvite(player), + InvitationToCreateASquad(player), invitingPlayer, player.Name ) } } - def SquadActionMembershipAcceptInvite( - tplayer: Player, - invitedPlayer: Long, - acceptedInvite: Option[Invitation], - invitedPlayerSquadOpt: Option[SquadFeatures] - ): Unit = { - val availableForJoiningSquad = notLimitedByEnrollmentInSquad(invitedPlayerSquadOpt, invitedPlayer) - acceptedInvite match { - case Some(RequestRole(petitioner, features, position)) - if SquadInvitationManager.canEnrollInSquad(features, petitioner.CharId) => - //player requested to join a squad's specific position - //invitedPlayer is actually the squad leader; petitioner is the actual "invitedPlayer" - if (JoinSquad(petitioner, features, position)) { - DeliverAcceptanceMessages(invitedPlayer, petitioner.CharId, petitioner.Name) - CleanUpInvitesForSquadAndPosition(features, position) - } - - case Some(IndirectInvite(recruit, features)) - if SquadInvitationManager.canEnrollInSquad(features, recruit.CharId) => - //tplayer / invitedPlayer is actually the squad leader - val recruitCharId = recruit.CharId - HandleVacancyInvite(features, recruitCharId, invitedPlayer, recruit) match { - case Some((_, line)) => - DeliverAcceptanceMessages(invitedPlayer, recruitCharId, recruit.Name) - JoinSquad(recruit, features, line) - CleanUpAllInvitesWithPlayer(recruitCharId) - CleanUpInvitesForSquadAndPosition(features, line) - //TODO since we are the squad leader, we do not want to brush off our queued squad invite tasks - case _ => ; - } - - case Some(VacancyInvite(invitingPlayer, _, features)) - if availableForJoiningSquad && SquadInvitationManager.canEnrollInSquad(features, invitedPlayer) => - //accepted an invitation to join an existing squad - HandleVacancyInvite(features, invitedPlayer, invitingPlayer, tplayer) match { - case Some((_, line)) => - DeliverAcceptanceMessages(invitingPlayer, invitedPlayer, tplayer.Name) - JoinSquad(tplayer, features, line) - CleanUpQueuedInvites(invitedPlayer) - CleanUpInvitesForSquadAndPosition(features, line) - case _ => ; - } - - case Some(SpontaneousInvite(invitingPlayer)) - if availableForJoiningSquad => - SquadMembershipAcceptInviteAction(invitingPlayer, tplayer, invitedPlayer) - - case Some(LookingForSquadRoleInvite(member, features, position)) - if availableForJoiningSquad && SquadInvitationManager.canEnrollInSquad(features, invitedPlayer) => - val invitingPlayer = member.CharId - features.ProxyInvites = features.ProxyInvites.filterNot { _ == invitedPlayer } - if (JoinSquad(tplayer, features, position)) { - //join this squad - DeliverAcceptanceMessages(invitingPlayer, invitedPlayer, tplayer.Name) - CleanUpQueuedInvites(tplayer.CharId) - CleanUpInvitesForSquadAndPosition(features, position) - } - - case Some(ProximityInvite(member, features, position)) - if availableForJoiningSquad && SquadInvitationManager.canEnrollInSquad(features, invitedPlayer) => - val invitingPlayer = member.CharId - features.ProxyInvites = features.ProxyInvites.filterNot { _ == invitedPlayer } - if (JoinSquad(tplayer, features, position)) { - //join this squad - DeliverAcceptanceMessages(invitingPlayer, invitedPlayer, tplayer.Name) - CleanUpAllInvitesWithPlayer(invitedPlayer) - val squad = features.Squad - if (squad.Size == squad.Capacity) { - //all available squad positions filled; terminate all remaining invitations - CleanUpAllInvitesToSquad(features) - } - } else { - ReloadProximityInvite(tplayer.Zone.Players, invitedPlayer, features, position) //TODO ? - } - - case _ => - //the invite either timed-out or was withdrawn or is now invalid - (previousInvites.get(invitedPlayer) match { - case Some(SpontaneousInvite(leader)) => (leader.CharId, leader.Name) - case Some(VacancyInvite(charId, name, _)) => (charId, name) - case Some(ProximityInvite(member, _, _)) => (member.CharId, member.Name) - case Some(LookingForSquadRoleInvite(member, _, _)) => (member.CharId, member.Name) - case _ => (0L, "") - }) match { - case (0L, "") => ; - case (charId, name) => - subs.Publish( - charId, - SquadResponse.Membership(SquadResponseType.Cancel, 0, 0, charId, Some(0L), name, unk5 = false, Some(None)) - ) - } - } - } - - def DeliverAcceptanceMessages( - squadLeader: Long, - joiningPlayer: Long, - joiningPlayerName: String - ): Unit = { - val msg = SquadResponse.Membership( - SquadResponseType.Accept, - 0, - 0, - joiningPlayer, - Some(squadLeader), - joiningPlayerName, - unk5 = false, - Some(None) - ) - subs.Publish(squadLeader, msg) - subs.Publish(joiningPlayer, msg.copy(unk5 = true)) - } - - def notLimitedByEnrollmentInSquad(squadOpt: Option[SquadFeatures], charId: Long): Boolean = { - squadOpt match { - case Some(features) if features.Squad.Membership.exists { _.CharId == charId } => - EnsureEmptySquad(features) - case Some(_) => - false - case None => - true - } - } - - def SquadMembershipAcceptInviteAction(invitingPlayer: Player, player: Player, invitedPlayer: Long): Unit = { - //originally, we were invited by someone into a new squad they would form - val invitingPlayerCharId = invitingPlayer.CharId - if (invitingPlayerCharId != invitedPlayer) { - //generate a new squad, with invitingPlayer as the leader - val result = ask(parent, SquadService.PerformStartSquad(invitingPlayer)) - result.onComplete { - case Success(FinishStartSquad(features)) => - HandleVacancyInvite(features, player.CharId, invitingPlayerCharId, player) match { - case Some((_, line)) => - subs.Publish( - invitedPlayer, - SquadResponse.Membership( - SquadResponseType.Accept, - 0, - 0, - invitedPlayer, - Some(invitingPlayerCharId), - "", - unk5=true, - Some(None) - ) - ) - subs.Publish( - invitingPlayerCharId, - SquadResponse.Membership( - SquadResponseType.Accept, - 0, - 0, - invitingPlayerCharId, - Some(invitedPlayer), - player.Name, - unk5=false, - Some(None) - ) - ) - JoinSquad(player, features, line) - CleanUpQueuedInvites(player.CharId) - case _ => ; - } - case _ => ; - } - } - } - - def handleProximityInvite(zone: Zone, invitingPlayer: Long, features: SquadFeatures): Unit = { + def createProximityInvite(zone: Zone, invitingPlayer: Long, features: SquadFeatures): Unit = { val squad = features.Squad val sguid = squad.GUID val origSearchForRole = features.SearchForRole @@ -351,7 +187,7 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { newRecruitment = newRecruitment :+ key squad.Membership.zipWithIndex.filterNot { case (_, index) => index == position } case None => - CleanUpQueuedInvitesForSquadAndPosition(features, position) + cleanUpQueuedInvitesForSquadAndPosition(features, position) squad.Membership.zipWithIndex } case _ => @@ -378,15 +214,15 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { ) ) positionsToRecruitFor.foreach { case (_, position) => - FindSoldiersWithinScopeAndInvite( + findSoldiersWithinScopeAndInvite( squad.Leader, features, position, players, features.ProxyInvites ++ newRecruitment, - ProximityEnvelope + proximityEnvelope ) match { - case None => ; + case None => () case Some(id) => newRecruitment = newRecruitment :+ id } @@ -399,149 +235,39 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { //if searching for a position originally, convert the active invite to proximity invite, or remove it val key = newRecruitment.head (origSearchForRole, invites.get(key)) match { - case (Some(-1), _) => ; + case (Some(-1), _) => () case (Some(position), Some(LookingForSquadRoleInvite(member, _, _))) => invites(key) = ProximityInvite(member, features, position) - case _ => ; + case _ => () } } } - def handleAcceptance(player: Player, charId: Long, squadOpt: Option[SquadFeatures]): Unit = { - SquadActionMembershipAcceptInvite(player, charId, RemoveInvite(charId), squadOpt) - NextInviteAndRespond(charId) - } + /* invitation reloads */ - def handleRejection( - tplayer: Player, - rejectingPlayer: Long, - squadsToLeaders: List[(PlanetSideGUID, SquadFeatures)] - ): Unit = { - val rejectedBid = RemoveInvite(rejectingPlayer) - FormatRejection( - DoRejection(rejectedBid, tplayer, rejectingPlayer, squadsToLeaders), - tplayer - ) - NextInviteAndRespond(rejectingPlayer) - } - - def ParseRejection( - rejectedBid: Option[Invitation], - @unused tplayer: Player, - rejectingPlayer: Long, - squadsToLeaders: List[(PlanetSideGUID, SquadFeatures)] - ): (Option[Long], Option[Long]) = { - rejectedBid match { - case Some(SpontaneousInvite(leader)) => - //rejectingPlayer is the would-be squad member; the would-be squad leader sent the request and was rejected - val invitingPlayerCharId = leader.CharId - (Some(rejectingPlayer), Some(invitingPlayerCharId)) - - case Some(VacancyInvite(leader, _, _)) - /*if SquadInvitationManager.notLeaderOfThisSquad(squadsToLeaders, features.Squad.GUID, rejectingPlayer)*/ => - //rejectingPlayer is the would-be squad member; the squad leader sent the request and was rejected - (Some(rejectingPlayer), Some(leader)) - - case Some(ProximityInvite(_, _, _)) - /*if SquadInvitationManager.notLeaderOfThisSquad(squadsToLeaders, features.Squad.GUID, rejectingPlayer)*/ => - //rejectingPlayer is the would-be squad member; the squad leader sent the request and was rejected - (Some(rejectingPlayer), None) - - case Some(LookingForSquadRoleInvite(member, _, _)) - if member.CharId != rejectingPlayer => - val leaderCharId = member.CharId - //rejectingPlayer is the would-be squad member; the squad leader sent the request and was rejected - (Some(rejectingPlayer), Some(leaderCharId)) - - case Some(RequestRole(rejected, features, _)) - if SquadInvitationManager.notLeaderOfThisSquad(squadsToLeaders, features.Squad.GUID, rejected.CharId) => - //rejected is the would-be squad member; rejectingPlayer is the squad leader who rejected the request - (Some(rejectingPlayer), None) - - case _ => //TODO IndirectInvite, etc., but how to handle them? - (None, None) + def listCurrentInvitations(charId: Long): List[String] = { + ((invites.get(charId), queuedInvites.get(charId)) match { + case (Some(invite), Some(invites)) => + invite +: invites + case (Some(invite), None) => + List(invite) + case (None, Some(invites)) => + invites + case _ => + List() + }).collect { + case RequestToJoinSquadRole(player, _, _) => player.Name + case IndirectInvite(player, features) if !features.Squad.Leader.Name.equals(player.Name) => player.Name } } - def DoRejection( - rejectedBid: Option[Invitation], - tplayer: Player, - rejectingPlayer: Long, - squadsToLeaders: List[(PlanetSideGUID, SquadFeatures)] - ): (Option[Long], Option[Long], String) = { - rejectedBid match { - case Some(SpontaneousInvite(leader)) => - //rejectingPlayer is the would-be squad member; the would-be squad leader sent the request and was rejected - val invitingPlayerCharId = leader.CharId - Refused(rejectingPlayer, invitingPlayerCharId) - (Some(rejectingPlayer), Some(invitingPlayerCharId), "anonymous") - - case Some(VacancyInvite(leader, _, features)) - /*if SquadInvitationManager.notLeaderOfThisSquad(squadsToLeaders, features.Squad.GUID, rejectingPlayer)*/ => - //rejectingPlayer is the would-be squad member; the squad leader sent the request and was rejected - Refused(rejectingPlayer, leader) - (Some(rejectingPlayer), Some(leader), features.Squad.Task) - - case Some(ProximityInvite(_, features, position)) - /*if SquadInvitationManager.notLeaderOfThisSquad(squadsToLeaders, features.Squad.GUID, rejectingPlayer)*/ => - //rejectingPlayer is the would-be squad member; the squad leader sent the request and was rejected - ReloadProximityInvite( - tplayer.Zone.Players, - rejectingPlayer, - features, - position - ) - (Some(rejectingPlayer), None, features.Squad.Task) - - case Some(LookingForSquadRoleInvite(member, features, position)) - if member.CharId != rejectingPlayer => - val leaderCharId = member.CharId - //rejectingPlayer is the would-be squad member; the squad leader sent the request and was rejected - ReloadSearchForRoleInvite( - LivePlayerList.WorldPopulation { _ => true }, - rejectingPlayer, - features, - position - ) - (Some(rejectingPlayer), Some(leaderCharId), features.Squad.Task) - - case Some(RequestRole(rejected, features, _)) - if SquadInvitationManager.notLeaderOfThisSquad(squadsToLeaders, features.Squad.GUID, rejected.CharId) => - //rejected is the would-be squad member; rejectingPlayer is the squad leader who rejected the request - features.DeniedPlayers(rejected.CharId) - (Some(rejectingPlayer), None, features.Squad.Task) - - case _ => //TODO IndirectInvite, etc., but how to handle them? - (None, None, "n|a") - } + def reloadActiveInvite(charId: Long): Unit = { + invites + .get(charId) + .foreach(respondToInvite(charId, _)) } - def FormatRejection( - rejectedPair: (Option[Long], Option[Long], String), - tplayer: Player - ): Unit = { - rejectedPair match { - case (Some(rejected), Some(inviter), squadName) => - subs.Publish( - rejected, - SquadResponse.Membership(SquadResponseType.Reject, 0, 0, rejected, Some(inviter), "", unk5=true, Some(None)) - ) - subs.Publish( - inviter, - SquadResponse.Membership(SquadResponseType.Reject, 0, 0, inviter, Some(rejected), tplayer.Name, unk5=false, Some(None)) - ) - subs.Publish(rejected, SquadResponse.SquadRelatedComment(s"Your request to join squad '$squadName' has been refused.")) - case (Some(rejected), None, squadName) => - subs.Publish( - rejected, - SquadResponse.Membership(SquadResponseType.Reject, 0, 0, rejected, Some(rejected), "", unk5=true, Some(None)) - ) - subs.Publish(rejected, SquadResponse.SquadRelatedComment(s"Your request to join squad '$squadName' has been refused.")) - case _ => () - } - } - - def ReloadSearchForRoleInvite( + def reloadSearchForRoleInvite( scope: List[Avatar], rejectingPlayer: Long, features: SquadFeatures, @@ -549,15 +275,15 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { ) : Unit = { //rejectingPlayer is the would-be squad member; the squad leader rejected the request val squadLeader = features.Squad.Leader - Refused(rejectingPlayer, squadLeader.CharId) + refused(rejectingPlayer, squadLeader.CharId) features.ProxyInvites = features.ProxyInvites.filterNot { _ == rejectingPlayer } - FindSoldiersWithinScopeAndInvite( + findSoldiersWithinScopeAndInvite( squadLeader, features, position, scope, features.ProxyInvites, - LookingForSquadRoleEnvelope + lookingForSquadRoleEnvelope ) match { case None => if (features.SearchForRole.contains(position) && features.ProxyInvites.isEmpty) { @@ -572,7 +298,7 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { } } - def ReloadProximityInvite( + def reloadProximityInvite( scope: List[Avatar], rejectingPlayer: Long, features: SquadFeatures, @@ -580,15 +306,15 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { ): Unit = { //rejectingPlayer is the would-be squad member; the squad leader rejected the request val squadLeader = features.Squad.Leader - Refused(rejectingPlayer, squadLeader.CharId) + refused(rejectingPlayer, squadLeader.CharId) features.ProxyInvites = features.ProxyInvites.filterNot { _ == rejectingPlayer } - FindSoldiersWithinScopeAndInvite( + findSoldiersWithinScopeAndInvite( squadLeader, features, position, scope, features.ProxyInvites, - ProximityEnvelope + proximityEnvelope ) match { case None => if (features.SearchForRole.contains(-1) && features.ProxyInvites.isEmpty) { @@ -600,25 +326,219 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { } } + /* acceptance */ + + def handleAcceptance(player: Player, charId: Long, squadOpt: Option[SquadFeatures]): Unit = { + removeInvite(charId) + .map { invite => + invite.handleAcceptance(manager = this, player, charId, squadOpt) + invite + } + .orElse { + //the invite either timed-out or was withdrawn or is now invalid + //originally, only InvitationToCreateASquad, InvitationToJoinSquad, ProximityInvite, LookingForSquadRoleInvite + previousInvites + .get(charId) + .map { invite => (invite.inviterCharId, invite.inviterName) } match { + case Some((0L, "")) => () + case Some((charId, name)) => + subs.Publish( + charId, + SquadResponse.Membership(SquadResponseType.Cancel, charId, Some(0L), name, unk5 = false) + ) + } + None + } + nextInviteAndRespond(charId) + } + + def tryChainAcceptance( + inviter: Player, + charId: Long, + list: List[Long], + features: SquadFeatures + ): Unit = { + //filter queued invites + lazy val squadName = features.Squad.Task + var foundPairs: List[(Player, Invitation)] = List() + val unmatchedInvites = queuedInvites + .getOrElse(charId, Nil) + .filter { + case invite @ RequestToJoinSquadRole(invitee, _, _) + if list.contains(invitee.CharId) && !features.Squad.Leader.Name.equals(invitee.Name) => + foundPairs = foundPairs :+ (invitee, invite) + false + case invite @ IndirectInvite(invitee, _) + if list.contains(invitee.CharId) && !features.Squad.Leader.Name.equals(invitee.Name) => + foundPairs = foundPairs :+ (invitee, invite) + false + case _ => + true + } + //handle active invite + val clearedActiveInvite = invites + .get(charId) + .collect { + case invite @ RequestToJoinSquadRole(invitee, _, _) + if list.contains(invitee.CharId) && !features.Squad.Leader.Name.equals(invitee.Name) => + invite.handleAcceptance(manager = this, inviter, invitee.CharId, Some(features)) + invites.remove(charId) + true + case invite @ IndirectInvite(invitee, _) + if list.contains(invitee.CharId) && !features.Squad.Leader.Name.equals(invitee.Name) => + invite.handleAcceptance(manager = this, inviter, invitee.CharId, Some(features)) + invites.remove(charId) + true + case _ => + false + } + //handle selected queued invites + val pairIterator = foundPairs.iterator + while (pairIterator.hasNext && features.Squad.Capacity < features.Squad.Size) { + val (player, invite) = pairIterator.next() + invite.handleAcceptance(manager = this, inviter, player.CharId, Some(features)) + } + //evaluate final squad composition + if (features.Squad.Capacity < features.Squad.Size) { + //replace unfiltered invites + if (unmatchedInvites.isEmpty) { + queuedInvites.remove(charId) + } else { + queuedInvites.put(charId, unmatchedInvites) + } + //manage next invitation + clearedActiveInvite.collect { + case true => nextInviteAndRespond(charId) + } + } else { + //squad is full + previousInvites.remove(charId) + queuedInvites.remove(charId) + clearedActiveInvite.collect { + case true => invites.remove(charId) + } + unmatchedInvites.foreach { _ => + subs.Publish(inviter.CharId, SquadResponse.SquadRelatedComment(s"Your request to join squad '$squadName' has been refused.")) + } + nextInviteAndRespond(charId) + } + //clean up any incomplete selected invites + pairIterator.foreach { case (_, _) => + subs.Publish(inviter.CharId, SquadResponse.SquadRelatedComment(s"Your request to join squad '$squadName' has been refused.")) + } + } + + def acceptanceMessages( + squadLeader: Long, + joiningPlayer: Long, + joiningPlayerName: String + ): Unit = { + val msg = SquadResponse.Membership(SquadResponseType.Accept, joiningPlayer, Some(squadLeader), joiningPlayerName, unk5 = false) + subs.Publish(squadLeader, msg) + subs.Publish(joiningPlayer, msg.copy(unk5 = true)) + } + + /* rejection */ + + def handleRejection( + tplayer: Player, + rejectingPlayer: Long, + squadsToLeaders: List[(PlanetSideGUID, SquadFeatures)] + ): Unit = { + removeInvite(rejectingPlayer) + .foreach { invite => + invite.handleRejection(manager = this, tplayer, rejectingPlayer, squadsToLeaders) + invite + } + nextInviteAndRespond(rejectingPlayer) + } + + def tryChainRejection( + inviter: Player, + charId: Long, + list: List[Long], + features: SquadFeatures): Unit = { + //handle queued invites + lazy val squadName = features.Squad.Task + val unmatchedInvites = queuedInvites + .getOrElse(charId, Nil) + .filter { + case invite @ RequestToJoinSquadRole(invitee, _, _) + if list.contains(invitee.CharId) && !features.Squad.Leader.Name.equals(invitee.Name) => + invite.doRejection(manager = this, inviter, charId) + subs.Publish(charId, SquadResponse.SquadRelatedComment(s"Your request to join squad '$squadName' has been refused.")) + false + case invite @ IndirectInvite(invitee, _) + if list.contains(invitee.CharId) && !features.Squad.Leader.Name.equals(invitee.Name) => + invite.doRejection(manager = this, inviter, charId) + subs.Publish(charId, SquadResponse.SquadRelatedComment(s"Your request to join squad '$squadName' has been refused.")) + false + case _ => + true + } + queuedInvites.put(charId, unmatchedInvites) + //handle active invite + invites + .get(charId) + .collect { + case invite @ RequestToJoinSquadRole(player, features, _) + if list.contains(player.CharId) && !features.Squad.Leader.Name.equals(player.Name) => + invite.doRejection(manager = this, inviter, charId) + subs.Publish(charId, SquadResponse.SquadRelatedComment(s"Your request to join squad '$squadName' has been refused.")) + invites.remove(charId) + nextInviteAndRespond(charId) + case invite @ IndirectInvite(player, features) + if list.contains(player.CharId) && !features.Squad.Leader.Name.equals(player.Name) => + invite.doRejection(manager = this, inviter, charId) + subs.Publish(charId, SquadResponse.SquadRelatedComment(s"Your request to join squad '$squadName' has been refused.")) + invites.remove(charId) + nextInviteAndRespond(charId) + case _ => () + } + } + + def tryChainRejectionAll(charId: Long, features: SquadFeatures): Unit = { + val squadName = features.Squad.Task + cleanUpAllInvitesToSquad(features) + .filterNot(_ == charId) + .foreach { refusedId => + subs.Publish(refusedId, SquadResponse.SquadRelatedComment(s"Your request to join squad '$squadName' has been refused.")) + } + } + + def rejectionMessage(rejectingPlayer: Long): Unit = { + subs.Publish( + rejectingPlayer, + SquadResponse.Membership(SquadResponseType.Reject, rejectingPlayer, Some(rejectingPlayer), "", unk5 = true) + ) + } + + def rejectionMessages( + rejectingPlayer: Long, + leaderCharId: Long, + name: String + ): Unit = { + subs.Publish( + rejectingPlayer, + SquadResponse.Membership(SquadResponseType.Reject, rejectingPlayer, Some(leaderCharId), "", unk5 = true) + ) + subs.Publish( + leaderCharId, + SquadResponse.Membership(SquadResponseType.Reject, leaderCharId, Some(rejectingPlayer), name, unk5 = false) + ) + } + + /* other special actions */ + def handleDisbanding(features: SquadFeatures): Unit = { - CleanUpAllInvitesToSquad(features) + cleanUpAllInvitesToSquad(features) } def handleCancelling(cancellingPlayer: Long): Unit = { - //get rid of SpontaneousInvite objects and VacancyInvite objects - invites.collect { - case (id, invite: SpontaneousInvite) if invite.InviterCharId == cancellingPlayer => RemoveInvite(id) - case (id, invite: VacancyInvite) if invite.InviterCharId == cancellingPlayer => RemoveInvite(id) - case (id, invite: LookingForSquadRoleInvite) if invite.InviterCharId == cancellingPlayer => RemoveInvite(id) - } + invites.collect { case (id, invite) if invite.appliesToPlayer(cancellingPlayer) => removeInvite(id) } queuedInvites.foreach { case (id: Long, inviteList) => - val inList = inviteList.filterNot { - case invite: SpontaneousInvite if invite.InviterCharId == cancellingPlayer => true - case invite: VacancyInvite if invite.InviterCharId == cancellingPlayer => true - case invite: LookingForSquadRoleInvite if invite.InviterCharId == cancellingPlayer => true - case _ => false - } + val inList = inviteList.filterNot(_.appliesToPlayer(cancellingPlayer)) if (inList.isEmpty) { queuedInvites.remove(id) } else { @@ -626,73 +546,101 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { } } //get rid of ProximityInvite objects - CleanUpAllProximityInvites(cancellingPlayer) + cleanUpAllProximityInvites(cancellingPlayer) } + def handleClosingSquad(features: SquadFeatures): Unit = { + cleanUpAllInvitesToSquad(features) + } + + def handleCleanup(charId: Long): Unit = { + cleanUpAllInvitesWithPlayer(charId) + } + + def handleLeave(charId: Long): Unit = { + refusedPlayers.remove(charId) + cleanUpAllInvitesWithPlayer(charId) + } + + /* other special actions, promotion */ + def handlePromotion( - sponsoringPlayer: Long, - promotedPlayer: Long, - ): Unit = { - ShiftInvitesToPromotedSquadLeader(sponsoringPlayer, promotedPlayer) + sponsoringPlayer: Long, + promotedPlayer: Long, + ): Unit = { + shiftInvitesToPromotedSquadLeader(sponsoringPlayer, promotedPlayer) } - def handleDefinitionAction( - player: Player, - action: SquadRequestAction, - features: SquadFeatures - ): Unit = { - import SquadRequestAction._ - action match { - //the following actions cause changes with the squad composition or with invitations - case AutoApproveInvitationRequests(_) => - SquadActionDefinitionAutoApproveInvitationRequests(player, features) - case FindLfsSoldiersForRole(position) => - SquadActionDefinitionFindLfsSoldiersForRole(player, position, features) - case CancelFind() => - SquadActionDefinitionCancelFind(None) - case SelectRoleForYourself(position) => - SquadActionDefinitionSelectRoleForYourselfAsInvite(player, features, position) - case _: CancelSelectRoleForYourself => - SquadActionDefinitionCancelSelectRoleForYourself(player, features) - case _ => ; + def shiftInvitesToPromotedSquadLeader( + sponsoringPlayer: Long, + promotedPlayer: Long + ): Unit = { + val leaderInvite = invites.remove(sponsoringPlayer) + val leaderQueuedInvites = queuedInvites.remove(sponsoringPlayer).toList.flatten + val (invitesToConvert, invitesToAppend) = (invites.remove(promotedPlayer).orElse(previousInvites.get(promotedPlayer)), leaderInvite) match { + case (Some(activePromotedInvite), Some(outLeaderInvite)) => + //the promoted player has an active invite; queue these + val promotedQueuedInvites = queuedInvites.remove(promotedPlayer).toList.flatten + nextInvite(promotedPlayer) + nextInvite(sponsoringPlayer) + (activePromotedInvite +: (outLeaderInvite +: leaderQueuedInvites), promotedQueuedInvites) + + case (Some(activePromotedInvite), None) => + //the promoted player has an active invite; queue these + val promotedQueuedInvites = queuedInvites.remove(promotedPlayer).toList.flatten + nextInvite(promotedPlayer) + (activePromotedInvite :: leaderQueuedInvites, promotedQueuedInvites) + + case (None, Some(outLeaderInvite)) => + //no active invite for the promoted player, but the leader had an active invite; trade the queued invites + nextInvite(sponsoringPlayer) + (outLeaderInvite +: leaderQueuedInvites, queuedInvites.remove(promotedPlayer).toList.flatten) + + case (None, None) => + //no active invites for anyone; assign the first queued invite from the promoting player, if available, and queue the rest + (leaderQueuedInvites, queuedInvites.remove(promotedPlayer).toList.flatten) + } + //move over promoted invites + invitesToConvert ++ invitesToAppend match { + case Nil => () + case x :: Nil => + addInviteAndRespond(promotedPlayer, x, x.inviterCharId, x.inviterName) + case x :: xs => + addInviteAndRespond(promotedPlayer, x, x.inviterCharId, x.inviterName) + queuedInvites += promotedPlayer -> xs } } - def SquadActionDefinitionAutoApproveInvitationRequests( - tplayer: Player, - features: SquadFeatures - ): Unit = { - SquadActionDefinitionAutoApproveInvitationRequests(tplayer.CharId, features) - } + /* squad definition features */ - def SquadActionDefinitionAutoApproveInvitationRequests( - charId: Long, - features: SquadFeatures - ): Unit = { + def autoApproveInvitationRequests( + charId: Long, + features: SquadFeatures + ): Unit = { //allowed auto-approval - resolve the requests (only) val (requests, others) = (invites.get(charId) match { case Some(invite) => invite +: queuedInvites.getOrElse(charId, Nil) case None => queuedInvites.getOrElse(charId, Nil) }) - .partition({ case _: RequestRole => true; case _ => false }) + .partition({ case _: RequestToJoinSquadRole => true; case _ => false }) invites.remove(charId) queuedInvites.remove(charId) previousInvites.remove(charId) - //RequestRole invitations that still have to be handled + //RequestToJoinSquadRole invitations that still have to be handled val squad = features.Squad var remainingRequests = requests.collect { - case request: RequestRole => (request, request.player) + case request: RequestToJoinSquadRole => (request, request.requestee) } var unfulfilled = List[Player]() //give roles to people who requested specific positions (1 to 9).foreach { position => val (discovered, remainder) = remainingRequests.partition { - case (request: RequestRole, _) => request.position == position + case (request: RequestToJoinSquadRole, _) => request.position == position case _ => false } unfulfilled ++= (discovered - .find { case (_, player) => JoinSquad(player, features, position) } match { + .find { case (_, player) => joinSquad(player, features, position) } match { case Some((_, player)) => remainingRequests = remainder.filterNot { case (_, p) => p.CharId == player.CharId } discovered.filterNot { case (_, p) => p.CharId == player.CharId } @@ -704,34 +652,34 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { //fill any incomplete role by trying to match all sorts of unfulfilled invitations var otherInvites = unfulfilled ++ others.collect { - case invite: SpontaneousInvite => invite.player - case invite: IndirectInvite => invite.player + case invite: InvitationToCreateASquad => invite.futureSquadLeader + case invite: IndirectInvite => invite.recruitOrOwner } .distinctBy { _.CharId } (1 to 9).foreach { position => if (squad.Availability(position)) { otherInvites.zipWithIndex.find { case (invitedPlayer, _) => - JoinSquad(invitedPlayer, features, position) + joinSquad(invitedPlayer, features, position) } match { case Some((_, index)) => otherInvites = otherInvites.take(index) ++ otherInvites.drop(index+1) - case None => ; + case None => () } } } //cleanup searches by squad leader features.SearchForRole match { - case Some(-1) => CleanUpAllProximityInvites(charId) - case Some(_) => SquadActionDefinitionCancelFind(Some(features)) - case None => ; + case Some(-1) => cleanUpAllProximityInvites(charId) + case Some(_) => cancelFind(Some(features)) + case None => () } } - def SquadActionDefinitionFindLfsSoldiersForRole( - tplayer: Player, - position: Int, - features: SquadFeatures - ): Unit = { + def findLfsSoldiersForRole( + tplayer: Player, + features: SquadFeatures, + position: Int + ): Unit = { val squad = features.Squad val sguid = squad.GUID (features.SearchForRole match { @@ -748,7 +696,7 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { case Some(pos) => //some other role is undergoing recruitment; cancel and redirect efforts for new position features.SearchForRole = None - CleanUpQueuedInvitesForSquadAndPosition(features, pos) + cleanUpQueuedInvitesForSquadAndPosition(features, pos) Some( invites.filter { case (_, LookingForSquadRoleInvite(_, _features, role)) => _features.Squad.GUID == sguid && role == pos @@ -770,15 +718,15 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { ) ) //search! - FindSoldiersWithinScopeAndInvite( + findSoldiersWithinScopeAndInvite( squad.Leader, features, position, LivePlayerList.WorldPopulation { _ => true }, list, - LookingForSquadRoleEnvelope + lookingForSquadRoleEnvelope ) match { - case None => ; + case None => () case Some(id) => features.ProxyInvites = List(id) features.SearchForRole = position @@ -786,7 +734,7 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { } } - def SquadActionDefinitionCancelFind(lSquadOpt: Option[SquadFeatures]): Unit = { + def cancelFind(lSquadOpt: Option[SquadFeatures]): Unit = { lSquadOpt match { case Some(features) => features.SearchForRole match { @@ -802,7 +750,7 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { } .keys .foreach { charId => - RemoveInvite(charId) + removeInvite(charId) } //remove queued invites queuedInvites.foreach { @@ -818,90 +766,77 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { } //remove yet-to-be invitedPlayers features.ProxyInvites = Nil - case _ => ; + case _ => () } - case _ => ; + case _ => () } } - /** the following action can be performed by an unaffiliated player */ - def SquadActionDefinitionSelectRoleForYourselfAsInvite( - tplayer: Player, - features: SquadFeatures, - position: Int - ): Unit = { + /* the following action can be performed by an unaffiliated player */ + def selectRoleForYourselfAsInvite( + tplayer: Player, + features: SquadFeatures, + position: Int + ): Unit = { //not a member of any squad, but we might become a member of this one val squad = features.Squad if (squad.isAvailable(position, tplayer.avatar.certifications)) { //we could join directly but we need permission from the squad leader first if (features.AutoApproveInvitationRequests) { - SquadActionMembershipAcceptInvite( - tplayer, - squad.Leader.CharId, - Some(RequestRole(tplayer, features, position)), - None - ) + invitations.RequestToJoinSquadRole(tplayer, features, position).handleAcceptance(manager = this, tplayer, squad.Leader.CharId, None) } else { //circumvent tests in AddInviteAndRespond - val requestRole = RequestRole(tplayer, features, position) val charId = squad.Leader.CharId - InviteResponseTemplate(indirectInviteResp)( - requestRole, - AddInvite(charId, requestRole), - charId, - invitingPlayer = 0L, //we ourselves technically are ... - tplayer.Name - ) + val requestRole = invitations.RequestToJoinSquadRole(tplayer, features, position) + if (addInvite(charId, requestRole).contains(requestRole)) { + requestRole.handleInvitation(indirectInviteResp)( + manager = this, + charId, + invitingPlayer = 0L, //we ourselves technically are ... + tplayer.Name + ) + } } } } - /** the following action can be performed by anyone who has tried to join a squad */ - def SquadActionDefinitionCancelSelectRoleForYourself( - tplayer: Player, - features: SquadFeatures - ): Unit = { + /* the following action can be performed by anyone who has tried to join a squad */ + def cancelSelectRoleForYourself( + tplayer: Player, + features: SquadFeatures + ): Unit = { val cancellingPlayer = tplayer.CharId //assumption: a player who is cancelling will rarely end up with their invite queued val squad = features.Squad val leaderCharId = squad.Leader.CharId - //clean up any active RequestRole invite entry where we are the player who wants to join the leader's squad + //clean up any active RequestToJoinSquadRole invite entry where we are the player who wants to join the leader's squad ((invites.get(leaderCharId) match { case out @ Some(entry) - if entry.isInstanceOf[RequestRole] && - entry.asInstanceOf[RequestRole].player.CharId == cancellingPlayer => + if entry.isInstanceOf[RequestToJoinSquadRole] && + entry.asInstanceOf[RequestToJoinSquadRole].requestee.CharId == cancellingPlayer => out case _ => None }) match { - case Some(entry: RequestRole) => - RemoveInvite(leaderCharId) + case Some(entry: RequestToJoinSquadRole) => + removeInvite(leaderCharId) subs.Publish( leaderCharId, - SquadResponse.Membership( - SquadResponseType.Cancel, - 0, - 0, - cancellingPlayer, - None, - entry.player.Name, - unk5=false, - Some(None) - ) + SquadResponse.Membership(SquadResponseType.Cancel, cancellingPlayer, None, entry.requestee.Name, unk5 = false ) ) - NextInviteAndRespond(leaderCharId) + nextInviteAndRespond(leaderCharId) Some(true) case _ => None }).orElse( - //look for a queued RequestRole entry where we are the player who wants to join the leader's squad + //look for a queued RequestToJoinSquadRole entry where we are the player who wants to join the leader's squad (queuedInvites.get(leaderCharId) match { case Some(_list) => ( _list, _list.indexWhere { entry => - entry.isInstanceOf[RequestRole] && - entry.asInstanceOf[RequestRole].player.CharId == cancellingPlayer + entry.isInstanceOf[RequestToJoinSquadRole] && + entry.asInstanceOf[RequestToJoinSquadRole].requestee.CharId == cancellingPlayer } ) case None => @@ -910,36 +845,18 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { case (_, -1) => None //no change case (list, _) if list.size == 1 => - val entry = list.head.asInstanceOf[RequestRole] + val entry = list.head.asInstanceOf[RequestToJoinSquadRole] subs.Publish( leaderCharId, - SquadResponse.Membership( - SquadResponseType.Cancel, - 0, - 0, - cancellingPlayer, - None, - entry.player.Name, - unk5=false, - Some(None) - ) + SquadResponse.Membership(SquadResponseType.Cancel, cancellingPlayer, None, entry.requestee.Name, unk5 = false) ) queuedInvites.remove(leaderCharId) Some(true) case (list, index) => - val entry = list(index).asInstanceOf[RequestRole] + val entry = list(index).asInstanceOf[RequestToJoinSquadRole] subs.Publish( leaderCharId, - SquadResponse.Membership( - SquadResponseType.Cancel, - 0, - 0, - cancellingPlayer, - None, - entry.player.Name, - unk5=false, - Some(None) - ) + SquadResponse.Membership(SquadResponseType.Cancel, cancellingPlayer, None, entry.requestee.Name, unk5 = false) ) queuedInvites(leaderCharId) = list.take(index) ++ list.drop(index + 1) Some(true) @@ -947,255 +864,180 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { ) } - def handleClosingSquad(features: SquadFeatures): Unit = { - CleanUpAllInvitesToSquad(features) + /* squad interaction */ + + def askToCreateANewSquad(invitingPlayer: Player): Future[Any] = { + //originally, we were invited by someone into a new squad they would form + //generate a new squad, with invitingPlayer as the leader + ask(parent, SquadService.PerformStartSquad(invitingPlayer)) } - def handleCleanup(charId: Long): Unit = { - CleanUpAllInvitesWithPlayer(charId) - } - - def handleLeave(charId: Long): Unit = { - refused.remove(charId) - CleanUpAllInvitesWithPlayer(charId) - } - - def resendActiveInvite(charId: Long): Unit = { - invites.get(charId) match { - case Some(invite) => - RespondToInvite(charId, invite) - case None => ; - } - } - - def listCurrentInvitations(charId: Long): List[String] = { - ((invites.get(charId), queuedInvites.get(charId)) match { - case (Some(invite), Some(invites)) => - invite +: invites - case (Some(invite), None) => - List(invite) - case (None, Some(invites)) => - invites - case _ => - List() - }).collect { - case RequestRole(player, _, _) => player.Name - case IndirectInvite(player, features) if !features.Squad.Leader.Name.equals(player.Name) => player.Name - } - } - - def tryChainAcceptance( - inviter: Player, - charId: Long, - list: List[Long], - features: SquadFeatures - ): Unit = { - //filter queued invites - lazy val squadToLeader = List((features.Squad.GUID, features)) - lazy val squadName = features.Squad.Task - var foundPairs: List[(Player, Invitation)] = List() - val unmatchedInvites = queuedInvites - .getOrElse(charId, Nil) - .filter { - case invite @ RequestRole(invitee, _, _) - if list.contains(invitee.CharId) && !features.Squad.Leader.Name.equals(invitee.Name) => - foundPairs = foundPairs :+ (invitee, invite) - false - case invite @ IndirectInvite(invitee, _) - if list.contains(invitee.CharId) && !features.Squad.Leader.Name.equals(invitee.Name) => - foundPairs = foundPairs :+ (invitee, invite) - false - case _ => - true - } - //handle active invite - val clearedActiveInvite = invites - .get(charId) - .collect { - case invite @ RequestRole(invitee, _, _) - if list.contains(invitee.CharId) && !features.Squad.Leader.Name.equals(invitee.Name) => - SquadActionMembershipAcceptInvite(inviter, invitee.CharId, Some(invite), Some(features)) - invites.remove(charId) - true - case invite @ IndirectInvite(invitee, _) - if list.contains(invitee.CharId) && !features.Squad.Leader.Name.equals(invitee.Name) => - SquadActionMembershipAcceptInvite(inviter, invitee.CharId, Some(invite), Some(features)) - invites.remove(charId) - true - case _ => - false - } - //handle selected queued invites - val pairIterator = foundPairs.iterator - while (pairIterator.hasNext && features.Squad.Capacity < features.Squad.Size) { - val (player, invite) = pairIterator.next() - SquadActionMembershipAcceptInvite(inviter, player.CharId, Some(invite), Some(features)) - } - //evaluate final squad composition - if (features.Squad.Capacity < features.Squad.Size) { - //replace unfiltered invites - if (unmatchedInvites.isEmpty) { - queuedInvites.remove(charId) - } else { - queuedInvites.put(charId, unmatchedInvites) - } - //manage next invitation - clearedActiveInvite.collect { - case true => NextInviteAndRespond(charId) - } - } else { - //squad is full - previousInvites.remove(charId) - queuedInvites.remove(charId) - clearedActiveInvite.collect { - case true => invites.remove(charId) - } - unmatchedInvites.foreach { invite => - val (refusedId, _) = ParseRejection(Some(invite), inviter, inviter.CharId, squadToLeader) - subs.Publish(refusedId, SquadResponse.SquadRelatedComment(s"Your request to join squad '$squadName' has been refused.")) - } - NextInviteAndRespond(charId) - } - //clean up any incomplete selected invites - pairIterator.foreach { case (_, invite) => - val (refusedId, _) = ParseRejection(Some(invite), inviter, inviter.CharId, squadToLeader) - subs.Publish(refusedId, SquadResponse.SquadRelatedComment(s"Your request to join squad '$squadName' has been refused.")) - } - } - - def tryChainRejection( - inviter: Player, - charId: Long, - list: List[Long], - features: SquadFeatures): Unit = { - //handle queued invites - lazy val squadToLeader = List((features.Squad.GUID, features)) - lazy val squadName = features.Squad.Task - val unmatchedInvites = queuedInvites - .getOrElse(charId, Nil) - .filter { - case invite @ RequestRole(invitee, _, _) - if list.contains(invitee.CharId) && !features.Squad.Leader.Name.equals(invitee.Name) => - val (refusedId, _, _) = DoRejection(Some(invite), inviter, charId, squadToLeader) - subs.Publish(refusedId, SquadResponse.SquadRelatedComment(s"Your request to join squad '$squadName' has been refused.")) - false - case invite @ IndirectInvite(invitee, _) - if list.contains(invitee.CharId) && !features.Squad.Leader.Name.equals(invitee.Name) => - val (refusedId, _, _) = DoRejection(Some(invite), inviter, charId, squadToLeader) - subs.Publish(refusedId, SquadResponse.SquadRelatedComment(s"Your request to join squad '$squadName' has been refused.")) - false - case _ => - true - } - queuedInvites.put(charId, unmatchedInvites) - //handle active invite - invites - .get(charId) - .collect { - case invite @ RequestRole(player, features, _) - if list.contains(player.CharId) && !features.Squad.Leader.Name.equals(player.Name) => - val (refusedId, _, _) = DoRejection(Some(invite), inviter, charId, squadToLeader) - subs.Publish(refusedId, SquadResponse.SquadRelatedComment(s"Your request to join squad '$squadName' has been refused.")) - invites.remove(charId) - NextInviteAndRespond(charId) - case invite @ IndirectInvite(player, features) - if list.contains(player.CharId) && !features.Squad.Leader.Name.equals(player.Name) => - val (refusedId, _, _) = DoRejection(Some(invite), inviter, charId, squadToLeader) - subs.Publish(refusedId, SquadResponse.SquadRelatedComment(s"Your request to join squad '$squadName' has been refused.")) - invites.remove(charId) - NextInviteAndRespond(charId) - case _ => () - } - } - - def tryChainRejectionAll(charId: Long, features: SquadFeatures): Unit = { - val squadName = features.Squad.Task - CleanUpAllInvitesToSquad(features) - .filterNot(_ == charId) - .foreach { refusedId => - subs.Publish(refusedId, SquadResponse.SquadRelatedComment(s"Your request to join squad '$squadName' has been refused.")) - } - } - - def ShiftInvitesToPromotedSquadLeader( - sponsoringPlayer: Long, - promotedPlayer: Long - ): Unit = { - val leaderInvite = invites.remove(sponsoringPlayer) - val leaderQueuedInvites = queuedInvites.remove(sponsoringPlayer).toList.flatten - val (invitesToConvert, invitesToAppend) = (invites.remove(promotedPlayer).orElse(previousInvites.get(promotedPlayer)), leaderInvite) match { - case (Some(activePromotedInvite), Some(outLeaderInvite)) => - //the promoted player has an active invite; queue these - val promotedQueuedInvites = queuedInvites.remove(promotedPlayer).toList.flatten - NextInvite(promotedPlayer) - NextInvite(sponsoringPlayer) - (activePromotedInvite +: (outLeaderInvite +: leaderQueuedInvites), promotedQueuedInvites) - - case (Some(activePromotedInvite), None) => - //the promoted player has an active invite; queue these - val promotedQueuedInvites = queuedInvites.remove(promotedPlayer).toList.flatten - NextInvite(promotedPlayer) - (activePromotedInvite :: leaderQueuedInvites, promotedQueuedInvites) - - case (None, Some(outLeaderInvite)) => - //no active invite for the promoted player, but the leader had an active invite; trade the queued invites - NextInvite(sponsoringPlayer) - (outLeaderInvite +: leaderQueuedInvites, queuedInvites.remove(promotedPlayer).toList.flatten) - - case (None, None) => - //no active invites for anyone; assign the first queued invite from the promoting player, if available, and queue the rest - (leaderQueuedInvites, queuedInvites.remove(promotedPlayer).toList.flatten) - } - moveOverPromotedInvites(promotedPlayer, invitesToConvert, invitesToAppend) - } - - def moveOverPromotedInvites( - targetPlayer: Long, - convertableInvites: List[Invitation], - otherInvitations: List[Invitation] - ): Unit = { - convertableInvites ++ otherInvitations match { - case Nil => ; - case x :: Nil => - AddInviteAndRespond(targetPlayer, x, x.InviterCharId, x.InviterName) - case x :: xs => - AddInviteAndRespond(targetPlayer, x, x.InviterCharId, x.InviterName) - queuedInvites += targetPlayer -> xs + def notLimitedByEnrollmentInSquad(squadOpt: Option[SquadFeatures], charId: Long): Boolean = { + squadOpt match { + case Some(features) if features.Squad.Membership.exists { _.CharId == charId } => + ensureEmptySquad(features) + case Some(_) => + false + case None => + true } } /** - * Enqueue a newly-submitted invitation object - * either as the active position or into the inactive positions - * and dispatch a response for any invitation object that is discovered. - * Implementation of a workflow. - * - * @see `AddInvite` - * @see `indirectInviteResp` - * @param invitedPlayer the unique character identifier for the player being invited; - * in actuality, represents the player who will address the invitation object - * @param targetInvite a comparison invitation object - * @param invitingPlayer the unique character identifier for the player who invited the former - * @param name a name to be used in message composition + * Determine whether a player is sufficiently unemployed + * and has no grand delusions of being a squad leader. + * @see `CloseSquad` + * @param features an optional squad + * @return `true`, if the target player possesses no squad or the squad is nonexistent; + * `false`, otherwise */ - def AddInviteAndRespond( + def ensureEmptySquad(features: Option[SquadFeatures]): Boolean = { + features match { + case Some(squad) => ensureEmptySquad(squad) + case None => true + } + } + + /** + * Determine whether a player is sufficiently unemployed + * and has no grand delusions of being a squad leader. + * @see `CloseSquad` + * @param features the squad + * @return `true`, if the target player possesses no squad or a squad that is suitably nonexistent; + * `false`, otherwise + */ + def ensureEmptySquad(features: SquadFeatures): Boolean = { + val ensuredEmpty = features.Squad.Size <= 1 + if (ensuredEmpty) { + cleanUpAllInvitesToSquad(features) + } + ensuredEmpty + } + + /** + * Behaviors and exchanges necessary to complete the fulfilled recruitment process for the squad role.
+ *
+ * This operation is fairly safe to call whenever a player is to be inducted into a squad. + * The aforementioned player must have a callback retained in `subs.UserEvents` + * and conditions imposed by both the role and the player must be satisfied. + * @see `CleanUpAllInvitesWithPlayer` + * @see `Squad.isAvailable` + * @see `Squad.Switchboard` + * @see `SquadSubscriptionEntity.MonitorSquadDetails` + * @see `SquadSubscriptionEntity.Publish` + * @see `SquadSubscriptionEntity.Join` + * @see `SquadSubscriptionEntity.UserEvents` + * @param player the new squad member; + * this player is NOT the squad leader + * @param features the squad the player is joining + * @param position the squad member role that the player will be filling + * @return `true`, if the player joined the squad in some capacity; + * `false`, if the player did not join the squad or is already a squad member + */ + def joinSquad(player: Player, features: SquadFeatures, position: Int): Boolean = { + cleanUpAllInvitesWithPlayer(player.CharId) + parent ! SquadService.PerformJoinSquad(player, features, position) + true + } + + /* refusal and allowance */ + + /** + * This player has been refused to join squads by these players, or to form squads with these players. + * @param charId the player who refused other players + * @return the list of other players who have refused this player + */ + def refused(charId: Long): List[Long] = refusedPlayers.getOrElse(charId, Nil) + + /** + * This player has been refused to join squads by this squad leaders, or to form squads with this other player. + * @param charId the player who is being refused + * @param refusedCharId the player who refused + * @return the list of other players who have refused this player + */ + def refused(charId: Long, refusedCharId: Long): List[Long] = { + if (charId != refusedCharId) { + refused(charId, List(refusedCharId)) + } else { + Nil + } + } + + /** + * This player has been refused to join squads by these squad leaders, or to form squads with these other players. + * @param charId the player who is being refused + * @param list the players who refused + * @return the list of other players who have refused this player + */ + def refused(charId: Long, list: List[Long]): List[Long] = { + refusedPlayers.get(charId) match { + case Some(refusedList) => + refusedPlayers(charId) = list ++ refusedList + refused(charId) + case None => + Nil + } + } + + /** + * This player was previously refused to join squads by this squad leaders, or to form squads with this other player. + * They are now allowed. + * @param charId the player who is being refused + * @param permittedCharId the player who was previously refused + * @return the list of other players who have refused this player + */ + def allowed(charId: Long, permittedCharId: Long): List[Long] = { + if (charId != permittedCharId) { + allowed(charId, List(permittedCharId)) + } else { + Nil + } + } + + /** + * This player has been refused to join squads by these squad leaders, or to form squads with these other players. + * They are now allowed. + * @param charId the player who is being refused + * @param list the players who was previously refused + * @return the list of other players who have refused this player + */ + def allowed(charId: Long, list: List[Long]): List[Long] = { + refusedPlayers.get(charId) match { + case Some(refusedList) => + refusedPlayers(charId) = refusedList.filterNot(list.contains) + refused(charId) + case None => + Nil + } + } + + /* enqueue invite */ + + /** + * Enqueue a newly-submitted invitation object + * either as the active position or into the inactive positions + * and dispatch a response for any invitation object that is discovered. + * Implementation of a workflow. + * @see `AddInvite` + * @see `indirectInviteResp` + * @param invitedPlayer the unique character identifier for the player being invited; + * in actuality, represents the player who will address the invitation object + * @param targetInvite a comparison invitation object + * @param invitingPlayer the unique character identifier for the player who invited the former + * @param name a name to be used in message composition + */ + def addInviteAndRespond( invitedPlayer: Long, targetInvite: Invitation, invitingPlayer: Long, name: String, autoApprove: Boolean = false ): Unit = { - val (player, approval) = targetInvite match { - case IndirectInvite(_player, _) => (_player, autoApprove) - case RequestRole(_player, _, _) => (_player, autoApprove) - case _ => (null, false) - } - if (approval) { - SquadActionMembershipAcceptInvite(player, invitingPlayer, Some(targetInvite), None) - } else { - InviteResponseTemplate(indirectInviteResp)( - targetInvite, - AddInvite(invitedPlayer, targetInvite), + if (targetInvite.canBeAutoApproved && autoApprove) { + targetInvite.handleAcceptance(manager = this, targetInvite.getPlayer, invitingPlayer, None) + } else if (addInvite(invitedPlayer, targetInvite).contains(targetInvite)) { + targetInvite.handleInvitation(indirectInviteResp)( + manager = this, invitedPlayer, invitingPlayer, name @@ -1204,36 +1046,29 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { } /** - * Enqueue a newly-submitted invitation object - * either as the active position or into the inactive positions - * and dispatch a response for any invitation object that is discovered. - * Implementation of a workflow. - * - * @see `AddInvite` - * @see `altIndirectInviteResp` - * @param targetInvite a comparison invitation object - * @param invitedPlayer the unique character identifier for the player being invited - * @param invitingPlayer the unique character identifier for the player who invited the former - * @param name a name to be used in message composition - */ - def AltAddInviteAndRespond( + * Enqueue a newly-submitted invitation object + * either as the active position or into the inactive positions + * and dispatch a response for any invitation object that is discovered. + * Implementation of a workflow. + * @see `AddInvite` + * @see `altIndirectInviteResp` + * @param targetInvite a comparison invitation object + * @param invitedPlayer the unique character identifier for the player being invited + * @param invitingPlayer the unique character identifier for the player who invited the former + * @param name a name to be used in message composition + */ + def altAddInviteAndRespond( invitedPlayer: Long, targetInvite: Invitation, invitingPlayer: Long, name: String, autoApprove: Boolean = false ): Unit = { - val (player, approval) = targetInvite match { - case IndirectInvite(_player, _) => (_player, autoApprove) - case RequestRole(_player, _, _) => (_player, autoApprove) - case _ => (null, false) - } - if (approval) { - SquadActionMembershipAcceptInvite(player, invitingPlayer, Some(targetInvite), None) - } else { - InviteResponseTemplate(altIndirectInviteResp)( - targetInvite, - AddInvite(invitedPlayer, targetInvite), + if (targetInvite.canBeAutoApproved && autoApprove) { + targetInvite.handleAcceptance(manager = this, targetInvite.getPlayer, invitingPlayer, None) + } else if (addInvite(invitedPlayer, targetInvite).contains(targetInvite)) { + targetInvite.handleInvitation(altIndirectInviteResp)( + manager = this, invitedPlayer, invitingPlayer, name @@ -1242,20 +1077,19 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { } /** - * Component method used for the response behavior for processing the invitation object as an `IndirectInvite` object. - * - * @see `HandleRequestRole` - * @param invite the original invitation object that started this process - * @param player the target of the response and invitation - * @param invitedPlayer the unique character identifier for the player being invited; - * in actuality, represents the player who will address the invitation object; - * not useful here - * @param invitingPlayer the unique character identifier for the player who invited the former; - * not useful here - * @param name a name to be used in message composition; - * not useful here - * @return na - */ + * Component method used for the response behavior for processing the invitation object as an `IndirectInvite` object. + * @see `SquadInvitationManager.HandleRequestRole` + * @param invite the original invitation object that started this process + * @param player the target of the response and invitation + * @param invitedPlayer the unique character identifier for the player being invited; + * in actuality, represents the player who will address the invitation object; + * not useful here + * @param invitingPlayer the unique character identifier for the player who invited the former; + * not useful here + * @param name a name to be used in message composition; + * not useful here + * @return na + */ def indirectInviteResp( invite: IndirectInvite, player: Player, @@ -1263,21 +1097,20 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { invitingPlayer: Long, name: String ): Boolean = { - HandleRequestRole(player, invite) + SquadInvitationManager.handleRequestRole(manager = this, player, invite) } /** - * Component method used for the response behavior for processing the invitation object as an `IndirectInvite` object. - * - * @see `HandleRequestRole` - * @param invite the original invitation object that started this process - * @param player the target of the response and invitation - * @param invitedPlayer the unique character identifier for the player being invited - * in actuality, represents the player who will address the invitation object - * @param invitingPlayer the unique character identifier for the player who invited the former - * @param name a name to be used in message composition - * @return na - */ + * Component method used for the response behavior for processing the invitation object as an `IndirectInvite` object. + * @see `SquadInvitationManager.HandleRequestRole` + * @param invite the original invitation object that started this process + * @param player the target of the response and invitation + * @param invitedPlayer the unique character identifier for the player being invited + * in actuality, represents the player who will address the invitation object + * @param invitingPlayer the unique character identifier for the player who invited the former + * @param name a name to be used in message composition + * @return na + */ def altIndirectInviteResp( invite: IndirectInvite, player: Player, @@ -1287,185 +1120,48 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { ): Boolean = { subs.Publish( invitingPlayer, - SquadResponse.Membership( - SquadResponseType.Accept, - 0, - 0, - invitingPlayer, - Some(invitedPlayer), - player.Name, - unk5 = false, - Some(None) - ) + SquadResponse.Membership(SquadResponseType.Accept, invitingPlayer, Some(invitedPlayer), player.Name, unk5 = false) ) - HandleRequestRole(player, invite) + SquadInvitationManager.handleRequestRole(manager = this, player, invite) } /** - * A branched response for processing (new) invitation objects that have been submitted to the system.
- *
- * A comparison is performed between the original invitation object and an invitation object - * that represents the potential modification or redirection of the current active invitation obect. - * Any further action is only performed when an "is equal" comparison is `true`. - * When passing, the system publishes up to two messages - * to users that would anticipate being informed of squad join activity. - * - * @param indirectInviteFunc the method that cans the responding behavior should an `IndirectInvite` object being consumed - * @param targetInvite a comparison invitation object; - * represents the unmodified, unadjusted invite - * @param actualInvite a comparaison invitation object; - * proper use of this field should be the output of another process upon the following `actualInvite` - * @param invitedPlayer the unique character identifier for the player being invited - * in actuality, represents the player who will address the invitation object - * @param invitingPlayer the unique character identifier for the player who invited the former - * @param name a name to be used in message composition - */ - def InviteResponseTemplate(indirectInviteFunc: (IndirectInvite, Player, Long, Long, String) => Boolean)( - targetInvite: Invitation, - actualInvite: Option[Invitation], - invitedPlayer: Long, - invitingPlayer: Long, - name: String - ): Unit = { - if (actualInvite.contains(targetInvite)) { - //immediately respond - targetInvite match { - case VacancyInvite(charId, _name, _) => - subs.Publish( - invitedPlayer, - SquadResponse.Membership( - SquadResponseType.Invite, - 0, - 0, - charId, - Some(invitedPlayer), - _name, - unk5 = false, - Some(None) - ) - ) - subs.Publish( - charId, - SquadResponse.Membership( - SquadResponseType.Invite, - 0, - 0, - invitedPlayer, - Some(charId), - _name, - unk5 = true, - Some(None) - ) - ) - - case _bid@IndirectInvite(player, _) => - indirectInviteFunc(_bid, player, invitedPlayer, invitingPlayer, name) - - case _bid@SpontaneousInvite(player) => - val bidInvitingPlayer = _bid.InviterCharId - subs.Publish( - invitedPlayer, - SquadResponse.Membership( - SquadResponseType.Invite, - 0, - 0, - bidInvitingPlayer, - Some(invitedPlayer), - player.Name, - unk5 = false, - Some(None) - ) - ) - subs.Publish( - bidInvitingPlayer, - SquadResponse.Membership( - SquadResponseType.Invite, - 0, - 0, - invitedPlayer, - Some(bidInvitingPlayer), - player.Name, - unk5 = true, - Some(None) - ) - ) - - case _bid@RequestRole(player, _, _) => - HandleRequestRole(player, _bid) - - case LookingForSquadRoleInvite(member, _, _) => - subs.Publish( - invitedPlayer, - SquadResponse.Membership( - SquadResponseType.Invite, - 0, - 0, - invitedPlayer, - Some(member.CharId), - member.Name, - unk5 = false, - Some(None) - ) - ) - - case ProximityInvite(member, _, _) => - subs.Publish( - invitedPlayer, - SquadResponse.Membership( - SquadResponseType.Invite, - 0, - 0, - invitedPlayer, - Some(member.CharId), - member.Name, - unk5 = false, - Some(None) - ) - ) - - case _ => - log.warn(s"AddInviteAndRespond: can not parse discovered unhandled invitation type - $targetInvite") - } - } - } - - /** - * Assign a provided invitation object to either the active or inactive position for a player.
- *
- * The determination for the active position is whether or not something is currently in the active position - * or whether some mechanism tried to shift invitation object into the active position - * but found nothing to shift. - * If an invitation object originating from the reported player already exists, - * a new one is not appended to the inactive queue. - * This method should always be used as the entry point for the active and inactive invitation options - * or as a part of the entry point for the aforesaid options. - * - * @see `AddInviteAndRespond` - * @see `AltAddInviteAndRespond` - * @param invitedPlayer the unique character identifier for the player being invited; - * in actuality, represents the player who will address the invitation object - * @param invite the "new" invitation envelop object - * @return an optional invite; - * the invitation object in the active invite position; - * `None`, if it is not added to either the active option or inactive position - */ - def AddInvite(invitedPlayer: Long, invite: Invitation): Option[Invitation] = { + * Assign a provided invitation object to either the active or inactive position for a player.
+ *
+ * The determination for the active position is whether or not something is currently in the active position + * or whether some mechanism tried to shift invitation object into the active position + * but found nothing to shift. + * If an invitation object originating from the reported player already exists, + * a new one is not appended to the inactive queue. + * This method should always be used as the entry point for the active and inactive invitation options + * or as a part of the entry point for the aforesaid options. + * + * @see `AddInviteAndRespond` + * @see `AltAddInviteAndRespond` + * @param invitedPlayer the unique character identifier for the player being invited; + * in actuality, represents the player who will address the invitation object + * @param invite the "new" invitation envelop object + * @return an optional invite; + * the invitation object in the active invite position; + * `None`, if it is not added to either the active option or inactive position + */ + def addInvite(invitedPlayer: Long, invite: Invitation): Option[Invitation] = { invites.get(invitedPlayer).orElse(previousInvites.get(invitedPlayer)) match { case Some(_bid) => //the new invite may not interact with the active invite; add to queued invites queuedInvites.get(invitedPlayer) match { case Some(bidList) => //ensure that new invite does not interact with the queue's invites by invitingPlayer info - val inviteInviterCharId = invite.InviterCharId + val inviteInviterCharId = invite.inviterCharId if ( - _bid.InviterCharId != inviteInviterCharId && !bidList.exists { eachBid => - eachBid.InviterCharId == inviteInviterCharId + _bid.inviterCharId != inviteInviterCharId && !bidList.exists { eachBid => + eachBid.inviterCharId == inviteInviterCharId } ) { queuedInvites(invitedPlayer) = invite match { - case _: RequestRole => - //RequestRole is to be expedited - val (normals, others) = bidList.partition(_.isInstanceOf[RequestRole]) + case _: RequestToJoinSquadRole => + //RequestToJoinSquadRole is to be expedited + val (normals, others) = bidList.partition(_.isInstanceOf[RequestToJoinSquadRole]) (normals :+ invite) ++ others case _ => bidList :+ invite @@ -1475,7 +1171,7 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { None } case None => - if (_bid.InviterCharId != invite.InviterCharId) { + if (_bid.inviterCharId != invite.inviterCharId) { queuedInvites(invitedPlayer) = List(invite) Some(_bid) } else { @@ -1490,22 +1186,77 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { } /** - * Select the next invitation object to be shifted into the active position.
- *
- * The determination for the active position is whether or not something is currently in the active position - * or whether some mechanism tried to shift invitation object into the active position - * but found nothing to shift. - * After handling of the previous invitation object has completed or finished, - * the temporary block on adding new invitations is removed - * and any queued inactive invitation on the head of the inactive queue is shifted into the active position. - * @see `NextInviteAndRespond` - * @param invitedPlayer the unique character identifier for the player being invited; - * in actuality, represents the player who will address the invitation object - * @return an optional invite; - * the invitation object in the active invite position; - * `None`, if not shifted into the active position - */ - def NextInvite(invitedPlayer: Long): Option[Invitation] = { + * Resolve an invitation to a general, not guaranteed, position in someone else's squad.
+ *
+ * Originally, the instigating type of invitation object was a "`InvitationToJoinSquad`" + * which indicated a type of undirected invitation extended from the squad leader to another player + * but the resolution is generalized enough to suffice for a number of invitation objects. + * First, an actual position is determined; + * then, the squad is tested for recruitment conditions, + * including whether the person who solicited the would-be member is still the squad leader. + * If the recruitment is manual and the squad leader is not the same as the recruiting player, + * then the real squad leader is sent an indirect query regarding the player's eligibility. + * These `IndirectInvite` invitation objects also are handled by calls to `HandleVacancyInvite`. + * @see `AltAddInviteAndRespond` + * @see `IndirectInvite` + * @see `SquadFeatures::AutoApproveInvitationRequests` + * @see `InvitationToJoinSquad` + * @param features the squad + * @param invitedPlayer the unique character identifier for the player being invited + * @param invitingPlayer the unique character identifier for the player who invited the former + * @param recruit the player being invited + * @return the squad object and a role position index, if properly invited; + * `None`, otherwise + */ + def handleVacancyInvite( + features: SquadFeatures, + invitedPlayer: Long, + invitingPlayer: Long, + recruit: Player + ): Option[(SquadFeatures, Int)] = { + //accepted an invitation to join an existing squad + val squad = features.Squad + squad + .Membership + .zipWithIndex + .find { case (_, index) => squad.isAvailable(index, recruit.avatar.certifications) } match { + case Some((_, line)) => + //position in squad found + if (!features.AutoApproveInvitationRequests && squad.Leader.CharId != invitingPlayer) { + //the inviting player was not the squad leader and this decision should be bounced off the squad leader + altAddInviteAndRespond( + squad.Leader.CharId, + IndirectInvite(recruit, features), + invitingPlayer, + name = "" + ) + log.debug(s"HandleVacancyInvite: ${recruit.Name} must await an invitation from the leader of squad ${squad.Task}") + None + } else { + Some((features, line)) + } + case _ => + None + } + } + + /** + * Select the next invitation object to be shifted into the active position.
+ *
+ * The determination for the active position is whether or not something is currently in the active position + * or whether some mechanism tried to shift invitation object into the active position + * but found nothing to shift. + * After handling of the previous invitation object has completed or finished, + * the temporary block on adding new invitations is removed + * and any queued inactive invitation on the head of the inactive queue is shifted into the active position. + * @see `NextInviteAndRespond` + * @param invitedPlayer the unique character identifier for the player being invited; + * in actuality, represents the player who will address the invitation object + * @return an optional invite; + * the invitation object in the active invite position; + * `None`, if not shifted into the active position + */ + def nextInvite(invitedPlayer: Long): Option[Invitation] = { previousInvites.remove(invitedPlayer) invites.get(invitedPlayer) match { case None => @@ -1533,55 +1284,53 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { } /** - * Select the next invitation object to be shifted into the active position - * and dispatch a response for any invitation object that is discovered. - * @see `NextInvite` - * @see `RespondToInvite` - * @param invitedPlayer the unique character identifier for the player being invited; - * in actuality, represents the player who will address the invitation object - */ - def NextInviteAndRespond(invitedPlayer: Long): Unit = { - NextInvite(invitedPlayer) match { - case Some(invite) => - RespondToInvite(invitedPlayer, invite) - case None => ; - } + * Select the next invitation object to be shifted into the active position + * and dispatch a response for any invitation object that is discovered. + * @see `NextInvite` + * @see `RespondToInvite` + * @param invitedPlayer the unique character identifier for the player being invited; + * in actuality, represents the player who will address the invitation object + */ + def nextInviteAndRespond(invitedPlayer: Long): Unit = { + nextInvite(invitedPlayer) + .collect { invite => + respondToInvite(invitedPlayer, invite) + invite + } } /** - * Compose the response to an invitation. - * Use standard handling methods for `IndirectInvite` invitation envelops. - * @see `InviteResponseTemplate` - * @param invitedPlayer the unique character identifier for the player being invited; - * in actuality, represents the player who will address the invitation object - * @param invite the invitation envelope used to recover information about the action being taken - */ - def RespondToInvite(invitedPlayer: Long, invite: Invitation): Unit = { - InviteResponseTemplate(indirectInviteResp)( - invite, - Some(invite), + * Compose the response to an invitation. + * Use standard handling methods for `IndirectInvite` invitation envelops. + * @param invitedPlayer the unique character identifier for the player being invited; + * in actuality, represents the player who will address the invitation object + * @param invite the invitation envelope used to recover information about the action being taken + */ + def respondToInvite(invitedPlayer: Long, invite: Invitation): Unit = { + invite.handleInvitation(indirectInviteResp)( + manager = this, invitedPlayer, - invite.InviterCharId, - invite.InviterName + invite.inviterCharId, + invite.inviterName ) } /** - * Remove any invitation object from the active position. - * Flag the temporary field to indicate that the active position, while technically available, - * should not yet have a new invitation object shifted into it yet. - * This is the "proper" way to demote invitation objects from the active position - * whether or not they are to be handled - * except in cases of manipulative cleanup. - * @see `NextInvite` - * @see `NextInviteAndRespond` - * @param invitedPlayer the unique character identifier for the player being invited; - * in actuality, represents the player who will address the invitation object - * @return an optional invite; - * the invitation object formerly in the active invite position; - * `None`, if no invitation was in the active position - */ - def RemoveInvite(invitedPlayer: Long): Option[Invitation] = { + * Remove any invitation object from the active position. + * Flag the temporary field to indicate that the active position, while technically available, + * should not yet have a new invitation object shifted into it yet. + * This is the "proper" way to demote invitation objects from the active position + * whether or not they are to be handled + * except in cases of manipulative cleanup. + * @see `NextInvite` + * @see `NextInviteAndRespond` + * @param invitedPlayer the unique character identifier for the player being invited; + * in actuality, represents the player who will address the invitation object + * @return an optional invite; + * the invitation object formerly in the active invite position; + * `None`, if no invitation was in the active position + */ + def removeInvite(invitedPlayer: Long): Option[Invitation] = { invites.remove(invitedPlayer) match { case out @ Some(invite) => previousInvites += invitedPlayer -> invite @@ -1591,235 +1340,46 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { } } - /** - * Resolve an invitation to a general, not guaranteed, position in someone else's squad.
- *
- * Originally, the instigating type of invitation object was a "`VacancyInvite`" - * which indicated a type of undirected invitation extended from the squad leader to another player - * but the resolution is generalized enough to suffice for a number of invitation objects. - * First, an actual position is determined; - * then, the squad is tested for recruitment conditions, - * including whether the person who solicited the would-be member is still the squad leader. - * If the recruitment is manual and the squad leader is not the same as the recruiting player, - * then the real squad leader is sent an indirect query regarding the player's eligibility. - * These `IndirectInvite` invitation objects also are handled by calls to `HandleVacancyInvite`. - * @see `AltAddInviteAndRespond` - * @see `IndirectInvite` - * @see `SquadFeatures::AutoApproveInvitationRequests` - * @see `VacancyInvite` - * @param features the squad - * @param invitedPlayer the unique character identifier for the player being invited - * @param invitingPlayer the unique character identifier for the player who invited the former - * @param recruit the player being invited - * @return the squad object and a role position index, if properly invited; - * `None`, otherwise - */ - def HandleVacancyInvite( - features: SquadFeatures, - invitedPlayer: Long, - invitingPlayer: Long, - recruit: Player - ): Option[(SquadFeatures, Int)] = { - //accepted an invitation to join an existing squad + /* search */ + + def findSoldiersWithinScopeAndInvite( + invitingPlayer: Member, + features: SquadFeatures, + position: Int, + scope: List[Avatar], + excluded: List[Long], + invitationEnvelopFunc: (Member, SquadFeatures, Int) => Invitation + ): Option[Long] = { + val invitingPlayerCharId = invitingPlayer.CharId + val invitingPlayerName = invitingPlayer.Name val squad = features.Squad - squad.Membership.zipWithIndex.find({ - case (_, index) => - squad.isAvailable(index, recruit.avatar.certifications) - }) match { - case Some((_, line)) => - //position in squad found - if (!features.AutoApproveInvitationRequests && squad.Leader.CharId != invitingPlayer) { - //the inviting player was not the squad leader and this decision should be bounced off the squad leader - AltAddInviteAndRespond( - squad.Leader.CharId, - IndirectInvite(recruit, features), - invitingPlayer, - name = "" - ) - log.debug(s"HandleVacancyInvite: ${recruit.Name} must await an invitation from the leader of squad ${squad.Task}") - None - } else { - Some((features, line)) - } - case _ => + val faction = squad.Faction + val squadLeader = squad.Leader.CharId + val deniedAndExcluded = features.DeniedPlayers() ++ excluded + val requirementsToMeet = squad.Membership(position).Requirements + //find a player who is of the same faction as the squad, is LFS, and is eligible for the squad position + scope + .find { avatar => + val charId = avatar.id + faction == avatar.faction && + avatar.lookingForSquad && + !deniedAndExcluded.contains(charId) && + !refusedPlayers(charId).contains(squadLeader) && + requirementsToMeet.intersect(avatar.certifications) == requirementsToMeet && + charId != invitingPlayerCharId //don't send invite to yourself. can cause issues if rejected + } match { + case None => None + case Some(invitedPlayer) => + //add invitation for position in squad + val invite = invitationEnvelopFunc(invitingPlayer, features, position) + val id = invitedPlayer.id + addInviteAndRespond(id, invite, invitingPlayerCharId, invitingPlayerName) + Some(id) } } - /** - * An overloaded entry point to the functionality for handling one player requesting a specific squad role. - * - * @param player the player who wants to join the squad - * @param bid a specific kind of `Invitation` object - * @return `true`, if the player is not denied the possibility of joining the squad; - * `false`, otherwise, of it the squad does not exist - */ - def HandleRequestRole(player: Player, bid: RequestRole): Boolean = { - HandleRequestRole(player, bid.features, bid) - } - - /** - * An overloaded entry point to the functionality for handling indirection when messaging the squad leader about an invite. - * - * @param player the player who wants to join the squad - * @param bid a specific kind of `Invitation` object - * @return `true`, if the player is not denied the possibility of joining the squad; - * `false`, otherwise, of it the squad does not exist - */ - def HandleRequestRole(player: Player, bid: IndirectInvite): Boolean = { - HandleRequestRole(player, bid.features, bid) - } - - /** - * The functionality for handling indirection - * for handling one player requesting a specific squad role - * or when messaging the squad leader about an invite.
- *
- * At this point in the squad join process, the only consent required is that of the squad leader. - * An automatic consent flag exists on the squad; - * but, if that is not set, then the squad leader must be asked whether or not to accept or to reject the recruit. - * If the squad leader changes in the middle or the latter half of the process, - * the invitation may still fail even if the old squad leader accepts. - * If the squad leader changes in the middle of the latter half of the process, - * the inquiry might be posed again of the new squad leader, of whether to accept or to reject the recruit. - * - * @param player the player who wants to join the squad - * @param features the squad - * @param bid the `Invitation` object that was the target of this request - * @return `true`, if the player is not denied the possibility of joining the squad; - * `false`, otherwise, of it the squad does not exist - */ - def HandleRequestRole(player: Player, features: SquadFeatures, bid: Invitation): Boolean = { - val leaderCharId = features.Squad.Leader.CharId - subs.Publish(leaderCharId, SquadResponse.WantsSquadPosition(leaderCharId, player.Name)) - true - } - - /** - * Determine whether a player is sufficiently unemployed - * and has no grand delusions of being a squad leader. - * @see `CloseSquad` - * @param features an optional squad - * @return `true`, if the target player possesses no squad or the squad is nonexistent; - * `false`, otherwise - */ - def EnsureEmptySquad(features: Option[SquadFeatures]): Boolean = { - features match { - case Some(squad) => EnsureEmptySquad(squad) - case None => true - } - } - - /** - * Determine whether a player is sufficiently unemployed - * and has no grand delusions of being a squad leader. - * @see `CloseSquad` - * @param features the squad - * @return `true`, if the target player possesses no squad or a squad that is suitably nonexistent; - * `false`, otherwise - */ - def EnsureEmptySquad(features: SquadFeatures): Boolean = { - val ensuredEmpty = features.Squad.Size <= 1 - if (ensuredEmpty) { - CleanUpAllInvitesToSquad(features) - } - ensuredEmpty - } - - /** - * Behaviors and exchanges necessary to complete the fulfilled recruitment process for the squad role.
- *
- * This operation is fairly safe to call whenever a player is to be inducted into a squad. - * The aforementioned player must have a callback retained in `subs.UserEvents` - * and conditions imposed by both the role and the player must be satisfied. - * @see `CleanUpAllInvitesWithPlayer` - * @see `Squad.isAvailable` - * @see `Squad.Switchboard` - * @see `SquadSubscriptionEntity.MonitorSquadDetails` - * @see `SquadSubscriptionEntity.Publish` - * @see `SquadSubscriptionEntity.Join` - * @see `SquadSubscriptionEntity.UserEvents` - * @param player the new squad member; - * this player is NOT the squad leader - * @param features the squad the player is joining - * @param position the squad member role that the player will be filling - * @return `true`, if the player joined the squad in some capacity; - * `false`, if the player did not join the squad or is already a squad member - */ - def JoinSquad(player: Player, features: SquadFeatures, position: Int): Boolean = { - CleanUpAllInvitesWithPlayer(player.CharId) - parent ! SquadService.PerformJoinSquad(player, features, position) - true - } - - /** - * This player has been refused to join squads by these players, or to form squads with these players. - * @param charId the player who refused other players - * @return the list of other players who have refused this player - */ - def Refused(charId: Long): List[Long] = refused.getOrElse(charId, Nil) - - /** - * This player has been refused to join squads by this squad leaders, or to form squads with this other player. - * @param charId the player who is being refused - * @param refusedCharId the player who refused - * @return the list of other players who have refused this player - */ - def Refused(charId: Long, refusedCharId: Long): List[Long] = { - if (charId != refusedCharId) { - Refused(charId, List(refusedCharId)) - } else { - Nil - } - } - - /** - * This player has been refused to join squads by these squad leaders, or to form squads with these other players. - * @param charId the player who is being refused - * @param list the players who refused - * @return the list of other players who have refused this player - */ - def Refused(charId: Long, list: List[Long]): List[Long] = { - refused.get(charId) match { - case Some(refusedList) => - refused(charId) = list ++ refusedList - Refused(charId) - case None => - Nil - } - } - - /** - * This player has been refused to join squads by this squad leaders, or to form squads with this other player. - * They are now allowed. - * @param charId the player who is being refused - * @param permittedCharId the player who was previously refused - * @return the list of other players who have refused this player - */ - def Allowed(charId: Long, permittedCharId: Long): List[Long] = { - if (charId != permittedCharId) { - Allowed(charId, List(permittedCharId)) - } else { - Nil - } - } - - /** - * This player has been refused to join squads by these squad leaders, or to form squads with these other players. - * They are now allowed. - * @param charId the player who is being refused - * @param list the players who was previously refused - * @return the list of other players who have refused this player - */ - def Allowed(charId: Long, list: List[Long]): List[Long] = { - refused.get(charId) match { - case Some(refusedList) => - refused(charId) = refusedList.filterNot(list.contains) - Refused(charId) - case None => - Nil - } - } + /* invite clean-up */ /** * Remove all inactive invites associated with this player. @@ -1827,24 +1387,16 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { * in actuality, represents the player who will address the invitation object * @return a list of the removed inactive invitation objects */ - def CleanUpQueuedInvites(charId: Long): Unit = { - val allSquadGuids = queuedInvites.remove(charId) match { - case Some(bidList) => - bidList.collect { - case VacancyInvite(_, _, guid) => guid - case IndirectInvite(_, guid) => guid - case LookingForSquadRoleInvite(_, guid, _) => guid - case ProximityInvite(_, guid, _) => guid - case RequestRole(_, guid, _) => guid - } - case None => - Nil - } + def cleanUpQueuedInvites(charId: Long): Unit = { val list = List(charId) - allSquadGuids.foreach { CleanUpSquadFeatures(list, _, position = -1) } + queuedInvites + .remove(charId) + .map(_.flatMap(_.getOptionalSquad)) + .getOrElse(Nil) + .foreach(cleanUpSquadFeatures(list, _, position = -1)) } - def CleanUpSquadFeatures(removed: List[Long], features: SquadFeatures, position: Int): Unit = { + def cleanUpSquadFeatures(removed: List[Long], features: SquadFeatures, position: Int): Unit = { features.ProxyInvites = features.ProxyInvites.filterNot(removed.contains) if (features.ProxyInvites.isEmpty) { features.SearchForRole = None @@ -1862,10 +1414,10 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { * @param features the squad * @param position the role position index */ - def CleanUpInvitesForSquadAndPosition(features: SquadFeatures, position: Int): Unit = { + def cleanUpInvitesForSquadAndPosition(features: SquadFeatures, position: Int): Unit = { val guid = features.Squad.GUID - CleanUpSquadFeatures( - RemoveActiveInvitesForSquadAndPosition(guid, position) ++ RemoveQueuedInvitesForSquadAndPosition(guid, position), + cleanUpSquadFeatures( + removeActiveInvitesForSquadAndPosition(guid, position) ++ removeQueuedInvitesForSquadAndPosition(guid, position), features, position ) @@ -1875,15 +1427,15 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { * Remove all inactive invitation objects that are related to the particular squad and the particular role in the squad. * Specifically used to safely disarm obsolete invitation objects by specific criteria. * Affects only certain invitation object types. - * @see `RequestRole` + * @see `RequestToJoinSquadRole` * @see `LookingForSquadRoleInvite` * @see `CleanUpInvitesForSquadAndPosition` * @param features the squa * @param position the role position index */ - def CleanUpQueuedInvitesForSquadAndPosition(features: SquadFeatures, position: Int): Unit = { - CleanUpSquadFeatures( - RemoveQueuedInvitesForSquadAndPosition(features.Squad.GUID, position), + def cleanUpQueuedInvitesForSquadAndPosition(features: SquadFeatures, position: Int): Unit = { + cleanUpSquadFeatures( + removeQueuedInvitesForSquadAndPosition(features.Squad.GUID, position), features, position ) @@ -1893,30 +1445,28 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { * Remove all invitation objects that are related to the particular squad. * Specifically used to safely disarm obsolete invitation objects by specific criteria. * Affects all invitation object types and all data structures that deal with the squad. - * @see `RequestRole` + * @see `RequestToJoinSquadRole` * @see `IndirectInvite` * @see `LookingForSquadRoleInvite` * @see `ProximityInvite` * @see `RemoveInvite` - * @see `VacancyInvite` + * @see `InvitationToJoinSquad` * @param features the squad identifier */ - def CleanUpAllInvitesToSquad(features: SquadFeatures): List[Long] = { + def cleanUpAllInvitesToSquad(features: SquadFeatures): List[Long] = { val guid = features.Squad.GUID //clean up invites val activeInviteIds = { val keys = invites.keys.toSeq - invites.values.zipWithIndex + invites + .values + .zipWithIndex .collect { - case (VacancyInvite(_, _, f), index) if f.Squad.GUID == guid => index - case (IndirectInvite(_, f), index) if f.Squad.GUID == guid => index - case (LookingForSquadRoleInvite(_, f, _), index) if f.Squad.GUID == guid => index - case (ProximityInvite(_, f, _), index) if f.Squad.GUID == guid => index - case (RequestRole(_, f, _), index) if f.Squad.GUID == guid => index + case (invite, index) if invite.appliesToSquad(guid) => index } .map { index => val key = keys(index) - RemoveInvite(key) + removeInvite(key) key } .toList @@ -1928,14 +1478,7 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { .collect { case (queue, index) => val key = keys(index) - val (targets, retained) = queue.partition { - case VacancyInvite(_, _, f) => f.Squad.GUID == guid - case IndirectInvite(_, f) => f.Squad.GUID == guid - case LookingForSquadRoleInvite(_, f, _) => f.Squad.GUID == guid - case ProximityInvite(_, f, _) => f.Squad.GUID == guid - case RequestRole(_, f, _) => f.Squad.GUID == guid - case _ => false - } + val (targets, retained) = queue.partition(_.appliesToSquad(guid)) if (retained.isEmpty) { queuedInvites.remove(key) } else { @@ -1951,7 +1494,7 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { .toList } val allInviteIds = (activeInviteIds ++ queuedInviteIds).distinct - CleanUpSquadFeatures(allInviteIds, features, position = -1) + cleanUpSquadFeatures(allInviteIds, features, position = -1) allInviteIds } @@ -1959,40 +1502,28 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { * Remove all active and inactive invitation objects that are related to the particular player. * Specifically used to safely disarm obsolete invitation objects by specific criteria. * Affects all invitation object types and all data structures that deal with the player. - * @see `RequestRole` + * @see `RequestToJoinSquadRole` * @see `IndirectInvite` * @see `LookingForSquadRoleInvite` * @see `RemoveInvite` * @see `CleanUpAllProximityInvites` - * @see `VacancyInvite` + * @see `InvitationToJoinSquad` * @param charId the player's unique identifier number */ - def CleanUpAllInvitesWithPlayer(charId: Long): Unit = { + def cleanUpAllInvitesWithPlayer(charId: Long): Unit = { //clean up our active invitation - val charIdInviteSquadGuid = RemoveInvite(charId) match { - case Some(VacancyInvite(_, _, guid)) => Some(guid) - case Some(IndirectInvite(_, guid)) => Some(guid) - case Some(LookingForSquadRoleInvite(_, guid, _)) => Some(guid) - case Some(ProximityInvite(_, guid, _)) => Some(guid) - case Some(RequestRole(_, guid, _)) => Some(guid) - case _ => None - } + val charIdInviteSquadGuid = removeInvite(charId).flatMap(_.getOptionalSquad) //clean up invites val (activeInviteIds, activeSquadGuids) = { val keys = invites.keys.toSeq - invites.values.zipWithIndex - .collect { - case (SpontaneousInvite(player), index) if player.CharId == charId => (index, None) - case (VacancyInvite(player, _, guid), index) if player == charId => (index, Some(guid)) - case (IndirectInvite(player, guid), index) if player.CharId == charId => (index, Some(guid)) - case (LookingForSquadRoleInvite(member, guid, _), index) if member.CharId == charId => (index, Some(guid)) - case (ProximityInvite(member, guid, _), index) if member.CharId == charId => (index, Some(guid)) - case (RequestRole(player, guid, _), index) if player.CharId == charId => (index, Some(guid)) - } - .map { case (index, guid) => - val key = keys(index) - RemoveInvite(key) - (key, guid) + invites + .values + .zipWithIndex + .map { + case (invite, index) => + val key = keys(index) + removeInvite(key) + (key, invite.getOptionalSquad) } .unzip } @@ -2003,16 +1534,8 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { .collect { case (queue, index) => val key = keys(index) - val (targets, retained) = if(key != charId) { - queue.partition { - case SpontaneousInvite(player) => player.CharId == charId - case VacancyInvite(player, _, _) => player == charId - case IndirectInvite(player, _) => player.CharId == charId - case LookingForSquadRoleInvite(member, _, _) => member.CharId == charId - case ProximityInvite(member, _, _) => member.CharId == charId - case RequestRole(player, _, _) => player.CharId == charId - case _ => false - } + val (targets, retained) = if (key != charId) { + queue.partition(_.appliesToPlayer(charId)) } else { (queue, Nil) } @@ -2022,16 +1545,7 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { queuedInvites += key -> retained } if (targets.nonEmpty) { - Some(( - key, - targets.collect { - case VacancyInvite(_, _, guid) => guid - case IndirectInvite(_, guid) => guid - case LookingForSquadRoleInvite(_, guid, _) => guid - case ProximityInvite(_, guid, _) => guid - case RequestRole(_, guid, _) => guid - } - )) + Some((key, targets.flatMap(_.getOptionalSquad))) } else { None } @@ -2044,7 +1558,7 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { ((activeSquadGuids.toSeq :+ charIdInviteSquadGuid) ++ queuedSquadGuids) .flatten .distinct - .foreach { guid => CleanUpSquadFeatures(allInvites, guid, position = -1) } + .foreach { guid => cleanUpSquadFeatures(allInvites, guid, position = -1) } } /** @@ -2052,7 +1566,7 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { * This is related to recruitment from the perspective of the recruiter. * @param charId the player */ - def CleanUpAllProximityInvites(charId: Long): Unit = { + def cleanUpAllProximityInvites(charId: Long): Unit = { //clean up invites val (activeInviteIds, activeSquadGuids) = { val keys = invites.keys.toSeq @@ -2060,7 +1574,7 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { .collect { case (ProximityInvite(member, guid, _), index) if member.CharId == charId => (index, Some(guid)) } .map { case (index, guid) => val key = keys(index) - RemoveInvite(key) + removeInvite(key) (key, guid) } .unzip @@ -2068,7 +1582,9 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { //tidy the queued invitations val (queuedInviteIds, queuedSquadGuids) = { val keys = queuedInvites.keys.toSeq - queuedInvites.values.zipWithIndex + queuedInvites + .values + .zipWithIndex .collect { case (queue, index) => val key = keys(index) @@ -2095,14 +1611,14 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { (activeSquadGuids.toSeq ++ queuedSquadGuids) .flatten .distinct - .foreach { guid => CleanUpSquadFeatures(allInvites, guid, position = -1) } + .foreach { guid => cleanUpSquadFeatures(allInvites, guid, position = -1) } } /** * Remove all active and inactive proximity squad invites for a specific squad. * @param features the squad */ - def CleanUpProximityInvites(features: SquadFeatures): Unit = { + def cleanUpProximityInvites(features: SquadFeatures): Unit = { val squadGuid = features.Squad.GUID //clean up invites val activeInviteIds = { @@ -2113,7 +1629,7 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { } .map { index => val key = keys(index) - RemoveInvite(key) + removeInvite(key) key } } @@ -2126,7 +1642,7 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { val key = keys(index) val (targets, retained) = queue.partition { case ProximityInvite(_, _squad, _) => _squad.Squad.GUID == squadGuid - case _ => false + case _ => false } if (retained.isEmpty) { queuedInvites.remove(key) @@ -2142,7 +1658,7 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { .flatten .toList } - CleanUpSquadFeatures((activeInviteIds ++ queuedInviteIds).toList.distinct, features, position = -1) + cleanUpSquadFeatures((activeInviteIds ++ queuedInviteIds).toList.distinct, features, position = -1) } /** @@ -2151,7 +1667,7 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { * Specifically used to safely disarm obsolete invitation objects related to the specific criteria. * Affects only certain invitation object types * including "player requesting role" and "leader requesting recruiting role". - * @see `RequestRole` + * @see `RequestToJoinSquadRole` * @see `LookingForSquadRoleInvite` * @see `ProximityInvite` * @see `RemoveInvite` @@ -2159,17 +1675,17 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { * @param position the role position index * @return the character ids of all players whose invites were removed */ - def RemoveActiveInvitesForSquadAndPosition(guid: PlanetSideGUID, position: Int): List[Long] = { + def removeActiveInvitesForSquadAndPosition(guid: PlanetSideGUID, position: Int): List[Long] = { val keys = invites.keys.toSeq - invites.values.zipWithIndex + invites + .values + .zipWithIndex .collect { - case (LookingForSquadRoleInvite(_, _squad, pos), index) if _squad.Squad.GUID == guid && pos == position => index - case (ProximityInvite(_, _squad, pos), index) if _squad.Squad.GUID == guid && pos == position => index - case (RequestRole(_, _squad, pos), index) if _squad.Squad.GUID == guid && pos == position => index + case (invite, index) if invite.appliesToSquadAndPosition(guid, position) => index } .map { index => val key = keys(index) - RemoveInvite(key) + removeInvite(key) key } .toList @@ -2179,25 +1695,20 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { * Remove all inactive invitation objects that are related to the particular squad and the particular role in the squad. * Specifically used to safely disarm obsolete invitation objects by specific criteria. * Affects only certain invitation object types. - * @see `RequestRole` + * @see `RequestToJoinSquadRole` * @see `LookingForSquadRoleInvite` * @see `CleanUpInvitesForSquadAndPosition` * @param guid the squad identifier * @param position the role position index * @return the character ids of all players whose invites were removed */ - def RemoveQueuedInvitesForSquadAndPosition(guid: PlanetSideGUID, position: Int): List[Long] = { + def removeQueuedInvitesForSquadAndPosition(guid: PlanetSideGUID, position: Int): List[Long] = { val keys = queuedInvites.keys.toSeq queuedInvites.values.zipWithIndex .collect { case (queue, index) => val key = keys(index) - val (targets, retained) = queue.partition { - case LookingForSquadRoleInvite(_, _squad, pos) => _squad.Squad.GUID == guid && pos == position - case ProximityInvite(_, _squad, pos) => _squad.Squad.GUID == guid && pos == position - case RequestRole(_, _squad, pos) => _squad.Squad.GUID == guid && pos == position - case _ => false - } + val (targets, retained) = queue.partition(_.appliesToSquadAndPosition(guid, position)) if (retained.isEmpty) { queuedInvites.remove(key) } else { @@ -2213,153 +1724,84 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { .toList } - def FindSoldiersWithinScopeAndInvite( - invitingPlayer: Member, - features: SquadFeatures, - position: Int, - scope: List[Avatar], - excluded: List[Long], - invitationEnvelopFunc: (Member, SquadFeatures, Int) => Invitation - ): Option[Long] = { - val invitingPlayerCharId = invitingPlayer.CharId - val invitingPlayerName = invitingPlayer.Name - val squad = features.Squad - val faction = squad.Faction - val squadLeader = squad.Leader.CharId - val deniedAndExcluded = features.DeniedPlayers() ++ excluded - val requirementsToMeet = squad.Membership(position).Requirements - //find a player who is of the same faction as the squad, is LFS, and is eligible for the squad position - scope - .find { avatar => - val charId = avatar.id - faction == avatar.faction && - avatar.lookingForSquad && - !deniedAndExcluded.contains(charId) && - !refused(charId).contains(squadLeader) && - requirementsToMeet.intersect(avatar.certifications) == requirementsToMeet && - charId != invitingPlayerCharId //don't send invite to yourself. can cause issues if rejected - } match { - case None => - None - case Some(invitedPlayer) => - //add invitation for position in squad - val invite = invitationEnvelopFunc(invitingPlayer, features, position) - val id = invitedPlayer.id - AddInviteAndRespond(id, invite, invitingPlayerCharId, invitingPlayerName) - Some(id) - } + def publish(to: Long, msg: SquadResponse.Response): Unit = { + subs.Publish(to, msg) } } object SquadInvitationManager { final case class Join(charId: Long) - /** - * The base of all objects that exist for the purpose of communicating invitation from one player to the next. - * @param char_id the inviting player's unique identifier number - * @param name the inviting player's name - */ - sealed abstract class Invitation(char_id: Long, name: String) { - def InviterCharId: Long = char_id - def InviterName: String = name - } - - /** - * Utilized when one player attempts to join an existing squad in a specific role. - * Accessed by the joining player from the squad detail window. - * This invitation is handled by the squad leader. - * @param player the player who requested the role - * @param features the squad with the role - * @param position the index of the role - */ - private case class RequestRole(player: Player, features: SquadFeatures, position: Int) - extends Invitation(player.CharId, player.Name) - - /** - * Utilized when one squad member issues an invite for some other player. - * Accessed by an existing squad member using the "Invite" menu option on another player. - * This invitation is handled by the player who would join the squad. - * @param char_id the unique character identifier of the player who sent the invite - * @param name the name the player who sent the invite - * @param features the squad - */ - private case class VacancyInvite(char_id: Long, name: String, features: SquadFeatures) - extends Invitation(char_id, name) - - /** - * Utilized to redirect an (accepted) invitation request to the proper squad leader. - * No direct action causes this message. - * Depending on the situation, either the squad leader or the player who would join the squad handle this invitation. - * @param player the player who would be joining the squad; - * may or may not have actually requested it in the first place - * @param features the squad - */ - private case class IndirectInvite(player: Player, features: SquadFeatures) - extends Invitation(player.CharId, player.Name) - - /** - * Utilized in conjunction with an external queuing data structure - * to search for and submit requests to other players - * for the purposes of fill out unoccupied squad roles. - * This invitation is handled by the player who would be joining the squad. - * @param leader the squad leader - * @param features the squad - * @param position the index of a role - */ - private case class ProximityInvite(leader: Member, features: SquadFeatures, position: Int) - extends Invitation(leader.CharId, leader.Name) - - /** - * Utilized in conjunction with an external queuing data structure - * to search for and submit requests to other players - * for the purposes of fill out an unoccupied squad role. - * This invitation is handled by the player who would be joining the squad. - * @param leader the squad leader - * @param features the squad with the role - * @param position the index of the role - */ - private case class LookingForSquadRoleInvite(leader: Member, features: SquadFeatures, position: Int) - extends Invitation(leader.CharId, leader.Name) - - /** - * Utilized when one player issues an invite for some other player for a squad that does not yet exist. - * This invitation is handled by the player who would be joining the squad. - * @param player the player who wishes to become the leader of a squad - */ - private case class SpontaneousInvite(player: Player) extends Invitation(player.CharId, player.Name) - - /** - * na - * @param invitingPlayer na - * @param features na - * @param position na - * @return na - */ - private def ProximityEnvelope( - invitingPlayer: Member, - features: SquadFeatures, - position: Int - ): Invitation = { - ProximityInvite(invitingPlayer, features, position) - } - - /** - * na - * @param invitingPlayer na - * @param features na - * @param position na - * @return na - */ - private def LookingForSquadRoleEnvelope( - invitingPlayer: Member, - features: SquadFeatures, - position: Int - ): Invitation = { - LookingForSquadRoleInvite(invitingPlayer, features, position) - } - final case class FinishStartSquad(features: SquadFeatures) + /** + * na + * @param invitingPlayer na + * @param features na + * @param position na + * @return na + */ + private def proximityEnvelope( + invitingPlayer: Member, + features: SquadFeatures, + position: Int + ): Invitation = { + invitations.ProximityInvite(invitingPlayer, features, position) + } + + /** + * na + * @param invitingPlayer na + * @param features na + * @param position na + * @return na + */ + private def lookingForSquadRoleEnvelope( + invitingPlayer: Member, + features: SquadFeatures, + position: Int + ): Invitation = { + invitations.LookingForSquadRoleInvite(invitingPlayer, features, position) + } + + /** + * Overloaded entry point to functionality for handling indirection + * for handling one player requesting a specific squad role + * or when messaging the squad leader about an invite. + * @param player the player who wants to join the squad + * @param bid a specific kind of `Invitation` object + * @return `true`, if the player is not denied the possibility of joining the squad; + * `false`, otherwise, of it the squad does not exist + */ + def handleRequestRole(manager: SquadInvitationManager, player: Player, bid: Invitation): Boolean = { + bid.getOptionalSquad.exists(handleRequestRole(manager, player, _, bid)) + } + + /** + * The functionality for handling indirection + * for handling one player requesting a specific squad role + * or when messaging the squad leader about an invite.
+ *
+ * At this point in the squad join process, the only consent required is that of the squad leader. + * An automatic consent flag exists on the squad; + * but, if that is not set, then the squad leader must be asked whether or not to accept or to reject the recruit. + * If the squad leader changes in the middle or the latter half of the process, + * the invitation may still fail even if the old squad leader accepts. + * If the squad leader changes in the middle of the latter half of the process, + * the inquiry might be posed again of the new squad leader, of whether to accept or to reject the recruit. + * + * @param player the player who wants to join the squad + * @param features the squad + * @param bid the `Invitation` object that was the target of this request + * @return `true`, if the player is not denied the possibility of joining the squad; + * `false`, otherwise, of it the squad does not exist + */ + def handleRequestRole(manager: SquadInvitationManager, player: Player, features: SquadFeatures, bid: Invitation): Boolean = { + val leaderCharId = features.Squad.Leader.CharId + manager.publish(leaderCharId, SquadResponse.WantsSquadPosition(leaderCharId, player.Name)) + true + } + def canEnrollInSquad(features: SquadFeatures, charId: Long): Boolean = { !features.Squad.Membership.exists { _.CharId == charId } } diff --git a/src/main/scala/net/psforever/services/teamwork/SquadService.scala b/src/main/scala/net/psforever/services/teamwork/SquadService.scala index 317e1aae..8d8ef04c 100644 --- a/src/main/scala/net/psforever/services/teamwork/SquadService.scala +++ b/src/main/scala/net/psforever/services/teamwork/SquadService.scala @@ -1,25 +1,23 @@ -// Copyright (c) 2019-2022 PSForever +// Copyright (c) 2019-2024 PSForever package net.psforever.services.teamwork import akka.actor.{Actor, ActorRef, Terminated} -import net.psforever.actors.session.SessionActor -import net.psforever.packet.game.ChatMsg -import net.psforever.types.ChatMessageType - import java.io.{PrintWriter, StringWriter} import scala.annotation.unused import scala.collection.concurrent.TrieMap import scala.collection.mutable // +import net.psforever.actors.session.SessionActor import net.psforever.objects.{LivePlayerList, Player} import net.psforever.objects.teamwork.{Member, Squad, SquadFeatures} import net.psforever.objects.avatar.{Avatar, Certification} import net.psforever.objects.definition.converter.StatConverter import net.psforever.objects.zones.Zone +import net.psforever.packet.game.ChatMsg import net.psforever.packet.game.SquadAction._ import net.psforever.packet.game.{PlanetSideZoneID, SquadDetail, SquadInfo, SquadPositionDetail, SquadPositionEntry, SquadAction => SquadRequestAction} import net.psforever.services.Service -import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, SquadRequestType, SquadResponseType} +import net.psforever.types.{ChatMessageType, PlanetSideEmpire, PlanetSideGUID, SquadRequestType, SquadResponseType} class SquadService extends Actor { import SquadService._ @@ -75,8 +73,6 @@ class SquadService extends Actor { private def info(msg: String): Unit = log.info(msg) - private def debug(msg: String): Unit = log.debug(msg) - override def postStop(): Unit = { //squads and members (users) squadFeatures.foreach { @@ -143,13 +139,11 @@ class SquadService extends Actor { * @param charId the potential member identifier * @return the discovered squad, or `None` */ - def GetParticipatingSquad(charId: Long): Option[SquadFeatures] = - memberToSquad.get(charId) match { - case Some(id) => - squadFeatures.get(id) - case None => - None - } + def GetParticipatingSquad(charId: Long): Option[SquadFeatures] = { + memberToSquad + .get(charId) + .flatMap(GetSquad) + } /** * If this player is a member of any squad, discover that squad. @@ -171,28 +165,25 @@ class SquadService extends Actor { * the expectation is that the provided squad is a known participating squad * @return the discovered squad, or `None` */ - def GetLeadingSquad(charId: Long, opt: Option[SquadFeatures]): Option[SquadFeatures] = - opt.orElse(GetParticipatingSquad(charId)) match { - case Some(features) => - if (features.Squad.Leader.CharId == charId) { - Some(features) - } else { - None - } - case _ => - None - } + def GetLeadingSquad(charId: Long, opt: Option[SquadFeatures]): Option[SquadFeatures] = { + opt + .orElse(GetParticipatingSquad(charId)) + .collect { + case features if features.Squad.Leader.CharId == charId => + features + } + } def receive: Receive = { //subscribe to a faction's channel - necessary to receive updates about listed squads - case Service.Join(faction) if "TRNCVS".indexOf(faction) > -1 => + case Service.Join(faction) if SquadService.FactionWordSalad.indexOf(faction) > -1 => JoinByFaction(faction, sender()) //subscribe to the player's personal channel - necessary for future and previous squad information case Service.Join(char_id) => JoinByCharacterId(char_id, sender()) - case Service.Leave(Some(faction)) if "TRNCVS".indexOf(faction) > -1 => + case Service.Leave(Some(faction)) if SquadService.FactionWordSalad.indexOf(faction) > -1 => LeaveByFaction(faction, sender()) case Service.Leave(Some(char_id)) => @@ -244,7 +235,7 @@ class SquadService extends Actor { UpdateSquadListWhenListed(features, changes) case SquadService.ResendActiveInvite(charId) => - invitations.resendActiveInvite(charId) + invitations.reloadActiveInvite(charId) case SquadService.ListAllCurrentInvites(charId) => ListCurrentInvitations(charId) @@ -259,6 +250,121 @@ class SquadService extends Actor { log.warn(s"Unhandled message $msg from ${sender()}") } + def SquadActionMembership(tplayer: Player, zone: Zone, action: Any): Unit = { + action match { + case SquadAction.Membership(SquadRequestType.Invite, invitingPlayer, Some(_invitedPlayer), invitedName, _) => + SquadActionMembershipInvite(tplayer, invitingPlayer, _invitedPlayer, invitedName) + + case SquadAction.Membership(SquadRequestType.ProximityInvite, invitingPlayer, _, _, _) => + SquadActionMembershipProximityInvite(zone, invitingPlayer) + + case SquadAction.Membership(SquadRequestType.Accept, invitedPlayer, _, _, _) => + SquadActionMembershipAccept(tplayer, invitedPlayer) + + case SquadAction.Membership(SquadRequestType.Leave, actingPlayer, _leavingPlayer, name, _) => + SquadActionMembershipLeave(tplayer, actingPlayer, _leavingPlayer, name) + + case SquadAction.Membership(SquadRequestType.Reject, rejectingPlayer, _, _, _) => + SquadActionMembershipReject(tplayer, rejectingPlayer) + + case SquadAction.Membership(SquadRequestType.Disband, char_id, _, _, _) => + SquadActionMembershipDisband(char_id) + + case SquadAction.Membership(SquadRequestType.Cancel, cancellingPlayer, _, _, _) => + SquadActionMembershipCancel(cancellingPlayer) + + case SquadAction.Membership(SquadRequestType.Promote, _, _, _, _) => () + // case SquadAction.Membership(SquadRequestType.Promote, promotingPlayer, Some(_promotedPlayer), promotedName, _) => + // SquadActionMembershipPromote(promotingPlayer, _promotedPlayer, promotedName, SquadServiceMessage(tplayer, zone, action), sender()) + + case SquadAction.Membership(event, _, _, _, _) => + info(s"SquadAction.Membership: $event is not yet supported") + + case msg => + log.warn(s"Unhandled message $msg from ${sender()}") + } + } + + def SquadActionDefinition( + message: SquadServiceMessage, + action: SquadRequestAction, + guid: PlanetSideGUID + ): Unit = { + val tplayer = message.tplayer + (action match { + //the following actions only perform an action upon the squad + case _: ChangeSquadPurpose => GetOrCreateSquadOnlyIfLeader(tplayer) + case _: ChangeSquadZone => GetOrCreateSquadOnlyIfLeader(tplayer) + case _: AddSquadMemberPosition => GetOrCreateSquadOnlyIfLeader(tplayer) + case _: ChangeSquadMemberRequirementsRole => GetOrCreateSquadOnlyIfLeader(tplayer) + case _: ChangeSquadMemberRequirementsDetailedOrders => GetOrCreateSquadOnlyIfLeader(tplayer) + case _: ChangeSquadMemberRequirementsCertifications => GetOrCreateSquadOnlyIfLeader(tplayer) + case _: LocationFollowsSquadLead => GetOrCreateSquadOnlyIfLeader(tplayer) + case _: RequestListSquad => GetOrCreateSquadOnlyIfLeader(tplayer) + case _: StopListSquad => GetLeadingSquad(tplayer, None) + //the following actions cause changes with the squad composition or with invitations + case AutoApproveInvitationRequests(_) => + GetOrCreateSquadOnlyIfLeader(tplayer) + .foreach(features => invitations.autoApproveInvitationRequests(tplayer.CharId, features)) + None + case CloseSquadMemberPosition(position) => + GetOrCreateSquadOnlyIfLeader(tplayer) + .collect { + case features if features.Squad.Membership(position).CharId > 0 => + LeaveSquad(features.Squad.Membership(position).CharId, features) + features + } + case FindLfsSoldiersForRole(position) => + GetLeadingSquad(tplayer, None) + .foreach(features => invitations.findLfsSoldiersForRole(tplayer, features, position)) + None + case CancelFind() => + GetLeadingSquad(tplayer, None) + .foreach(features => invitations.cancelFind(Some(features))) + None + case SelectRoleForYourself(position) => + GetParticipatingSquad(tplayer) match { + case out @ Some(features) => + if (features.Squad.GUID == guid) { + out + } else { + //this isn't the squad we're looking for by GUID; as a precaution, reload all of the published squad list + val faction = tplayer.Faction + subs.Publish(faction, SquadResponse.InitList(PublishedLists(tplayer.Faction))) + None + } + case _ => + GetSquad(guid) + .foreach(features => invitations.selectRoleForYourselfAsInvite(tplayer, features, position)) + None + } + case _: CancelSelectRoleForYourself => + GetSquad(guid) + .foreach(features => invitations.cancelSelectRoleForYourself(tplayer, features)) + None + case search: SearchForSquadsWithParticularRole => + SquadActionDefinitionSearchForSquadsWithParticularRole(tplayer, search) + None + case _: CancelSquadSearch => + SquadActionDefinitionCancelSquadSearch(tplayer.CharId) + None + case _: DisplaySquad => + GetSquad(guid) match { + case out @ Some(_) => + SquadActionDefinitionDisplaySquad(tplayer, guid) + out + case None => + None + } + case _: SquadInitializationIssue => + SquadActionDefinitionSquadInitializationIssue(tplayer, guid) + None + case _ => + GetSquad(guid) + }) + .foreach(features => features.Switchboard.tell(message, sender())) + } + /** * Subscribe to a faction-wide channel. * @param faction sub-channel name @@ -337,23 +443,17 @@ class SquadService extends Actor { */ def LeaveInGeneral(sender: ActorRef): Unit = { context.unwatch(sender) - subs.UserEvents.find { - case (_, subscription) => (subscription eq sender) || subscription.path.equals(sender.path) - } match { - case Some((to, _)) => - LeaveService(to, sender) - case _ => () - } + subs + .UserEvents + .find { case (_, subscription) => (subscription eq sender) || subscription.path.equals(sender.path) } + .foreach { case (to, _) => LeaveService(to, sender) } } - def performStartSquad(sender: ActorRef, player: Player): Unit = { + def performStartSquad(sender: ActorRef, player: Player): Option[SquadFeatures] = { val invitingPlayerCharId = player.CharId if (EnsureEmptySquad(invitingPlayerCharId)) { - GetParticipatingSquad(player) match { - case Some(participating) => - //invitingPlayer became part of a squad while invited player was answering the original summons - Some(participating) - case _ => + GetParticipatingSquad(player) + .orElse { //generate a new squad, with invitingPlayer as the leader val features = StartSquad(player) val squad = features.Squad @@ -361,7 +461,9 @@ class SquadService extends Actor { subs.Publish(invitingPlayerCharId, SquadResponse.IdentifyAsSquadLeader(squad.GUID)) sender.tell(SquadInvitationManager.FinishStartSquad(features), self) Some(features) - } + } + } else { + None } } @@ -381,52 +483,14 @@ class SquadService extends Actor { def SquadActionInitCharId(tplayer: Player): Unit = { val charId = tplayer.CharId - GetParticipatingSquad(charId) match { - case None => () - case Some(features) => - features.Switchboard ! SquadSwitchboard.Join(tplayer, 0, sender()) - } + GetParticipatingSquad(charId) + .foreach(features => features.Switchboard.tell(SquadSwitchboard.Join(tplayer, 0, sender()), self)) } def SquadServiceReloadSquadDecoration(faction: PlanetSideEmpire.Value, to: Long): Unit = { ApplySquadDecorationToEntriesForUser(faction, to) } - def SquadActionMembership(tplayer: Player, zone: Zone, action: Any): Unit = { - action match { - case SquadAction.Membership(SquadRequestType.Invite, invitingPlayer, Some(_invitedPlayer), invitedName, _) => - SquadActionMembershipInvite(tplayer, invitingPlayer, _invitedPlayer, invitedName) - - case SquadAction.Membership(SquadRequestType.ProximityInvite, invitingPlayer, _, _, _) => - SquadActionMembershipProximityInvite(zone, invitingPlayer) - - case SquadAction.Membership(SquadRequestType.Accept, invitedPlayer, _, _, _) => - SquadActionMembershipAccept(tplayer, invitedPlayer) - - case SquadAction.Membership(SquadRequestType.Leave, actingPlayer, _leavingPlayer, name, _) => - SquadActionMembershipLeave(tplayer, actingPlayer, _leavingPlayer, name) - - case SquadAction.Membership(SquadRequestType.Reject, rejectingPlayer, _, _, _) => - SquadActionMembershipReject(tplayer, rejectingPlayer) - - case SquadAction.Membership(SquadRequestType.Disband, char_id, _, _, _) => - SquadActionMembershipDisband(char_id) - - case SquadAction.Membership(SquadRequestType.Cancel, cancellingPlayer, _, _, _) => - SquadActionMembershipCancel(cancellingPlayer) - - case SquadAction.Membership(SquadRequestType.Promote, _, _, _, _) => - () -// case SquadAction.Membership(SquadRequestType.Promote, promotingPlayer, Some(_promotedPlayer), promotedName, _) => -// SquadActionMembershipPromote(promotingPlayer, _promotedPlayer, promotedName, SquadServiceMessage(tplayer, zone, action), sender()) - - case SquadAction.Membership(event, _, _, _, _) => - debug(s"SquadAction.Membership: $event is not yet supported") - - case _ => () - } - } - def SquadActionMembershipInvite( tplayer: Player, invitingPlayer: Long, @@ -445,59 +509,57 @@ class SquadService extends Actor { }) .headOption .collectFirst { - //important: squads must know about the person too + //important: squads must know about the person too a => subs.UserEvents.keys.find(_ == a.id) - }.flatten match { - case Some(invitedPlayer) if invitingPlayer != invitedPlayer => - (GetParticipatingSquad(invitingPlayer), GetParticipatingSquad(invitedPlayer)) match { - case (Some(features1), Some(features2)) - if features1.Squad.GUID == features2.Squad.GUID => - //both players are in the same squad; no need to do anything + } + .flatten + .collect { + case invitedPlayer if invitingPlayer != invitedPlayer => + (GetParticipatingSquad(invitingPlayer), GetParticipatingSquad(invitedPlayer)) match { + case (Some(features1), Some(features2)) + if features1.Squad.GUID == features2.Squad.GUID => + //both players are in the same squad; no need to do anything - case (Some(invitersFeatures), Some(invitedFeatures)) if { - val squad1 = invitersFeatures.Squad - val squad2 = invitedFeatures.Squad - squad1.Leader.CharId == invitingPlayer && squad2.Leader.CharId == invitedPlayer && - squad1.Size > 1 && squad2.Size > 1 } => - //we might do some platoon chicanery with this case later - //TODO platoons + case (Some(invitersFeatures), Some(invitedFeatures)) if { + val squad1 = invitersFeatures.Squad + val squad2 = invitedFeatures.Squad + squad1.Leader.CharId == invitingPlayer && squad2.Leader.CharId == invitedPlayer && + squad1.Size > 1 && squad2.Size > 1 } => + //we might do some platoon chicanery with this case later + //TODO platoons - case (Some(invitersFeatures), Some(invitedFeatures)) - if invitedFeatures.Squad.Size == 1 => - //both players belong to squads, but the invitedPlayer's squad (invitedFeatures) is underutilized - //treat the same as "the classic situation" using invitersFeatures - invitations.createVacancyInvite(tplayer, invitedPlayer, invitersFeatures) + case (Some(invitersFeatures), Some(invitedFeatures)) + if invitedFeatures.Squad.Size == 1 => + //both players belong to squads, but the invitedPlayer's squad (invitedFeatures) is underutilized + //treat the same as "the classic situation" using invitersFeatures + invitations.createInvitationToJoinSquad(tplayer, invitedPlayer, invitersFeatures) - case (Some(invitersFeatures), Some(invitedFeatures)) - if invitersFeatures.Squad.Size == 1 => - //both players belong to squads, but the invitingPlayer's squad is underutilized by comparison - //treat the same as "indirection ..." using squad2 - invitations.createIndirectInvite(tplayer, invitedPlayer, invitedFeatures) + case (Some(invitersFeatures), Some(invitedFeatures)) + if invitersFeatures.Squad.Size == 1 => + //both players belong to squads, but the invitingPlayer's squad is underutilized by comparison + //treat the same as "indirection ..." using squad2 + invitations.createIndirectInvite(tplayer, invitedPlayer, invitedFeatures) - case (Some(features), None) => - //the classic situation - invitations.createVacancyInvite(tplayer, invitedPlayer, features) + case (Some(features), None) => + //the classic situation + invitations.createInvitationToJoinSquad(tplayer, invitedPlayer, features) - case (None, Some(features)) => - //indirection; we're trying to invite ourselves to someone else's squad - invitations.createIndirectInvite(tplayer, invitedPlayer, features) + case (None, Some(features)) => + //indirection; we're trying to invite ourselves to someone else's squad + invitations.createIndirectInvite(tplayer, invitedPlayer, features) - case (None, None) => - //neither the invited player nor the inviting player belong to any squad - invitations.createSpontaneousInvite(tplayer, invitedPlayer) + case (None, None) => + //neither the invited player nor the inviting player belong to any squad + invitations.createInvitationToCreateASquad(tplayer, invitedPlayer) - case _ => () - } - case _ => () - } + case _ => () + } + } } def SquadActionMembershipProximityInvite(zone: Zone, invitingPlayer: Long): Unit = { - GetLeadingSquad(invitingPlayer, None) match { - case Some(features) => - invitations.handleProximityInvite(zone, invitingPlayer, features) - case _ => () - } + GetLeadingSquad(invitingPlayer, None) + .foreach(features => invitations.createProximityInvite(zone, invitingPlayer, features)) } def SquadActionMembershipAccept(tplayer: Player, invitedPlayer: Long): Unit = { @@ -505,8 +567,8 @@ class SquadService extends Actor { } def SquadActionMembershipLeave(tplayer: Player, actingPlayer: Long, _leavingPlayer: Option[Long], name: String): Unit = { - GetParticipatingSquad(actingPlayer) match { - case Some(features) => + GetParticipatingSquad(actingPlayer) + .foreach { features => val squad = features.Squad val leader = squad.Leader.CharId (if (name.nonEmpty) { @@ -523,58 +585,37 @@ class SquadService extends Actor { case Some(id) => subs.UserEvents.keys.find(_ == id) case None => None } - }) match { - case _ @ Some(leavingPlayer) - if GetParticipatingSquad(leavingPlayer).contains(features) => //kicked player must be in the same squad - if (actingPlayer == leader) { - if (leavingPlayer == leader || squad.Size == 2) { - //squad leader is leaving his own squad, so it will be disbanded - //OR squad is only composed of two people, so it will be closed-out when one of them leaves - DisbandSquad(features) - } else { - //kicked by the squad leader - subs.Publish( - leavingPlayer, - SquadResponse.Membership( - SquadResponseType.Leave, - 0, - 0, - leavingPlayer, - Some(leader), - tplayer.Name, - unk5=false, - Some(None) - ) - ) - subs.Publish( - leader, - SquadResponse.Membership( - SquadResponseType.Leave, - 0, - 0, - leader, - Some(leavingPlayer), - "", - unk5=true, - Some(None) - ) - ) - LeaveSquad(leavingPlayer, features) - } - } else if (leavingPlayer == actingPlayer) { - if (squad.Size == 2) { - //squad is only composed of two people, so it will be closed-out when one of them leaves - DisbandSquad(features) - } else { - //leaving the squad of own accord - LeaveSquad(actingPlayer, features) - } + }) + .collect { case leavingPlayer + if GetParticipatingSquad(leavingPlayer).contains(features) => //kicked player must be in the same squad + if (actingPlayer == leader) { + if (leavingPlayer == leader || squad.Size == 2) { + //squad leader is leaving his own squad, so it will be disbanded + //OR squad is only composed of two people, so it will be closed-out when one of them leaves + DisbandSquad(features) + } else { + //kicked by the squad leader + subs.Publish( + leavingPlayer, + SquadResponse.Membership(SquadResponseType.Leave, leavingPlayer, Some(leader), tplayer.Name, unk5 = false) + ) + subs.Publish( + leader, + SquadResponse.Membership( SquadResponseType.Leave, leader, Some(leavingPlayer), "", unk5 = true) + ) + LeaveSquad(leavingPlayer, features) } - - case _ => () + } else if (leavingPlayer == actingPlayer) { + if (squad.Size == 2) { + //squad is only composed of two people, so it will be closed-out when one of them leaves + DisbandSquad(features) + } else { + //leaving the squad of own accord + LeaveSquad(actingPlayer, features) + } + } } - case _ => () - } + } } def SquadActionMembershipReject(tplayer: Player, rejectingPlayer: Long): Unit = { @@ -586,11 +627,8 @@ class SquadService extends Actor { } def SquadActionMembershipDisband(charId: Long): Unit = { - GetLeadingSquad(charId, None) match { - case Some(features) => - DisbandSquad(features) - case None => () - } + GetLeadingSquad(charId, None) + .foreach(features => DisbandSquad(features)) } def SquadActionMembershipCancel(cancellingPlayer: Long): Unit = { @@ -605,17 +643,17 @@ class SquadService extends Actor { msg: SquadServiceMessage, ref: ActorRef ): Unit = { - val promotedPlayer: Long = subs.UserEvents.keys.find(_ == promotionCandidatePlayer).orElse({ - LivePlayerList - .WorldPopulation({ case (_, a: Avatar) => a.name.equalsIgnoreCase(promotionCandidateName) }) - .headOption match { - case Some(a) => Some(a.id) - case None => None + val promotedPlayer: Long = subs + .UserEvents + .keys + .find(_ == promotionCandidatePlayer) + .orElse { + LivePlayerList + .WorldPopulation({ case (_, a: Avatar) => a.name.equalsIgnoreCase(promotionCandidateName) }) + .headOption + .map(_.id.toLong) } - }) match { - case Some(player: Long) => player - case _ => -1L - } + .getOrElse(-1L) //sponsorPlayer should be squad leader (GetLeadingSquad(sponsoringPlayer, None), GetParticipatingSquad(promotedPlayer)) match { case (Some(features), Some(features2)) if features.Squad.GUID == features2.Squad.GUID => @@ -639,113 +677,15 @@ class SquadService extends Actor { message: SquadServiceMessage, tplayer: Player ): Unit = { - GetParticipatingSquad(tplayer) match { - case Some(features) => + GetParticipatingSquad(tplayer) + .collect { features => features.Switchboard.tell(message, sender()) - case None => + features + } + .orElse { log.warn(s"Unsupported squad waypoint behavior: $message") - } - } - - def SquadActionDefinition( - message: SquadServiceMessage, - action: SquadRequestAction, - guid: PlanetSideGUID - ): Unit = { - val tplayer = message.tplayer - (action match { - //the following actions only perform an action upon the squad - case _: ChangeSquadPurpose => GetOrCreateSquadOnlyIfLeader(tplayer) - case _: ChangeSquadZone => GetOrCreateSquadOnlyIfLeader(tplayer) - case _: AddSquadMemberPosition => GetOrCreateSquadOnlyIfLeader(tplayer) - case _: ChangeSquadMemberRequirementsRole => GetOrCreateSquadOnlyIfLeader(tplayer) - case _: ChangeSquadMemberRequirementsDetailedOrders => GetOrCreateSquadOnlyIfLeader(tplayer) - case _: ChangeSquadMemberRequirementsCertifications => GetOrCreateSquadOnlyIfLeader(tplayer) - case _: LocationFollowsSquadLead => GetOrCreateSquadOnlyIfLeader(tplayer) - case _: RequestListSquad => GetOrCreateSquadOnlyIfLeader(tplayer) - case _: StopListSquad => GetLeadingSquad(tplayer, None) - //the following actions cause changes with the squad composition or with invitations - case AutoApproveInvitationRequests(_) => - GetOrCreateSquadOnlyIfLeader(tplayer) match { - case out @ Some(features) => - invitations.handleDefinitionAction(tplayer, action, features) - out - case None => - None - } - case CloseSquadMemberPosition(position) => - GetOrCreateSquadOnlyIfLeader(tplayer) match { - case out @ Some(features) - if features.Squad.Membership(position).CharId > 0 => - val squad = features.Squad - LeaveSquad(squad.Membership(position).CharId, features) - out - case _ => - None - } - case FindLfsSoldiersForRole(_) => - GetLeadingSquad(tplayer, None) match { - case Some(features) => - invitations.handleDefinitionAction(tplayer, action, features) - case _ => () - } None - case CancelFind() => - GetLeadingSquad(tplayer, None) match { - case Some(features) => - invitations.handleDefinitionAction(tplayer, action, features) - case _ => () - } - None - case SelectRoleForYourself(_) => - GetParticipatingSquad(tplayer) match { - case out @ Some(features) => - if (features.Squad.GUID == guid) { - out - } else { - //this isn't the squad we're looking for by GUID; as a precaution, reload all of the published squad list - val faction = tplayer.Faction - subs.Publish(faction, SquadResponse.InitList(PublishedLists(tplayer.Faction))) - None - } - case _ => - GetSquad(guid) match { - case Some(features) => - invitations.handleDefinitionAction(tplayer, action, features) - case _ => () - } - None - } - case _: CancelSelectRoleForYourself => - GetSquad(guid) match { - case Some(features) => - invitations.handleDefinitionAction(tplayer, action, features) - case _ => () - } - None - case _/*search*/: SearchForSquadsWithParticularRole => -// SquadActionDefinitionSearchForSquadsWithParticularRole(tplayer, search) - None - case _: CancelSquadSearch => -// SquadActionDefinitionCancelSquadSearch(tplayer.CharId) - None - case _: DisplaySquad => - GetSquad(guid) match { - case out @ Some(_) => - SquadActionDefinitionDisplaySquad(tplayer, guid) - out - case None => - None - } - case _: SquadInitializationIssue => - SquadActionDefinitionSquadInitializationIssue(tplayer, guid) - None - case _ => - GetSquad(guid) - }) match { - case Some(features) => features.Switchboard.tell(message, sender()) - case None => () - } + } } def SquadActionUpdate( @@ -753,10 +693,8 @@ class SquadService extends Actor { char_id: Long, replyTo: ActorRef, ): Unit = { - GetParticipatingSquad(char_id) match { - case Some(features) => features.Switchboard.tell(message, replyTo) - case None => () - } + GetParticipatingSquad(char_id) + .foreach(features => features.Switchboard.tell(message, replyTo)) } def GetOrCreateSquadOnlyIfLeader(player: Player): Option[SquadFeatures] = { @@ -776,14 +714,15 @@ class SquadService extends Actor { criteria: SearchForSquadsWithParticularRole ): Unit = { val charId = tplayer.CharId - searchData.get(charId) match { - case Some(_) => () - //already searching, so do nothing(?) - case None => + searchData + .get(charId) + .orElse { val data = SquadService.SearchCriteria(tplayer.Faction, criteria) searchData.put(charId, data) SquadActionDefinitionSearchForSquadsUsingCriteria(charId, data) - } + None + } + //if already searching, do nothing } private def SquadActionDefinitionSearchForSquadsUsingCriteria( @@ -797,22 +736,23 @@ class SquadService extends Actor { } private def SearchForSquadsResults(criteria: SquadService.SearchCriteria): List[PlanetSideGUID] = { - publishedLists.get(criteria.faction) match { - case Some(squads) if squads.nonEmpty => - squads.flatMap { guid => SearchForSquadsResults(criteria, guid) }.toList - case _ => - Nil - } + publishedLists + .get(criteria.faction) + .collect { + case squads if squads.nonEmpty => + squads.flatMap { guid => SearchForSquadsResults(criteria, guid) }.toList + } + .getOrElse(Nil) } def SquadActionDefinitionCancelSquadSearch(charId: Long): Unit = { - searchData.remove(charId) match { - case None => () - case Some(data) => - SearchForSquadsResults(data).foreach { guid => - subs.Publish(charId, SquadResponse.SquadDecoration(guid, squadFeatures(guid).Squad)) - } - } + searchData + .remove(charId) + .map(SearchForSquadsResults) + .getOrElse(Nil) + .foreach { guid => + subs.Publish(charId, SquadResponse.SquadDecoration(guid, squadFeatures(guid).Squad)) + } } private def SearchForSquadsResults( @@ -872,14 +812,14 @@ class SquadService extends Actor { } def CleanUpSquadFeatures(removed: List[Long], guid: PlanetSideGUID, @unused position: Int): Unit = { - GetSquad(guid) match { - case Some(features) => - features.ProxyInvites = features.ProxyInvites.filterNot(removed.contains) - if (features.ProxyInvites.isEmpty) { + GetSquad(guid) + .collect { + case features if features.ProxyInvites.isEmpty => + features.ProxyInvites = features.ProxyInvites.filterNot(removed.contains) features.SearchForRole = None - } - case None => () - } + case features => + features.ProxyInvites = features.ProxyInvites.filterNot(removed.contains) + } } /** @@ -945,18 +885,19 @@ class SquadService extends Actor { def JoinSquad(player: Player, features: SquadFeatures, position: Int): Boolean = { val charId = player.CharId val squad = features.Squad - subs.UserEvents.get(charId) match { - case Some(events) - if squad.isAvailable(position, player.avatar.certifications) && - EnsureEmptySquad(charId) => - memberToSquad(charId) = squad.GUID - subs.MonitorSquadDetails.subtractOne(charId) - invitations.handleCleanup(charId) - features.Switchboard ! SquadSwitchboard.Join(player, position, events) - true - case _ => - false - } + subs + .UserEvents + .get(charId) + .collect { + case events + if squad.isAvailable(position, player.avatar.certifications) && EnsureEmptySquad(charId) => + memberToSquad(charId) = squad.GUID + subs.MonitorSquadDetails.subtractOne(charId) + invitations.handleCleanup(charId) + features.Switchboard ! SquadSwitchboard.Join(player, position, events) + true + } + .getOrElse(false) } /** @@ -968,16 +909,16 @@ class SquadService extends Actor { * `false`, otherwise */ def EnsureEmptySquad(charId: Long): Boolean = { - GetParticipatingSquad(charId) match { - case None => - true - case Some(features) if features.Squad.Size == 1 => - CloseSquad(features.Squad) - true - case _ => - log.warn("EnsureEmptySquad: the invited player is already a member of a squad and can not join a second one") - false - } + GetParticipatingSquad(charId) + .collect { + case features if features.Squad.Size == 1 => + CloseSquad(features.Squad) + true + case _ => + log.warn("EnsureEmptySquad: the invited player is already a member of a squad and can not join a second one") + false + } + .getOrElse(true) } /** @@ -992,15 +933,15 @@ class SquadService extends Actor { def LeaveSquad(charId: Long, features: SquadFeatures): Boolean = { val squad = features.Squad val membership = squad.Membership.zipWithIndex - membership.find { case (_member, _) => _member.CharId == charId } match { - case Some(_) if squad.Leader.CharId != charId => + membership + .find { case (_member, _) => _member.CharId == charId } + .collect { case _ if squad.Leader.CharId != charId => memberToSquad.remove(charId) subs.MonitorSquadDetails.subtractOne(charId) features.Switchboard ! SquadSwitchboard.Leave(charId) true - case _ => - false - } + } + .getOrElse(false) } /** @@ -1079,7 +1020,7 @@ class SquadService extends Actor { ) //the squad is being disbanded, the squad events channel is also going away; use cached character ids info(s"Squad #${squad.GUID.guid} has been disbanded.") - subs.Publish(leader, SquadResponse.Membership(SquadResponseType.Disband, 0, 0, leader, None, "", unk5=true, Some(None))) + subs.Publish(leader, SquadResponse.Membership(SquadResponseType.Disband, leader, None, "", unk5=true)) } /** @@ -1107,7 +1048,7 @@ class SquadService extends Actor { (membership.filterNot(_ == leader) ++ subs.PublishToMonitorTargets(squad.GUID, Nil)) .toSet .foreach { charId : Long => - subs.Publish(charId, SquadResponse.Membership(SquadResponseType.Disband, 0, 0, charId, None, "", unk5=false, Some(None))) + subs.Publish(charId, SquadResponse.Membership(SquadResponseType.Disband, charId, None, "", unk5=false)) } } @@ -1355,24 +1296,25 @@ class SquadService extends Actor { if (listOfCharIds.nonEmpty) { invitations.tryChainAcceptance(player, charId, listOfCharIds, features) } else { - invitations.SquadActionDefinitionAutoApproveInvitationRequests(charId, features) + invitations.autoApproveInvitationRequests(charId, features) } } } def ChainRejectionFromSquad(player: Player, charId: Long, listOfCharIds: List[Long]): Unit = { GetLeadingSquad(charId, None) - .foreach { features => - if (listOfCharIds.nonEmpty) { + .collect { + case features if listOfCharIds.nonEmpty => invitations.tryChainRejection(player, charId, listOfCharIds, features) - } else { + case features => invitations.tryChainRejectionAll(charId, features) - } } } } object SquadService { + final private val FactionWordSalad: String = "TRNCVS" + final case class PerformStartSquad(player: Player) final case class PerformJoinSquad(player: Player, features: SquadFeatures, position: Int) diff --git a/src/main/scala/net/psforever/services/teamwork/SquadServiceResponse.scala b/src/main/scala/net/psforever/services/teamwork/SquadServiceResponse.scala index 8935627e..e35d757b 100644 --- a/src/main/scala/net/psforever/services/teamwork/SquadServiceResponse.scala +++ b/src/main/scala/net/psforever/services/teamwork/SquadServiceResponse.scala @@ -41,6 +41,16 @@ object SquadResponse { unk5: Boolean, unk6: Option[Option[String]] ) extends Response //see SquadMembershipResponse + object Membership { + def apply( + requestType: SquadResponseType.Value, + unk3: Long, + unk4: Option[Long], + playerName: String, + unk5: Boolean + ): Membership = new Membership(requestType, unk1 = 0, unk2 = 0, unk3, unk4, playerName, unk5, Some(None)) + } + final case class WantsSquadPosition(leader_char_id: Long, bid_name: String) extends Response final case class Join(squad: Squad, positionsToUpdate: List[Int], channel: String, ref: ActorRef) extends Response final case class Leave(squad: Squad, positionsToUpdate: List[(Long, Int)]) extends Response diff --git a/src/main/scala/net/psforever/services/teamwork/invitations/IndirectInvite.scala b/src/main/scala/net/psforever/services/teamwork/invitations/IndirectInvite.scala new file mode 100644 index 00000000..8e6a1ee8 --- /dev/null +++ b/src/main/scala/net/psforever/services/teamwork/invitations/IndirectInvite.scala @@ -0,0 +1,80 @@ +// Copyright (c) 2024 PSForever +package net.psforever.services.teamwork.invitations + +import net.psforever.objects.Player +import net.psforever.objects.teamwork.SquadFeatures +import net.psforever.services.teamwork.SquadInvitationManager +import net.psforever.types.PlanetSideGUID + +import scala.annotation.unused + +/** + * Utilized to redirect an (accepted) invitation request to the proper squad leader. + * No direct action causes this message. + * Depending on the situation, either the squad leader or the player who would join the squad handle this invitation. + * + * @param recruitOrOwner player who would be joining the squad; + * may or may not have actually requested it in the first place + * @param features squad + */ +final case class IndirectInvite(recruitOrOwner: Player, features: SquadFeatures) + extends Invitation(recruitOrOwner.CharId, recruitOrOwner.Name) { + def handleInvitation(indirectInviteFunc: (IndirectInvite, Player, Long, Long, String) => Boolean)( + manager: SquadInvitationManager, + invitedPlayer: Long, + invitingPlayer: Long, + otherName: String + ): Unit = { + indirectInviteFunc(this, recruitOrOwner, invitedPlayer, invitingPlayer, otherName) + } + + def handleAcceptance( + manager: SquadInvitationManager, + @unused player: Player, + invitedPlayer: Long, + @unused invitedPlayerSquadOpt: Option[SquadFeatures] + ): Unit = { + //tplayer / invitedPlayer is actually the squad leader + if (SquadInvitationManager.canEnrollInSquad(features, recruitOrOwner.CharId)) { + val recruitCharId = recruitOrOwner.CharId + manager.handleVacancyInvite(features, recruitCharId, invitedPlayer, recruitOrOwner) match { + case Some((_, line)) => + manager.acceptanceMessages(invitedPlayer, recruitCharId, recruitOrOwner.Name) + manager.joinSquad(recruitOrOwner, features, line) + manager.cleanUpAllInvitesWithPlayer(recruitCharId) + manager.cleanUpInvitesForSquadAndPosition(features, line) + //TODO since we are the squad leader, we do not want to brush off our queued squad invite tasks + case _ => () + } + } + } + + def handleRejection( + manager: SquadInvitationManager, + player: Player, + rejectingPlayer: Long, + @unused squadsToLeaders: List[(PlanetSideGUID, SquadFeatures)] + ): Unit = { + //todo how to do this? + } + + def doRejection( + manager: SquadInvitationManager, + player: Player, + rejectingPlayer: Long + ): Unit = { + //todo how to do this? + } + + def canBeAutoApproved: Boolean = true + + def getOptionalSquad: Option[SquadFeatures] = Some(features) + + def getPlayer: Player = recruitOrOwner + + def appliesToPlayer(playerCharId: Long): Boolean = playerCharId == recruitOrOwner.CharId + + def appliesToSquad(guid: PlanetSideGUID): Boolean = features.Squad.GUID == guid + + def appliesToSquadAndPosition(guid: PlanetSideGUID, squadPosition: Int): Boolean = false +} diff --git a/src/main/scala/net/psforever/services/teamwork/invitations/Invitation.scala b/src/main/scala/net/psforever/services/teamwork/invitations/Invitation.scala new file mode 100644 index 00000000..82e20769 --- /dev/null +++ b/src/main/scala/net/psforever/services/teamwork/invitations/Invitation.scala @@ -0,0 +1,75 @@ +// Copyright (c) 2024 PSForever +package net.psforever.services.teamwork.invitations + +import net.psforever.objects.Player +import net.psforever.objects.teamwork.SquadFeatures +import net.psforever.services.teamwork.SquadInvitationManager +import net.psforever.types.PlanetSideGUID + +/** + * The base of all objects that exist for the purpose of communicating invitation from one player to the next. + * @param charId inviting player's unique identifier number + * @param name inviting player's name + */ +abstract class Invitation(charId: Long, name: String) { + def inviterCharId: Long = charId + def inviterName: String = name + + /** + * A branched response for processing (new) invitation objects that have been submitted to the system.
+ *
+ * A comparison is performed between the original invitation object and an invitation object + * that represents the potential modification or redirection of the current active invitation obect. + * Any further action is only performed when an "is equal" comparison is `true`. + * When passing, the system publishes up to two messages + * to users that would anticipate being informed of squad join activity. + * @param indirectInviteFunc the method that cans the responding behavior should an `IndirectInvite` object being consumed + * @param invitedPlayer the unique character identifier for the player being invited; + * in actuality, represents the player who will address the invitation object + * @param invitingPlayer the unique character identifier for the player who invited the former + * @param otherName a name to be used in message composition + */ + def handleInvitation(indirectInviteFunc: (IndirectInvite, Player, Long, Long, String) => Boolean)( + manager: SquadInvitationManager, + invitedPlayer: Long, + invitingPlayer: Long, + otherName: String + ): Unit + + def handleAcceptance( + manager: SquadInvitationManager, + player: Player, + invitedPlayer: Long, + invitedPlayerSquadOpt: Option[SquadFeatures] + ): Unit + + def handleRejection( + manager: SquadInvitationManager, + player: Player, + rejectingPlayer: Long, + squadsToLeaders: List[(PlanetSideGUID, SquadFeatures)] + ): Unit + + def doRejection( + manager: SquadInvitationManager, + player: Player, + rejectingPlayer: Long + ): Unit + + def canBeAutoApproved: Boolean + + def getOptionalSquad: Option[SquadFeatures] + + /** + * na + * @return active player entity associated with this invite; + * can be `null` as some invitations do not retain such character data + */ + def getPlayer: Player + + def appliesToPlayer(playerCharId: Long): Boolean + + def appliesToSquad(guid: PlanetSideGUID): Boolean + + def appliesToSquadAndPosition(guid: PlanetSideGUID, squadPosition: Int): Boolean +} diff --git a/src/main/scala/net/psforever/services/teamwork/invitations/InvitationToCreateASquad.scala b/src/main/scala/net/psforever/services/teamwork/invitations/InvitationToCreateASquad.scala new file mode 100644 index 00000000..60a2bcf0 --- /dev/null +++ b/src/main/scala/net/psforever/services/teamwork/invitations/InvitationToCreateASquad.scala @@ -0,0 +1,112 @@ +// Copyright (c) 2024 PSForever +package net.psforever.services.teamwork.invitations + +import net.psforever.objects.Player +import net.psforever.objects.teamwork.SquadFeatures +import net.psforever.services.teamwork.SquadInvitationManager.FinishStartSquad +import net.psforever.services.teamwork.{SquadInvitationManager, SquadResponse} +import net.psforever.types.{PlanetSideGUID, SquadResponseType} + +import scala.annotation.unused +import scala.util.Success + +/** + * Utilized when one player issues an invite for some other player for a squad that does not yet exist. + * This invitation is handled by the player who would be joining the squad. + * + * @param futureSquadLeader player who wishes to become the leader of a squad + */ +final case class InvitationToCreateASquad(futureSquadLeader: Player) + extends Invitation(futureSquadLeader.CharId, futureSquadLeader.Name) { + def handleInvitation(indirectInviteFunc: (IndirectInvite, Player, Long, Long, String) => Boolean)( + manager: SquadInvitationManager, + invitedPlayer: Long, + invitingPlayer: Long, + otherName: String + ): Unit = { + manager.publish( + invitedPlayer, + SquadResponse.Membership(SquadResponseType.Invite, inviterCharId, Some(invitedPlayer), futureSquadLeader.Name, unk5 = false) + ) + manager.publish( + inviterCharId, + SquadResponse.Membership(SquadResponseType.Invite, invitedPlayer, Some(inviterCharId), futureSquadLeader.Name, unk5 = true) + ) + } + + def handleAcceptance( + manager: SquadInvitationManager, + player: Player, + invitedPlayer: Long, + invitedPlayerSquadOpt: Option[SquadFeatures] + ): Unit = { + if (manager.notLimitedByEnrollmentInSquad(invitedPlayerSquadOpt, invitedPlayer)) { + //accepted an invitation to join an existing squad + import scala.concurrent.ExecutionContext.Implicits.global + val leaderCharId = futureSquadLeader.CharId + manager + .askToCreateANewSquad(futureSquadLeader) + .onComplete { + case Success(FinishStartSquad(features)) => + manager.handleVacancyInvite(features, invitedPlayer, leaderCharId, player) match { + case Some((_, line)) => + manager.publish( + invitedPlayer, + SquadResponse.Membership(SquadResponseType.Accept, invitedPlayer, Some(leaderCharId), "", unk5 = true) + ) + manager.publish( + leaderCharId, + SquadResponse.Membership(SquadResponseType.Accept, leaderCharId, Some(invitedPlayer), player.Name, unk5 = false) + ) + manager.joinSquad(player, features, line) + manager.cleanUpQueuedInvites(invitedPlayer) + case _ => () + } + case _ => () + } + } + } + + def handleRejection( + manager: SquadInvitationManager, + player: Player, + rejectingPlayer: Long, + @unused squadsToLeaders: List[(PlanetSideGUID, SquadFeatures)] + ): Unit = { + //rejectingPlayer is the would-be squad member; the would-be squad leader sent the request and was rejected + val invitingPlayerCharId = futureSquadLeader.CharId + doRejection(manager, player, rejectingPlayer) + manager.publish( + rejectingPlayer, + SquadResponse.Membership(SquadResponseType.Reject, rejectingPlayer, Some(invitingPlayerCharId), "", unk5 = true) + ) + manager.publish( + invitingPlayerCharId, + SquadResponse.Membership(SquadResponseType.Reject, invitingPlayerCharId, Some(rejectingPlayer), player.Name, unk5 = false) + ) + manager.publish( + rejectingPlayer, + SquadResponse.SquadRelatedComment(s"Your request to form a squad has been refused.") + ) + } + + def doRejection( + manager: SquadInvitationManager, + player: Player, + rejectingPlayer: Long + ): Unit = { + manager.refused(rejectingPlayer, futureSquadLeader.CharId) + } + + def canBeAutoApproved: Boolean = false + + def getOptionalSquad: Option[SquadFeatures] = None + + def getPlayer: Player = futureSquadLeader + + def appliesToPlayer(playerCharId: Long): Boolean = playerCharId == futureSquadLeader.CharId + + def appliesToSquad(guid: PlanetSideGUID): Boolean = false + + def appliesToSquadAndPosition(guid: PlanetSideGUID, squadPosition: Int): Boolean = false +} diff --git a/src/main/scala/net/psforever/services/teamwork/invitations/InvitationToJoinSquad.scala b/src/main/scala/net/psforever/services/teamwork/invitations/InvitationToJoinSquad.scala new file mode 100644 index 00000000..39228652 --- /dev/null +++ b/src/main/scala/net/psforever/services/teamwork/invitations/InvitationToJoinSquad.scala @@ -0,0 +1,95 @@ +// Copyright (c) 2024 PSForever +package net.psforever.services.teamwork.invitations + +import net.psforever.objects.Player +import net.psforever.objects.teamwork.SquadFeatures +import net.psforever.services.teamwork.{SquadInvitationManager, SquadResponse} +import net.psforever.types.{PlanetSideGUID, SquadResponseType} + +import scala.annotation.unused + +/** + * Utilized when one squad member issues an invite for some other player. + * Accessed by an existing squad member using the "Invite" menu option on another player. + * This invitation is handled by the player who would join the squad. + * + * @param charId unique character identifier of the player who sent the invite + * @param name name the player who sent the invite + * @param features the squad + */ +final case class InvitationToJoinSquad(charId: Long, name: String, features: SquadFeatures) + extends Invitation(charId, name) { + def handleInvitation(indirectInviteFunc: (IndirectInvite, Player, Long, Long, String) => Boolean)( + manager: SquadInvitationManager, + invitedPlayer: Long, + invitingPlayer: Long, + otherName: String + ): Unit = { + manager.publish( + invitedPlayer, + SquadResponse.Membership(SquadResponseType.Invite, charId, Some(invitedPlayer), name, unk5 = false) + ) + manager.publish( + charId, + SquadResponse.Membership(SquadResponseType.Invite, invitedPlayer, Some(charId), name, unk5 = true) + ) + } + + def handleAcceptance( + manager: SquadInvitationManager, + player: Player, + invitedPlayer: Long, + @unused invitedPlayerSquadOpt: Option[SquadFeatures] + ): Unit = { + if ( + manager.notLimitedByEnrollmentInSquad(invitedPlayerSquadOpt, invitedPlayer) && + SquadInvitationManager.canEnrollInSquad(features, invitedPlayer) + ) { + //accepted an invitation to join an existing squad + manager.handleVacancyInvite(features, invitedPlayer, charId, player) match { + case Some((_, line)) => + manager.acceptanceMessages(charId, invitedPlayer, player.Name) + manager.joinSquad(player, features, line) + manager.cleanUpQueuedInvites(invitedPlayer) + manager.cleanUpInvitesForSquadAndPosition(features, line) + case _ => () + } + } + } + + def handleRejection( + manager: SquadInvitationManager, + player: Player, + rejectingPlayer: Long, + @unused squadsToLeaders: List[(PlanetSideGUID, SquadFeatures)] + ): Unit = { + /*if SquadInvitationManager.notLeaderOfThisSquad(squadsToLeaders, features.Squad.GUID, rejectingPlayer)*/ + //rejectingPlayer is the would-be squad member; the squad leader sent the request and was rejected + doRejection(manager, player, rejectingPlayer) + manager.rejectionMessages(rejectingPlayer, charId, player.Name) + manager.publish( + rejectingPlayer, + SquadResponse.SquadRelatedComment(s"Your request to join squad '${features.Squad.Task}' has been refused.") + ) + } + + def doRejection( + manager: SquadInvitationManager, + player: Player, + rejectingPlayer: Long + ): Unit = { + manager.refused(rejectingPlayer, charId) + } + + def canBeAutoApproved: Boolean = false + + def getOptionalSquad: Option[SquadFeatures] = Some(features) + + def getPlayer: Player = null + + def appliesToPlayer(playerCharId: Long): Boolean = playerCharId == charId + + def appliesToSquad(guid: PlanetSideGUID): Boolean = features.Squad.GUID == guid + + def appliesToSquadAndPosition(guid: PlanetSideGUID, squadPosition: Int): Boolean = false +} diff --git a/src/main/scala/net/psforever/services/teamwork/invitations/LookingForSquadRoleInvite.scala b/src/main/scala/net/psforever/services/teamwork/invitations/LookingForSquadRoleInvite.scala new file mode 100644 index 00000000..9d9d3713 --- /dev/null +++ b/src/main/scala/net/psforever/services/teamwork/invitations/LookingForSquadRoleInvite.scala @@ -0,0 +1,96 @@ +// Copyright (c) 2024 PSForever +package net.psforever.services.teamwork.invitations + +import net.psforever.objects.teamwork.{Member, SquadFeatures} +import net.psforever.objects.{LivePlayerList, Player} +import net.psforever.services.teamwork.{SquadInvitationManager, SquadResponse} +import net.psforever.types.{PlanetSideGUID, SquadResponseType} + +import scala.annotation.unused + +/** + * Utilized in conjunction with an external queuing data structure + * to search for and submit requests to other players + * for the purposes of fill out an unoccupied squad role. + * This invitation is handled by the player who would be joining the squad. + * + * @param squadLeader squad leader + * @param features squad with the role + * @param position index of the role + */ +final case class LookingForSquadRoleInvite(squadLeader: Member, features: SquadFeatures, position: Int) + extends Invitation(squadLeader.CharId, squadLeader.Name) { + def handleInvitation(indirectInviteFunc: (IndirectInvite, Player, Long, Long, String) => Boolean)( + manager: SquadInvitationManager, + invitedPlayer: Long, + invitingPlayer: Long, + otherName: String + ): Unit = { + manager.publish( + invitedPlayer, + SquadResponse.Membership(SquadResponseType.Invite, invitedPlayer, Some(squadLeader.CharId), squadLeader.Name, unk5 = false) + ) + } + + def handleAcceptance( + manager: SquadInvitationManager, + player: Player, + invitedPlayer: Long, + @unused invitedPlayerSquadOpt: Option[SquadFeatures] + ): Unit = { + if ( + manager.notLimitedByEnrollmentInSquad(invitedPlayerSquadOpt, invitedPlayer) && + SquadInvitationManager.canEnrollInSquad(features, invitedPlayer) + ) { + val invitingPlayer = squadLeader.CharId + features.ProxyInvites = features.ProxyInvites.filterNot { _ == invitedPlayer } + if (manager.joinSquad(player, features, position)) { + //join this squad + manager.acceptanceMessages(invitingPlayer, invitedPlayer, player.Name) + manager.cleanUpQueuedInvites(player.CharId) + manager.cleanUpInvitesForSquadAndPosition(features, position) + } + } + } + + def handleRejection( + manager: SquadInvitationManager, + player: Player, + rejectingPlayer: Long, + @unused squadsToLeaders: List[(PlanetSideGUID, SquadFeatures)] + ): Unit = { + val leaderCharId = squadLeader.CharId + //rejectingPlayer is the would-be squad member; the squad leader sent the request and was rejected + doRejection(manager, player, rejectingPlayer) + manager.rejectionMessages(rejectingPlayer, leaderCharId, player.Name) + manager.publish( + rejectingPlayer, + SquadResponse.SquadRelatedComment(s"Your request to join squad '${features.Squad.Task}' has been refused.") + ) + } + + def doRejection( + manager: SquadInvitationManager, + player: Player, + rejectingPlayer: Long + ): Unit = { + manager.reloadSearchForRoleInvite( + LivePlayerList.WorldPopulation { _ => true }, + rejectingPlayer, + features, + position + ) + } + + def canBeAutoApproved: Boolean = false + + def getOptionalSquad: Option[SquadFeatures] = Some(features) + + def getPlayer: Player = null + + def appliesToPlayer(playerCharId: Long): Boolean = playerCharId == squadLeader.CharId + + def appliesToSquad(guid: PlanetSideGUID): Boolean = features.Squad.GUID == guid + + def appliesToSquadAndPosition(guid: PlanetSideGUID, squadPosition: Int): Boolean = appliesToSquad(guid) && position == squadPosition +} diff --git a/src/main/scala/net/psforever/services/teamwork/invitations/ProximityInvite.scala b/src/main/scala/net/psforever/services/teamwork/invitations/ProximityInvite.scala new file mode 100644 index 00000000..6f2f77fd --- /dev/null +++ b/src/main/scala/net/psforever/services/teamwork/invitations/ProximityInvite.scala @@ -0,0 +1,104 @@ +// Copyright (c) 2024 PSForever +package net.psforever.services.teamwork.invitations + +import net.psforever.objects.Player +import net.psforever.objects.teamwork.{Member, SquadFeatures} +import net.psforever.services.teamwork.{SquadInvitationManager, SquadResponse} +import net.psforever.types.{PlanetSideGUID, SquadResponseType} + +import scala.annotation.unused + +/** + * Utilized in conjunction with an external queuing data structure + * to search for and submit requests to other players + * for the purposes of fill out unoccupied squad roles. + * This invitation is handled by the player who would be joining the squad. + * + * @param squadLeader squad leader + * @param features squad + * @param position index of a role + */ +final case class ProximityInvite(squadLeader: Member, features: SquadFeatures, position: Int) + extends Invitation(squadLeader.CharId, squadLeader.Name) { + def handleInvitation(indirectInviteFunc: (IndirectInvite, Player, Long, Long, String) => Boolean)( + manager: SquadInvitationManager, + invitedPlayer: Long, + invitingPlayer: Long, + otherName: String + ): Unit = { + manager.publish( + invitedPlayer, + SquadResponse.Membership( + SquadResponseType.Invite, + invitedPlayer, + Some(squadLeader.CharId), + squadLeader.Name, + unk5 = false + ) + ) + } + + def handleAcceptance( + manager: SquadInvitationManager, + player: Player, + invitedPlayer: Long, + invitedPlayerSquadOpt: Option[SquadFeatures] + ): Unit = { + if ( + manager.notLimitedByEnrollmentInSquad(invitedPlayerSquadOpt, invitedPlayer) && + SquadInvitationManager.canEnrollInSquad(features, invitedPlayer) + ) { + val invitingPlayer = squadLeader.CharId + features.ProxyInvites = features.ProxyInvites.filterNot { _ == invitedPlayer } + if (manager.joinSquad(player, features, position)) { + //join this squad + manager.acceptanceMessages(invitingPlayer, invitedPlayer, player.Name) + manager.cleanUpAllInvitesWithPlayer(invitedPlayer) + val squad = features.Squad + if (squad.Size == squad.Capacity) { + //all available squad positions filled; terminate all remaining invitations + manager.cleanUpAllInvitesToSquad(features) + } + } else { + manager.reloadProximityInvite(player.Zone.Players, invitedPlayer, features, position) //TODO ? + } + } + + } + + def handleRejection( + manager: SquadInvitationManager, + player: Player, + rejectingPlayer: Long, + @unused squadsToLeaders: List[(PlanetSideGUID, SquadFeatures)] + ): Unit = { + /*if SquadInvitationManager.notLeaderOfThisSquad(squadsToLeaders, features.Squad.GUID, rejectingPlayer)*/ + //rejectingPlayer is the would-be squad member; the squad leader sent the request and was rejected + doRejection(manager, player, rejectingPlayer) + manager.rejectionMessage(rejectingPlayer) + manager.publish( + rejectingPlayer, + SquadResponse.SquadRelatedComment(s"Your request to join squad '${features.Squad.Task}' has been refused.") + ) + } + + def doRejection( + manager: SquadInvitationManager, + player: Player, + rejectingPlayer: Long + ): Unit = { + manager.reloadProximityInvite(player.Zone.Players, rejectingPlayer, features, position) + } + + def canBeAutoApproved: Boolean = false + + def getOptionalSquad: Option[SquadFeatures] = Some(features) + + def getPlayer: Player = null + + def appliesToPlayer(playerCharId: Long): Boolean = playerCharId == squadLeader.CharId + + def appliesToSquad(guid: PlanetSideGUID): Boolean = features.Squad.GUID == guid + + def appliesToSquadAndPosition(guid: PlanetSideGUID, squadPosition: Int): Boolean = appliesToSquad(guid) && position == squadPosition +} diff --git a/src/main/scala/net/psforever/services/teamwork/invitations/RequestToJoinSquadRole.scala b/src/main/scala/net/psforever/services/teamwork/invitations/RequestToJoinSquadRole.scala new file mode 100644 index 00000000..e44ad011 --- /dev/null +++ b/src/main/scala/net/psforever/services/teamwork/invitations/RequestToJoinSquadRole.scala @@ -0,0 +1,84 @@ +// Copyright (c) 2024 PSForever +package net.psforever.services.teamwork.invitations + +import net.psforever.objects.Player +import net.psforever.objects.teamwork.SquadFeatures +import net.psforever.services.teamwork.{SquadInvitationManager, SquadResponse} +import net.psforever.types.PlanetSideGUID + +import scala.annotation.unused + +/** + * Utilized when one player attempts to join an existing squad in a specific role. + * Accessed by the joining player from the squad detail window. + * This invitation is handled by the squad leader. + * + * @param requestee player who requested the role + * @param features squad with the role + * @param position index of the role + */ +final case class RequestToJoinSquadRole(requestee: Player, features: SquadFeatures, position: Int) + extends Invitation(requestee.CharId, requestee.Name) { + def handleInvitation(indirectInviteFunc: (IndirectInvite, Player, Long, Long, String) => Boolean)( + manager: SquadInvitationManager, + invitedPlayer: Long, + invitingPlayer: Long, + otherName: String + ): Unit = { + SquadInvitationManager.handleRequestRole(manager, requestee, bid = this) + } + + def handleAcceptance( + manager: SquadInvitationManager, + @unused player: Player, + invitedPlayer: Long, + @unused invitedPlayerSquadOpt: Option[SquadFeatures] + ): Unit = { + //player requested to join a squad's specific position + //invitedPlayer is actually the squad leader; petitioner is the actual "invitedPlayer" + if ( + SquadInvitationManager.canEnrollInSquad(features, requestee.CharId) && + manager.joinSquad(requestee, features, position) + ) { + manager.acceptanceMessages(invitedPlayer, requestee.CharId, requestee.Name) + manager.cleanUpInvitesForSquadAndPosition(features, position) + } + } + + def handleRejection( + manager: SquadInvitationManager, + @unused player: Player, + rejectingPlayer: Long, + @unused squadsToLeaders: List[(PlanetSideGUID, SquadFeatures)] + ): Unit = { + if (SquadInvitationManager.notLeaderOfThisSquad(squadsToLeaders, features.Squad.GUID, requestee.CharId)) { + //rejected is the would-be squad member; rejectingPlayer is the squad leader who rejected the request + doRejection(manager, player, rejectingPlayer) + manager.rejectionMessage(rejectingPlayer) + manager.publish( + rejectingPlayer, + SquadResponse.SquadRelatedComment(s"Your request to join squad '${features.Squad.Task}' has been refused.") + ) + } + } + + def doRejection( + manager: SquadInvitationManager, + player: Player, + rejectingPlayer: Long + ): Unit = { + features.DeniedPlayers(requestee.CharId) + } + + def canBeAutoApproved: Boolean = true + + def getOptionalSquad: Option[SquadFeatures] = Some(features) + + def getPlayer: Player = requestee + + def appliesToPlayer(playerCharId: Long): Boolean = playerCharId == requestee.CharId + + def appliesToSquad(guid: PlanetSideGUID): Boolean = features.Squad.GUID == guid + + def appliesToSquadAndPosition(guid: PlanetSideGUID, squadPosition: Int): Boolean = appliesToSquad(guid) && position == squadPosition +} From aeb6b8f2a9e321eab8e93a40eac7d8c42c6c9d4f Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Tue, 17 Sep 2024 02:30:58 -0400 Subject: [PATCH 04/25] 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/support/ChatOperations.scala | 39 ++++- .../actors/session/support/SessionData.scala | 1 + .../support/SessionSquadHandlers.scala | 14 ++ .../session/support/ZoningOperations.scala | 148 +++++++++++++++--- .../teamwork/SquadInvitationManager.scala | 1 - 12 files changed, 310 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 eb430a45..e9936cda 100644 --- a/src/main/scala/net/psforever/actors/session/normal/AvatarHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/AvatarHandlerLogic.scala @@ -538,6 +538,7 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A continent.actor ! ZoneActor.RewardThisDeath(player) //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 fdfa1c9b..60185fac 100644 --- a/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala @@ -99,6 +99,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 7ebfdde2..d155cb25 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 { @@ -59,6 +63,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))) @@ -106,12 +111,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 => @@ -270,59 +332,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 5ed83c9b..77d3a99f 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 { @@ -217,6 +219,7 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex 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 e96e6cee..160bda39 100644 --- a/src/main/scala/net/psforever/actors/session/spectator/AvatarHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/spectator/AvatarHandlerLogic.scala @@ -450,6 +450,7 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A sessionLogic.zoning.CancelZoningProcess() //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 869369fc..e1f6415f 100644 --- a/src/main/scala/net/psforever/actors/session/spectator/GeneralLogic.scala +++ b/src/main/scala/net/psforever/actors/session/spectator/GeneralLogic.scala @@ -56,6 +56,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/spectator/SquadHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/spectator/SquadHandlerLogic.scala index fae3a681..9db5f6d4 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/support/ChatOperations.scala b/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala index 5bfe20f7..5aebd100 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.spectator.SpectatorMode import net.psforever.actors.session.{AvatarActor, SessionActor} 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 java.util.concurrent.{Executors, TimeUnit} 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 @@ -78,6 +82,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) @@ -1291,22 +1299,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 44c7ed18..44769b87 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionData.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionData.scala @@ -554,6 +554,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 1aaea60c..25c43274 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 @@ -36,6 +38,9 @@ object SessionSquadHandlers { armor: Int, position: Vector3 ) + def rethrowSquadServiceResponse(response: SquadResponse.Response)(sessionLogic: SessionData): Unit = { + sessionLogic.context.self ! SquadServiceResponse("", response) + } } class SessionSquadHandlers( @@ -84,6 +89,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 } @@ -345,4 +351,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 49bb0833..7b15dedc 100644 --- a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala @@ -6,10 +6,12 @@ import akka.actor.typed.scaladsl.adapter._ import akka.actor.{ActorContext, ActorRef, Cancellable, typed} import akka.pattern.ask import akka.util.Timeout +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.zones.blockmap.BlockMapEntity 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", @@ -185,6 +186,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], @@ -671,11 +680,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) } @@ -1863,7 +1876,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 @@ -1886,9 +1898,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 */ @@ -1984,16 +2001,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 => @@ -2010,8 +2030,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 } } @@ -2019,8 +2040,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 } @@ -2118,11 +2140,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) } @@ -2899,6 +2925,7 @@ class ZoningOperations( respawnTimer.cancel() reviveTimer.cancel() deadState = DeadState.RespawnTime + avatarActive = false sendResponse( AvatarDeadStateMessage( DeadState.RespawnTime, @@ -3268,7 +3295,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)) } } } @@ -3529,8 +3556,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 } @@ -3741,6 +3766,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 } @@ -3775,7 +3801,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)) @@ -3783,6 +3809,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 d03d0739..ad9b6cb9 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} From 93f528d4d94e2da75260e8525b41f42ab330cbc6 Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Tue, 24 Sep 2024 04:51:19 -0400 Subject: [PATCH 05/25] separated specific types of invitations and some of the messaging logic associated with them into separate classes; added custom messages everywhere; reorganized methods in an effort to improve code readability; light on the testing --- .../session/normal/SquadHandlerLogic.scala | 4 +- .../session/spectator/SquadHandlerLogic.scala | 4 +- .../session/support/ZoningOperations.scala | 4 +- .../teamwork/SquadInvitationManager.scala | 912 ++++++++++-------- .../services/teamwork/SquadService.scala | 26 +- .../teamwork/SquadServiceResponse.scala | 4 +- .../teamwork/invitations/IndirectInvite.scala | 129 ++- .../teamwork/invitations/Invitation.scala | 13 + .../InvitationToCreateASquad.scala | 80 +- .../invitations/InvitationToJoinSquad.scala | 88 +- .../LookingForSquadRoleInvite.scala | 97 +- ...PermissionToReverseInvitationToSquad.scala | 90 ++ .../invitations/ProximityInvite.scala | 73 +- .../invitations/RequestToJoinSquadRole.scala | 84 +- 14 files changed, 1094 insertions(+), 514 deletions(-) create mode 100644 src/main/scala/net/psforever/services/teamwork/invitations/PermissionToReverseInvitationToSquad.scala 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 d155cb25..6d8bfd8e 100644 --- a/src/main/scala/net/psforever/actors/session/normal/SquadHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/SquadHandlerLogic.scala @@ -361,8 +361,8 @@ class SquadHandlerLogic(val ops: SessionSquadHandlers, implicit val context: Act 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 SquadResponse.SquadRelatedComment(comment, messageType) => + sendResponse(ChatMsg(messageType, comment)) case _ => () } 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 9db5f6d4..b7321760 100644 --- a/src/main/scala/net/psforever/actors/session/spectator/SquadHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/spectator/SquadHandlerLogic.scala @@ -194,8 +194,8 @@ class SquadHandlerLogic(val ops: SessionSquadHandlers, implicit val context: Act 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 SquadResponse.SquadRelatedComment(comment, messageType) => + sendResponse(ChatMsg(messageType, comment)) case _ => () } 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 7b15dedc..7cc6a1c6 100644 --- a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala @@ -1903,7 +1903,7 @@ class ZoningOperations( private[session] var respawnTimer: Cancellable = Default.Cancellable private var queuedActivities: Seq[SpawnOperations.ActivityQueuedTask] = Seq() - private var initialActivityDelay: Int = 4 + private val initialActivityDelay: Int = 4 private var nextActivityDelay: Int = 0 private var statisticsPacketFunc: () => Unit = loginAvatarStatisticsFields @@ -1914,6 +1914,7 @@ class ZoningOperations( val ReleaseAvatarRequestMessage() = pkt log.info(s"${player.Name} on ${continent.id} has released") reviveTimer.cancel() + avatarActive = false GoToDeploymentMap() HandleReleaseAvatar(player, continent) } @@ -3777,6 +3778,7 @@ class ZoningOperations( zoningStatus = Zoning.Status.None player.death_by = math.min(player.death_by, 0) player.allowInteraction = true + avatarActive = true nextSpawnPoint.foreach { tube => sendResponse(PlayerStateShiftMessage(ShiftState(0, tube.Position, tube.Orientation.z))) nextSpawnPoint = None diff --git a/src/main/scala/net/psforever/services/teamwork/SquadInvitationManager.scala b/src/main/scala/net/psforever/services/teamwork/SquadInvitationManager.scala index ad9b6cb9..eb7dd322 100644 --- a/src/main/scala/net/psforever/services/teamwork/SquadInvitationManager.scala +++ b/src/main/scala/net/psforever/services/teamwork/SquadInvitationManager.scala @@ -4,11 +4,12 @@ package net.psforever.services.teamwork import akka.actor.ActorRef import akka.pattern.ask import akka.util.Timeout + import scala.collection.mutable import scala.concurrent.duration._ import scala.concurrent.Future // -import net.psforever.objects.{LivePlayerList, Player} +import net.psforever.objects.Player import net.psforever.objects.avatar.Avatar import net.psforever.objects.teamwork.{Member, SquadFeatures} import net.psforever.objects.zones.Zone @@ -119,14 +120,24 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { } } + def createPermissionToRedirectInvite(player: Player, invitingPlayer: Long, features: SquadFeatures): Unit = { + val leader = features.Squad.Leader.CharId + addInviteAndRespond( + leader, + IndirectInvite(player, features), + invitingPlayer, + player.Name + ) + } + def createIndirectInvite(player: Player, invitingPlayer: Long, features: SquadFeatures): Unit = { val invitedPlayer = player.CharId - val squad2 = features.Squad - val leader = squad2.Leader.CharId + val squad = features.Squad + val leader = squad.Leader.CharId allowed(invitedPlayer, invitingPlayer) allowed(leader, invitingPlayer) lazy val preface = s"$invitingPlayer's invitation got reversed to $invitedPlayer's squad, but" - if (squad2.Size == squad2.Capacity) { + if (squad.Size == squad.Capacity) { log.debug(s"$preface the squad has no available positions") } else if (refused(invitingPlayer).contains(invitedPlayer)) { log.debug(s"$preface $invitedPlayer repeated a previous refusal to $invitingPlayer's invitation offer") @@ -186,7 +197,9 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { newRecruitment = newRecruitment :+ key squad.Membership.zipWithIndex.filterNot { case (_, index) => index == position } case None => - cleanUpQueuedInvitesForSquadAndPosition(features, position) + val comment = s"An invitation to squad '${features.Squad.Task}' was filled by a different player." + cleanUpQueuedInvitesForSquadAndPosition(features.Squad.GUID, position) + .foreach { id => subs.Publish(id, SquadResponse.SquadRelatedComment(comment)) } squad.Membership.zipWithIndex } case _ => @@ -196,7 +209,8 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { .collect { case (member, index) if member.CharId == 0 && squad.Availability(index) => (member, index) } .sortBy({ _._1.Requirements.foldLeft(0)(_ + _.value) })(Ordering.Int.reverse) //find recruits - val players = zone.LivePlayers.map { _.avatar } + val faction = squad.Faction + val players = zone.Players.filter(_.faction == faction) if (positionsToRecruitFor.nonEmpty && players.nonEmpty) { //does this do anything? subs.Publish( @@ -212,19 +226,19 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { Some(None) ) ) - positionsToRecruitFor.foreach { case (_, position) => - findSoldiersWithinScopeAndInvite( - squad.Leader, - features, - position, - players, - features.ProxyInvites ++ newRecruitment, - proximityEnvelope - ) match { - case None => () - case Some(id) => + positionsToRecruitFor + .foreach { case (_, position) => + findSoldiersWithinScopeAndInvite( + squad.Leader, + features, + position, + players, + features.ProxyInvites ++ newRecruitment, + proximityEnvelope + ) + .collect { id => newRecruitment = newRecruitment :+ id - } + } } } if (newRecruitment.isEmpty) { @@ -236,7 +250,7 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { (origSearchForRole, invites.get(key)) match { case (Some(-1), _) => () case (Some(position), Some(LookingForSquadRoleInvite(member, _, _))) => - invites(key) = ProximityInvite(member, features, position) + invites.put(key, ProximityInvite(member, features, position)) case _ => () } } @@ -287,7 +301,10 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { case None => if (features.SearchForRole.contains(position) && features.ProxyInvites.isEmpty) { features.SearchForRole = None - //TODO message the squadLeader.CharId to indicate that there are no more candidates for this position + subs.Publish( + squadLeader.CharId, + SquadResponse.SquadRelatedComment("Exhausted all possible candidates to fill the open squad position.") + ) } case Some(id) => features.ProxyInvites = features.ProxyInvites :+ id @@ -314,15 +331,21 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { scope, features.ProxyInvites, proximityEnvelope - ) match { - case None => + ) + .collect { id => + features.ProxyInvites = features.ProxyInvites :+ id + id + } + .orElse { if (features.SearchForRole.contains(-1) && features.ProxyInvites.isEmpty) { features.SearchForRole = None - //TODO message the squadLeader.CharId to indicate that there are no more candidates for this position + subs.Publish( + squadLeader.CharId, + SquadResponse.SquadRelatedComment("Exhausted all possible local candidates to fill the open squad positions.") + ) } - case Some(id) => - features.ProxyInvites = features.ProxyInvites :+ id - } + None + } } /* acceptance */ @@ -497,11 +520,12 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { } def tryChainRejectionAll(charId: Long, features: SquadFeatures): Unit = { - val squadName = features.Squad.Task - cleanUpAllInvitesToSquad(features) + val comment = s"Your request to join squad '${features.Squad.Task}' has been refused." + cleanUpAllInvitesForSquad(features.Squad.GUID) + .map(_._1) .filterNot(_ == charId) .foreach { refusedId => - subs.Publish(refusedId, SquadResponse.SquadRelatedComment(s"Your request to join squad '$squadName' has been refused.")) + subs.Publish(refusedId, SquadResponse.SquadRelatedComment(comment)) } } @@ -530,35 +554,73 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { /* other special actions */ def handleDisbanding(features: SquadFeatures): Unit = { - cleanUpAllInvitesToSquad(features) + cleanUpAllInvitesForSquad(features.Squad.GUID) } - def handleCancelling(cancellingPlayer: Long): Unit = { - invites.collect { case (id, invite) if invite.appliesToPlayer(cancellingPlayer) => removeInvite(id) } - queuedInvites.foreach { - case (id: Long, inviteList) => - val inList = inviteList.filterNot(_.appliesToPlayer(cancellingPlayer)) - if (inList.isEmpty) { - queuedInvites.remove(id) - } else { - queuedInvites(id) = inList + def handleCancelling( + cancellingPlayer: Long, + player: Player, + featureOpt: Option[SquadFeatures] + ): Unit = { + featureOpt + .collect { + case features if features.SearchForRole.contains(-1L) => + //cancel proximity invites + features.SearchForRole = None + features.ProxyInvites = Nil + val queuedButCancelled = cleanUpQueuedProximityInvitesForPlayer(cancellingPlayer) + val activeButCancelled = cleanUpActiveProximityInvitesForPlayer(cancellingPlayer) + activeButCancelled.collect { case (id, invites) => + invites.foreach(_.handleCancel(manager = this, player, id)) + } + if (queuedButCancelled.nonEmpty || activeButCancelled.nonEmpty) { + subs.Publish( + cancellingPlayer, + SquadResponse.SquadRelatedComment("You have cancelled proximity invitations for your squad recruitment.") + ) + } + true + case features if features.SearchForRole.nonEmpty => + //cancel search for role + cancelSelectRoleForYourself(player, features) + subs.Publish( + cancellingPlayer, + SquadResponse.SquadRelatedComment("You have cancelled search for role invitations for your squad recruitment.") + ) + true + } + .orElse { + //todo cancel any request to join squad role or invitation to create a squad + None + } + .orElse { + //we have nothing special to cancel; search everything and see what we have dipped our feet in + val queuedButCancelled = cleanUpQueuedInvitesForPlayer(cancellingPlayer) + val activeButCancelled = cleanUpActiveInvitesForPlayer(cancellingPlayer) + activeButCancelled.collect { case (id, invites) => + invites.foreach(_.handleCancel(manager = this, player, id)) } - } - //get rid of ProximityInvite objects - cleanUpAllProximityInvites(cancellingPlayer) + if (queuedButCancelled.nonEmpty || activeButCancelled.nonEmpty) { + subs.Publish( + cancellingPlayer, + SquadResponse.SquadRelatedComment("You have cancelled some invitations and/or squad requests.") + ) + } + None + } } def handleClosingSquad(features: SquadFeatures): Unit = { - cleanUpAllInvitesToSquad(features) + cleanUpAllInvitesForSquad(features.Squad.GUID) } def handleCleanup(charId: Long): Unit = { - cleanUpAllInvitesWithPlayer(charId) + cleanUpAllInvitesForPlayer(charId) } def handleLeave(charId: Long): Unit = { refusedPlayers.remove(charId) - cleanUpAllInvitesWithPlayer(charId) + cleanUpAllInvitesForPlayer(charId) } /* other special actions, promotion */ @@ -606,7 +668,7 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { addInviteAndRespond(promotedPlayer, x, x.inviterCharId, x.inviterName) case x :: xs => addInviteAndRespond(promotedPlayer, x, x.inviterCharId, x.inviterName) - queuedInvites += promotedPlayer -> xs + queuedInvites.put(promotedPlayer, xs) } } @@ -652,7 +714,7 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { var otherInvites = unfulfilled ++ others.collect { case invite: InvitationToCreateASquad => invite.futureSquadLeader - case invite: IndirectInvite => invite.recruitOrOwner + case invite: IndirectInvite => invite.originalRequester } .distinctBy { _.CharId } (1 to 9).foreach { position => @@ -668,7 +730,7 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { } //cleanup searches by squad leader features.SearchForRole match { - case Some(-1) => cleanUpAllProximityInvites(charId) + case Some(-1) => cleanUpAllProximityInvitesForPlayer(charId) case Some(_) => cancelFind(Some(features)) case None => () } @@ -678,59 +740,62 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { tplayer: Player, features: SquadFeatures, position: Int - ): Unit = { + ): List[(Long, List[Invitation])] = { val squad = features.Squad val sguid = squad.GUID - (features.SearchForRole match { - case None => - Some(Nil) - case Some(-1) => + val list = features + .SearchForRole + .collect { + case -1 => //a proximity invitation has not yet cleared; nothing will be gained by trying to invite for a specific role log.debug("FindLfsSoldiersForRole: waiting for proximity invitations to clear") None - case Some(pos) if pos == position => + case pos if pos == position => //already recruiting for this specific position in the squad? do nothing log.debug("FindLfsSoldiersForRole: already recruiting for this position; client-server mismatch?") None - case Some(pos) => + case pos => //some other role is undergoing recruitment; cancel and redirect efforts for new position + val comment = s"An invitation to join squad '${features.Squad.Task}' has been rescinded." features.SearchForRole = None - cleanUpQueuedInvitesForSquadAndPosition(features, pos) - Some( - invites.filter { - case (_, LookingForSquadRoleInvite(_, _features, role)) => _features.Squad.GUID == sguid && role == pos - case _ => false - }.keys.toList - ) - }) match { - case None => - features.SearchForRole = None - case Some(list) => - //this will update the role entry in the GUI to visually indicate being searched for; only one will be displayed at a time - subs.Publish( - tplayer.CharId, - SquadResponse.Detail( - sguid, - SquadDetail().Members( - List(SquadPositionEntry(position, SquadPositionDetail().CharId(char_id = 0L).Name(name = ""))) - ) - ) - ) - //search! - findSoldiersWithinScopeAndInvite( - squad.Leader, - features, - position, - LivePlayerList.WorldPopulation { _ => true }, - list, - lookingForSquadRoleEnvelope - ) match { - case None => () - case Some(id) => - features.ProxyInvites = List(id) - features.SearchForRole = position + val retiredQueuedInvitations = cleanUpQueuedInvitesForSquadAndPosition(features.Squad.GUID, pos) + val retiredInvites = invites.collect { case (id, invite) if invite.appliesToSquadAndPosition(sguid, pos) => (id, List(invite)) } + retiredInvites.foreach { case (id, _) => + invites.remove(id) + nextInviteAndRespond(id) } + SquadInvitationManager.moveListElementsToMap(retiredQueuedInvitations, retiredInvites) + retiredInvites.foreach { case (id, _) => + subs.Publish(id, SquadResponse.SquadRelatedComment(comment)) + } + Some(retiredInvites.toList) + } + .flatten + .getOrElse(Nil) + //this will update the role entry in the GUI to visually indicate being searched for; only one will be displayed at a time + subs.Publish( + tplayer.CharId, + SquadResponse.Detail( + sguid, + SquadDetail().Members( + List(SquadPositionEntry(position, SquadPositionDetail().CharId(char_id = 0L).Name(name = ""))) + ) + ) + ) + //search! + val faction = squad.Faction + findSoldiersWithinScopeAndInvite( + squad.Leader, + features, + position, + tplayer.Zone.Players.filter(_.faction == faction), + Nil, + lookingForSquadRoleEnvelope + ).collect { id => + features.ProxyInvites = List(id) + features.SearchForRole = position } + list } def cancelFind(lSquadOpt: Option[SquadFeatures]): Unit = { @@ -857,7 +922,7 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { leaderCharId, SquadResponse.Membership(SquadResponseType.Cancel, cancellingPlayer, None, entry.requestee.Name, unk5 = false) ) - queuedInvites(leaderCharId) = list.take(index) ++ list.drop(index + 1) + queuedInvites.put(leaderCharId, list.take(index) ++ list.drop(index + 1)) Some(true) } ) @@ -908,7 +973,7 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { def ensureEmptySquad(features: SquadFeatures): Boolean = { val ensuredEmpty = features.Squad.Size <= 1 if (ensuredEmpty) { - cleanUpAllInvitesToSquad(features) + cleanUpAllInvitesForSquad(features.Squad.GUID) } ensuredEmpty } @@ -919,7 +984,7 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { * This operation is fairly safe to call whenever a player is to be inducted into a squad. * The aforementioned player must have a callback retained in `subs.UserEvents` * and conditions imposed by both the role and the player must be satisfied. - * @see `CleanUpAllInvitesWithPlayer` + * @see `CleanUpAllInvitesForPlayer` * @see `Squad.isAvailable` * @see `Squad.Switchboard` * @see `SquadSubscriptionEntity.MonitorSquadDetails` @@ -934,7 +999,7 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { * `false`, if the player did not join the squad or is already a squad member */ def joinSquad(player: Player, features: SquadFeatures, position: Int): Boolean = { - cleanUpAllInvitesWithPlayer(player.CharId) + cleanUpAllInvitesForPlayer(player.CharId) parent ! SquadService.PerformJoinSquad(player, features, position) true } @@ -971,7 +1036,7 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { def refused(charId: Long, list: List[Long]): List[Long] = { refusedPlayers.get(charId) match { case Some(refusedList) => - refusedPlayers(charId) = list ++ refusedList + refusedPlayers.put(charId, list ++ refusedList) refused(charId) case None => Nil @@ -1003,7 +1068,7 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { def allowed(charId: Long, list: List[Long]): List[Long] = { refusedPlayers.get(charId) match { case Some(refusedList) => - refusedPlayers(charId) = refusedList.filterNot(list.contains) + refusedPlayers.put(charId, refusedList.filterNot(list.contains)) refused(charId) case None => Nil @@ -1157,7 +1222,7 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { eachBid.inviterCharId == inviteInviterCharId } ) { - queuedInvites(invitedPlayer) = invite match { + val restoredInvites = invite match { case _: RequestToJoinSquadRole => //RequestToJoinSquadRole is to be expedited val (normals, others) = bidList.partition(_.isInstanceOf[RequestToJoinSquadRole]) @@ -1165,13 +1230,14 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { case _ => bidList :+ invite } + queuedInvites.put(invitedPlayer, restoredInvites) Some(_bid) } else { None } case None => if (_bid.inviterCharId != invite.inviterCharId) { - queuedInvites(invitedPlayer) = List(invite) + queuedInvites.put(invitedPlayer, List(invite)) Some(_bid) } else { None @@ -1179,7 +1245,7 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { } case None => - invites(invitedPlayer) = invite + invites.put(invitedPlayer, invite) Some(invite) } } @@ -1265,11 +1331,11 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { case Nil => None case x :: Nil => - invites(invitedPlayer) = x + invites.put(invitedPlayer, x) queuedInvites.remove(invitedPlayer) Some(x) case x :: xs => - invites(invitedPlayer) = x + invites.put(invitedPlayer, x) queuedInvites(invitedPlayer) = xs Some(x) } @@ -1332,7 +1398,7 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { def removeInvite(invitedPlayer: Long): Option[Invitation] = { invites.remove(invitedPlayer) match { case out @ Some(invite) => - previousInvites += invitedPlayer -> invite + previousInvites.put(invitedPlayer, invite) out case None => None @@ -1354,210 +1420,231 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { val squad = features.Squad val faction = squad.Faction val squadLeader = squad.Leader.CharId - val deniedAndExcluded = features.DeniedPlayers() ++ excluded + val deniedAndExcluded = (features.DeniedPlayers() ++ excluded) :+ squadLeader val requirementsToMeet = squad.Membership(position).Requirements //find a player who is of the same faction as the squad, is LFS, and is eligible for the squad position scope .find { avatar => val charId = avatar.id - faction == avatar.faction && - avatar.lookingForSquad && + avatar.lookingForSquad && + avatar.faction == faction && + charId != squadLeader && + charId != invitingPlayerCharId && !deniedAndExcluded.contains(charId) && !refusedPlayers(charId).contains(squadLeader) && - requirementsToMeet.intersect(avatar.certifications) == requirementsToMeet && - charId != invitingPlayerCharId //don't send invite to yourself. can cause issues if rejected - } match { - case None => - None - case Some(invitedPlayer) => + requirementsToMeet.intersect(avatar.certifications) == requirementsToMeet + } + .collect { invitedPlayer => //add invitation for position in squad - val invite = invitationEnvelopFunc(invitingPlayer, features, position) val id = invitedPlayer.id - addInviteAndRespond(id, invite, invitingPlayerCharId, invitingPlayerName) - Some(id) + addInviteAndRespond( + id, + invitationEnvelopFunc(invitingPlayer, features, position), + invitingPlayerCharId, + invitingPlayerName + ) + id } } /* invite clean-up */ - /** - * Remove all inactive invites associated with this player. - * @param charId the unique character identifier for the player being invited; - * in actuality, represents the player who will address the invitation object - * @return a list of the removed inactive invitation objects - */ - def cleanUpQueuedInvites(charId: Long): Unit = { - val list = List(charId) - queuedInvites - .remove(charId) - .map(_.flatMap(_.getOptionalSquad)) - .getOrElse(Nil) - .foreach(cleanUpSquadFeatures(list, _, position = -1)) - } - - def cleanUpSquadFeatures(removed: List[Long], features: SquadFeatures, position: Int): Unit = { - features.ProxyInvites = features.ProxyInvites.filterNot(removed.contains) - if (features.ProxyInvites.isEmpty) { - features.SearchForRole = None - } + private def removeActiveInvites(invitationFilteringRule: (Long, Invitation) => Boolean): List[(Long, List[Invitation])] = { + invites + .collect { + case out @ (id, invite) if invitationFilteringRule(id, invite) => out + } + .map { case (id, invite) => + removeInvite(id) + nextInviteAndRespond(id) + (id, List(invite)) + } + .toList } /** - * Remove all invitation objects - * that are related to the particular squad and the particular role in the squad. - * Specifically used to safely disarm obsolete invitation objects related to the specific criteria. - * Affects only certain invitation object types - * including "player requesting role" and "leader requesting recruiting role". - * @see `RemoveActiveInvitesForSquadAndPosition` - * @see `RemoveQueuedInvitesForSquadAndPosition` - * @param features the squad - * @param position the role position index - */ - def cleanUpInvitesForSquadAndPosition(features: SquadFeatures, position: Int): Unit = { - val guid = features.Squad.GUID - cleanUpSquadFeatures( - removeActiveInvitesForSquadAndPosition(guid, position) ++ removeQueuedInvitesForSquadAndPosition(guid, position), - features, - position + * Remove all active invitation objects that are related to the particular player. + * Specifically used to safely disarm obsolete invitation objects by specific criteria. + * Affects the active invitation list and any squads related to that player. + * @param charId the player's unique identifier number + * @return a list of the removed inactive invitation objects + */ + def cleanUpActiveInvitesForPlayer(charId: Long): List[(Long, List[Invitation])] = { + SquadInvitationManager.cleanUpInvites(removeActiveInvitesForPlayer(charId), List(charId)) + } + + /** + * Remove all active invitation objects that are related to the particular player. + * Specifically used to safely disarm obsolete invitation objects by specific criteria. + * Affects the active invitation list. + * @param charId the player's unique identifier number + * @return a list of the removed inactive invitation objects + */ + private def removeActiveInvitesForPlayer(charId: Long): List[(Long, List[Invitation])] = { + removeActiveInvites({ (id: Long, invite: Invitation) => id == charId || invite.appliesToPlayer(charId) }) + } + + /** + * Remove all inactive invitation objects that are related to the particular player. + * Specifically used to safely disarm obsolete invitation objects by specific criteria. + * Affects the queued invitation list and any squads related to that player. + * @param charId the player's unique identifier number + * @return a list of the removed inactive invitation objects + */ + def cleanUpQueuedInvitesForPlayer(charId: Long): List[(Long, List[Invitation])] = { + SquadInvitationManager.cleanUpInvites(removeQueuedInvitesForPlayer(charId), List(charId)) + } + + /** + * Remove all inactive invitation objects that are related to the particular player. + * Specifically used to safely disarm obsolete invitation objects by specific criteria. + * Affects the inactive invitation list. + * @param charId the player's unique identifier number + * @return a list of the removed inactive invitation objects + */ + private def removeQueuedInvitesForPlayer(charId: Long): List[(Long, List[Invitation])] = { + SquadInvitationManager.removeQueuedInvites( + queuedInvites, + { invite: Invitation => invite.appliesToPlayer(charId) } ) } /** - * Remove all inactive invitation objects that are related to the particular squad and the particular role in the squad. - * Specifically used to safely disarm obsolete invitation objects by specific criteria. - * Affects only certain invitation object types. - * @see `RequestToJoinSquadRole` - * @see `LookingForSquadRoleInvite` - * @see `CleanUpInvitesForSquadAndPosition` - * @param features the squa - * @param position the role position index - */ - def cleanUpQueuedInvitesForSquadAndPosition(features: SquadFeatures, position: Int): Unit = { - cleanUpSquadFeatures( - removeQueuedInvitesForSquadAndPosition(features.Squad.GUID, position), - features, - position + * Remove all invitation objects that are related to the particular player. + * Specifically used to safely disarm obsolete invitation objects by specific criteria. + * Affects the active invitation list and the inactive invitation list and any squads related to that player. + * @param charId the player's unique identifier number + * @return a list of the removed inactive invitation objects + */ + def cleanUpAllInvitesForPlayer(charId: Long): List[(Long, List[Invitation])] = { + SquadInvitationManager.cleanUpAllInvites( + removeQueuedInvitesForPlayer(charId), + removeActiveInvitesForPlayer(charId) ) } /** - * Remove all invitation objects that are related to the particular squad. - * Specifically used to safely disarm obsolete invitation objects by specific criteria. - * Affects all invitation object types and all data structures that deal with the squad. - * @see `RequestToJoinSquadRole` - * @see `IndirectInvite` - * @see `LookingForSquadRoleInvite` - * @see `ProximityInvite` - * @see `RemoveInvite` - * @see `InvitationToJoinSquad` - * @param features the squad identifier - */ - def cleanUpAllInvitesToSquad(features: SquadFeatures): List[Long] = { - val guid = features.Squad.GUID - //clean up invites - val activeInviteIds = { - val keys = invites.keys.toSeq - invites - .values - .zipWithIndex - .collect { - case (invite, index) if invite.appliesToSquad(guid) => index - } - .map { index => - val key = keys(index) - removeInvite(key) - key - } - .toList - } - //tidy the queued invitations - val queuedInviteIds = { - val keys = queuedInvites.keys.toSeq - queuedInvites.values.zipWithIndex - .collect { - case (queue, index) => - val key = keys(index) - val (targets, retained) = queue.partition(_.appliesToSquad(guid)) - if (retained.isEmpty) { - queuedInvites.remove(key) - } else { - queuedInvites += key -> retained - } - if (targets.nonEmpty) { - Some(key) - } else { - None - } - } - .flatten - .toList - } - val allInviteIds = (activeInviteIds ++ queuedInviteIds).distinct - cleanUpSquadFeatures(allInviteIds, features, position = -1) - allInviteIds + * Remove all active invitation objects that are related to the particular squad. + * Specifically used to safely disarm obsolete invitation objects by specific criteria. + * @param guid squad identifier + */ + def cleanUpActiveInvitesForSquad(guid: PlanetSideGUID): List[(Long, List[Invitation])] = { + SquadInvitationManager.cleanUpInvites(removeActiveInvitesForSquad(guid), Nil) + } + + private def removeActiveInvitesForSquad(guid: PlanetSideGUID): List[(Long, List[Invitation])] = { + removeActiveInvites({ (_: Long, invite: Invitation) => invite.appliesToSquad(guid) }) } /** - * Remove all active and inactive invitation objects that are related to the particular player. - * Specifically used to safely disarm obsolete invitation objects by specific criteria. - * Affects all invitation object types and all data structures that deal with the player. - * @see `RequestToJoinSquadRole` - * @see `IndirectInvite` - * @see `LookingForSquadRoleInvite` - * @see `RemoveInvite` - * @see `CleanUpAllProximityInvites` - * @see `InvitationToJoinSquad` - * @param charId the player's unique identifier number - */ - def cleanUpAllInvitesWithPlayer(charId: Long): Unit = { - //clean up our active invitation - val charIdInviteSquadGuid = removeInvite(charId).flatMap(_.getOptionalSquad) - //clean up invites - val (activeInviteIds, activeSquadGuids) = { - val keys = invites.keys.toSeq - invites - .values - .zipWithIndex - .map { - case (invite, index) => - val key = keys(index) - removeInvite(key) - (key, invite.getOptionalSquad) - } - .unzip - } - //tidy the queued invitations - val (queuedInviteIds, queuedSquadGuids) = { - val keys = queuedInvites.keys.toSeq - queuedInvites.values.zipWithIndex - .collect { - case (queue, index) => - val key = keys(index) - val (targets, retained) = if (key != charId) { - queue.partition(_.appliesToPlayer(charId)) - } else { - (queue, Nil) - } - if (retained.isEmpty) { - queuedInvites.remove(key) - } else { - queuedInvites += key -> retained - } - if (targets.nonEmpty) { - Some((key, targets.flatMap(_.getOptionalSquad))) - } else { - None - } - } - .flatten - .toList - .unzip - } - val allInvites = (activeInviteIds ++ queuedInviteIds).toList.distinct - ((activeSquadGuids.toSeq :+ charIdInviteSquadGuid) ++ queuedSquadGuids) - .flatten - .distinct - .foreach { guid => cleanUpSquadFeatures(allInvites, guid, position = -1) } + * Remove all queued invitation objects that are related to the particular squad. + * Specifically used to safely disarm obsolete invitation objects by specific criteria. + * @param guid squad identifier + */ + def cleanUpQueuedInvitesForSquad(guid: PlanetSideGUID): List[(Long, List[Invitation])] = { + SquadInvitationManager.cleanUpInvites(removeQueuedInvitesForSquad(guid), Nil) + } + + private def removeQueuedInvitesForSquad(guid: PlanetSideGUID): List[(Long, List[Invitation])] = { + SquadInvitationManager.removeQueuedInvites( + queuedInvites, + { invite: Invitation => invite.appliesToSquad(guid) } + ) + } + + /** + * Remove all invitation objects that are related to the particular squad. + * Specifically used to safely disarm obsolete invitation objects by specific criteria. + * Affects all invitation object types and all data structures that deal with the squad. + * @param guid squad identifier + */ + def cleanUpAllInvitesForSquad(guid: PlanetSideGUID): List[(Long, List[Invitation])] = { + SquadInvitationManager.cleanUpAllInvites( + removeQueuedInvitesForSquad(guid), + removeActiveInvitesForSquad(guid) + ) + } + + /** + * Remove all active invitation objects + * that are related to the particular squad and the particular role in the squad. + * Specifically used to safely disarm obsolete invitation objects related to the specific criteria. + * Affects only certain invitation object types + * including "player requesting role" and "leader requesting recruiting role". + * @param guid the squad identifier + * @param position the role position index + * @return the character ids of all players whose invites were removed + */ + def cleanUpActiveInvitesForSquadAndPosition(guid: PlanetSideGUID, position: Int): List[(Long, List[Invitation])] = { + SquadInvitationManager.cleanUpInvites(removeActiveInvitesForSquadAndPosition(guid, position), Nil) + } + + private def removeActiveInvitesForSquadAndPosition(guid: PlanetSideGUID, position: Int): List[(Long, List[Invitation])] = { + removeActiveInvites({ (_: Long, invite: Invitation) => invite.appliesToSquadAndPosition(guid, position) }) + } + + /** + * Remove all inactive invitation objects that are related to the particular squad and the particular role in the squad. + * Specifically used to safely disarm obsolete invitation objects by specific criteria. + * Affects only certain invitation object types. + * @param guid the squad identifier + * @param position the role position index + * @return the character ids of all players whose invites were removed + */ + def cleanUpQueuedInvitesForSquadAndPosition(guid: PlanetSideGUID, position: Int): List[(Long, List[Invitation])] = { + SquadInvitationManager.cleanUpInvites(removeQueuedInvitesForSquadAndPosition(guid, position), Nil) + } + + private def removeQueuedInvitesForSquadAndPosition(guid: PlanetSideGUID, position: Int): List[(Long, List[Invitation])] = { + SquadInvitationManager.removeQueuedInvites( + queuedInvites, + { invite: Invitation => invite.appliesToSquadAndPosition(guid, position) } + ) + } + + /** + * Remove all invitation objects + * that are related to the particular squad and the particular role in the squad. + * Specifically used to safely disarm obsolete invitation objects related to the specific criteria. + * Affects only certain invitation object types + * including "player requesting role" and "leader requesting recruiting role". + * @see `RemoveActiveInvitesForSquadAndPosition` + * @see `RemoveQueuedInvitesForSquadAndPosition` + * @param guid squad identifier + * @param position the role position index + */ + def cleanUpAllInvitesForSquadAndPosition(guid: PlanetSideGUID, position: Int): List[(Long, List[Invitation])] = { + SquadInvitationManager.cleanUpAllInvites( + removeQueuedInvitesForSquadAndPosition(guid, position), + removeActiveInvitesForSquadAndPosition(guid, position) + ) + } + + def cleanUpActiveProximityInvitesForPlayer(charId: Long): List[(Long, List[Invitation])] = { + SquadInvitationManager.cleanUpInvites(removeActiveProximityInvitesForPlayer(charId), List(charId)) + } + + private def removeActiveProximityInvitesForPlayer(charId: Long): List[(Long, List[Invitation])] = { + removeActiveInvites({ + (_: Long, invite: Invitation) => invite match { + case invite: ProximityInvite => invite.appliesToPlayer(charId) + case _ => false + } + }) + } + + def cleanUpQueuedProximityInvitesForPlayer(charId: Long): List[(Long, List[Invitation])] = { + SquadInvitationManager.cleanUpInvites(removeQueuedProximityInvitesForPlayer(charId), Nil) + } + + private def removeQueuedProximityInvitesForPlayer(charId: Long): List[(Long, List[Invitation])] = { + SquadInvitationManager.removeQueuedInvites( + queuedInvites, + { + case invite: ProximityInvite => invite.appliesToPlayer(charId) + case _ => false + } + ) } /** @@ -1565,162 +1652,41 @@ class SquadInvitationManager(subs: SquadSubscriptionEntity, parent: ActorRef) { * This is related to recruitment from the perspective of the recruiter. * @param charId the player */ - def cleanUpAllProximityInvites(charId: Long): Unit = { - //clean up invites - val (activeInviteIds, activeSquadGuids) = { - val keys = invites.keys.toSeq - invites.values.zipWithIndex - .collect { case (ProximityInvite(member, guid, _), index) if member.CharId == charId => (index, Some(guid)) } - .map { case (index, guid) => - val key = keys(index) - removeInvite(key) - (key, guid) - } - .unzip - } - //tidy the queued invitations - val (queuedInviteIds, queuedSquadGuids) = { - val keys = queuedInvites.keys.toSeq - queuedInvites - .values - .zipWithIndex - .collect { - case (queue, index) => - val key = keys(index) - val (targets, retained) = queue.partition { - case ProximityInvite(member, _, _) => member.CharId == charId - case _ => false - } - if (retained.isEmpty) { - queuedInvites.remove(key) - } else { - queuedInvites += key -> retained - } - if (targets.nonEmpty) { - Some((key, targets.collect { case ProximityInvite(_, guid, _) => guid } )) - } else { - None - } - } - .flatten - .toList - .unzip - } - val allInvites = (activeInviteIds ++ queuedInviteIds).toList.distinct - (activeSquadGuids.toSeq ++ queuedSquadGuids) - .flatten - .distinct - .foreach { guid => cleanUpSquadFeatures(allInvites, guid, position = -1) } + def cleanUpAllProximityInvitesForPlayer(charId: Long): List[(Long, List[Invitation])] = { + SquadInvitationManager.cleanUpAllInvites( + removeQueuedProximityInvitesForPlayer(charId), + removeActiveProximityInvitesForPlayer(charId) + ) + } + + private def removeActiveProximityInvitesForSquad(guid: PlanetSideGUID): List[(Long, List[Invitation])] = { + removeActiveInvites({ + (_: Long, invite: Invitation) => invite match { + case invite: ProximityInvite => invite.appliesToSquad(guid) + case _ => false + } + }) + } + + private def removeQueuedProximityInvitesForSquad(guid: PlanetSideGUID): List[(Long, List[Invitation])] = { + SquadInvitationManager.removeQueuedInvites( + queuedInvites, + { + case invite: ProximityInvite => invite.appliesToSquad(guid) + case _ => false + } + ) } /** * Remove all active and inactive proximity squad invites for a specific squad. - * @param features the squad + * @param guid squad identifier */ - def cleanUpProximityInvites(features: SquadFeatures): Unit = { - val squadGuid = features.Squad.GUID - //clean up invites - val activeInviteIds = { - val keys = invites.keys.toSeq - invites.values.zipWithIndex - .collect { - case (ProximityInvite(_, _squad, _), index) if _squad.Squad.GUID == squadGuid => index - } - .map { index => - val key = keys(index) - removeInvite(key) - key - } - } - //tidy the queued invitations - val queuedInviteIds = { - val keys = queuedInvites.keys.toSeq - queuedInvites.values.zipWithIndex - .collect { - case (queue, index) => - val key = keys(index) - val (targets, retained) = queue.partition { - case ProximityInvite(_, _squad, _) => _squad.Squad.GUID == squadGuid - case _ => false - } - if (retained.isEmpty) { - queuedInvites.remove(key) - } else { - queuedInvites += key -> retained - } - if (targets.nonEmpty) { - keys.lift(index) - } else { - None - } - } - .flatten - .toList - } - cleanUpSquadFeatures((activeInviteIds ++ queuedInviteIds).toList.distinct, features, position = -1) - } - - /** - * Remove all active invitation objects - * that are related to the particular squad and the particular role in the squad. - * Specifically used to safely disarm obsolete invitation objects related to the specific criteria. - * Affects only certain invitation object types - * including "player requesting role" and "leader requesting recruiting role". - * @see `RequestToJoinSquadRole` - * @see `LookingForSquadRoleInvite` - * @see `ProximityInvite` - * @see `RemoveInvite` - * @param guid the squad identifier - * @param position the role position index - * @return the character ids of all players whose invites were removed - */ - def removeActiveInvitesForSquadAndPosition(guid: PlanetSideGUID, position: Int): List[Long] = { - val keys = invites.keys.toSeq - invites - .values - .zipWithIndex - .collect { - case (invite, index) if invite.appliesToSquadAndPosition(guid, position) => index - } - .map { index => - val key = keys(index) - removeInvite(key) - key - } - .toList - } - - /** - * Remove all inactive invitation objects that are related to the particular squad and the particular role in the squad. - * Specifically used to safely disarm obsolete invitation objects by specific criteria. - * Affects only certain invitation object types. - * @see `RequestToJoinSquadRole` - * @see `LookingForSquadRoleInvite` - * @see `CleanUpInvitesForSquadAndPosition` - * @param guid the squad identifier - * @param position the role position index - * @return the character ids of all players whose invites were removed - */ - def removeQueuedInvitesForSquadAndPosition(guid: PlanetSideGUID, position: Int): List[Long] = { - val keys = queuedInvites.keys.toSeq - queuedInvites.values.zipWithIndex - .collect { - case (queue, index) => - val key = keys(index) - val (targets, retained) = queue.partition(_.appliesToSquadAndPosition(guid, position)) - if (retained.isEmpty) { - queuedInvites.remove(key) - } else { - queuedInvites += key -> retained - } - if (targets.nonEmpty) { - Some(key) - } else { - None - } - } - .flatten - .toList + def cleanUpAllProximityInvitesForSquad(guid: PlanetSideGUID): List[(Long, List[Invitation])] = { + SquadInvitationManager.cleanUpAllInvites( + removeQueuedProximityInvitesForSquad(guid), + removeActiveProximityInvitesForSquad(guid) + ) } def publish(to: Long, msg: SquadResponse.Response): Unit = { @@ -1811,4 +1777,94 @@ object SquadInvitationManager { case None => false } } + + private def moveListElementsToMap[T](fromList: List[(Long, List[T])], toMap: mutable.LongMap[List[T]]): Unit = { + fromList.foreach { case (id, listElements) => + toMap.get(id) match { + case None => + toMap.put(id, listElements) + case Some(mapElements) => + toMap.put(id, listElements ++ mapElements) + } + } + } + + private def cleanUpSquadFeatures(removed: List[Long], features: SquadFeatures, position: Int): Unit = { + features.ProxyInvites = features.ProxyInvites.filterNot(removed.contains) + if (features.ProxyInvites.isEmpty) { + features.SearchForRole = None + } + } + + /** + * Remove all active invitation objects that are related to the particular player. + * Specifically used to safely disarm obsolete invitation objects by specific criteria. + * Affects the active invitation list and any squads related to that player. + * @param proposedRemovalIds list of unique character identifiers to be eliminated from squad information; + * if empty, the unique character identifiers from the other parameter will be used instead + * @return a list of the removed inactive invitation objects + */ + private def cleanUpInvites( + list: List[(Long, List[Invitation])], + proposedRemovalIds: List[Long] + ): List[(Long, List[Invitation])] = { + val (idList, invites) = list.unzip + val removalList = proposedRemovalIds + .collectFirst(_ => proposedRemovalIds) + .getOrElse(idList) + invites + .flatten + .flatMap(_.getOptionalSquad) + .distinctBy(_.Squad.GUID) + .foreach(cleanUpSquadFeatures(removalList, _, position = -1)) + list + } + + /** + * Remove all invitation objects that are related to the particular squad. + * Specifically used to safely disarm obsolete invitation objects by specific criteria. + * Affects all invitation object types and all data structures that deal with the squad. + */ + private def cleanUpAllInvites( + queuedList: List[(Long, List[Invitation])], + activeList: List[(Long, List[Invitation])] + ): List[(Long, List[Invitation])] = { + val activeInvitesMap = mutable.LongMap.from(activeList) + val (removalList, featureList) = { + val (ids, inviteLists) = (activeInvitesMap ++ queuedList).unzip + ( + ids.toList, + inviteLists + .flatMap { invites => + invites.flatMap(_.getOptionalSquad) + } + ) + } + moveListElementsToMap(queuedList, activeInvitesMap) + featureList + .toSeq + .distinctBy(_.Squad.GUID) + .foreach { features => + cleanUpSquadFeatures(removalList, features, position = -1) + } + activeInvitesMap.toList + } + + private def removeQueuedInvites( + inviteMap: mutable.LongMap[List[Invitation]], + partitionRule: Invitation => Boolean + ): List[(Long, List[Invitation])] = { + inviteMap + .toList + .flatMap { + case (id, invites) => + val (found, retained) = invites.partition(partitionRule) + if (retained.nonEmpty) { + inviteMap.put(id, retained) + } else { + inviteMap.remove(id) + } + found.collectFirst { _ => (id, found) } + } + } } diff --git a/src/main/scala/net/psforever/services/teamwork/SquadService.scala b/src/main/scala/net/psforever/services/teamwork/SquadService.scala index 8d8ef04c..a3d731a6 100644 --- a/src/main/scala/net/psforever/services/teamwork/SquadService.scala +++ b/src/main/scala/net/psforever/services/teamwork/SquadService.scala @@ -271,7 +271,7 @@ class SquadService extends Actor { SquadActionMembershipDisband(char_id) case SquadAction.Membership(SquadRequestType.Cancel, cancellingPlayer, _, _, _) => - SquadActionMembershipCancel(cancellingPlayer) + SquadActionMembershipCancel(cancellingPlayer, tplayer) case SquadAction.Membership(SquadRequestType.Promote, _, _, _, _) => () // case SquadAction.Membership(SquadRequestType.Promote, promotingPlayer, Some(_promotedPlayer), promotedName, _) => @@ -329,8 +329,10 @@ class SquadService extends Actor { out } else { //this isn't the squad we're looking for by GUID; as a precaution, reload all of the published squad list + val charId = tplayer.CharId val faction = tplayer.Faction - subs.Publish(faction, SquadResponse.InitList(PublishedLists(tplayer.Faction))) + searchData.remove(charId) + subs.Publish(charId, SquadResponse.InitList(PublishedLists(faction))) None } case _ => @@ -538,7 +540,7 @@ class SquadService extends Actor { if invitersFeatures.Squad.Size == 1 => //both players belong to squads, but the invitingPlayer's squad is underutilized by comparison //treat the same as "indirection ..." using squad2 - invitations.createIndirectInvite(tplayer, invitedPlayer, invitedFeatures) + invitations.createPermissionToRedirectInvite(tplayer, invitedPlayer, invitedFeatures) case (Some(features), None) => //the classic situation @@ -546,7 +548,7 @@ class SquadService extends Actor { case (None, Some(features)) => //indirection; we're trying to invite ourselves to someone else's squad - invitations.createIndirectInvite(tplayer, invitedPlayer, features) + invitations.createPermissionToRedirectInvite(tplayer, invitedPlayer, features) case (None, None) => //neither the invited player nor the inviting player belong to any squad @@ -631,9 +633,13 @@ class SquadService extends Actor { .foreach(features => DisbandSquad(features)) } - def SquadActionMembershipCancel(cancellingPlayer: Long): Unit = { + def SquadActionMembershipCancel(cancellingPlayer: Long, player: Player): Unit = { //get rid of SpontaneousInvite objects and VacancyInvite objects - invitations.handleCancelling(cancellingPlayer) + invitations.handleCancelling( + cancellingPlayer, + player, + GetLeadingSquad(cancellingPlayer, None) + ) } def SquadActionMembershipPromote( @@ -729,10 +735,8 @@ class SquadService extends Actor { charId: Long, criteria: SquadService.SearchCriteria ): Unit = { - subs.Publish( - charId, - SquadResponse.SquadSearchResults(SearchForSquadsResults(criteria)) - ) + subs.Publish(charId, SquadResponse.InitList(PublishedLists(criteria.faction))) + subs.Publish(charId, SquadResponse.SquadSearchResults(SearchForSquadsResults(criteria))) } private def SearchForSquadsResults(criteria: SquadService.SearchCriteria): List[PlanetSideGUID] = { @@ -950,7 +954,7 @@ class SquadService extends Actor { * will still leave the squad, but will not attempt to send feedback to the said unreachable client. * If the player is in the process of unsubscribing from the service, * the no-messaging pathway is useful to avoid accumulating dead letters. - * @see `CleanUpAllInvitesToSquad` + * @see `CleanUpAllInvitesForSquad` * @see `SquadDetail` * @see `SquadSubscriptionEntity.Publish` * @see `TryResetSquadId` diff --git a/src/main/scala/net/psforever/services/teamwork/SquadServiceResponse.scala b/src/main/scala/net/psforever/services/teamwork/SquadServiceResponse.scala index e35d757b..bd275329 100644 --- a/src/main/scala/net/psforever/services/teamwork/SquadServiceResponse.scala +++ b/src/main/scala/net/psforever/services/teamwork/SquadServiceResponse.scala @@ -5,7 +5,7 @@ import akka.actor.ActorRef import net.psforever.objects.avatar.Certification import net.psforever.objects.teamwork.Squad import net.psforever.packet.game.{SquadDetail, SquadInfo, WaypointEventAction, WaypointInfo} -import net.psforever.types.{PlanetSideGUID, SquadResponseType, SquadWaypoint} +import net.psforever.types.{ChatMessageType, PlanetSideGUID, SquadResponseType, SquadWaypoint} import net.psforever.services.GenericEventBusMsg final case class SquadServiceResponse(channel: String, exclude: Iterable[Long], response: SquadResponse.Response) @@ -84,5 +84,5 @@ object SquadResponse { zoneNumber: Int ) extends Response - final case class SquadRelatedComment(str: String) extends Response + final case class SquadRelatedComment(str: String, messageType: ChatMessageType = ChatMessageType.UNK_227) extends Response } diff --git a/src/main/scala/net/psforever/services/teamwork/invitations/IndirectInvite.scala b/src/main/scala/net/psforever/services/teamwork/invitations/IndirectInvite.scala index 8e6a1ee8..cb90cb78 100644 --- a/src/main/scala/net/psforever/services/teamwork/invitations/IndirectInvite.scala +++ b/src/main/scala/net/psforever/services/teamwork/invitations/IndirectInvite.scala @@ -3,29 +3,30 @@ package net.psforever.services.teamwork.invitations import net.psforever.objects.Player import net.psforever.objects.teamwork.SquadFeatures -import net.psforever.services.teamwork.SquadInvitationManager +import net.psforever.services.teamwork.{SquadInvitationManager, SquadResponse} import net.psforever.types.PlanetSideGUID import scala.annotation.unused /** * Utilized to redirect an (accepted) invitation request to the proper squad leader. + * An anticipated result of clarifying permission to request invitation + * to a squad belonging to some player who is not the squad leader. * No direct action causes this message. - * Depending on the situation, either the squad leader or the player who would join the squad handle this invitation. - * - * @param recruitOrOwner player who would be joining the squad; - * may or may not have actually requested it in the first place + * This invitation is handled by the squad leader. + * @param originalRequester player who would be joining the squad; + * also the player who invited the player who will become the squad leader * @param features squad */ -final case class IndirectInvite(recruitOrOwner: Player, features: SquadFeatures) - extends Invitation(recruitOrOwner.CharId, recruitOrOwner.Name) { +final case class IndirectInvite(originalRequester: Player, features: SquadFeatures) + extends Invitation(originalRequester.CharId, originalRequester.Name) { def handleInvitation(indirectInviteFunc: (IndirectInvite, Player, Long, Long, String) => Boolean)( manager: SquadInvitationManager, invitedPlayer: Long, invitingPlayer: Long, otherName: String ): Unit = { - indirectInviteFunc(this, recruitOrOwner, invitedPlayer, invitingPlayer, otherName) + indirectInviteFunc(this, originalRequester, invitedPlayer, invitingPlayer, otherName) } def handleAcceptance( @@ -35,17 +36,59 @@ final case class IndirectInvite(recruitOrOwner: Player, features: SquadFeatures) @unused invitedPlayerSquadOpt: Option[SquadFeatures] ): Unit = { //tplayer / invitedPlayer is actually the squad leader - if (SquadInvitationManager.canEnrollInSquad(features, recruitOrOwner.CharId)) { - val recruitCharId = recruitOrOwner.CharId - manager.handleVacancyInvite(features, recruitCharId, invitedPlayer, recruitOrOwner) match { - case Some((_, line)) => - manager.acceptanceMessages(invitedPlayer, recruitCharId, recruitOrOwner.Name) - manager.joinSquad(recruitOrOwner, features, line) - manager.cleanUpAllInvitesWithPlayer(recruitCharId) - manager.cleanUpInvitesForSquadAndPosition(features, line) - //TODO since we are the squad leader, we do not want to brush off our queued squad invite tasks - case _ => () - } + if (SquadInvitationManager.canEnrollInSquad(features, originalRequester.CharId)) { + val leaderCharId = player.CharId + val invitedPlayer = originalRequester.CharId + manager + .handleVacancyInvite(features, invitedPlayer, invitedPlayer, originalRequester) + .collect { + case (_, position) if manager.joinSquad(originalRequester, features, position) => + manager.acceptanceMessages(invitedPlayer, invitedPlayer, originalRequester.Name) + //clean up invitations specifically for this squad and this position + val cleanedUpActiveInvitesForSquadAndPosition = manager.cleanUpActiveInvitesForSquadAndPosition(features.Squad.GUID, position) + cleanedUpActiveInvitesForSquadAndPosition.collect { case (id, _) => + manager.publish( + id, + SquadResponse.SquadRelatedComment(s"An invitation to join a squad has ended.") + ) + } + val cleanedUpQueuedInvites = manager.cleanUpQueuedInvitesForSquadAndPosition(features.Squad.GUID, position) + if (features.Squad.Capacity == features.Squad.Size) { + val cleanedUpActiveInvites = manager.cleanUpActiveInvitesForSquad(features.Squad.GUID) + cleanedUpActiveInvites.collect { case (id, invites) => + invites.foreach(_.handleCancel(manager, player, id)) + manager.publish( + id, + SquadResponse.SquadRelatedComment(s"An invitation to join a squad has ended.") + ) + } + (manager.cleanUpQueuedInvitesForSquad(features.Squad.GUID) ++ cleanedUpActiveInvites ++ cleanedUpQueuedInvites).collectFirst { case _ => + manager.publish( + leaderCharId, + SquadResponse.SquadRelatedComment(s"You had invitations that were cancelled due to this action.") + ) + } + } else if (cleanedUpQueuedInvites.nonEmpty) { + manager.publish( + leaderCharId, + SquadResponse.SquadRelatedComment(s"You had invitations that were cancelled due to this action.") + ) + } + features + } + .orElse { + manager.publish( + leaderCharId, + SquadResponse.SquadRelatedComment(s"Your invitation to ${player.Name} was accepted, but failed.") + ) + manager.publish( + invitedPlayer, + SquadResponse.SquadRelatedComment(s"You have failed to joined the squad '${features.Squad.Task}'.") + ) + None + } + } else { + } } @@ -55,7 +98,15 @@ final case class IndirectInvite(recruitOrOwner: Player, features: SquadFeatures) rejectingPlayer: Long, @unused squadsToLeaders: List[(PlanetSideGUID, SquadFeatures)] ): Unit = { - //todo how to do this? + doRejection(manager, player, rejectingPlayer) + manager.publish( + originalRequester.CharId, + SquadResponse.SquadRelatedComment(s"Your request to join the squad has been refused.") + ) + manager.publish( + rejectingPlayer, + SquadResponse.SquadRelatedComment(s"You refused ${originalRequester.Name}'s request to join this squad.") + ) } def doRejection( @@ -63,16 +114,48 @@ final case class IndirectInvite(recruitOrOwner: Player, features: SquadFeatures) player: Player, rejectingPlayer: Long ): Unit = { - //todo how to do this? + features.DeniedPlayers(originalRequester.CharId) + } + + def handleCancel( + manager: SquadInvitationManager, + player: Player, + handlingPlayer: Long + ): Unit = { + val invitingPlayer = originalRequester.CharId + val invitingPlayerName = originalRequester.Name + val actingPlayer = player.CharId + val leaderCharId = features.Squad.Leader.CharId + val leaderName = features.Squad.Leader.Name + if (actingPlayer == handlingPlayer) { + manager.publish( + invitingPlayer, + SquadResponse.SquadRelatedComment(s"You were declined admission to a squad.") + ) + } else if (actingPlayer == invitingPlayer) { + manager.publish( + leaderCharId, + SquadResponse.SquadRelatedComment(s"$invitingPlayerName has rescinded the offer to join the squad.") + ) + } else { + manager.publish( + leaderCharId, + SquadResponse.SquadRelatedComment(s"The request from $invitingPlayerName to join the squad is no longer valid.") + ) + manager.publish( + invitingPlayer, + SquadResponse.SquadRelatedComment(s"The offer to $leaderName to join the squad is no longer valid.") + ) + } } def canBeAutoApproved: Boolean = true def getOptionalSquad: Option[SquadFeatures] = Some(features) - def getPlayer: Player = recruitOrOwner + def getPlayer: Player = originalRequester - def appliesToPlayer(playerCharId: Long): Boolean = playerCharId == recruitOrOwner.CharId + def appliesToPlayer(playerCharId: Long): Boolean = playerCharId == originalRequester.CharId def appliesToSquad(guid: PlanetSideGUID): Boolean = features.Squad.GUID == guid diff --git a/src/main/scala/net/psforever/services/teamwork/invitations/Invitation.scala b/src/main/scala/net/psforever/services/teamwork/invitations/Invitation.scala index 82e20769..0d6d2fa3 100644 --- a/src/main/scala/net/psforever/services/teamwork/invitations/Invitation.scala +++ b/src/main/scala/net/psforever/services/teamwork/invitations/Invitation.scala @@ -56,6 +56,19 @@ abstract class Invitation(charId: Long, name: String) { rejectingPlayer: Long ): Unit + /** + * na + * + * @param manager subscription package + * @param handlingPlayer player who was intended to handle this invitation + * @param player player who caused cleanup action + */ + def handleCancel( + manager: SquadInvitationManager, + player: Player, + handlingPlayer: Long + ): Unit + def canBeAutoApproved: Boolean def getOptionalSquad: Option[SquadFeatures] diff --git a/src/main/scala/net/psforever/services/teamwork/invitations/InvitationToCreateASquad.scala b/src/main/scala/net/psforever/services/teamwork/invitations/InvitationToCreateASquad.scala index 60a2bcf0..efbdd908 100644 --- a/src/main/scala/net/psforever/services/teamwork/invitations/InvitationToCreateASquad.scala +++ b/src/main/scala/net/psforever/services/teamwork/invitations/InvitationToCreateASquad.scala @@ -48,21 +48,50 @@ final case class InvitationToCreateASquad(futureSquadLeader: Player) .askToCreateANewSquad(futureSquadLeader) .onComplete { case Success(FinishStartSquad(features)) => - manager.handleVacancyInvite(features, invitedPlayer, leaderCharId, player) match { - case Some((_, line)) => - manager.publish( - invitedPlayer, - SquadResponse.Membership(SquadResponseType.Accept, invitedPlayer, Some(leaderCharId), "", unk5 = true) - ) + manager + .handleVacancyInvite(features, invitedPlayer, leaderCharId, player) + .collect { + case (_, line) if manager.joinSquad(player, features, line) => + manager.publish( + leaderCharId, + SquadResponse.Membership(SquadResponseType.Accept, invitedPlayer, Some(leaderCharId), "", unk5 = false) + ) + manager.publish( + invitedPlayer, + SquadResponse.Membership(SquadResponseType.Accept, leaderCharId, Some(invitedPlayer), player.Name, unk5 = true) + ) + //all invitations involving the invited person must be cancelled due to the nature of this acceptance + manager.cleanUpQueuedInvitesForPlayer(invitedPlayer) + val cleanedUpActiveInvites = manager.cleanUpAllInvitesForPlayer(invitedPlayer) + cleanedUpActiveInvites.collect { case (id, invites) => + invites.foreach(_.handleCancel(manager, player, id)) + manager.publish( + id, + SquadResponse.SquadRelatedComment(s"An invitation involving ${futureSquadLeader.Name} has ended.") + ) + } + features + } + .orElse { manager.publish( leaderCharId, - SquadResponse.Membership(SquadResponseType.Accept, leaderCharId, Some(invitedPlayer), player.Name, unk5 = false) + SquadResponse.SquadRelatedComment(s"Though a squad has been created, a member could not join it.") ) - manager.joinSquad(player, features, line) - manager.cleanUpQueuedInvites(invitedPlayer) - case _ => () + manager.publish( + invitedPlayer, + SquadResponse.SquadRelatedComment(s"You could not join ${futureSquadLeader.Name} squad.") + ) + None + } + //since a squad was created, currently operated by the leader, all invitations related to the leader have changed + manager.cleanUpAllInvitesForPlayer(leaderCharId).collectFirst { _ => + manager.publish( + leaderCharId, + SquadResponse.SquadRelatedComment(s"You had invitations that were cancelled due to this action.") + ) } - case _ => () + case _ => + org.log4s.getLogger("InvitationToCreateASquad").error("could not create a squad when requested") } } } @@ -98,6 +127,35 @@ final case class InvitationToCreateASquad(futureSquadLeader: Player) manager.refused(rejectingPlayer, futureSquadLeader.CharId) } + def handleCancel( + manager: SquadInvitationManager, + player: Player, + handlingPlayer: Long + ): Unit = { + val actingPlayer = player.CharId + val leaderCharId = futureSquadLeader.CharId + if (actingPlayer == handlingPlayer) { + manager.publish( + leaderCharId, + SquadResponse.SquadRelatedComment(s"${player.Name} has declined joining into a squad with you, or the offer is no longer valid.") + ) + } else if (actingPlayer == leaderCharId) { + manager.publish( + handlingPlayer, + SquadResponse.SquadRelatedComment(s"${futureSquadLeader.Name} has decided not to join into a squad with you, or the offer is no longer valid.") + ) + } else { + manager.publish( + leaderCharId, + SquadResponse.SquadRelatedComment(s"The offer to ${player.Name} to join into a squad with you is no longer valid.") + ) + manager.publish( + handlingPlayer, + SquadResponse.SquadRelatedComment(s"The offer from ${futureSquadLeader.Name} join into a squad with you is no longer valid.") + ) + } + } + def canBeAutoApproved: Boolean = false def getOptionalSquad: Option[SquadFeatures] = None diff --git a/src/main/scala/net/psforever/services/teamwork/invitations/InvitationToJoinSquad.scala b/src/main/scala/net/psforever/services/teamwork/invitations/InvitationToJoinSquad.scala index 39228652..8f43482d 100644 --- a/src/main/scala/net/psforever/services/teamwork/invitations/InvitationToJoinSquad.scala +++ b/src/main/scala/net/psforever/services/teamwork/invitations/InvitationToJoinSquad.scala @@ -46,14 +46,56 @@ final case class InvitationToJoinSquad(charId: Long, name: String, features: Squ SquadInvitationManager.canEnrollInSquad(features, invitedPlayer) ) { //accepted an invitation to join an existing squad - manager.handleVacancyInvite(features, invitedPlayer, charId, player) match { - case Some((_, line)) => - manager.acceptanceMessages(charId, invitedPlayer, player.Name) - manager.joinSquad(player, features, line) - manager.cleanUpQueuedInvites(invitedPlayer) - manager.cleanUpInvitesForSquadAndPosition(features, line) - case _ => () - } + val leaderCharId = charId + manager + .handleVacancyInvite(features, invitedPlayer, charId, player) + .collect { + case (_, line) if manager.joinSquad(player, features, line) => + //manager.acceptanceMessages(charId, invitedPlayer, player.Name) + manager.publish( + leaderCharId, + SquadResponse.SquadRelatedComment(s"Your invitation to ${player.Name} was accepted.") + ) + manager.publish( + invitedPlayer, + SquadResponse.SquadRelatedComment(s"You have joined the squad '${features.Squad.Task}'.") + ) + //all invitations involving the invited person must be cancelled due to the nature of this acceptance + manager.cleanUpQueuedInvitesForPlayer(invitedPlayer).collect { case (id, _) => + manager.publish( + id, + SquadResponse.SquadRelatedComment(s"An invitation involving ${player.Name} has ended.") + ) + } + if (features.Squad.Capacity == features.Squad.Size) { + val cleanedUpActiveInvites = manager.cleanUpActiveInvitesForSquad(features.Squad.GUID) + cleanedUpActiveInvites.collect { case (id, invites) => + invites.foreach(_.handleCancel(manager, player, id)) + manager.publish( + id, + SquadResponse.SquadRelatedComment(s"An invitation to join a squad has ended.") + ) + } + (manager.cleanUpQueuedInvitesForSquad(features.Squad.GUID) ++ cleanedUpActiveInvites).collectFirst { case _ => + manager.publish( + leaderCharId, + SquadResponse.SquadRelatedComment(s"You had invitations that were cancelled due to this action.") + ) + } + } + features + } + .orElse { + manager.publish( + leaderCharId, + SquadResponse.SquadRelatedComment(s"Your invitation to ${player.Name} was accepted, but failed.") + ) + manager.publish( + invitedPlayer, + SquadResponse.SquadRelatedComment(s"You have failed to joined the squad '${features.Squad.Task}'.") + ) + None + } } } @@ -81,6 +123,36 @@ final case class InvitationToJoinSquad(charId: Long, name: String, features: Squ manager.refused(rejectingPlayer, charId) } + def handleCancel( + manager: SquadInvitationManager, + player: Player, + handlingPlayer: Long + ): Unit = { + val actingPlayer = player.CharId + val leaderCharId = features.Squad.Leader.CharId + val leaderName = features.Squad.Leader.Name + if (actingPlayer == handlingPlayer) { + manager.publish( + leaderCharId, + SquadResponse.SquadRelatedComment(s"${player.Name} has declined to join the squad.") + ) + } else if (actingPlayer == leaderCharId) { + manager.publish( + handlingPlayer, + SquadResponse.SquadRelatedComment(s"$leaderName has rescinded the offer to join the squad.") + ) + } else { + manager.publish( + leaderCharId, + SquadResponse.SquadRelatedComment(s"The offer to ${player.Name} to join the squad is no longer valid.") + ) + manager.publish( + handlingPlayer, + SquadResponse.SquadRelatedComment(s"The offer from $leaderName to join the squad is no longer valid.") + ) + } + } + def canBeAutoApproved: Boolean = false def getOptionalSquad: Option[SquadFeatures] = Some(features) diff --git a/src/main/scala/net/psforever/services/teamwork/invitations/LookingForSquadRoleInvite.scala b/src/main/scala/net/psforever/services/teamwork/invitations/LookingForSquadRoleInvite.scala index 9d9d3713..7994efb4 100644 --- a/src/main/scala/net/psforever/services/teamwork/invitations/LookingForSquadRoleInvite.scala +++ b/src/main/scala/net/psforever/services/teamwork/invitations/LookingForSquadRoleInvite.scala @@ -2,7 +2,7 @@ package net.psforever.services.teamwork.invitations import net.psforever.objects.teamwork.{Member, SquadFeatures} -import net.psforever.objects.{LivePlayerList, Player} +import net.psforever.objects.Player import net.psforever.services.teamwork.{SquadInvitationManager, SquadResponse} import net.psforever.types.{PlanetSideGUID, SquadResponseType} @@ -38,18 +38,64 @@ final case class LookingForSquadRoleInvite(squadLeader: Member, features: SquadF invitedPlayer: Long, @unused invitedPlayerSquadOpt: Option[SquadFeatures] ): Unit = { + val leaderCharId = squadLeader.CharId if ( manager.notLimitedByEnrollmentInSquad(invitedPlayerSquadOpt, invitedPlayer) && - SquadInvitationManager.canEnrollInSquad(features, invitedPlayer) + SquadInvitationManager.canEnrollInSquad(features, invitedPlayer) && + manager.joinSquad(player, features, position) ) { - val invitingPlayer = squadLeader.CharId - features.ProxyInvites = features.ProxyInvites.filterNot { _ == invitedPlayer } - if (manager.joinSquad(player, features, position)) { - //join this squad - manager.acceptanceMessages(invitingPlayer, invitedPlayer, player.Name) - manager.cleanUpQueuedInvites(player.CharId) - manager.cleanUpInvitesForSquadAndPosition(features, position) + //join this squad + //manager.acceptanceMessages(invitedPlayer, requestee.CharId, requestee.Name) + val msg = SquadResponse.Membership(SquadResponseType.Accept, invitedPlayer, Some(leaderCharId), player.Name, unk5 = false) + manager.publish(leaderCharId, msg) + manager.publish(invitedPlayer, msg.copy(unk5 = true)) +// manager.publish( +// invitedPlayer, +// SquadResponse.SquadRelatedComment(s"You have accepted ${squadLeader.Name}'s request to join a squad.") +// ) +// manager.publish( +// leaderCharId, +// SquadResponse.SquadRelatedComment(s"${player.Name} has agreed to joined your squad.") +// ) + //clean up invitations specifically for this squad and this position + val cleanedUpActiveInvitesForSquadAndPosition = manager.cleanUpActiveInvitesForSquadAndPosition(features.Squad.GUID, position) + cleanedUpActiveInvitesForSquadAndPosition.collect { case (id, _) => + manager.publish( + id, + SquadResponse.SquadRelatedComment(s"An invitation to join a squad has ended.") + ) } + val cleanedUpQueuedInvites = manager.cleanUpQueuedInvitesForSquadAndPosition(features.Squad.GUID, position) + if (features.Squad.Capacity == features.Squad.Size) { + val cleanedUpActiveInvites = manager.cleanUpActiveInvitesForSquad(features.Squad.GUID) + cleanedUpActiveInvites.collect { case (id, invites) => + invites.foreach(_.handleCancel(manager, player, id)) + manager.publish( + id, + SquadResponse.SquadRelatedComment(s"An invitation to join a squad has ended.") + ) + } + (manager.cleanUpQueuedInvitesForSquad(features.Squad.GUID) ++ cleanedUpActiveInvites ++ cleanedUpQueuedInvites).collectFirst { case _ => + manager.publish( + leaderCharId, + SquadResponse.SquadRelatedComment(s"You had invitations that were cancelled due to this action.") + ) + } + } else if (cleanedUpQueuedInvites.nonEmpty) { + manager.publish( + leaderCharId, + SquadResponse.SquadRelatedComment(s"You had invitations that were cancelled due to this action.") + ) + } + } else { + manager.publish( + invitedPlayer, + SquadResponse.SquadRelatedComment(s"Your accepted an invitation to squad '${features.Squad.Task}', but it failed.") + ) + manager.publish( + leaderCharId, + SquadResponse.SquadRelatedComment(s"An accepted request to join your squad has failed.") + ) } } @@ -74,14 +120,45 @@ final case class LookingForSquadRoleInvite(squadLeader: Member, features: SquadF player: Player, rejectingPlayer: Long ): Unit = { + val faction = player.Faction manager.reloadSearchForRoleInvite( - LivePlayerList.WorldPopulation { _ => true }, + player.Zone.Players.filter(_.faction == faction), rejectingPlayer, features, position ) } + def handleCancel( + manager: SquadInvitationManager, + player: Player, + handlingPlayer: Long + ): Unit = { + val actingPlayer = player.CharId + val leaderCharId = features.Squad.Leader.CharId + val leaderName = features.Squad.Leader.Name + if (actingPlayer == handlingPlayer) { + manager.publish( + leaderCharId, + SquadResponse.SquadRelatedComment(s"${player.Name} has declined to join the squad.") + ) + } else if (actingPlayer == leaderCharId) { + manager.publish( + handlingPlayer, + SquadResponse.SquadRelatedComment(s"$leaderName has rescinded the offer to join the squad.") + ) + } else { + manager.publish( + leaderCharId, + SquadResponse.SquadRelatedComment(s"The offer to ${player.Name} to join the squad is no longer valid.") + ) + manager.publish( + handlingPlayer, + SquadResponse.SquadRelatedComment(s"The offer from $leaderName to join the squad is no longer valid.") + ) + } + } + def canBeAutoApproved: Boolean = false def getOptionalSquad: Option[SquadFeatures] = Some(features) diff --git a/src/main/scala/net/psforever/services/teamwork/invitations/PermissionToReverseInvitationToSquad.scala b/src/main/scala/net/psforever/services/teamwork/invitations/PermissionToReverseInvitationToSquad.scala new file mode 100644 index 00000000..f12a9cdd --- /dev/null +++ b/src/main/scala/net/psforever/services/teamwork/invitations/PermissionToReverseInvitationToSquad.scala @@ -0,0 +1,90 @@ +// Copyright (c) 2024 PSForever +package net.psforever.services.teamwork.invitations + +import net.psforever.objects.Player +import net.psforever.objects.teamwork.SquadFeatures +import net.psforever.services.teamwork.{SquadInvitationManager, SquadResponse} +import net.psforever.types.PlanetSideGUID + +/** + * When requesting to that some other player join a newly-formed squad, + * but that player is actually the member of a squad already, + * this offer is extended to convert the invitation request into a different invitation request. + * The "different invitation" will be asking the leader of the other player's squad if our player can join it. + * Only technically an "invitation" in that sense, just for the purposes of handling it. + * This "invitation" is handled by the player who tried to initiate the original invitation to the other player. + * @param initialRequest player who would be joining the squad + * @param invitedPlayer player who would be joining the squad (unique character id) + * @param invitedPlayerSquad squad + */ +case class PermissionToReverseInvitationToSquad(initialRequest: Player, invitedPlayer: Long, invitedPlayerSquad: SquadFeatures) + extends Invitation(initialRequest.CharId, initialRequest.Name) { + + def handleInvitation(indirectInviteFunc: (IndirectInvite, Player, Long, Long, String) => Boolean)( + manager: SquadInvitationManager, + invitedPlayer: Long, + invitingPlayer: Long, + otherName: String + ): Unit = { + manager.publish( + invitingPlayer, + SquadResponse.SquadRelatedComment(s"\\#6 The player you tried to invite already belongs to a squad.") + ) + manager.publish( + invitingPlayer, + SquadResponse.SquadRelatedComment(s"\\#6Would you like to try join that squad? (respond with \\#3/accept\\#6 or \\#3/cancel\\#6)") + ) + } + + def handleAcceptance( + manager: SquadInvitationManager, + player: Player, + invitedPlayer: Long, + invitedPlayerSquadOpt: Option[SquadFeatures] + ): Unit = { + manager.createIndirectInvite(player, invitedPlayer, invitedPlayerSquad) //should put it at the front of the list + } + + def handleRejection( + manager: SquadInvitationManager, + player: Player, + rejectingPlayer: Long, + squadsToLeaders: List[(PlanetSideGUID, SquadFeatures)] + ): Unit = { + /* wordless rejection */ + } + + def doRejection( + manager: SquadInvitationManager, + player: Player, + rejectingPlayer: Long + ): Unit = { + /* wordless rejection */ + } + + def handleCancel( + manager: SquadInvitationManager, + player: Player, + handlingPlayer: Long + ): Unit = { + val actingPlayer = player.CharId + if (actingPlayer != handlingPlayer) { + manager.publish( + handlingPlayer, + SquadResponse.SquadRelatedComment(s"A question regarding squad invitations no longer matters.") + ) + } + } + + def canBeAutoApproved: Boolean = false + + def getOptionalSquad: Option[SquadFeatures] = Some(invitedPlayerSquad) + + def getPlayer: Player = initialRequest + + def appliesToPlayer(playerCharId: Long): Boolean = invitedPlayer == playerCharId + + def appliesToSquad(guid: PlanetSideGUID): Boolean = invitedPlayerSquad.Squad.GUID == guid + + def appliesToSquadAndPosition(guid: PlanetSideGUID, squadPosition: Int): Boolean = false +} diff --git a/src/main/scala/net/psforever/services/teamwork/invitations/ProximityInvite.scala b/src/main/scala/net/psforever/services/teamwork/invitations/ProximityInvite.scala index 6f2f77fd..658a6b36 100644 --- a/src/main/scala/net/psforever/services/teamwork/invitations/ProximityInvite.scala +++ b/src/main/scala/net/psforever/services/teamwork/invitations/ProximityInvite.scala @@ -44,26 +44,46 @@ final case class ProximityInvite(squadLeader: Member, features: SquadFeatures, p invitedPlayer: Long, invitedPlayerSquadOpt: Option[SquadFeatures] ): Unit = { + val leaderCharId = squadLeader.CharId + //this cleanup activity always happens + features.ProxyInvites = features.ProxyInvites.filterNot { _ == invitedPlayer } if ( manager.notLimitedByEnrollmentInSquad(invitedPlayerSquadOpt, invitedPlayer) && - SquadInvitationManager.canEnrollInSquad(features, invitedPlayer) + SquadInvitationManager.canEnrollInSquad(features, invitedPlayer) && + manager.joinSquad(player, features, position) ) { - val invitingPlayer = squadLeader.CharId - features.ProxyInvites = features.ProxyInvites.filterNot { _ == invitedPlayer } - if (manager.joinSquad(player, features, position)) { - //join this squad - manager.acceptanceMessages(invitingPlayer, invitedPlayer, player.Name) - manager.cleanUpAllInvitesWithPlayer(invitedPlayer) - val squad = features.Squad - if (squad.Size == squad.Capacity) { - //all available squad positions filled; terminate all remaining invitations - manager.cleanUpAllInvitesToSquad(features) + //join this squad + //manager.acceptanceMessages(invitingPlayer, invitedPlayer, player.Name) + val msg = SquadResponse.Membership(SquadResponseType.Accept, invitedPlayer, Some(leaderCharId), player.Name, unk5 = false) + manager.publish(leaderCharId, msg) + manager.publish(invitedPlayer, msg.copy(unk5 = true)) + //clean up invitations specifically for this squad and this position + val cleanedUpQueuedInvites = manager.cleanUpQueuedInvitesForSquadAndPosition(features.Squad.GUID, position) + if (features.Squad.Capacity == features.Squad.Size) { + val cleanedUpActiveInvites = manager.cleanUpActiveInvitesForSquad(features.Squad.GUID) + cleanedUpActiveInvites.collect { case (id, invites) => + invites.foreach(_.handleCancel(manager, player, id)) + manager.publish( + id, + SquadResponse.SquadRelatedComment(s"An invitation to join a squad has ended.") + ) } - } else { - manager.reloadProximityInvite(player.Zone.Players, invitedPlayer, features, position) //TODO ? + (manager.cleanUpQueuedInvitesForSquad(features.Squad.GUID) ++ cleanedUpActiveInvites ++ cleanedUpQueuedInvites).collectFirst { case _ => + manager.publish( + leaderCharId, + SquadResponse.SquadRelatedComment(s"You had invitations that were cancelled due to this action.") + ) + } + } else if (cleanedUpQueuedInvites.nonEmpty) { + manager.publish( + leaderCharId, + SquadResponse.SquadRelatedComment(s"You had invitations that were cancelled due to this action.") + ) } + } else { + //if able to attempt to accept this proximity invite, recruitment is still ongoing + manager.reloadProximityInvite(player.Zone.Players, invitedPlayer, features, position) } - } def handleRejection( @@ -90,6 +110,31 @@ final case class ProximityInvite(squadLeader: Member, features: SquadFeatures, p manager.reloadProximityInvite(player.Zone.Players, rejectingPlayer, features, position) } + def handleCancel( + manager: SquadInvitationManager, + player: Player, + handlingPlayer: Long + ): Unit = { + val actingPlayer = player.CharId + val leaderCharId = squadLeader.CharId + if (actingPlayer == handlingPlayer) { + manager.publish( + handlingPlayer, + SquadResponse.SquadRelatedComment(s"You have declined an offer to join a squad.") + ) + } else if (actingPlayer == leaderCharId) { + manager.publish( + handlingPlayer, + SquadResponse.SquadRelatedComment(s"The offer to join a squad has been cancelled.") + ) + } else { + manager.publish( + handlingPlayer, + SquadResponse.SquadRelatedComment(s"The offer to join into a squad is no longer valid.") + ) + } + } + def canBeAutoApproved: Boolean = false def getOptionalSquad: Option[SquadFeatures] = Some(features) diff --git a/src/main/scala/net/psforever/services/teamwork/invitations/RequestToJoinSquadRole.scala b/src/main/scala/net/psforever/services/teamwork/invitations/RequestToJoinSquadRole.scala index e44ad011..598633b5 100644 --- a/src/main/scala/net/psforever/services/teamwork/invitations/RequestToJoinSquadRole.scala +++ b/src/main/scala/net/psforever/services/teamwork/invitations/RequestToJoinSquadRole.scala @@ -36,12 +36,60 @@ final case class RequestToJoinSquadRole(requestee: Player, features: SquadFeatur ): Unit = { //player requested to join a squad's specific position //invitedPlayer is actually the squad leader; petitioner is the actual "invitedPlayer" + val leaderCharId = player.CharId + val requestingPlayer = requestee.CharId if ( SquadInvitationManager.canEnrollInSquad(features, requestee.CharId) && manager.joinSquad(requestee, features, position) ) { - manager.acceptanceMessages(invitedPlayer, requestee.CharId, requestee.Name) - manager.cleanUpInvitesForSquadAndPosition(features, position) + //manager.acceptanceMessages(invitedPlayer, requestee.CharId, requestee.Name) + manager.publish( + leaderCharId, + SquadResponse.SquadRelatedComment(s"You have accepted ${requestee.Name}'s request to join your squad.") + ) + manager.publish( + requestingPlayer, + SquadResponse.SquadRelatedComment(s"You have joined the squad '${features.Squad.Task}'.") + ) + //clean up invitations specifically for this squad and this position + val cleanedUpActiveInvitesForSquadAndPosition = manager.cleanUpActiveInvitesForSquadAndPosition(features.Squad.GUID, position) + cleanedUpActiveInvitesForSquadAndPosition.collect { case (id, invites) => + invites.foreach(_.handleCancel(manager, player, id)) + manager.publish( + id, + SquadResponse.SquadRelatedComment(s"An invitation to join a squad has ended.") + ) + } + val cleanedUpQueuedInvites = manager.cleanUpQueuedInvitesForSquadAndPosition(features.Squad.GUID, position) + if (features.Squad.Capacity == features.Squad.Size) { + val cleanedUpActiveInvites = manager.cleanUpActiveInvitesForSquad(features.Squad.GUID) + cleanedUpActiveInvites.collect { case (id, _) => + manager.publish( + id, + SquadResponse.SquadRelatedComment(s"An invitation to join a squad has ended.") + ) + } + (manager.cleanUpQueuedInvitesForSquad(features.Squad.GUID) ++ cleanedUpActiveInvites ++ cleanedUpQueuedInvites).collectFirst { case _ => + manager.publish( + leaderCharId, + SquadResponse.SquadRelatedComment(s"You had invitations that were cancelled due to this action.") + ) + } + } else if (cleanedUpQueuedInvites.nonEmpty) { + manager.publish( + leaderCharId, + SquadResponse.SquadRelatedComment(s"You had invitations that were cancelled due to this action.") + ) + } + } else { + manager.publish( + requestingPlayer, + SquadResponse.SquadRelatedComment(s"Your invitation to squad '${features.Squad.Task}' was accepted, but failed.") + ) + manager.publish( + leaderCharId, + SquadResponse.SquadRelatedComment(s"An accepted request to join your squad has failed.") + ) } } @@ -70,6 +118,38 @@ final case class RequestToJoinSquadRole(requestee: Player, features: SquadFeatur features.DeniedPlayers(requestee.CharId) } + def handleCancel( + manager: SquadInvitationManager, + player: Player, + handlingPlayer: Long + ): Unit = { + val invitingPlayer = requestee.CharId + val invitingPlayerName = requestee.Name + val actingPlayer = player.CharId + val leaderCharId = features.Squad.Leader.CharId + val leaderName = features.Squad.Leader.Name + if (actingPlayer == handlingPlayer) { + manager.publish( + invitingPlayer, + SquadResponse.SquadRelatedComment(s"You were declined admission to a squad.") + ) + } else if (actingPlayer == invitingPlayer) { + manager.publish( + leaderCharId, + SquadResponse.SquadRelatedComment(s"$invitingPlayerName has rescinded the offer to join the squad.") + ) + } else { + manager.publish( + leaderCharId, + SquadResponse.SquadRelatedComment(s"The request from $invitingPlayerName to join the squad is no longer valid.") + ) + manager.publish( + invitingPlayer, + SquadResponse.SquadRelatedComment(s"The offer to $leaderName to join the squad is no longer valid.") + ) + } + } + def canBeAutoApproved: Boolean = true def getOptionalSquad: Option[SquadFeatures] = Some(features) From cc398af2299c449953de0f4a398101aab28ffd6e Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Sun, 29 Jun 2025 15:58:47 -0400 Subject: [PATCH 06/25] max armor reminder, log kicked player --- .../net/psforever/actors/session/support/ZoningOperations.scala | 1 + .../psforever/services/account/AccountPersistenceService.scala | 1 + 2 files changed, 2 insertions(+) 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 7cc6a1c6..b8ca0679 100644 --- a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala @@ -3162,6 +3162,7 @@ class ZoningOperations( statisticsPacketFunc() if (tplayer.ExoSuit == ExoSuitType.MAX) { sendResponse(PlanetsideAttributeMessage(guid, 7, tplayer.Capacitor.toLong)) + sendResponse(PlanetsideAttributeMessage(guid, 4, tplayer.Armor)) } // AvatarAwardMessage //populateAvatarAwardRibbonsFunc(1, 20L) diff --git a/src/main/scala/net/psforever/services/account/AccountPersistenceService.scala b/src/main/scala/net/psforever/services/account/AccountPersistenceService.scala index 1d2fe0f4..3ff3206a 100644 --- a/src/main/scala/net/psforever/services/account/AccountPersistenceService.scala +++ b/src/main/scala/net/psforever/services/account/AccountPersistenceService.scala @@ -318,6 +318,7 @@ class PersistenceMonitor( } kicked = true kickTime = time.orElse(Some(300L)) + PerformLogout() case Logout(_) => kickTime match { From 5b272949e07e821a23037c52c0403aeb68247522 Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Mon, 30 Jun 2025 13:00:58 -0400 Subject: [PATCH 07/25] tell others my armor --- .../net/psforever/actors/session/support/ZoningOperations.scala | 1 + 1 file changed, 1 insertion(+) 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 b8ca0679..cccc0ee3 100644 --- a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala @@ -3163,6 +3163,7 @@ class ZoningOperations( if (tplayer.ExoSuit == ExoSuitType.MAX) { sendResponse(PlanetsideAttributeMessage(guid, 7, tplayer.Capacitor.toLong)) sendResponse(PlanetsideAttributeMessage(guid, 4, tplayer.Armor)) + continent.AvatarEvents ! AvatarServiceMessage(continent.id, AvatarAction.PlanetsideAttributeToAll(guid, 4, tplayer.Armor)) } // AvatarAwardMessage //populateAvatarAwardRibbonsFunc(1, 20L) From 6bc17261986a649aee9993adaf8fd8f85b676432 Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Mon, 30 Jun 2025 20:28:16 -0400 Subject: [PATCH 08/25] tell me armor for any max --- .../net/psforever/actors/session/support/ZoningOperations.scala | 2 ++ 1 file changed, 2 insertions(+) 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 cccc0ee3..11d80fef 100644 --- a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala @@ -3165,6 +3165,8 @@ class ZoningOperations( sendResponse(PlanetsideAttributeMessage(guid, 4, tplayer.Armor)) continent.AvatarEvents ! AvatarServiceMessage(continent.id, AvatarAction.PlanetsideAttributeToAll(guid, 4, tplayer.Armor)) } + // for issue #1269 + continent.AllPlayers.filter(_.ExoSuit == ExoSuitType.MAX).foreach(max => sendResponse(PlanetsideAttributeMessage(max.GUID, 4, max.Armor))) // AvatarAwardMessage //populateAvatarAwardRibbonsFunc(1, 20L) From 7d8dd522594472df1e6dc6d8b4c49ad3256c4878 Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Sun, 6 Jul 2025 15:24:07 -0400 Subject: [PATCH 09/25] gate fixes --- .../session/support/ZoningOperations.scala | 3 ++- .../net/psforever/objects/SpawnPoint.scala | 4 +--- .../global/GlobalDefinitionsBuilding.scala | 8 ++++---- .../scala/net/psforever/zones/Zones.scala | 20 ++++++++++++++++++- 4 files changed, 26 insertions(+), 9 deletions(-) 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 11d80fef..40c26da6 100644 --- a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala @@ -1282,6 +1282,7 @@ class ZoningOperations( ICS.FindZone(_.id == zoneId, context.self) )) } else { + vehicle.Velocity = None sessionLogic.general.unaccessContainer(vehicle) LoadZoneCommonTransferActivity() player.VehicleSeated = vehicle.GUID @@ -2993,7 +2994,7 @@ class ZoningOperations( /** * na * @param target player being spawned - * @param position where player is being placed in the game wqrld + * @param position where player is being placed in the game world * @param orientation in what direction the player is facing in the game world * @param onThisSide description of the containing environment * @param goingToZone common designation for the zone diff --git a/src/main/scala/net/psforever/objects/SpawnPoint.scala b/src/main/scala/net/psforever/objects/SpawnPoint.scala index d6f800dd..e3fc8ca5 100644 --- a/src/main/scala/net/psforever/objects/SpawnPoint.scala +++ b/src/main/scala/net/psforever/objects/SpawnPoint.scala @@ -140,8 +140,6 @@ object SpawnPoint { def CavernGate(innerRadius: Float)(obj: SpawnPoint, target: PlanetSideGameObject): (Vector3, Vector3) = { val (a, b) = metaGate(obj, target, innerRadius) target match { - case v: Vehicle if GlobalDefinitions.isFlightVehicle(v.Definition) => - (a.xy + Vector3.z((target.Position.z + a.z) * 0.5f), b) case m: MountableEntity => m.BailProtection = true (a + Vector3.z(obj.Definition.UseRadius * 0.5f), b) @@ -154,7 +152,7 @@ object SpawnPoint { val (a, b) = metaGate(obj, target, innerRadius) target match { case v: Vehicle if GlobalDefinitions.isFlightVehicle(v.Definition) => - (a.xy + Vector3.z((target.Position.z + a.z) * 0.5f), b) + (a, b) case _ => (a + Vector3.z(flightlessZOffset), b) } diff --git a/src/main/scala/net/psforever/objects/global/GlobalDefinitionsBuilding.scala b/src/main/scala/net/psforever/objects/global/GlobalDefinitionsBuilding.scala index 74a48782..ca18a2e0 100644 --- a/src/main/scala/net/psforever/objects/global/GlobalDefinitionsBuilding.scala +++ b/src/main/scala/net/psforever/objects/global/GlobalDefinitionsBuilding.scala @@ -129,8 +129,8 @@ object GlobalDefinitionsBuilding { hst.NoWarp += colossus_flight hst.NoWarp += peregrine_gunner hst.NoWarp += peregrine_flight - hst.SpecificPointFunc = SpawnPoint.CavernGate(innerRadius = 6f) - + hst.SpecificPointFunc = SpawnPoint.CavernGate(innerRadius = 12f) + warpgate.Name = "warpgate" warpgate.UseRadius = 67.81070029f warpgate.SOIRadius = 302 //301.8713f @@ -141,8 +141,8 @@ object GlobalDefinitionsBuilding { warpgate_cavern.UseRadius = 19.72639434f warpgate_cavern.SOIRadius = 41 warpgate_cavern.VehicleAllowance = true - warpgate_cavern.SpecificPointFunc = SpawnPoint.CavernGate(innerRadius = 4.5f) - + warpgate_cavern.SpecificPointFunc = SpawnPoint.CavernGate(innerRadius = 12f) + warpgate_small.Name = "warpgate_small" warpgate_small.UseRadius = 69.03687655f warpgate_small.SOIRadius = 103 diff --git a/src/main/scala/net/psforever/zones/Zones.scala b/src/main/scala/net/psforever/zones/Zones.scala index 6f969468..f1463dfd 100644 --- a/src/main/scala/net/psforever/zones/Zones.scala +++ b/src/main/scala/net/psforever/zones/Zones.scala @@ -295,7 +295,25 @@ object Zones { WarpGate.Structure(Vector3(structure.absX, structure.absY, structure.absZ), GlobalDefinitions.hst) ) ) - case objectType if warpGateTypes.contains(objectType) => + case objectType @ "warpgate_cavern" if warpGateTypes.contains(objectType) => + zoneMap.addLocalBuilding( + structure.objectName, + structure.guid, + structure.mapId.get, + FoundationBuilder( + WarpGate.Structure(Vector3(structure.absX, structure.absY, structure.absZ), GlobalDefinitions.warpgate_cavern) + ) + ) + case objectType @ "warpgate_small" if warpGateTypes.contains(objectType) => + zoneMap.addLocalBuilding( + structure.objectName, + structure.guid, + structure.mapId.get, + FoundationBuilder( + WarpGate.Structure(Vector3(structure.absX, structure.absY, structure.absZ), GlobalDefinitions.warpgate_small) + ) + ) + case objectType @ "warpgate" if warpGateTypes.contains(objectType) => zoneMap.addLocalBuilding( structure.objectName, structure.guid, From 3f7f6ee4c08e48e644571eaef0fcdaec3e800e2b Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Thu, 17 Jul 2025 15:35:10 -0400 Subject: [PATCH 10/25] cud emp and os --- .../resources/overrides/game_objects0.adb.lst | 2 +- .../actors/session/AvatarActor.scala | 66 +++++++++++- .../actors/session/SessionActor.scala | 3 +- .../normal/WeaponAndProjectileLogic.scala | 6 +- .../spectator/WeaponAndProjectileLogic.scala | 13 +-- .../WeaponAndProjectileOperations.scala | 80 +++++++++++++- .../session/support/ZoningOperations.scala | 2 +- .../net/psforever/objects/OrbitalStrike.scala | 61 +++++++++++ .../net/psforever/objects/SpecialEmp.scala | 102 ++++++++++++++++++ .../net/psforever/objects/avatar/Avatar.scala | 7 ++ .../objects/avatar/PlayerControl.scala | 6 ++ .../net/psforever/objects/zones/Zone.scala | 47 ++++++++ .../game/PlanetsideAttributeMessage.scala | 4 + .../psforever/packet/game/UplinkRequest.scala | 6 +- 14 files changed, 382 insertions(+), 23 deletions(-) create mode 100644 src/main/scala/net/psforever/objects/OrbitalStrike.scala diff --git a/server/src/main/resources/overrides/game_objects0.adb.lst b/server/src/main/resources/overrides/game_objects0.adb.lst index d476e61a..1ea38dca 100644 --- a/server/src/main/resources/overrides/game_objects0.adb.lst +++ b/server/src/main/resources/overrides/game_objects0.adb.lst @@ -23,7 +23,7 @@ add_property boomer_trigger equiptime 500 add_property chainblade equiptime 250 add_property chainblade holstertime 250 add_property colossus_flight requirement_award0 false -add_property command_detonater allowed false +add_property command_detonater allowed true add_property command_detonater equiptime 500 add_property command_detonater holstertime 500 add_property cycler equiptime 600 diff --git a/src/main/scala/net/psforever/actors/session/AvatarActor.scala b/src/main/scala/net/psforever/actors/session/AvatarActor.scala index c68b0f47..12291c4a 100644 --- a/src/main/scala/net/psforever/actors/session/AvatarActor.scala +++ b/src/main/scala/net/psforever/actors/session/AvatarActor.scala @@ -154,6 +154,10 @@ object AvatarActor { final case class UpdatePurchaseTime(definition: BasicDefinition, time: LocalDateTime = LocalDateTime.now()) extends Command + /** rchase time for the use of calculating cooldowns */ + final case class UpdateCUDTime(action: String, time: LocalDateTime = LocalDateTime.now()) + extends Command + /** Set use time for the use of calculating cooldowns */ final case class UpdateUseTime(definition: BasicDefinition, time: LocalDateTime = LocalDateTime.now()) extends Command @@ -459,7 +463,15 @@ object AvatarActor { case _ => () } } catch { - case _: Exception => () + case _: Exception => + val cooldown = LocalDateTime.parse(b) + name match { + case "orbital_strike" if now.compareTo(cooldown.plusMillis(3.hours.toMillis.toInt)) == -1 => + cooldowns.put(name, cooldown) + case "emp_blast" | "reveal_friendlies" | "reveal_enemies" if now.compareTo(cooldown.plusMillis(20.minutes.toMillis.toInt)) == -1 => + cooldowns.put(name, cooldown) + case _ => () + } } case _ => log.warn(s"ignoring invalid cooldown string: '$value'") @@ -1549,6 +1561,21 @@ class AvatarActor( } Behaviors.same + case UpdateCUDTime(action, time) => + var theTimes = avatar.cooldowns.purchase + var updateTheTimes: Boolean = false + Avatar.cudCooldowns.get(action) match { + case Some(_) => + //only send for items with cooldowns + updateTheTimes = true + theTimes = theTimes.updated(action, time) + case _ => () + } + if (updateTheTimes) { + avatarCopy(avatar.copy(cooldowns = avatar.cooldowns.copy(purchase = theTimes))) + } + Behaviors.same + case UpdateUseTime(definition, time) => if (!Avatar.useCooldowns.contains(definition)) { log.warn(s"${avatar.name} is updating a use time for item '${definition.Name}' that has no cooldown") @@ -2655,6 +2682,43 @@ class AvatarActor( } if (keysToDrop.nonEmpty) { val cdown = avatar.cooldowns + val cud = Array("orbital_strike", "emp_blast", "reveal_friendlies", "reveal_enemies") + keysToDrop.foreach { key => + if (cud.contains(key)) { + avatar + .cooldowns + .purchase + .find { case (name, _) => name.equals(key) } + .flatMap { case (name, purchaseTime) => + val secondsSincePurchase = Seconds.secondsBetween(purchaseTime, LocalDateTime.now()).getSeconds + Avatar + .cudCooldowns + .find(_._1.equals(name)) + .collect { + case (action, cooldown) => + (action, cooldown.toSeconds - secondsSincePurchase) + } + .orElse { + None + } + .collect { + case (action, remainingTime) if remainingTime > 0 => + val convertTime = remainingTime * 1000 + keysToDrop = keysToDrop.filterNot(_ == action) + action match { + case "orbital_strike" => + sessionActor ! SessionActor.SendResponse(PlanetsideAttributeMessage(session.get.player.GUID, 60, convertTime)) + case "emp_blast" => + sessionActor ! SessionActor.SendResponse(PlanetsideAttributeMessage(session.get.player.GUID, 59, convertTime)) + case "reveal_enemies" => + sessionActor ! SessionActor.SendResponse(PlanetsideAttributeMessage(session.get.player.GUID, 58, convertTime)) + case "reveal_friendlies" => + sessionActor ! SessionActor.SendResponse(PlanetsideAttributeMessage(session.get.player.GUID, 57, convertTime)) + } + } + } + } + } avatarCopy(avatar.copy(cooldowns = cdown.copy(purchase = cdown.purchase.removedAll(keysToDrop)))) } } diff --git a/src/main/scala/net/psforever/actors/session/SessionActor.scala b/src/main/scala/net/psforever/actors/session/SessionActor.scala index 899d3265..25b5c377 100644 --- a/src/main/scala/net/psforever/actors/session/SessionActor.scala +++ b/src/main/scala/net/psforever/actors/session/SessionActor.scala @@ -517,7 +517,8 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case packet: WeaponLazeTargetPositionMessage => logic.shooting.handleWeaponLazeTargetPosition(packet) - case _: UplinkRequest => () + case packet: UplinkRequest => + logic.shooting.handleUplinkRequest(packet) case packet: HitMessage => logic.shooting.handleDirectHit(packet) diff --git a/src/main/scala/net/psforever/actors/session/normal/WeaponAndProjectileLogic.scala b/src/main/scala/net/psforever/actors/session/normal/WeaponAndProjectileLogic.scala index 0d33f5ab..785cb06c 100644 --- a/src/main/scala/net/psforever/actors/session/normal/WeaponAndProjectileLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/WeaponAndProjectileLogic.scala @@ -8,8 +8,8 @@ import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObjec import net.psforever.objects.{BoomerDeployable, BoomerTrigger, Player, SpecialEmp, Tool, Vehicle} import net.psforever.objects.vital.base.{DamageResolution, DamageType} import net.psforever.objects.zones.{Zone, ZoneProjectile} -import net.psforever.packet.game.{AIDamage, AvatarGrenadeStateMessage, ChangeAmmoMessage, ChangeFireModeMessage, ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, HitMessage, LashMessage, LongRangeProjectileInfoMessage, ProjectileStateMessage, ReloadMessage, SplashHitMessage, UplinkRequest, WeaponDelayFireMessage, WeaponDryFireMessage, WeaponFireMessage, WeaponLazeTargetPositionMessage} -import net.psforever.types.Vector3 +import net.psforever.packet.game.{AIDamage, AvatarGrenadeStateMessage, ChangeAmmoMessage, ChangeFireModeMessage, ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, HitMessage, LashMessage, LongRangeProjectileInfoMessage, OrbitalStrikeWaypointMessage, ProjectileStateMessage, ReloadMessage, SplashHitMessage, TriggerEffectMessage, TriggeredEffectLocation, UplinkRequest, UplinkRequestType, UplinkResponse, WeaponDelayFireMessage, WeaponDryFireMessage, WeaponFireMessage, WeaponLazeTargetPositionMessage} +import net.psforever.types.{ValidPlanetSideGUID, Vector3} object WeaponAndProjectileLogic { def apply(ops: WeaponAndProjectileOperations): WeaponAndProjectileLogic = { @@ -50,7 +50,7 @@ class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit } def handleUplinkRequest(packet: UplinkRequest): Unit = { - sessionLogic.administrativeKick(player) + ops.handleUplinkRequest(packet) } def handleAvatarGrenadeState(pkt: AvatarGrenadeStateMessage): Unit = { diff --git a/src/main/scala/net/psforever/actors/session/spectator/WeaponAndProjectileLogic.scala b/src/main/scala/net/psforever/actors/session/spectator/WeaponAndProjectileLogic.scala index 89a95dac..ab500276 100644 --- a/src/main/scala/net/psforever/actors/session/spectator/WeaponAndProjectileLogic.scala +++ b/src/main/scala/net/psforever/actors/session/spectator/WeaponAndProjectileLogic.scala @@ -28,18 +28,7 @@ class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit def handleWeaponLazeTargetPosition(pkt: WeaponLazeTargetPositionMessage): Unit = { /* intentionally blank */ } - def handleUplinkRequest(packet: UplinkRequest): Unit = { - val UplinkRequest(code, _, _) = packet - val playerFaction = player.Faction - //todo this is not correct - code match { - case UplinkRequestType.RevealFriendlies => - sendResponse(UplinkResponse(code.value, continent.LivePlayers.count(_.Faction == playerFaction))) - case UplinkRequestType.RevealEnemies => - sendResponse(UplinkResponse(code.value, continent.LivePlayers.count(_.Faction != playerFaction))) - case _ => () - } - } + def handleUplinkRequest(packet: UplinkRequest): Unit = { /* intentionally blank */ } def handleAvatarGrenadeState(pkt: AvatarGrenadeStateMessage): Unit = { /* intentionally blank */ } diff --git a/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala b/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala index 5ade93f0..eebb5646 100644 --- a/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala @@ -3,6 +3,8 @@ package net.psforever.actors.session.support import akka.actor.{ActorContext, typed} import net.psforever.login.WorldSession.{CountAmmunition, CountGrenades, FindAmmoBoxThatUses, FindEquipmentStock, FindToolThatUses, PutEquipmentInInventoryOrDrop, PutNewEquipmentInInventoryOrDrop, RemoveOldEquipmentFromInventory} +import net.psforever.objects.OrbitalStrike.{cr4_os, cr5_os} +import net.psforever.objects.SpecialEmp.{cr3_emp, cr4_emp, cr5_emp} import net.psforever.objects.ballistics.ProjectileQuality import net.psforever.objects.definition.{ProjectileDefinition, SpecialExoSuitDefinition} import net.psforever.objects.entity.SimpleWorldEntity @@ -23,7 +25,9 @@ import net.psforever.objects.vital.etc.OicwLilBuddyReason import net.psforever.objects.vital.interaction.DamageInteraction import net.psforever.objects.vital.projectile.ProjectileReason import net.psforever.objects.zones.exp.ToDatabase -import net.psforever.types.{ChatMessageType, Vector3} +import net.psforever.packet.game.UplinkRequest +import net.psforever.services.local.{LocalAction, LocalServiceMessage} +import net.psforever.types.{ChatMessageType, PlanetSideEmpire, ValidPlanetSideGUID, Vector3} import net.psforever.util.Config import scala.collection.mutable @@ -89,6 +93,7 @@ class WeaponAndProjectileOperations( ) extends CommonSessionInterfacingFunctionality { var shooting: mutable.Set[PlanetSideGUID] = mutable.Set.empty //ChangeFireStateMessage_Start var prefire: mutable.Set[PlanetSideGUID] = mutable.Set.empty //if WeaponFireMessage precedes ChangeFireStateMessage_Start + private[session] var orbitalStrikePos: Option[Vector3] = None private[session] var shootingStart: mutable.HashMap[PlanetSideGUID, Long] = mutable.HashMap[PlanetSideGUID, Long]() private[session] var shootingStop: mutable.HashMap[PlanetSideGUID, Long] = mutable.HashMap[PlanetSideGUID, Long]() private[session] val shotsFired: mutable.HashMap[Int,Int] = mutable.HashMap[Int,Int]() @@ -292,6 +297,79 @@ class WeaponAndProjectileOperations( } } + def handleUplinkRequest(pkt: UplinkRequest): Unit = { + val UplinkRequest(code, pos, _) = pkt + val playerFaction = player.Faction + code match { + case UplinkRequestType.RevealFriendlies => () + /*sendResponse(UplinkResponse(code.value, 0)) + sendResponse(PlanetsideAttributeMessage(player.GUID, 57, 10000)) //1200000 + avatarActor ! AvatarActor.UpdateCUDTime("reveal_friendlies")*/ + case UplinkRequestType.RevealEnemies => () + /*sendResponse(UplinkResponse(code.value, 0)) + sendResponse(PlanetsideAttributeMessage(player.GUID, 58, 10000)) //1200000 + avatarActor ! AvatarActor.UpdateCUDTime("reveal_enemies") + These are seen in a few logs, but didn't work. Unclear what these do or what '10' in Event1 represents + sendResponse(UplinkPositionEvent(5, Event0(5))) + sendResponse(UplinkPositionEvent(4, Event1(4, 10))) + sendResponse(UplinkPositionEvent(6, Event0(6)))*/ + case UplinkRequestType.ElectroMagneticPulse => + val cr = player.avatar.cr.value + val empSize = cr match { + case 3 => cr3_emp + case 4 => cr4_emp + case 5 => cr5_emp + } + val empColor = if (playerFaction != PlanetSideEmpire.NEUTRAL) { s"explosion_emp_${playerFaction.toString.toLowerCase}" } else { "explosion_emp_bo" } + sendResponse(UplinkResponse(code.value, 0)) + sendResponse(PlanetsideAttributeMessage(player.GUID, 59, 10000)) //1200000 + avatarActor ! AvatarActor.UpdateCUDTime("emp_blast") + player.Zone.LocalEvents ! LocalServiceMessage(s"${player.Zone.id}", + LocalAction.SendPacket(TriggerEffectMessage(ValidPlanetSideGUID(0), empColor, None, Some(TriggeredEffectLocation(player.Position, Vector3(0, 0, 90)))))) + context.system.scheduler.scheduleOnce(delay = 1 seconds) { + Zone.serverSideDamage(player.Zone, player, empSize, SpecialEmp.createEmpInteraction(empSize, player.Position), + ExplosiveDeployableControl.detectionForExplosiveSource(player), Zone.findAllTargets) + } + case UplinkRequestType.OrbitalStrike => + player.Zone.LocalEvents ! LocalServiceMessage(s"$playerFaction", LocalAction.SendPacket(OrbitalStrikeWaypointMessage(player.GUID, pos.get.x, pos.get.y))) + sendResponse(UplinkResponse(code.value, 0)) + orbitalStrikePos = pos + case UplinkRequestType.Unknown5 => + val cr = player.avatar.cr.value + val strikeType = playerFaction match { + case PlanetSideEmpire.NC => + if (cr == 4) {"explosion_bluedeath_nc"} else {"explosion_bluedeath_nc_lrg"} + case PlanetSideEmpire.TR => + if (cr == 4) {"explosion_bluedeath_tr"} else {"explosion_bluedeath_tr_lrg"} + case PlanetSideEmpire.VS => + if (cr == 4) {"explosion_bluedeath_vs"} else {"explosion_bluedeath_vs_lrg"} + case PlanetSideEmpire.NEUTRAL => + if (cr == 4) {"explosion_bluedeath_bo"} else {"explosion_bluedeath_bo_lrg"} + } + val osSize = cr match { + case 4 => cr4_os + case 5 => cr5_os + } + sendResponse(UplinkResponse(code.value, 0)) + sendResponse(PlanetsideAttributeMessage(player.GUID, 60, 10000)) //10800000 + avatarActor ! AvatarActor.UpdateCUDTime("orbital_strike") + context.system.scheduler.scheduleOnce(delay = 5 seconds) { + player.Zone.LocalEvents ! LocalServiceMessage(s"${player.Zone.id}", + LocalAction.SendPacket(TriggerEffectMessage(ValidPlanetSideGUID(0), strikeType, None, Some(TriggeredEffectLocation(orbitalStrikePos.get, Vector3(0, 0, 90)))))) + player.Zone.LocalEvents ! LocalServiceMessage(s"$playerFaction", LocalAction.SendPacket(OrbitalStrikeWaypointMessage(player.GUID, None))) + context.system.scheduler.scheduleOnce(delay = 5 seconds) { + val sectorTargets = Zone.findOrbitalStrikeTargets(player.Zone, orbitalStrikePos.get, osSize.DamageRadius, Zone.getOrbitbalStrikeTargets) + val withinRange = sectorTargets.filter {target => Zone.orbitalStrikeDistanceCheck(orbitalStrikePos.get, target.Position, osSize.DamageRadius)} + withinRange.foreach { target => + target.Actor ! Vitality.Damage(DamageInteraction(SourceEntry(target), OrbitalStrike(PlayerSource(player)), target.Position).calculate()) + } + orbitalStrikePos = None + } + } + case _ => () + } + } + def handleChangeAmmo(pkt: ChangeAmmoMessage): Unit = { val ChangeAmmoMessage(item_guid, _) = pkt val (thing, equipment) = sessionLogic.findContainedEquipment() 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 40c26da6..761d373d 100644 --- a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala @@ -2640,7 +2640,6 @@ class ZoningOperations( sendResponse(ObjectCreateDetailedMessage(ObjectClass.avatar, guid, data)) log.debug(s"AvatarRejoin: ${player.Name} - $guid -> $data") } - avatarActor ! AvatarActor.RefreshPurchaseTimes() setupAvatarFunc = AvatarCreate //begin looking for conditions to set the avatar context.system.scheduler.scheduleOnce(delay = 750 millisecond, context.self, SessionActor.SetCurrentAvatar(player, 200)) @@ -3304,6 +3303,7 @@ class ZoningOperations( enqueueNewActivity(ActivityQueuedTask(ZoningOperations.reportProgressionSystem, 2)) } } + avatarActor ! AvatarActor.RefreshPurchaseTimes() } /** diff --git a/src/main/scala/net/psforever/objects/OrbitalStrike.scala b/src/main/scala/net/psforever/objects/OrbitalStrike.scala new file mode 100644 index 00000000..33634d85 --- /dev/null +++ b/src/main/scala/net/psforever/objects/OrbitalStrike.scala @@ -0,0 +1,61 @@ +// Copyright (c) 2025 PSForever +package net.psforever.objects + +import net.psforever.objects.sourcing.{PlayerSource, SourceEntry} +import net.psforever.objects.vital.{NoResistanceSelection, SimpleResolutions} +import net.psforever.objects.vital.base.{DamageReason, DamageResolution, DamageType} +import net.psforever.objects.vital.damage.DamageCalculations +import net.psforever.objects.vital.prop.{DamageProperties, DamageWithPosition} +import net.psforever.objects.vital.resolution.{DamageAndResistance, DamageResistanceModel} + +final case class OrbitalStrike(player: PlayerSource) + extends DamageReason { + def resolution: DamageResolution.Value = DamageResolution.Hit + + def same(test: DamageReason): Boolean = { + test.source eq source + } + + def source: DamageProperties = OrbitalStrike.source + + def damageModel: DamageAndResistance = OrbitalStrike.drm + + override def adversary : Option[SourceEntry] = Some(player) + +} + +object OrbitalStrike { + + final val cr4_os = new DamageWithPosition { + CausesDamageType = DamageType.Splash + SympatheticExplosion = true + Damage0 = 10000 + DamageAtEdge = 0.1f + DamageRadius = 10f + } + + final val cr5_os = new DamageWithPosition { + CausesDamageType = DamageType.Splash + SympatheticExplosion = true + Damage0 = 10000 + DamageAtEdge = 0.1f + DamageRadius = 20f + } + + private val source = new DamageProperties { + Damage0 = 10000 + Damage1 = 10000 + Damage2 = 10000 + Damage3 = 10000 + Damage4 = 10000 + DamageToHealthOnly = true + DamageToVehicleOnly = true + DamageToBattleframeOnly = true + } + + private val drm = new DamageResistanceModel { + DamageUsing = DamageCalculations.AgainstExoSuit + ResistUsing = NoResistanceSelection + Model = SimpleResolutions.calculate + } +} diff --git a/src/main/scala/net/psforever/objects/SpecialEmp.scala b/src/main/scala/net/psforever/objects/SpecialEmp.scala index 5578690a..b58a0e37 100644 --- a/src/main/scala/net/psforever/objects/SpecialEmp.scala +++ b/src/main/scala/net/psforever/objects/SpecialEmp.scala @@ -65,6 +65,108 @@ object SpecialEmp { innateDamage = emp } + final val cr3_emp = new DamageWithPosition { + CausesDamageType = DamageType.Splash + SympatheticExplosion = true + Damage0 = 0 + DamageAtEdge = 1.0f + DamageRadius = 10f + AdditionalEffect = true + JammedEffectDuration += TargetValidation( + EffectTarget.Category.Player, + EffectTarget.Validation.Player + ) -> 1000 + JammedEffectDuration += TargetValidation( + EffectTarget.Category.Vehicle, + EffectTarget.Validation.AMS + ) -> 5000 + JammedEffectDuration += TargetValidation( + EffectTarget.Category.Deployable, + EffectTarget.Validation.MotionSensor + ) -> 30000 + JammedEffectDuration += TargetValidation( + EffectTarget.Category.Deployable, + EffectTarget.Validation.Spitfire + ) -> 30000 + JammedEffectDuration += TargetValidation( + EffectTarget.Category.Turret, + EffectTarget.Validation.Turret + ) -> 30000 + JammedEffectDuration += TargetValidation( + EffectTarget.Category.Vehicle, + EffectTarget.Validation.VehicleNotAMS + ) -> 10000 + Modifiers = MaxDistanceCutoff + } + + final val cr4_emp = new DamageWithPosition { + CausesDamageType = DamageType.Splash + SympatheticExplosion = true + Damage0 = 0 + DamageAtEdge = 1.0f + DamageRadius = 15f + AdditionalEffect = true + JammedEffectDuration += TargetValidation( + EffectTarget.Category.Player, + EffectTarget.Validation.Player + ) -> 1000 + JammedEffectDuration += TargetValidation( + EffectTarget.Category.Vehicle, + EffectTarget.Validation.AMS + ) -> 5000 + JammedEffectDuration += TargetValidation( + EffectTarget.Category.Deployable, + EffectTarget.Validation.MotionSensor + ) -> 30000 + JammedEffectDuration += TargetValidation( + EffectTarget.Category.Deployable, + EffectTarget.Validation.Spitfire + ) -> 30000 + JammedEffectDuration += TargetValidation( + EffectTarget.Category.Turret, + EffectTarget.Validation.Turret + ) -> 30000 + JammedEffectDuration += TargetValidation( + EffectTarget.Category.Vehicle, + EffectTarget.Validation.VehicleNotAMS + ) -> 10000 + Modifiers = MaxDistanceCutoff + } + + final val cr5_emp = new DamageWithPosition { + CausesDamageType = DamageType.Splash + SympatheticExplosion = true + Damage0 = 0 + DamageAtEdge = 1.0f + DamageRadius = 20f + AdditionalEffect = true + JammedEffectDuration += TargetValidation( + EffectTarget.Category.Player, + EffectTarget.Validation.Player + ) -> 1000 + JammedEffectDuration += TargetValidation( + EffectTarget.Category.Vehicle, + EffectTarget.Validation.AMS + ) -> 5000 + JammedEffectDuration += TargetValidation( + EffectTarget.Category.Deployable, + EffectTarget.Validation.MotionSensor + ) -> 30000 + JammedEffectDuration += TargetValidation( + EffectTarget.Category.Deployable, + EffectTarget.Validation.Spitfire + ) -> 30000 + JammedEffectDuration += TargetValidation( + EffectTarget.Category.Turret, + EffectTarget.Validation.Turret + ) -> 30000 + JammedEffectDuration += TargetValidation( + EffectTarget.Category.Vehicle, + EffectTarget.Validation.VehicleNotAMS + ) -> 10000 + Modifiers = MaxDistanceCutoff + } + /** * Trigger an electromagnetic pulse. */ diff --git a/src/main/scala/net/psforever/objects/avatar/Avatar.scala b/src/main/scala/net/psforever/objects/avatar/Avatar.scala index 4e349ec5..8f0a41a1 100644 --- a/src/main/scala/net/psforever/objects/avatar/Avatar.scala +++ b/src/main/scala/net/psforever/objects/avatar/Avatar.scala @@ -68,6 +68,13 @@ object Avatar { GlobalDefinitions.trhev_pounder -> 5.minutes ) + val cudCooldowns: Map[String, FiniteDuration] = Map( + "orbital_strike" -> 1.minutes, // 3.hours + "emp_blast" -> 1.minutes, // 20.minutes + "reveal_friendlies" -> 1.minutes, // 20.minutes + "reveal_enemies" -> 1.minutes // 20.minutes + ) + val useCooldowns: Map[BasicDefinition, FiniteDuration] = Map( GlobalDefinitions.medkit -> 5.seconds, GlobalDefinitions.super_armorkit -> 20.minutes, diff --git a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala index 375f4cec..4e151f97 100644 --- a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala +++ b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala @@ -36,6 +36,7 @@ import net.psforever.objects.vital.collision.CollisionReason import net.psforever.objects.vital.etc.{PainboxReason, SuicideReason} import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult} import net.psforever.packet.PlanetSideGamePacket +import org.joda.time.{LocalDateTime, Seconds} import scala.concurrent.duration._ @@ -359,6 +360,11 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm equipment match { case Some(holsteredEquipment) => log.info(s"${player.Name} has put ${player.Sex.possessive} ${holsteredEquipment.Definition.Name} down") + //make sure the player didn't just initialte an orbital strike. If not (the if below is true), make sure waypoint is removed + if (holsteredEquipment.Definition == GlobalDefinitions.command_detonater && player.avatar.cr.value > 3 && + !player.avatar.cooldowns.purchase.exists(os => os._1 == "orbital_strike" && Seconds.secondsBetween(os._2, LocalDateTime.now()).getSeconds < 10)) { + player.Zone.LocalEvents ! LocalServiceMessage(s"${player.Faction}", LocalAction.SendPacket(OrbitalStrikeWaypointMessage(player.GUID, None))) + } case None => log.info(s"${player.Name} lowers ${player.Sex.possessive} hand") } diff --git a/src/main/scala/net/psforever/objects/zones/Zone.scala b/src/main/scala/net/psforever/objects/zones/Zone.scala index 45eb69a1..cbf828d0 100644 --- a/src/main/scala/net/psforever/objects/zones/Zone.scala +++ b/src/main/scala/net/psforever/objects/zones/Zone.scala @@ -1845,6 +1845,53 @@ object Zone { playerTargets ++ vehicleTargets ++ deployableTargets ++ soiTargets } + /** + * na + * @see `DamageWithPosition` + * @see `Zone.blockMap.sector` + * @param zone the zone in which the explosion should occur + * @param sourcePosition a position that is used as the origin of the explosion + * @param radius idistance + * @param getTargetsFromSector get this list of entities from a sector + * @return a list of affected entities + */ + def findOrbitalStrikeTargets( + zone: Zone, + sourcePosition: Vector3, + radius: Float, + getTargetsFromSector: SectorPopulation => List[PlanetSideServerObject with Vitality] + ): List[PlanetSideServerObject with Vitality] = { + getTargetsFromSector(zone.blockMap.sector(sourcePosition.xy, radius)) + } + + def getOrbitbalStrikeTargets(sector: SectorPopulation): List[PlanetSideServerObject with Vitality] = { + //collect all targets that can be damaged + //players + val playerTargets = sector.livePlayerList.filter { player => player.VehicleSeated.isEmpty && player.WhichSide == Sidedness.OutsideOf } + //vehicles + val vehicleTargets = sector.vehicleList.filterNot { _.Destroyed } + //deployables + val deployableTargets = sector.deployableList.filter { obj => !obj.Destroyed && obj.WhichSide == Sidedness.OutsideOf } + //amenities + val soiTargets = sector.amenityList.collect { + case amenity: Vitality if !amenity.Destroyed && amenity.WhichSide == Sidedness.OutsideOf && amenity.CanDamage => amenity } + //altogether ... + playerTargets ++ vehicleTargets ++ deployableTargets ++ soiTargets + } + + /** + * Check if targets returned from sector are within range of the imminent Orbital Strike + * @param p1 OS position + * @param p2 target position + * @param maxDistance radius of the Orbital Strike + * @return `true`, if the two entities are near enough to each other; + * `false`, otherwise + */ + def orbitalStrikeDistanceCheck(p1: Vector3, p2: Vector3, maxDistance: Float): Boolean = { + val radius = maxDistance * maxDistance + Vector3.DistanceSquared(p1.xy, p2.xy) <= radius + } + /** * na * @param instigation what previous event happened, if any, that caused this explosion diff --git a/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala b/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala index aaf908d1..87f94fb5 100644 --- a/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala +++ b/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala @@ -162,6 +162,10 @@ import scodec.codecs._ * * `55 - "Someone is attempting to Heal you". Value is 1`
* `56 - "Someone is attempting to Repair you". Value is 1`
+ * `57 - CUD Reveal Friendlies cooldown timer`
+ * `58 - CUD Reveal Enemies cooldown timer`
+ * `59 - CUD EMP Blast cooldown timer`
+ * `60 - CUD Orbital Strike cooldown timer`
* `64 - ????? related to using router telepads` * `67 - Enables base shields (from cavern module/lock)`
* `73 - "You are locked into the Core Beam. Charging your Module now.". Value is 1 to active`
diff --git a/src/main/scala/net/psforever/packet/game/UplinkRequest.scala b/src/main/scala/net/psforever/packet/game/UplinkRequest.scala index f4a66e59..2d419726 100644 --- a/src/main/scala/net/psforever/packet/game/UplinkRequest.scala +++ b/src/main/scala/net/psforever/packet/game/UplinkRequest.scala @@ -26,15 +26,15 @@ object UplinkRequestType extends IntEnum[UplinkRequestType] { case object OrbitalStrike extends UplinkRequestType(value = 4) - case object Unknown5 extends UplinkRequestType(value = 5) + case object Unknown5 extends UplinkRequestType(value = 5) // pull trigger to start orbital strike countdown case object Function6 extends UplinkRequestType(value = 6) - case object Function7 extends UplinkRequestType(value = 7) + case object Function7 extends UplinkRequestType(value = 7) // sent back by client after reveal enemies response case object Function8 extends UplinkRequestType(value = 8) - case object Unknown9 extends UplinkRequestType(value = 9) + case object Unknown9 extends UplinkRequestType(value = 9) // recall squad to sanc case object UnknownA extends UplinkRequestType(value = 10) From f41fee6c41529c83aa8e033ce95af0a6b0ed1552 Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Thu, 17 Jul 2025 20:19:49 -0400 Subject: [PATCH 11/25] just one os please --- .../support/WeaponAndProjectileOperations.scala | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala b/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala index eebb5646..7dba959f 100644 --- a/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala @@ -94,6 +94,7 @@ class WeaponAndProjectileOperations( var shooting: mutable.Set[PlanetSideGUID] = mutable.Set.empty //ChangeFireStateMessage_Start var prefire: mutable.Set[PlanetSideGUID] = mutable.Set.empty //if WeaponFireMessage precedes ChangeFireStateMessage_Start private[session] var orbitalStrikePos: Option[Vector3] = None + private[session] var orbitalStrikeInProgress: Boolean = false private[session] var shootingStart: mutable.HashMap[PlanetSideGUID, Long] = mutable.HashMap[PlanetSideGUID, Long]() private[session] var shootingStop: mutable.HashMap[PlanetSideGUID, Long] = mutable.HashMap[PlanetSideGUID, Long]() private[session] val shotsFired: mutable.HashMap[Int,Int] = mutable.HashMap[Int,Int]() @@ -335,6 +336,8 @@ class WeaponAndProjectileOperations( sendResponse(UplinkResponse(code.value, 0)) orbitalStrikePos = pos case UplinkRequestType.Unknown5 => + if (!orbitalStrikeInProgress) { + orbitalStrikeInProgress = true val cr = player.avatar.cr.value val strikeType = playerFaction match { case PlanetSideEmpire.NC => @@ -359,13 +362,18 @@ class WeaponAndProjectileOperations( player.Zone.LocalEvents ! LocalServiceMessage(s"$playerFaction", LocalAction.SendPacket(OrbitalStrikeWaypointMessage(player.GUID, None))) context.system.scheduler.scheduleOnce(delay = 5 seconds) { val sectorTargets = Zone.findOrbitalStrikeTargets(player.Zone, orbitalStrikePos.get, osSize.DamageRadius, Zone.getOrbitbalStrikeTargets) - val withinRange = sectorTargets.filter {target => Zone.orbitalStrikeDistanceCheck(orbitalStrikePos.get, target.Position, osSize.DamageRadius)} + val withinRange = sectorTargets.filter { target => Zone.orbitalStrikeDistanceCheck(orbitalStrikePos.get, target.Position, osSize.DamageRadius) } withinRange.foreach { target => target.Actor ! Vitality.Damage(DamageInteraction(SourceEntry(target), OrbitalStrike(PlayerSource(player)), target.Position).calculate()) } orbitalStrikePos = None + orbitalStrikeInProgress = false + } } } + else { + sendResponse(UplinkResponse(code.value, 0)) + } case _ => () } } From d74812cd817919c9c52f3f9a5ab4cf656528c091 Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Fri, 18 Jul 2025 21:29:42 -0400 Subject: [PATCH 12/25] we can reveal --- .../WeaponAndProjectileOperations.scala | 50 +++++++++++++------ .../net/psforever/objects/avatar/Avatar.scala | 8 +-- .../objects/definition/BasicDefinition.scala | 8 +++ .../global/GlobalDefinitionsVehicle.scala | 39 +++++++++++++++ 4 files changed, 87 insertions(+), 18 deletions(-) diff --git a/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala b/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala index 7dba959f..24ba6f33 100644 --- a/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala @@ -302,18 +302,40 @@ class WeaponAndProjectileOperations( val UplinkRequest(code, pos, _) = pkt val playerFaction = player.Faction code match { - case UplinkRequestType.RevealFriendlies => () - /*sendResponse(UplinkResponse(code.value, 0)) - sendResponse(PlanetsideAttributeMessage(player.GUID, 57, 10000)) //1200000 - avatarActor ! AvatarActor.UpdateCUDTime("reveal_friendlies")*/ - case UplinkRequestType.RevealEnemies => () - /*sendResponse(UplinkResponse(code.value, 0)) - sendResponse(PlanetsideAttributeMessage(player.GUID, 58, 10000)) //1200000 - avatarActor ! AvatarActor.UpdateCUDTime("reveal_enemies") - These are seen in a few logs, but didn't work. Unclear what these do or what '10' in Event1 represents - sendResponse(UplinkPositionEvent(5, Event0(5))) - sendResponse(UplinkPositionEvent(4, Event1(4, 10))) - sendResponse(UplinkPositionEvent(6, Event0(6)))*/ + case UplinkRequestType.RevealFriendlies => + val revealZone = player.Zone.Number + sendResponse(UplinkResponse(code.value, 0)) + sendResponse(PlanetsideAttributeMessage(player.GUID, 57, 1200000)) + avatarActor ! AvatarActor.UpdateCUDTime("reveal_friendlies") + sendResponse(UplinkPositionEvent(5, Event0(5))) + sendResponse(UplinkPositionEvent(4, Event1(4, revealZone))) + sendResponse(UplinkPositionEvent(6, Event0(6))) + val friendlies = player.Zone.LivePlayers.filter { friend => friend.Faction == player.Faction } + val friendlyVehicles = player.Zone.Vehicles.filter { vehicle => vehicle.Faction == player.Faction && !vehicle.Destroyed } + friendlies.foreach { f => + sendResponse(UplinkPositionEvent(0, Event2(0, Vector3(f.Position.x, f.Position.y, 0.0f), 255, revealZone, 0, 1118938442, 300000, 299080, Some(true)))) + } + friendlyVehicles.foreach { v => + sendResponse(UplinkPositionEvent(0, Event2(0, Vector3(v.Position.x, v.Position.y, 0.0f), v.Definition.MapRevealId, revealZone, 0, 1118938442, 300000, 299080, Some(true)))) + } + case UplinkRequestType.RevealEnemies => + val revealZone = player.Zone.Number + sendResponse(UplinkResponse(code.value, 0)) + sendResponse(PlanetsideAttributeMessage(player.GUID, 58, 1200000)) + avatarActor ! AvatarActor.UpdateCUDTime("reveal_enemies") + sendResponse(UplinkPositionEvent(5, Event0(5))) + sendResponse(UplinkPositionEvent(4, Event1(4, revealZone))) + sendResponse(UplinkPositionEvent(6, Event0(6))) + val enemies = player.Zone.LivePlayers.filter { enemy => enemy.Faction != player.Faction && + Zone.orbitalStrikeDistanceCheck(player.Position, enemy.Position, 200f)} //reusing distance check + val enemyVehicles = player.Zone.Vehicles.filter { vehicle => vehicle.Faction != player.Faction && !vehicle.Destroyed && + Zone.orbitalStrikeDistanceCheck(player.Position, vehicle.Position, 200f)} //reusing distance check + enemies.foreach { e => + sendResponse(UplinkPositionEvent(1, Event2(1, Vector3(e.Position.x, e.Position.y, 0.0f), 255, revealZone, 0, 1118938442, 300000, 299080, Some(false)))) + } + enemyVehicles.foreach { v => + sendResponse(UplinkPositionEvent(1, Event2(1, Vector3(v.Position.x, v.Position.y, 0.0f), v.Definition.MapRevealId, revealZone, 0, 1118938442, 300000, 299080, Some(false)))) + } case UplinkRequestType.ElectroMagneticPulse => val cr = player.avatar.cr.value val empSize = cr match { @@ -323,7 +345,7 @@ class WeaponAndProjectileOperations( } val empColor = if (playerFaction != PlanetSideEmpire.NEUTRAL) { s"explosion_emp_${playerFaction.toString.toLowerCase}" } else { "explosion_emp_bo" } sendResponse(UplinkResponse(code.value, 0)) - sendResponse(PlanetsideAttributeMessage(player.GUID, 59, 10000)) //1200000 + sendResponse(PlanetsideAttributeMessage(player.GUID, 59, 1200000)) avatarActor ! AvatarActor.UpdateCUDTime("emp_blast") player.Zone.LocalEvents ! LocalServiceMessage(s"${player.Zone.id}", LocalAction.SendPacket(TriggerEffectMessage(ValidPlanetSideGUID(0), empColor, None, Some(TriggeredEffectLocation(player.Position, Vector3(0, 0, 90)))))) @@ -354,7 +376,7 @@ class WeaponAndProjectileOperations( case 5 => cr5_os } sendResponse(UplinkResponse(code.value, 0)) - sendResponse(PlanetsideAttributeMessage(player.GUID, 60, 10000)) //10800000 + sendResponse(PlanetsideAttributeMessage(player.GUID, 60, 10800000)) avatarActor ! AvatarActor.UpdateCUDTime("orbital_strike") context.system.scheduler.scheduleOnce(delay = 5 seconds) { player.Zone.LocalEvents ! LocalServiceMessage(s"${player.Zone.id}", diff --git a/src/main/scala/net/psforever/objects/avatar/Avatar.scala b/src/main/scala/net/psforever/objects/avatar/Avatar.scala index 8f0a41a1..633943e2 100644 --- a/src/main/scala/net/psforever/objects/avatar/Avatar.scala +++ b/src/main/scala/net/psforever/objects/avatar/Avatar.scala @@ -69,10 +69,10 @@ object Avatar { ) val cudCooldowns: Map[String, FiniteDuration] = Map( - "orbital_strike" -> 1.minutes, // 3.hours - "emp_blast" -> 1.minutes, // 20.minutes - "reveal_friendlies" -> 1.minutes, // 20.minutes - "reveal_enemies" -> 1.minutes // 20.minutes + "orbital_strike" -> 3.hours, + "emp_blast" -> 20.minutes, + "reveal_friendlies" -> 20.minutes, + "reveal_enemies" -> 20.minutes ) val useCooldowns: Map[BasicDefinition, FiniteDuration] = Map( diff --git a/src/main/scala/net/psforever/objects/definition/BasicDefinition.scala b/src/main/scala/net/psforever/objects/definition/BasicDefinition.scala index 092d763a..9649420f 100644 --- a/src/main/scala/net/psforever/objects/definition/BasicDefinition.scala +++ b/src/main/scala/net/psforever/objects/definition/BasicDefinition.scala @@ -4,6 +4,7 @@ package net.psforever.objects.definition abstract class BasicDefinition { private var name: String = "definition" private var descriptor: Option[String] = None + private var mapRevealId: Int = 0 def Name: String = name @@ -20,4 +21,11 @@ abstract class BasicDefinition { descriptor = description Descriptor } + + def MapRevealId: Int = mapRevealId //for vehicle IDs used by reveal friendlies and enemies with a CUD + + def MapRevealId_=(mapRevealId: Int): Int = { + this.mapRevealId = mapRevealId + MapRevealId + } } diff --git a/src/main/scala/net/psforever/objects/global/GlobalDefinitionsVehicle.scala b/src/main/scala/net/psforever/objects/global/GlobalDefinitionsVehicle.scala index 5e7ae6da..c2fee38f 100644 --- a/src/main/scala/net/psforever/objects/global/GlobalDefinitionsVehicle.scala +++ b/src/main/scala/net/psforever/objects/global/GlobalDefinitionsVehicle.scala @@ -83,6 +83,7 @@ object GlobalDefinitionsVehicle { fury.collision.z = CollisionZData(Array((8f, 1), (24f, 35), (40f, 100), (48f, 175), (52f, 350))) fury.maxForwardSpeed = 90f fury.mass = 32.1f + fury.MapRevealId = 14 quadassault.Name = "quadassault" // Basilisk quadassault.MaxHealth = 650 @@ -119,6 +120,7 @@ object GlobalDefinitionsVehicle { quadassault.collision.z = CollisionZData(Array((8f, 1), (24f, 35), (40f, 100), (48f, 175), (52f, 350))) quadassault.maxForwardSpeed = 90f quadassault.mass = 32.1f + quadassault.MapRevealId = 30 quadstealth.Name = "quadstealth" // Wraith quadstealth.MaxHealth = 650 @@ -155,6 +157,7 @@ object GlobalDefinitionsVehicle { quadstealth.collision.z = CollisionZData(Array((8f, 1), (24f, 35), (40f, 100), (48f, 175), (52f, 350))) quadstealth.maxForwardSpeed = 90f quadstealth.mass = 32.1f + quadstealth.MapRevealId = 15 two_man_assault_buggy.Name = "two_man_assault_buggy" // Harasser two_man_assault_buggy.MaxHealth = 1250 @@ -193,6 +196,7 @@ object GlobalDefinitionsVehicle { two_man_assault_buggy.collision.z = CollisionZData(Array((7f, 1), (21f, 50), (35f, 150), (42f, 300), (45.5f, 600))) two_man_assault_buggy.maxForwardSpeed = 85f two_man_assault_buggy.mass = 52.4f + two_man_assault_buggy.MapRevealId = 1 skyguard.Name = "skyguard" skyguard.MaxHealth = 1000 @@ -232,6 +236,7 @@ object GlobalDefinitionsVehicle { skyguard.collision.z = CollisionZData(Array((7f, 1), (21f, 50), (35f, 150), (42f, 300), (45.4f, 600))) skyguard.maxForwardSpeed = 90f skyguard.mass = 78.9f + skyguard.MapRevealId = 16 threemanheavybuggy.Name = "threemanheavybuggy" // Marauder threemanheavybuggy.MaxHealth = 1700 @@ -274,6 +279,7 @@ object GlobalDefinitionsVehicle { threemanheavybuggy.collision.z = CollisionZData(Array((6f, 1), (18f, 50), (30f, 150), (36f, 300), (39f, 900))) threemanheavybuggy.maxForwardSpeed = 80f threemanheavybuggy.mass = 96.3f + threemanheavybuggy.MapRevealId = 18 twomanheavybuggy.Name = "twomanheavybuggy" // Enforcer twomanheavybuggy.MaxHealth = 1800 @@ -313,6 +319,7 @@ object GlobalDefinitionsVehicle { twomanheavybuggy.collision.z = CollisionZData(Array((6f, 1), (18f, 50), (30f, 150), (36f, 300), (39f, 900))) twomanheavybuggy.maxForwardSpeed = 80f twomanheavybuggy.mass = 83.2f + twomanhoverbuggy.MapRevealId = 20 twomanhoverbuggy.Name = "twomanhoverbuggy" // Thresher twomanhoverbuggy.MaxHealth = 1600 @@ -355,6 +362,7 @@ object GlobalDefinitionsVehicle { twomanhoverbuggy.collision.z = CollisionZData(Array((6f, 1), (18f, 50), (30f, 150), (36f, 300), (39f, 900))) twomanhoverbuggy.maxForwardSpeed = 85f twomanhoverbuggy.mass = 55.5f + twomanhoverbuggy.MapRevealId = 21 mediumtransport.Name = "mediumtransport" // Deliverer mediumtransport.MaxHealth = 2500 @@ -401,6 +409,7 @@ object GlobalDefinitionsVehicle { mediumtransport.collision.z = CollisionZData(Array((5f, 1), (15f, 50), (25f, 200), (30f, 750), (32.5f, 2000))) mediumtransport.maxForwardSpeed = 70f mediumtransport.mass = 108.5f + mediumtransport.MapRevealId = 10 battlewagon.Name = "battlewagon" // Raider battlewagon.MaxHealth = 2500 @@ -450,6 +459,7 @@ object GlobalDefinitionsVehicle { battlewagon.collision.z = CollisionZData(Array((5f, 1), (15f, 50), (25f, 200), (30f, 750), (32.5f, 2000))) //inherited from mediumtransport battlewagon.maxForwardSpeed = 65f battlewagon.mass = 108.5f + battlewagon.MapRevealId = 10 thunderer.Name = "thunderer" thunderer.MaxHealth = 2500 @@ -496,6 +506,7 @@ object GlobalDefinitionsVehicle { thunderer.collision.z = CollisionZData(Array((5f, 1), (15f, 50), (25f, 200), (30f, 750), (32.5f, 2000))) thunderer.maxForwardSpeed = 65f thunderer.mass = 108.5f + thunderer.MapRevealId = 29 aurora.Name = "aurora" aurora.MaxHealth = 2500 @@ -542,6 +553,7 @@ object GlobalDefinitionsVehicle { aurora.collision.z = CollisionZData(Array((5f, 1), (15f, 50), (25f, 200), (30f, 750), (32.5f, 2000))) aurora.maxForwardSpeed = 65f aurora.mass = 108.5f + aurora.MapRevealId = 28 apc_tr.Name = "apc_tr" // Juggernaut apc_tr.MaxHealth = 6000 @@ -612,6 +624,7 @@ object GlobalDefinitionsVehicle { apc_tr.collision.z = CollisionZData(Array((2f, 1), (6f, 50), (10f, 300), (12f, 1000), (13f, 3000))) apc_tr.maxForwardSpeed = 60f apc_tr.mass = 128.4f + apc_tr.MapRevealId = 32 apc_nc.Name = "apc_nc" // Vindicator apc_nc.MaxHealth = 6000 @@ -682,6 +695,7 @@ object GlobalDefinitionsVehicle { apc_nc.collision.z = CollisionZData(Array((2f, 1), (6f, 50), (10f, 300), (12f, 1000), (13f, 3000))) apc_nc.maxForwardSpeed = 60f apc_nc.mass = 128.4f + apc_nc.MapRevealId = 33 apc_vs.Name = "apc_vs" // Leviathan apc_vs.MaxHealth = 6000 @@ -752,6 +766,7 @@ object GlobalDefinitionsVehicle { apc_vs.collision.z = CollisionZData(Array((2f, 1), (6f, 50), (10f, 300), (12f, 1000), (13f, 3000))) apc_vs.maxForwardSpeed = 60f apc_vs.mass = 128.4f + apc_vs.MapRevealId = 41 lightning.Name = "lightning" lightning.MaxHealth = 2000 @@ -790,6 +805,7 @@ object GlobalDefinitionsVehicle { lightning.collision.z = CollisionZData(Array((6f, 1), (18f, 50), (30f, 150), (36f, 300), (39f, 750))) lightning.maxForwardSpeed = 74f lightning.mass = 100.2f + lightning.MapRevealId = 7 prowler.Name = "prowler" prowler.MaxHealth = 4800 @@ -833,6 +849,7 @@ object GlobalDefinitionsVehicle { prowler.collision.z = CollisionZData(Array((5f, 1), (15f, 50), (25f, 250), (30f, 600), (32.5f, 1500))) prowler.maxForwardSpeed = 57f prowler.mass = 510.5f + prowler.MapRevealId = 13 vanguard.Name = "vanguard" vanguard.MaxHealth = 5400 @@ -872,6 +889,7 @@ object GlobalDefinitionsVehicle { vanguard.collision.z = CollisionZData(Array((5f, 1), (15f, 50), (25f, 100), (30f, 250), (32.5f, 600))) vanguard.maxForwardSpeed = 60f vanguard.mass = 460.4f + vanguard.MapRevealId = 22 magrider.Name = "magrider" magrider.MaxHealth = 4200 @@ -913,6 +931,7 @@ object GlobalDefinitionsVehicle { magrider.collision.z = CollisionZData(Array((5f, 1), (15f, 50), (25f, 250), (30f, 600), (32.5f, 1500))) magrider.maxForwardSpeed = 65f magrider.mass = 75.3f + magrider.MapRevealId = 8 val utilityConverter = new UtilityVehicleConverter ant.Name = "ant" @@ -952,6 +971,7 @@ object GlobalDefinitionsVehicle { ant.collision.z = CollisionZData(Array((2f, 1), (6f, 50), (10f, 250), (12f, 500), (13f, 750))) ant.maxForwardSpeed = 65f ant.mass = 80.5f + ant.MapRevealId = 1 ams.Name = "ams" ams.MaxHealth = 3000 @@ -995,6 +1015,7 @@ object GlobalDefinitionsVehicle { ams.collision.z = CollisionZData(Array((2f, 1), (6f, 50), (10f, 250), (12f, 805), (13f, 3000))) ams.maxForwardSpeed = 70f ams.mass = 136.8f + ams.MapRevealId = 2 val variantConverter = new VariantVehicleConverter router.Name = "router" @@ -1039,6 +1060,7 @@ object GlobalDefinitionsVehicle { router.collision.z = CollisionZData(Array((6f, 1), (18f, 50), (30f, 150), (36f, 350), (39f, 900))) router.maxForwardSpeed = 60f router.mass = 60f + router.MapRevealId = 25 switchblade.Name = "switchblade" switchblade.MaxHealth = 1750 @@ -1085,6 +1107,7 @@ object GlobalDefinitionsVehicle { switchblade.collision.z = CollisionZData(Array((6f, 1), (18f, 50), (30f, 150), (36f, 350), (39f, 800))) switchblade.maxForwardSpeed = 80f switchblade.mass = 63.9f + switchblade.MapRevealId = 26 flail.Name = "flail" flail.MaxHealth = 2400 @@ -1127,6 +1150,7 @@ object GlobalDefinitionsVehicle { flail.collision.z = CollisionZData(Array((6f, 1), (18f, 50), (30f, 150), (36f, 350), (39f, 900))) flail.maxForwardSpeed = 55f flail.mass = 73.5f + flail.MapRevealId = 24 } /** @@ -1179,6 +1203,7 @@ object GlobalDefinitionsVehicle { mosquito.collision.z = CollisionZData(Array((3f, 1), (9f, 25), (15f, 50), (18f, 75), (19.5f, 100))) mosquito.maxForwardSpeed = 120f mosquito.mass = 53.6f + mosquito.MapRevealId = 11 lightgunship.Name = "lightgunship" // Reaver lightgunship.MaxHealth = 855 // Temporary - Correct Reaver Health from pre-"Coder Madness 2" Event @@ -1219,6 +1244,7 @@ object GlobalDefinitionsVehicle { lightgunship.collision.z = CollisionZData(Array((3f, 1), (9f, 30), (15f, 60), (18f, 90), (19.5f, 125))) lightgunship.maxForwardSpeed = 104f lightgunship.mass = 51.1f + lightgunship.MapRevealId = 6 wasp.Name = "wasp" wasp.MaxHealth = 515 @@ -1257,6 +1283,7 @@ object GlobalDefinitionsVehicle { wasp.collision.z = CollisionZData(Array((3f, 1), (9f, 25), (15f, 50), (18f, 75), (19.5f, 100))) //mosquito numbers wasp.maxForwardSpeed = 120f wasp.mass = 53.6f + wasp.MapRevealId = 11 liberator.Name = "liberator" liberator.MaxHealth = 2500 @@ -1305,6 +1332,7 @@ object GlobalDefinitionsVehicle { liberator.collision.z = CollisionZData(Array((3f, 1), (9f, 30), (15f, 60), (18f, 90), (19.5f, 125))) liberator.maxForwardSpeed = 90f liberator.mass = 82f + liberator.MapRevealId = 5 vulture.Name = "vulture" vulture.MaxHealth = 2500 @@ -1354,6 +1382,7 @@ object GlobalDefinitionsVehicle { vulture.collision.z = CollisionZData(Array((3f, 1), (9f, 30), (15f, 60), (18f, 90), (19.5f, 125))) vulture.maxForwardSpeed = 97f vulture.mass = 82f + vulture.MapRevealId = 5 dropship.Name = "dropship" // Galaxy dropship.MaxHealth = 5000 @@ -1430,6 +1459,7 @@ object GlobalDefinitionsVehicle { dropship.collision.z = CollisionZData(Array((3f, 5), (9f, 125), (15f, 250), (18f, 500), (19.5f, 1000))) dropship.maxForwardSpeed = 80f dropship.mass = 133f + dropship.MapRevealId = 4 galaxy_gunship.Name = "galaxy_gunship" galaxy_gunship.MaxHealth = 6000 @@ -1490,6 +1520,7 @@ object GlobalDefinitionsVehicle { galaxy_gunship.collision.z = CollisionZData(Array((3f, 5), (9f, 125), (15f, 250), (18f, 500), (19.5f, 1000))) galaxy_gunship.maxForwardSpeed = 85f galaxy_gunship.mass = 133f + galaxy_gunship.MapRevealId = 31 lodestar.Name = "lodestar" lodestar.MaxHealth = 5000 @@ -1542,6 +1573,7 @@ object GlobalDefinitionsVehicle { lodestar.collision.z = CollisionZData(Array((3f, 5), (9f, 125), (15f, 250), (18f, 500), (19.5f, 1000))) lodestar.maxForwardSpeed = 80f lodestar.mass = 128.2f + lodestar.MapRevealId = 34 phantasm.Name = "phantasm" phantasm.MaxHealth = 2500 @@ -1587,6 +1619,7 @@ object GlobalDefinitionsVehicle { phantasm.collision.z = CollisionZData(Array((3f, 1), (9f, 30), (15f, 60), (18f, 90), (19.5f, 125))) phantasm.maxForwardSpeed = 140f phantasm.mass = 100f + phantasm.MapRevealId = 35 droppod.Name = "droppod" droppod.MaxHealth = 20000 @@ -1724,6 +1757,7 @@ object GlobalDefinitionsVehicle { aphelion_gunner.collision.z = CollisionZData(Array((25f, 2), (40f, 4), (60f, 8), (85f, 16), (115f, 32))) aphelion_gunner.maxForwardSpeed = 17 aphelion_gunner.mass = 615.1f + aphelion_gunner.MapRevealId = 44 colossus_gunner.Name = "colossus_gunner" colossus_gunner.MaxHealth = 4500 @@ -1775,6 +1809,7 @@ object GlobalDefinitionsVehicle { colossus_gunner.collision.z = CollisionZData(Array((25f, 2), (40f, 4), (60f, 8), (85f, 16), (115f, 32))) colossus_gunner.maxForwardSpeed = 17 colossus_gunner.mass = 709.7f + colossus_gunner.MapRevealId = 48 peregrine_gunner.Name = "peregrine_gunner" peregrine_gunner.MaxHealth = 4500 @@ -1826,6 +1861,7 @@ object GlobalDefinitionsVehicle { peregrine_gunner.collision.z = CollisionZData(Array((25f, 2), (40f, 4), (60f, 8), (85f, 16), (115f, 32))) peregrine_gunner.maxForwardSpeed = 17 peregrine_gunner.mass = 713f + peregrine_gunner.MapRevealId = 52 val battleFrameFlightConverter = new BattleFrameFlightConverter aphelion_flight.Name = "aphelion_flight" @@ -1881,6 +1917,7 @@ object GlobalDefinitionsVehicle { aphelion_flight.collision.z = CollisionZData(Array((25f, 2), (40f, 4), (60f, 8), (85f, 16), (115f, 32))) aphelion_flight.maxForwardSpeed = 35 aphelion_flight.mass = 615.1f + aphelion_flight.MapRevealId = 45 colossus_flight.Name = "colossus_flight" colossus_flight.MaxHealth = 3500 @@ -1935,6 +1972,7 @@ object GlobalDefinitionsVehicle { colossus_flight.collision.z = CollisionZData(Array((25f, 2), (40f, 4), (60f, 8), (85f, 16), (115f, 32))) colossus_flight.maxForwardSpeed = 34 colossus_flight.mass = 709.7f + colossus_flight.MapRevealId = 49 peregrine_flight.Name = "peregrine_flight" peregrine_flight.MaxHealth = 3500 @@ -1989,5 +2027,6 @@ object GlobalDefinitionsVehicle { peregrine_flight.collision.z = CollisionZData(Array((25f, 2), (40f, 4), (60f, 8), (85f, 16), (115f, 32))) peregrine_flight.maxForwardSpeed = 35 peregrine_flight.mass = 713f + peregrine_flight.MapRevealId = 51 } } From 694a3195e311b5bbc829105df7fdf0307461b5d6 Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Sat, 19 Jul 2025 22:10:07 -0400 Subject: [PATCH 13/25] wrong name --- .../actors/session/support/WeaponAndProjectileOperations.scala | 2 +- src/main/scala/net/psforever/objects/avatar/PlayerControl.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala b/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala index 24ba6f33..cf4c36ad 100644 --- a/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala @@ -363,7 +363,7 @@ class WeaponAndProjectileOperations( val cr = player.avatar.cr.value val strikeType = playerFaction match { case PlanetSideEmpire.NC => - if (cr == 4) {"explosion_bluedeath_nc"} else {"explosion_bluedeath_nc_lrg"} + if (cr == 4) {"explosion_bluedeath"} else {"explosion_bluedeath_lrg"} case PlanetSideEmpire.TR => if (cr == 4) {"explosion_bluedeath_tr"} else {"explosion_bluedeath_tr_lrg"} case PlanetSideEmpire.VS => diff --git a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala index 4e151f97..34d6b19a 100644 --- a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala +++ b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala @@ -362,7 +362,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm log.info(s"${player.Name} has put ${player.Sex.possessive} ${holsteredEquipment.Definition.Name} down") //make sure the player didn't just initialte an orbital strike. If not (the if below is true), make sure waypoint is removed if (holsteredEquipment.Definition == GlobalDefinitions.command_detonater && player.avatar.cr.value > 3 && - !player.avatar.cooldowns.purchase.exists(os => os._1 == "orbital_strike" && Seconds.secondsBetween(os._2, LocalDateTime.now()).getSeconds < 10)) { + !player.avatar.cooldowns.purchase.exists(os => os._1 == "orbital_strike" && Seconds.secondsBetween(os._2, LocalDateTime.now()).getSeconds < 12)) { player.Zone.LocalEvents ! LocalServiceMessage(s"${player.Faction}", LocalAction.SendPacket(OrbitalStrikeWaypointMessage(player.GUID, None))) } case None => From 0166352001c562975dca6e8da4398839bf262b19 Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Sun, 20 Jul 2025 07:48:13 -0400 Subject: [PATCH 14/25] packet order --- .../session/support/WeaponAndProjectileOperations.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala b/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala index cf4c36ad..5be9ab5b 100644 --- a/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala @@ -309,15 +309,15 @@ class WeaponAndProjectileOperations( avatarActor ! AvatarActor.UpdateCUDTime("reveal_friendlies") sendResponse(UplinkPositionEvent(5, Event0(5))) sendResponse(UplinkPositionEvent(4, Event1(4, revealZone))) - sendResponse(UplinkPositionEvent(6, Event0(6))) val friendlies = player.Zone.LivePlayers.filter { friend => friend.Faction == player.Faction } val friendlyVehicles = player.Zone.Vehicles.filter { vehicle => vehicle.Faction == player.Faction && !vehicle.Destroyed } friendlies.foreach { f => - sendResponse(UplinkPositionEvent(0, Event2(0, Vector3(f.Position.x, f.Position.y, 0.0f), 255, revealZone, 0, 1118938442, 300000, 299080, Some(true)))) + sendResponse(UplinkPositionEvent(0, Event2(0, Vector3(f.Position.x, f.Position.y, 0.0f), 255, revealZone, 0, 1127348721, 300000, 298858, Some(true)))) } friendlyVehicles.foreach { v => - sendResponse(UplinkPositionEvent(0, Event2(0, Vector3(v.Position.x, v.Position.y, 0.0f), v.Definition.MapRevealId, revealZone, 0, 1118938442, 300000, 299080, Some(true)))) + sendResponse(UplinkPositionEvent(0, Event2(0, Vector3(v.Position.x, v.Position.y, 0.0f), v.Definition.MapRevealId, revealZone, 0, 1127348721, 300000, 298858, Some(true)))) } + sendResponse(UplinkPositionEvent(6, Event0(6))) case UplinkRequestType.RevealEnemies => val revealZone = player.Zone.Number sendResponse(UplinkResponse(code.value, 0)) @@ -325,7 +325,6 @@ class WeaponAndProjectileOperations( avatarActor ! AvatarActor.UpdateCUDTime("reveal_enemies") sendResponse(UplinkPositionEvent(5, Event0(5))) sendResponse(UplinkPositionEvent(4, Event1(4, revealZone))) - sendResponse(UplinkPositionEvent(6, Event0(6))) val enemies = player.Zone.LivePlayers.filter { enemy => enemy.Faction != player.Faction && Zone.orbitalStrikeDistanceCheck(player.Position, enemy.Position, 200f)} //reusing distance check val enemyVehicles = player.Zone.Vehicles.filter { vehicle => vehicle.Faction != player.Faction && !vehicle.Destroyed && @@ -336,6 +335,7 @@ class WeaponAndProjectileOperations( enemyVehicles.foreach { v => sendResponse(UplinkPositionEvent(1, Event2(1, Vector3(v.Position.x, v.Position.y, 0.0f), v.Definition.MapRevealId, revealZone, 0, 1118938442, 300000, 299080, Some(false)))) } + sendResponse(UplinkPositionEvent(6, Event0(6))) case UplinkRequestType.ElectroMagneticPulse => val cr = player.avatar.cr.value val empSize = cr match { From be30413714999487dd1a8e4f9442a1437003f415 Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Mon, 21 Jul 2025 07:51:23 -0400 Subject: [PATCH 15/25] found a friendly --- .../support/WeaponAndProjectileOperations.scala | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala b/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala index 5be9ab5b..8e18811e 100644 --- a/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala @@ -308,14 +308,14 @@ class WeaponAndProjectileOperations( sendResponse(PlanetsideAttributeMessage(player.GUID, 57, 1200000)) avatarActor ! AvatarActor.UpdateCUDTime("reveal_friendlies") sendResponse(UplinkPositionEvent(5, Event0(5))) - sendResponse(UplinkPositionEvent(4, Event1(4, revealZone))) + sendResponse(UplinkPositionEvent(3, Event1(3, revealZone))) val friendlies = player.Zone.LivePlayers.filter { friend => friend.Faction == player.Faction } val friendlyVehicles = player.Zone.Vehicles.filter { vehicle => vehicle.Faction == player.Faction && !vehicle.Destroyed } friendlies.foreach { f => - sendResponse(UplinkPositionEvent(0, Event2(0, Vector3(f.Position.x, f.Position.y, 0.0f), 255, revealZone, 0, 1127348721, 300000, 298858, Some(true)))) + sendResponse(UplinkPositionEvent(0, Event2(0, Vector3(f.Position.x, f.Position.y, 0.0f), 255, revealZone, 0, 1117348721, 300000, 299497, Some(true)))) } friendlyVehicles.foreach { v => - sendResponse(UplinkPositionEvent(0, Event2(0, Vector3(v.Position.x, v.Position.y, 0.0f), v.Definition.MapRevealId, revealZone, 0, 1127348721, 300000, 298858, Some(true)))) + sendResponse(UplinkPositionEvent(0, Event2(0, Vector3(v.Position.x, v.Position.y, 0.0f), v.Definition.MapRevealId, revealZone, 0, 1127348721, 300000, 299497, Some(true)))) } sendResponse(UplinkPositionEvent(6, Event0(6))) case UplinkRequestType.RevealEnemies => @@ -330,10 +330,10 @@ class WeaponAndProjectileOperations( val enemyVehicles = player.Zone.Vehicles.filter { vehicle => vehicle.Faction != player.Faction && !vehicle.Destroyed && Zone.orbitalStrikeDistanceCheck(player.Position, vehicle.Position, 200f)} //reusing distance check enemies.foreach { e => - sendResponse(UplinkPositionEvent(1, Event2(1, Vector3(e.Position.x, e.Position.y, 0.0f), 255, revealZone, 0, 1118938442, 300000, 299080, Some(false)))) + sendResponse(UplinkPositionEvent(1, Event2(1, Vector3(e.Position.x, e.Position.y, 0.0f), 255, revealZone, 0, 1138938442, 300000, 299080, Some(false)))) } enemyVehicles.foreach { v => - sendResponse(UplinkPositionEvent(1, Event2(1, Vector3(v.Position.x, v.Position.y, 0.0f), v.Definition.MapRevealId, revealZone, 0, 1118938442, 300000, 299080, Some(false)))) + sendResponse(UplinkPositionEvent(1, Event2(1, Vector3(v.Position.x, v.Position.y, 0.0f), v.Definition.MapRevealId, revealZone, 0, 1148938442, 300000, 299080, Some(false)))) } sendResponse(UplinkPositionEvent(6, Event0(6))) case UplinkRequestType.ElectroMagneticPulse => From 32287149c70a68f46c08a2c7d014dbda0909a2f1 Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Mon, 21 Jul 2025 10:38:08 -0400 Subject: [PATCH 16/25] density-alerts --- src/main/resources/application.conf | 12 ++++++ .../session/support/ZoningOperations.scala | 2 +- .../psforever/actors/zone/BuildingActor.scala | 7 +++ .../serverobject/structures/Building.scala | 43 ++++++++++++++++++- .../MajorFacilityHackParticipation.scala | 11 +++++ .../zones/SphereOfInfluenceActor.scala | 9 +++- .../scala/net/psforever/util/Config.scala | 9 +++- 7 files changed, 89 insertions(+), 4 deletions(-) diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf index cbb9bc88..6d56004c 100644 --- a/src/main/resources/application.conf +++ b/src/main/resources/application.conf @@ -508,6 +508,18 @@ game { capture-experience-points-modifier = 1f # Don't forget to pay back that debt. } + + alert { + # When a certain number of enemy players are within the SOI of a facility, an alert (DensityLevelUpdateMessage) + # will be dispatched to all players. Players of the owning faction will receive a chat warning (if in + # the same zone) and the map will flash the alert level over the facility until it changes + # Wiki says 25-30 + yellow = 1 + # Wiki says 30-60 + orange = 2 + # Wiki says 60+ + red = 3 + } } anti-cheat { 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 761d373d..25eaefdb 100644 --- a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala @@ -985,7 +985,7 @@ class ZoningOperations( */ def initFacility(continentNumber: Int, buildingNumber: Int, building: Building): Unit = { sendResponse(building.infoUpdateMessage()) - sendResponse(DensityLevelUpdateMessage(continentNumber, buildingNumber, List(0, 0, 0, 0, 0, 0, 0, 0))) + sendResponse(building.densityLevelUpdateMessage(building)) } /** diff --git a/src/main/scala/net/psforever/actors/zone/BuildingActor.scala b/src/main/scala/net/psforever/actors/zone/BuildingActor.scala index 8cfd403d..33492124 100644 --- a/src/main/scala/net/psforever/actors/zone/BuildingActor.scala +++ b/src/main/scala/net/psforever/actors/zone/BuildingActor.scala @@ -74,6 +74,8 @@ object BuildingActor { final case class PowerOff() extends Command + final case class DensityLevelUpdate(building: Building) extends Command + /** * Set a facility affiliated to one faction to be affiliated to a different faction. * @param details building and event system references @@ -226,6 +228,7 @@ class BuildingActor( case MapUpdate() => details.galaxyService ! GalaxyServiceMessage(GalaxyAction.MapUpdate(details.building.infoUpdateMessage())) + details.galaxyService ! GalaxyServiceMessage(GalaxyAction.SendResponse(details.building.densityLevelUpdateMessage(building))) Behaviors.same case AmenityStateChange(amenity, data) => @@ -245,6 +248,10 @@ class BuildingActor( case Ntu(msg) => logic.ntu(details, msg) + + case DensityLevelUpdate(building) => + details.galaxyService ! GalaxyServiceMessage(GalaxyAction.SendResponse(details.building.densityLevelUpdateMessage(building))) + Behaviors.same } } } diff --git a/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala b/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala index cf2634b2..adcf381b 100644 --- a/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala +++ b/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala @@ -11,13 +11,14 @@ import net.psforever.objects.serverobject.resourcesilo.ResourceSilo import net.psforever.objects.serverobject.tube.SpawnTube import net.psforever.objects.zones.Zone import net.psforever.objects.zones.blockmap.BlockMapEntity -import net.psforever.packet.game.BuildingInfoUpdateMessage +import net.psforever.packet.game.{BuildingInfoUpdateMessage, DensityLevelUpdateMessage} import net.psforever.types._ import scalax.collection.{Graph, GraphEdge} import akka.actor.typed.scaladsl.adapter._ import net.psforever.objects.serverobject.llu.{CaptureFlag, CaptureFlagSocket} import net.psforever.objects.serverobject.structures.participation.{MajorFacilityHackParticipation, NoParticipation, ParticipationLogic, TowerHackParticipation} import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal +import net.psforever.util.Config class Building( private val name: String, @@ -236,6 +237,46 @@ class Building( ) } + def densityLevelUpdateMessage(building: Building): DensityLevelUpdateMessage = { + if (building.PlayersInSOI.nonEmpty) { + val factionCounts: Map[PlanetSideEmpire.Value, Int] = + building.PlayersInSOI.groupBy(_.Faction).view.mapValues(_.size).toMap + val otherEmpireCounts: Map[PlanetSideEmpire.Value, Int] = PlanetSideEmpire.values.map { + faction => + val otherCount = factionCounts.filterNot(_._1 == faction).values.sum + faction -> otherCount + }.toMap + val trAlert = otherEmpireCounts.getOrElse(PlanetSideEmpire.TR, 0) match { + case count if count >= Config.app.game.alert.red => 3 + case count if count >= Config.app.game.alert.orange => 2 + case count if count >= Config.app.game.alert.yellow => 1 + case _ => 0 + } + val ncAlert = otherEmpireCounts.getOrElse(PlanetSideEmpire.NC, 0) match { + case count if count >= Config.app.game.alert.red => 3 + case count if count >= Config.app.game.alert.orange => 2 + case count if count >= Config.app.game.alert.yellow => 1 + case _ => 0 + } + val vsAlert = otherEmpireCounts.getOrElse(PlanetSideEmpire.VS, 0) match { + case count if count >= Config.app.game.alert.red => 3 + case count if count >= Config.app.game.alert.orange => 2 + case count if count >= Config.app.game.alert.yellow => 1 + case _ => 0 + } + val boAlert = otherEmpireCounts.getOrElse(PlanetSideEmpire.NEUTRAL, 0) match { + case count if count >= Config.app.game.alert.red => 3 + case count if count >= Config.app.game.alert.orange => 2 + case count if count >= Config.app.game.alert.yellow => 1 + case _ => 0 + } + DensityLevelUpdateMessage(Zone.Number, MapId, List(0, trAlert, 0, ncAlert, 0, vsAlert, 0, boAlert)) + } + else { //nobody is in this SOI + DensityLevelUpdateMessage(Zone.Number, MapId, List(0, 0, 0, 0, 0, 0, 0, 0)) + } + } + def hasLatticeBenefit(wantedBenefit: LatticeBenefit): Boolean = { val baseDownState = (NtuSource match { case Some(ntu) => ntu.NtuCapacitor < 1f diff --git a/src/main/scala/net/psforever/objects/serverobject/structures/participation/MajorFacilityHackParticipation.scala b/src/main/scala/net/psforever/objects/serverobject/structures/participation/MajorFacilityHackParticipation.scala index 230aa222..bb319dcc 100644 --- a/src/main/scala/net/psforever/objects/serverobject/structures/participation/MajorFacilityHackParticipation.scala +++ b/src/main/scala/net/psforever/objects/serverobject/structures/participation/MajorFacilityHackParticipation.scala @@ -9,6 +9,7 @@ import net.psforever.types.{ChatMessageType, PlanetSideEmpire, Vector3} import net.psforever.util.Config import akka.pattern.ask import akka.util.Timeout +import net.psforever.actors.zone.BuildingActor import net.psforever.objects.Player import net.psforever.objects.avatar.scoring.Kill import net.psforever.objects.serverobject.hackable.Hackable @@ -26,6 +27,8 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci private var hotSpotLayersOverTime: Seq[List[HotSpotInfo]] = Seq[List[HotSpotInfo]]() + var lastEnemyCount: List[Player] = List.empty + def TryUpdate(): Unit = { val list = building.PlayersInSOI if (list.nonEmpty) { @@ -37,6 +40,14 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci updateHotSpotInfoOverTime() updateTime(now) } + val enemies = list.filter(p => p.Faction != building.Faction) ++ + building.Zone.blockMap.sector(building).corpseList + .filter(p => Vector3.DistanceSquared(building.Position.xy, p.Position.xy) < building.Definition.SOIRadius * building.Definition.SOIRadius) + //alert defenders (actually goes to all clients) of population change for base alerts + if (Math.abs(enemies.length - lastEnemyCount.length) >= 1) { + building.Actor ! BuildingActor.DensityLevelUpdate(building) + } + lastEnemyCount = enemies building.CaptureTerminal .map(_.HackedBy) .collect { diff --git a/src/main/scala/net/psforever/objects/zones/SphereOfInfluenceActor.scala b/src/main/scala/net/psforever/objects/zones/SphereOfInfluenceActor.scala index 0510d5c0..4c5a4171 100644 --- a/src/main/scala/net/psforever/objects/zones/SphereOfInfluenceActor.scala +++ b/src/main/scala/net/psforever/objects/zones/SphereOfInfluenceActor.scala @@ -55,9 +55,16 @@ class SphereOfInfluenceActor(zone: Zone) extends Actor { sois.foreach { case (facility, radius) => val facilityXY = facility.Position.xy - facility.PlayersInSOI = zone.blockMap.sector(facility) + val playersOnFoot = zone.blockMap.sector(facility) .livePlayerList .filter(p => Vector3.DistanceSquared(facilityXY, p.Position.xy) < radius) + + val vehicleOccupants = zone.blockMap.sector(facility) + .vehicleList + .filter(v => Vector3.DistanceSquared(facilityXY, v.Position.xy) < radius) + .flatMap(_.Seats.values.flatMap(_.occupants)) + + facility.PlayersInSOI = playersOnFoot ++ vehicleOccupants } populateTick.cancel() populateTick = context.system.scheduler.scheduleOnce(5 seconds, self, SOI.Populate()) diff --git a/src/main/scala/net/psforever/util/Config.scala b/src/main/scala/net/psforever/util/Config.scala index 13d7d5c9..ef131424 100644 --- a/src/main/scala/net/psforever/util/Config.scala +++ b/src/main/scala/net/psforever/util/Config.scala @@ -164,7 +164,8 @@ case class GameConfig( experience: Experience, maxBattleRank: Int, promotion: PromotionSystem, - facilityHackTime: FiniteDuration + facilityHackTime: FiniteDuration, + alert: DensityAlert ) case class InstantActionConfig( @@ -326,3 +327,9 @@ case class PromotionSystem( supportExperiencePointsModifier: Float, captureExperiencePointsModifier: Float ) + +case class DensityAlert( + yellow: Int, + orange: Int, + red: Int +) From 30ff8d738f4e020552bd50af4e284342e91a5067 Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Tue, 22 Jul 2025 12:28:54 -0400 Subject: [PATCH 17/25] adjusted condition and weather --- src/main/resources/application.conf | 6 ++-- .../session/support/ZoningOperations.scala | 28 ++++++++++++++++++- .../MajorFacilityHackParticipation.scala | 16 +++++++++-- 3 files changed, 44 insertions(+), 6 deletions(-) diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf index 6d56004c..27b1e4a5 100644 --- a/src/main/resources/application.conf +++ b/src/main/resources/application.conf @@ -514,11 +514,11 @@ game { # will be dispatched to all players. Players of the owning faction will receive a chat warning (if in # the same zone) and the map will flash the alert level over the facility until it changes # Wiki says 25-30 - yellow = 1 + yellow = 20 # Wiki says 30-60 - orange = 2 + orange = 30 # Wiki says 60+ - red = 3 + red = 60 } } 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 25eaefdb..95c344ea 100644 --- a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala @@ -20,7 +20,7 @@ import net.psforever.objects.serverobject.turret.auto.AutomatedTurret import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource} import net.psforever.objects.vital.{InGameHistory, IncarnationActivity, ReconstructionActivity, SpawningActivity} import net.psforever.objects.zones.blockmap.BlockMapEntity -import net.psforever.packet.game.{CampaignStatistic, ChangeFireStateMessage_Start, HackState7, MailMessage, ObjectDetectedMessage, SessionStatistic, TriggeredSound} +import net.psforever.packet.game.{CampaignStatistic, ChangeFireStateMessage_Start, HackState7, MailMessage, ObjectDetectedMessage, SessionStatistic, TriggeredSound, WeatherMessage, CloudInfo, StormInfo} import net.psforever.services.chat.DefaultChannel import scala.concurrent.duration._ @@ -2531,6 +2531,19 @@ class ZoningOperations( sessionLogic.general.toggleTeleportSystem(obj, TelepadLike.AppraiseTeleportationSystem(obj, continent)) } } + //make weather happen + sendResponse(WeatherMessage(List(),List( + StormInfo(Vector3(0.1f, 0.15f, 0.0f), 240, 217), + StormInfo(Vector3(0.5f, 0.11f, 0.0f), 240, 215), + StormInfo(Vector3(0.15f, 0.4f, 0.0f), 249, 215), + StormInfo(Vector3(0.15f, 0.87f, 0.0f), 240, 215), + StormInfo(Vector3(0.3f, 0.65f, 0.0f), 240, 215), + StormInfo(Vector3(0.5f, 0.475f, 0.0f), 245, 215), + StormInfo(Vector3(0.725f, 0.38f, 0.0f), 243, 215), + StormInfo(Vector3(0.9f, 0.57f, 0.0f), 244, 215), + StormInfo(Vector3(0.9f, 0.9f, 0.0f), 243, 215), + StormInfo(Vector3(0.1f, 0.2f, 0.0f), 241, 215), + StormInfo(Vector3(0.95f, 0.2f, 0.0f), 241, 215)))) //begin looking for conditions to set the avatar context.system.scheduler.scheduleOnce(delay = 250 millisecond, context.self, SessionActor.SetCurrentAvatar(player, 200)) } @@ -2641,6 +2654,19 @@ class ZoningOperations( log.debug(s"AvatarRejoin: ${player.Name} - $guid -> $data") } setupAvatarFunc = AvatarCreate + //make weather happen + sendResponse(WeatherMessage(List(),List( + StormInfo(Vector3(0.1f, 0.15f, 0.0f), 240, 217), + StormInfo(Vector3(0.5f, 0.11f, 0.0f), 240, 215), + StormInfo(Vector3(0.15f, 0.4f, 0.0f), 249, 215), + StormInfo(Vector3(0.15f, 0.87f, 0.0f), 240, 215), + StormInfo(Vector3(0.3f, 0.65f, 0.0f), 240, 215), + StormInfo(Vector3(0.5f, 0.475f, 0.0f), 245, 215), + StormInfo(Vector3(0.725f, 0.38f, 0.0f), 243, 215), + StormInfo(Vector3(0.9f, 0.57f, 0.0f), 244, 215), + StormInfo(Vector3(0.9f, 0.9f, 0.0f), 243, 215), + StormInfo(Vector3(0.1f, 0.2f, 0.0f), 241, 215), + StormInfo(Vector3(0.95f, 0.2f, 0.0f), 241, 215)))) //begin looking for conditions to set the avatar context.system.scheduler.scheduleOnce(delay = 750 millisecond, context.self, SessionActor.SetCurrentAvatar(player, 200)) } diff --git a/src/main/scala/net/psforever/objects/serverobject/structures/participation/MajorFacilityHackParticipation.scala b/src/main/scala/net/psforever/objects/serverobject/structures/participation/MajorFacilityHackParticipation.scala index bb319dcc..fdd519ec 100644 --- a/src/main/scala/net/psforever/objects/serverobject/structures/participation/MajorFacilityHackParticipation.scala +++ b/src/main/scala/net/psforever/objects/serverobject/structures/participation/MajorFacilityHackParticipation.scala @@ -28,6 +28,7 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci private var hotSpotLayersOverTime: Seq[List[HotSpotInfo]] = Seq[List[HotSpotInfo]]() var lastEnemyCount: List[Player] = List.empty + var alertTimeMillis: Long = 0L def TryUpdate(): Unit = { val list = building.PlayersInSOI @@ -44,10 +45,21 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci building.Zone.blockMap.sector(building).corpseList .filter(p => Vector3.DistanceSquared(building.Position.xy, p.Position.xy) < building.Definition.SOIRadius * building.Definition.SOIRadius) //alert defenders (actually goes to all clients) of population change for base alerts - if (Math.abs(enemies.length - lastEnemyCount.length) >= 1) { + //straight away if higher alert, delay if pop decreases enough to lower alert + if ((enemies.length >= Config.app.game.alert.yellow && lastEnemyCount.length < Config.app.game.alert.yellow) || + (enemies.length >= Config.app.game.alert.orange && lastEnemyCount.length < Config.app.game.alert.orange) || + (enemies.length >= Config.app.game.alert.red && lastEnemyCount.length < Config.app.game.alert.red) || + (enemies.length < Config.app.game.alert.yellow && lastEnemyCount.length >= Config.app.game.alert.yellow && + now - alertTimeMillis > 30000L && Math.abs(enemies.length - lastEnemyCount.length) >= 3) || + (enemies.length < Config.app.game.alert.orange && lastEnemyCount.length >= Config.app.game.alert.orange && + now - alertTimeMillis > 30000L && Math.abs(enemies.length - lastEnemyCount.length) >= 3) || + (enemies.length < Config.app.game.alert.red && lastEnemyCount.length >= Config.app.game.alert.red && + now - alertTimeMillis > 30000L && Math.abs(enemies.length - lastEnemyCount.length) >= 3)) + { building.Actor ! BuildingActor.DensityLevelUpdate(building) + alertTimeMillis = now + lastEnemyCount = enemies } - lastEnemyCount = enemies building.CaptureTerminal .map(_.HackedBy) .collect { From eb5b869917817d671f4bb0bd41516cfdb270671a Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Thu, 24 Jul 2025 13:18:01 -0400 Subject: [PATCH 18/25] time change why not --- .../net/psforever/actors/session/support/ZoningOperations.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 95c344ea..9cdc234e 100644 --- a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala @@ -301,7 +301,7 @@ class ZoningOperations( continent.VehicleEvents ! Service.Join(continentId) continent.VehicleEvents ! Service.Join(factionChannel) if (sessionLogic.connectionState != 100) configZone(continent) - sendResponse(TimeOfDayMessage(1191182336)) + sendResponse(TimeOfDayMessage(1135214592)) //custom sendResponse(ReplicationStreamMessage(5, Some(6), Vector.empty)) //clear squad list sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(0), 112, 0)) // disable festive backpacks From ab1cb9dc0a9d6cda180b34422c01fab4cc2afc0c Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Mon, 28 Jul 2025 21:26:39 -0400 Subject: [PATCH 19/25] misc animations and turret range --- .../actors/session/csr/GeneralLogic.scala | 5 +++++ .../actors/session/normal/GeneralLogic.scala | 5 +++++ .../session/normal/VehicleHandlerLogic.scala | 2 +- .../spectator/VehicleHandlerLogic.scala | 2 +- .../session/support/ZoningOperations.scala | 12 ++++++++--- .../damage/DamageableEntity.scala | 12 +++++++---- .../turret/auto/AutomatedTurretBehavior.scala | 21 +++++++++++++------ .../game/GenericObjectActionMessage.scala | 1 + .../avatar/support/CorpseRemovalActor.scala | 2 +- 9 files changed, 46 insertions(+), 16 deletions(-) diff --git a/src/main/scala/net/psforever/actors/session/csr/GeneralLogic.scala b/src/main/scala/net/psforever/actors/session/csr/GeneralLogic.scala index 68f59b43..d9c76b6e 100644 --- a/src/main/scala/net/psforever/actors/session/csr/GeneralLogic.scala +++ b/src/main/scala/net/psforever/actors/session/csr/GeneralLogic.scala @@ -33,6 +33,7 @@ import net.psforever.packet.PlanetSideGamePacket import net.psforever.packet.game.{ActionCancelMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, BugReportMessage, ChangeFireModeMessage, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestMessage, ChatMsg, CollisionIs, ConnectToWorldRequestMessage, CreateShortcutMessage, DeadState, DeployObjectMessage, DisplayedAwardMessage, DropItemMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FriendsRequest, GenericAction, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, InvalidTerrainMessage, LootItemMessage, MoveItemMessage, ObjectDetectedMessage, ObjectHeldMessage, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, RequestDestroyMessage, TargetingImplantRequest, TerrainCondition, TradeMessage, UnuseItemMessage, UseItemMessage, VoiceHostInfo, VoiceHostRequest, ZipLineMessage} import net.psforever.services.RemoverActor import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} +import net.psforever.services.local.{LocalAction, LocalServiceMessage} import net.psforever.types.{CapacitorStateType, ChatMessageType, Cosmetic, ExoSuitType, PlanetSideEmpire, PlanetSideGUID, Vector3} import scala.util.Success @@ -181,7 +182,11 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex def handleEmote(pkt: EmoteMsg): Unit = { val EmoteMsg(avatarGuid, emote) = pkt + val pZone = player.Zone sendResponse(EmoteMsg(avatarGuid, emote)) + pZone.blockMap.sector(player).livePlayerList.collect { case t if t.GUID != player.GUID => + pZone.LocalEvents ! LocalServiceMessage(t.Name, LocalAction.SendResponse(EmoteMsg(avatarGuid, emote))) + } } def handleDropItem(pkt: DropItemMessage): Unit = { 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 60185fac..97a7a5f9 100644 --- a/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala @@ -40,6 +40,7 @@ import net.psforever.packet.PlanetSideGamePacket import net.psforever.packet.game.{ActionCancelMessage, ActionResultMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, BugReportMessage, ChangeFireModeMessage, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestAction, CharacterRequestMessage, ChatMsg, CollisionIs, ConnectToWorldRequestMessage, CreateShortcutMessage, DeadState, DeployObjectMessage, DisplayedAwardMessage, DropItemMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FriendsRequest, GenericAction, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, InvalidTerrainMessage, LootItemMessage, MoveItemMessage, ObjectDetectedMessage, ObjectHeldMessage, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, RequestDestroyMessage, TargetingImplantRequest, TerrainCondition, TradeMessage, UnuseItemMessage, UseItemMessage, VoiceHostInfo, VoiceHostRequest, ZipLineMessage} import net.psforever.services.account.{AccountPersistenceService, RetrieveAccountData} import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} +import net.psforever.services.local.{LocalAction, LocalServiceMessage} import net.psforever.services.local.support.CaptureFlagManager import net.psforever.types.{CapacitorStateType, ChatMessageType, Cosmetic, ExoSuitType, ImplantType, PlanetSideEmpire, PlanetSideGUID, Vector3} import net.psforever.util.Config @@ -200,7 +201,11 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex def handleEmote(pkt: EmoteMsg): Unit = { val EmoteMsg(avatarGuid, emote) = pkt + val pZone = player.Zone sendResponse(EmoteMsg(avatarGuid, emote)) + pZone.blockMap.sector(player).livePlayerList.collect { case t if t.GUID != player.GUID => + pZone.LocalEvents ! LocalServiceMessage(t.Name, LocalAction.SendResponse(EmoteMsg(avatarGuid, emote))) + } } def handleDropItem(pkt: DropItemMessage): Unit = { diff --git a/src/main/scala/net/psforever/actors/session/normal/VehicleHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/normal/VehicleHandlerLogic.scala index 64a7a67a..f6102ded 100644 --- a/src/main/scala/net/psforever/actors/session/normal/VehicleHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/VehicleHandlerLogic.scala @@ -231,7 +231,7 @@ class VehicleHandlerLogic(val ops: SessionVehicleHandlers, implicit val context: sendResponse(ObjectCreateDetailedMessage(itemType, itemGuid, ObjectCreateMessageParent(vehicleGuid, slot), itemData)) case VehicleResponse.UnloadVehicle(_, vehicleGuid) => - sendResponse(ObjectDeleteMessage(vehicleGuid, unk1=0)) + sendResponse(ObjectDeleteMessage(vehicleGuid, unk1=1)) if (sessionLogic.zoning.spawn.prevSpawnPoint.map(_.Owner).exists { case ams: Vehicle => ams.GUID == vehicleGuid && diff --git a/src/main/scala/net/psforever/actors/session/spectator/VehicleHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/spectator/VehicleHandlerLogic.scala index ac930161..d68308a6 100644 --- a/src/main/scala/net/psforever/actors/session/spectator/VehicleHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/spectator/VehicleHandlerLogic.scala @@ -197,7 +197,7 @@ class VehicleHandlerLogic(val ops: SessionVehicleHandlers, implicit val context: sendResponse(PlanetsideAttributeMessage(vehicleGuid, seatGroup, permission)) case VehicleResponse.UnloadVehicle(_, vehicleGuid) => - sendResponse(ObjectDeleteMessage(vehicleGuid, unk1=0)) + sendResponse(ObjectDeleteMessage(vehicleGuid, unk1=1)) case VehicleResponse.UnstowEquipment(itemGuid) if isNotSameTarget => //TODO prefer ObjectDetachMessage, but how to force ammo pools to update properly? 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 9cdc234e..4bb807c9 100644 --- a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala @@ -20,7 +20,8 @@ import net.psforever.objects.serverobject.turret.auto.AutomatedTurret import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource} import net.psforever.objects.vital.{InGameHistory, IncarnationActivity, ReconstructionActivity, SpawningActivity} import net.psforever.objects.zones.blockmap.BlockMapEntity -import net.psforever.packet.game.{CampaignStatistic, ChangeFireStateMessage_Start, HackState7, MailMessage, ObjectDetectedMessage, SessionStatistic, TriggeredSound, WeatherMessage, CloudInfo, StormInfo} +import net.psforever.packet.game.GenericAction.FirstPersonViewWithEffect +import net.psforever.packet.game.{CampaignStatistic, ChangeFireStateMessage_Start, CloudInfo, GenericActionMessage, GenericObjectActionEnum, HackState7, MailMessage, ObjectDetectedMessage, SessionStatistic, StormInfo, TriggeredSound, WeatherMessage} import net.psforever.services.chat.DefaultChannel import scala.concurrent.duration._ @@ -3000,10 +3001,10 @@ class ZoningOperations( case _ if player.HasGUID => // player is deconstructing self or instant action val player_guid = player.GUID - sendResponse(ObjectDeleteMessage(player_guid, 4)) + sendResponse(ObjectDeleteMessage(player_guid, unk1=1)) continent.AvatarEvents ! AvatarServiceMessage( continent.id, - AvatarAction.ObjectDelete(player_guid, player_guid, 4) + AvatarAction.ObjectDelete(player_guid, player_guid, unk=1) ) InGameHistory.SpawnReconstructionActivity(player, toZoneNumber, betterSpawnPoint) LoadZoneAsPlayerUsing(player, pos, ori, toSide, zoneId) @@ -3803,6 +3804,11 @@ class ZoningOperations( player.death_by = 1 } GoToDeploymentMap() + val pZone = player.Zone + sendResponse(GenericActionMessage(FirstPersonViewWithEffect)) + pZone.blockMap.sector(player).livePlayerList.collect { case t if t.GUID != player.GUID => + pZone.LocalEvents ! LocalServiceMessage(t.Name, LocalAction.SendGenericObjectActionMessage(t.GUID, player.GUID, GenericObjectActionEnum.PlayerDeconstructs)) + } } def stopDeconstructing(): Unit = { diff --git a/src/main/scala/net/psforever/objects/serverobject/damage/DamageableEntity.scala b/src/main/scala/net/psforever/objects/serverobject/damage/DamageableEntity.scala index b3d74ac1..e594de2d 100644 --- a/src/main/scala/net/psforever/objects/serverobject/damage/DamageableEntity.scala +++ b/src/main/scala/net/psforever/objects/serverobject/damage/DamageableEntity.scala @@ -2,6 +2,7 @@ package net.psforever.objects.serverobject.damage import net.psforever.objects.equipment.JammableUnit +import net.psforever.objects.serverobject.tube.SpawnTube import net.psforever.objects.vital.interaction.DamageResult import net.psforever.objects.vital.resolution.ResolutionCalculations import net.psforever.objects.zones.Zone @@ -199,9 +200,12 @@ object DamageableEntity { val tguid = target.GUID val attribution = attributionTo(cause, target.Zone) zone.AvatarEvents ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(tguid, 0, target.Health)) - zone.AvatarEvents ! AvatarServiceMessage( - zoneId, - AvatarAction.Destroy(tguid, attribution, Service.defaultPlayerGUID, target.Position) - ) + if (target.isInstanceOf[SpawnTube]) {}//do nothing to prevent issue #1057 + else { + zone.AvatarEvents ! AvatarServiceMessage( + zoneId, + AvatarAction.Destroy(tguid, attribution, Service.defaultPlayerGUID, target.Position) + ) + } } } diff --git a/src/main/scala/net/psforever/objects/serverobject/turret/auto/AutomatedTurretBehavior.scala b/src/main/scala/net/psforever/objects/serverobject/turret/auto/AutomatedTurretBehavior.scala index fad4c5c3..cd116dfd 100644 --- a/src/main/scala/net/psforever/objects/serverobject/turret/auto/AutomatedTurretBehavior.scala +++ b/src/main/scala/net/psforever/objects/serverobject/turret/auto/AutomatedTurretBehavior.scala @@ -205,7 +205,8 @@ trait AutomatedTurretBehavior { val now = System.currentTimeMillis() if ( currentTargetToken.isEmpty && - target.Faction != AutomatedTurretObject.Faction + target.Faction != AutomatedTurretObject.Faction && + now >= currentTargetLastShotTime ) { currentTargetLastShotTime = now currentTargetLocation = Some(target.Position) @@ -215,11 +216,19 @@ trait AutomatedTurretBehavior { true } else if ( currentTargetToken.contains(SourceUniqueness(target)) && - now - currentTargetLastShotTime < autoStats.map(_.cooldowns.missedShot).getOrElse(0L)) { - currentTargetLastShotTime = now - currentTargetLocation = Some(target.Position) - cancelSelfReportedAutoFire() - true + now - currentTargetLastShotTime < autoStats.map(_.cooldowns.missedShot).getOrElse(0L) + ) { + val escapeRange = autoStats.map(_.ranges.escape).getOrElse(400f) + val distSq = Vector3.DistanceSquared(target.Position, AutomatedTurretObject.Position) + val escapeSq = escapeRange * escapeRange + if (distSq <= escapeSq) { + currentTargetLastShotTime = now + currentTargetLocation = Some(target.Position) + cancelSelfReportedAutoFire() + true + } else { + false + } } else { false } diff --git a/src/main/scala/net/psforever/packet/game/GenericObjectActionMessage.scala b/src/main/scala/net/psforever/packet/game/GenericObjectActionMessage.scala index 916ee031..aa71a534 100644 --- a/src/main/scala/net/psforever/packet/game/GenericObjectActionMessage.scala +++ b/src/main/scala/net/psforever/packet/game/GenericObjectActionMessage.scala @@ -91,4 +91,5 @@ object GenericObjectActionEnum extends Enumeration { * Target: CaptureTerminal */ val FlagSpawned = Value(14) + val PlayerDeconstructs = Value(6) } diff --git a/src/main/scala/net/psforever/services/avatar/support/CorpseRemovalActor.scala b/src/main/scala/net/psforever/services/avatar/support/CorpseRemovalActor.scala index 062c7f15..974f0b68 100644 --- a/src/main/scala/net/psforever/services/avatar/support/CorpseRemovalActor.scala +++ b/src/main/scala/net/psforever/services/avatar/support/CorpseRemovalActor.scala @@ -25,7 +25,7 @@ class CorpseRemovalActor extends RemoverActor() { entry.zone.Population ! Zone.Corpse.Remove(entry.obj.asInstanceOf[Player]) context.parent ! AvatarServiceMessage( entry.zone.id, - AvatarAction.ObjectDelete(Service.defaultPlayerGUID, entry.obj.GUID) + AvatarAction.ObjectDelete(Service.defaultPlayerGUID, entry.obj.GUID, unk=1) ) } From 08b02846b9583d0cae669bde82a37d1c3dbf5780 Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Wed, 30 Jul 2025 10:39:50 -0400 Subject: [PATCH 20/25] gm sees anims and crouch bfr --- .../actors/session/csr/GeneralLogic.scala | 3 +++ .../actors/session/normal/GeneralLogic.scala | 3 +++ .../session/support/ZoningOperations.scala | 3 +++ .../scala/net/psforever/objects/Vehicles.scala | 16 +++++++++++++++- .../packet/game/GenericObjectActionMessage.scala | 1 + 5 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/main/scala/net/psforever/actors/session/csr/GeneralLogic.scala b/src/main/scala/net/psforever/actors/session/csr/GeneralLogic.scala index d9c76b6e..2cd1782e 100644 --- a/src/main/scala/net/psforever/actors/session/csr/GeneralLogic.scala +++ b/src/main/scala/net/psforever/actors/session/csr/GeneralLogic.scala @@ -187,6 +187,9 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex pZone.blockMap.sector(player).livePlayerList.collect { case t if t.GUID != player.GUID => pZone.LocalEvents ! LocalServiceMessage(t.Name, LocalAction.SendResponse(EmoteMsg(avatarGuid, emote))) } + pZone.AllPlayers.collect { case t if t.GUID != player.GUID && !t.allowInteraction => + pZone.LocalEvents ! LocalServiceMessage(t.Name, LocalAction.SendResponse(EmoteMsg(avatarGuid, emote))) + } } def handleDropItem(pkt: DropItemMessage): Unit = { 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 97a7a5f9..608f06d0 100644 --- a/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala @@ -206,6 +206,9 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex pZone.blockMap.sector(player).livePlayerList.collect { case t if t.GUID != player.GUID => pZone.LocalEvents ! LocalServiceMessage(t.Name, LocalAction.SendResponse(EmoteMsg(avatarGuid, emote))) } + pZone.AllPlayers.collect { case t if t.GUID != player.GUID && !t.allowInteraction => + pZone.LocalEvents ! LocalServiceMessage(t.Name, LocalAction.SendResponse(EmoteMsg(avatarGuid, emote))) + } } def handleDropItem(pkt: DropItemMessage): Unit = { 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 4bb807c9..ae62987a 100644 --- a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala @@ -3809,6 +3809,9 @@ class ZoningOperations( pZone.blockMap.sector(player).livePlayerList.collect { case t if t.GUID != player.GUID => pZone.LocalEvents ! LocalServiceMessage(t.Name, LocalAction.SendGenericObjectActionMessage(t.GUID, player.GUID, GenericObjectActionEnum.PlayerDeconstructs)) } + pZone.AllPlayers.collect { case t if t.GUID != player.GUID && !t.allowInteraction => + pZone.LocalEvents ! LocalServiceMessage(t.Name, LocalAction.SendGenericObjectActionMessage(t.GUID, player.GUID, GenericObjectActionEnum.PlayerDeconstructs)) + } } def stopDeconstructing(): Unit = { diff --git a/src/main/scala/net/psforever/objects/Vehicles.scala b/src/main/scala/net/psforever/objects/Vehicles.scala index 259dc284..019f4d7f 100644 --- a/src/main/scala/net/psforever/objects/Vehicles.scala +++ b/src/main/scala/net/psforever/objects/Vehicles.scala @@ -10,7 +10,7 @@ import net.psforever.objects.serverobject.transfer.TransferContainer import net.psforever.objects.serverobject.structures.WarpGate import net.psforever.objects.vehicles._ import net.psforever.objects.zones.Zone -import net.psforever.packet.game.{ChatMsg, HackMessage, HackState, HackState1, HackState7, TriggeredSound} +import net.psforever.packet.game.{ChatMsg, FrameVehicleStateMessage, GenericObjectActionEnum, GenericObjectActionMessage, HackMessage, HackState, HackState1, HackState7, TriggeredSound, VehicleStateMessage} import net.psforever.types.{ChatMessageType, DriveState, PlanetSideEmpire, PlanetSideGUID, Vector3} import net.psforever.services.Service import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} @@ -274,6 +274,20 @@ object Vehicles { VehicleAction.KickPassenger(player.GUID, 4, unk2 = false, tGuid) ) } + // In case BFR is occupied and may or may not be crouched + if (GlobalDefinitions.isBattleFrameVehicle(target.Definition) && target.Seat(0).isDefined) { + zone.LocalEvents ! LocalServiceMessage( + zoneid, + LocalAction.SendGenericObjectActionMessage(PlanetSideGUID(-1), target.GUID, GenericObjectActionEnum.BFRShieldsDown)) + zone.LocalEvents ! LocalServiceMessage( + zoneid, + LocalAction.SendResponse( + FrameVehicleStateMessage(target.GUID, 0, target.Position, target.Orientation, Some(Vector3(0f, 0f, 0f)), unk2=false, 0, 0, is_crouched=true, is_airborne=false, ascending_flight=false, 10, 0, 0))) + zone.LocalEvents ! LocalServiceMessage( + zoneid, + LocalAction.SendResponse( + VehicleStateMessage(target.GUID, 0, target.Position, target.Orientation, Some(Vector3(0f, 0f, 0f)), None, 0, 0, 15, is_decelerating=false, is_cloaked=false))) + } }) // If the vehicle can fly and is flying: deconstruct it; and well played to whomever managed to hack a plane in mid air if (target.Definition.CanFly && target.isFlying) { diff --git a/src/main/scala/net/psforever/packet/game/GenericObjectActionMessage.scala b/src/main/scala/net/psforever/packet/game/GenericObjectActionMessage.scala index aa71a534..8d4b7e4c 100644 --- a/src/main/scala/net/psforever/packet/game/GenericObjectActionMessage.scala +++ b/src/main/scala/net/psforever/packet/game/GenericObjectActionMessage.scala @@ -92,4 +92,5 @@ object GenericObjectActionEnum extends Enumeration { */ val FlagSpawned = Value(14) val PlayerDeconstructs = Value(6) + val BFRShieldsDown = Value(45) } From c416ba11df2e897281b40390133542a23280b411 Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Thu, 31 Jul 2025 01:12:16 -0400 Subject: [PATCH 21/25] allowing scaled back bfr accessibility (#1280) --- .../resources/overrides/game_objects0.adb.lst | 30 +++++++++++++---- .../WeaponAndProjectileOperations.scala | 2 +- .../BattleFrameFlightConverter.scala | 2 +- .../BattleFrameRoboticsConverter.scala | 2 +- .../global/GlobalDefinitionsProjectile.scala | 4 +-- .../global/GlobalDefinitionsVehicle.scala | 32 +++++++++---------- .../objects/vehicles/VehicleSubsystem.scala | 2 +- .../objects/vehicles/control/BfrControl.scala | 15 +++++---- .../vehicles/control/BfrFlightControl.scala | 8 ++--- .../resolution/ResolutionCalculations.scala | 2 +- 10 files changed, 59 insertions(+), 40 deletions(-) diff --git a/server/src/main/resources/overrides/game_objects0.adb.lst b/server/src/main/resources/overrides/game_objects0.adb.lst index 1ea38dca..b62b955a 100644 --- a/server/src/main/resources/overrides/game_objects0.adb.lst +++ b/server/src/main/resources/overrides/game_objects0.adb.lst @@ -96,12 +96,30 @@ add_property suppressor holstertime 600 add_property trek equiptime 500 add_property trek holstertime 500 add_property vulture requirement_award0 false -add_property aphelion allowed false +add_property aphelion allowed true add_property aphelion_flight allowed false -add_property aphelion_gunner allowed false -add_property colossus allowed false +add_property aphelion_gunner allowed true +add_property aphelion_armor_siphon allowed false +add_property aphelion_armor_siphon_left allowed false +add_property aphelion_armor_siphon_right allowed false +add_property aphelion_ntu_siphon allowed false +add_property aphelion_ntu_siphon_left allowed false +add_property aphelion_ntu_siphon_right allowed false +add_property colossus allowed true add_property colossus_flight allowed false -add_property colossus_gunner allowed false -add_property peregrine allowed false +add_property colossus_gunner allowed true +add_property colossus_armor_siphon allowed false +add_property colossus_armor_siphon_left allowed false +add_property colossus_armor_siphon_right allowed false +add_property colossus_ntu_siphon allowed false +add_property colossus_ntu_siphon_left allowed false +add_property colossus_ntu_siphon_right allowed false +add_property peregrine allowed true add_property peregrine_flight allowed false -add_property peregrine_gunner allowed false +add_property peregrine_gunner allowed true +add_property peregrine_armor_siphon allowed false +add_property peregrine_armor_siphon_left allowed false +add_property peregrine_armor_siphon_right allowed false +add_property peregrine_ntu_siphon allowed false +add_property peregrine_ntu_siphon_left allowed false +add_property peregrine_ntu_siphon_right allowed false diff --git a/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala b/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala index 8e18811e..047983bd 100644 --- a/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala @@ -870,7 +870,7 @@ class WeaponAndProjectileOperations( "BattleframeLeftArm" } else { "BattleframeRightArm" - }).get.Enabled + }).exists(_.Enabled) if (!mountIsEnabled) { //can't stop the local discharge, but it will not actually shoot anything; assert the magazine sendResponse(QuantityUpdateMessage(tool.AmmoSlot.Box.GUID, tool.Magazine)) diff --git a/src/main/scala/net/psforever/objects/definition/converter/BattleFrameFlightConverter.scala b/src/main/scala/net/psforever/objects/definition/converter/BattleFrameFlightConverter.scala index 7ed20718..9efc677c 100644 --- a/src/main/scala/net/psforever/objects/definition/converter/BattleFrameFlightConverter.scala +++ b/src/main/scala/net/psforever/objects/definition/converter/BattleFrameFlightConverter.scala @@ -110,6 +110,6 @@ class BattleFrameFlightConverter extends ObjectCreateConverter[Vehicle]() { } def showBfrShield(obj: Vehicle): Boolean = { - obj.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator).get.Enabled && obj.Shields > 0 + obj.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator).exists(_.Enabled) && obj.Shields > 0 } } diff --git a/src/main/scala/net/psforever/objects/definition/converter/BattleFrameRoboticsConverter.scala b/src/main/scala/net/psforever/objects/definition/converter/BattleFrameRoboticsConverter.scala index 411ff6a7..ea12b812 100644 --- a/src/main/scala/net/psforever/objects/definition/converter/BattleFrameRoboticsConverter.scala +++ b/src/main/scala/net/psforever/objects/definition/converter/BattleFrameRoboticsConverter.scala @@ -110,6 +110,6 @@ class BattleFrameRoboticsConverter extends ObjectCreateConverter[Vehicle]() { } def showBfrShield(obj: Vehicle): Boolean = { - obj.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator).get.Enabled && obj.Shields > 0 + obj.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator).exists(_.Enabled) && obj.Shields > 0 } } diff --git a/src/main/scala/net/psforever/objects/global/GlobalDefinitionsProjectile.scala b/src/main/scala/net/psforever/objects/global/GlobalDefinitionsProjectile.scala index 4771f421..be9f4e58 100644 --- a/src/main/scala/net/psforever/objects/global/GlobalDefinitionsProjectile.scala +++ b/src/main/scala/net/psforever/objects/global/GlobalDefinitionsProjectile.scala @@ -2056,7 +2056,7 @@ object GlobalDefinitionsProjectile { aphelion_plasma_rocket_projectile.DamageAtEdge = .1f aphelion_plasma_rocket_projectile.DamageRadius = 3f aphelion_plasma_rocket_projectile.ProjectileDamageType = DamageType.Splash - aphelion_plasma_rocket_projectile.DamageProxy = 96 //aphelion_plama_cloud + //aphelion_plasma_rocket_projectile.DamageProxy = 96 //aphelion_plama_cloud aphelion_plasma_rocket_projectile.InitialVelocity = 75 aphelion_plasma_rocket_projectile.Lifespan = 5f ProjectileDefinition.CalculateDerivedFields(aphelion_plasma_rocket_projectile) @@ -2221,7 +2221,7 @@ object GlobalDefinitionsProjectile { peregrine_particle_cannon_projectile.DamageAtEdge = 0.1f peregrine_particle_cannon_projectile.DamageRadius = 3f peregrine_particle_cannon_projectile.ProjectileDamageType = DamageType.Splash - peregrine_particle_cannon_projectile.DamageProxy = 655 //peregrine_particle_cannon_radiation_cloud + //peregrine_particle_cannon_projectile.DamageProxy = 655 //peregrine_particle_cannon_radiation_cloud peregrine_particle_cannon_projectile.InitialVelocity = 500 peregrine_particle_cannon_projectile.Lifespan = .6f ProjectileDefinition.CalculateDerivedFields(peregrine_particle_cannon_projectile) diff --git a/src/main/scala/net/psforever/objects/global/GlobalDefinitionsVehicle.scala b/src/main/scala/net/psforever/objects/global/GlobalDefinitionsVehicle.scala index c2fee38f..50876dd4 100644 --- a/src/main/scala/net/psforever/objects/global/GlobalDefinitionsVehicle.scala +++ b/src/main/scala/net/psforever/objects/global/GlobalDefinitionsVehicle.scala @@ -46,7 +46,7 @@ object GlobalDefinitionsVehicle { restriction = MaxOnly } - val controlSubsystem = List(VehicleSubsystemEntry.Controls) + val controlSubsystem: List[VehicleSubsystemEntry] = List(VehicleSubsystemEntry.Controls) fury.Name = "fury" fury.MaxHealth = 650 @@ -1162,7 +1162,7 @@ object GlobalDefinitionsVehicle { bailable = true } - val flightSubsystems = List(VehicleSubsystemEntry.Controls, VehicleSubsystemEntry.Ejection) + val flightSubsystems: List[VehicleSubsystemEntry] = List(VehicleSubsystemEntry.Controls, VehicleSubsystemEntry.Ejection) val variantConverter = new VariantVehicleConverter mosquito.Name = "mosquito" @@ -1684,20 +1684,20 @@ object GlobalDefinitionsVehicle { bailable = true } val normalSeat = new SeatDefinition() - val bfrSubsystems = List( - VehicleSubsystemEntry.BattleframeMovementServos, - VehicleSubsystemEntry.BattleframeSensorArray, - VehicleSubsystemEntry.BattleframeShieldGenerator, - VehicleSubsystemEntry.BattleframeTrunk + val bfrSubsystems: List[VehicleSubsystemEntry] = List( +// VehicleSubsystemEntry.BattleframeMovementServos, +// VehicleSubsystemEntry.BattleframeSensorArray, + VehicleSubsystemEntry.BattleframeShieldGenerator//, +// VehicleSubsystemEntry.BattleframeTrunk ) - val bfrGunnerSubsystems = List( + val bfrGunnerSubsystems: List[VehicleSubsystemEntry] = List( VehicleSubsystemEntry.BattleframeLeftArm, - VehicleSubsystemEntry.BattleframeRightArm, - VehicleSubsystemEntry.BattleframeLeftWeapon, - VehicleSubsystemEntry.BattleframeRightWeapon, - VehicleSubsystemEntry.BattleframeGunnerWeapon + VehicleSubsystemEntry.BattleframeRightArm//, +// VehicleSubsystemEntry.BattleframeLeftWeapon, +// VehicleSubsystemEntry.BattleframeRightWeapon, +// VehicleSubsystemEntry.BattleframeGunnerWeapon ) ++ bfrSubsystems - val bfrFlightSubsystems = List( + val bfrFlightSubsystems: List[VehicleSubsystemEntry] = List( VehicleSubsystemEntry.BattleframeFlightLeftArm, VehicleSubsystemEntry.BattleframeFlightRightArm, VehicleSubsystemEntry.BattleframeFlightLeftWeapon, @@ -1735,7 +1735,7 @@ object GlobalDefinitionsVehicle { aphelion_gunner.AutoPilotSpeeds = (5, 1) aphelion_gunner.Packet = battleFrameConverter aphelion_gunner.DestroyedModel = None - aphelion_gunner.destructionDelay = Some(4000L) + //aphelion_gunner.destructionDelay = Some(4000L) aphelion_gunner.JackingDuration = Array(0, 62, 60, 30) aphelion_gunner.RadiationShielding = 0.5f aphelion_gunner.DamageUsing = DamageCalculations.AgainstBfr @@ -1787,7 +1787,7 @@ object GlobalDefinitionsVehicle { colossus_gunner.AutoPilotSpeeds = (5, 1) colossus_gunner.Packet = battleFrameConverter colossus_gunner.DestroyedModel = None - colossus_gunner.destructionDelay = Some(4000L) + //colossus_gunner.destructionDelay = Some(4000L) colossus_gunner.JackingDuration = Array(0, 62, 60, 30) colossus_gunner.RadiationShielding = 0.5f colossus_gunner.DamageUsing = DamageCalculations.AgainstBfr @@ -1839,7 +1839,7 @@ object GlobalDefinitionsVehicle { peregrine_gunner.AutoPilotSpeeds = (5, 1) peregrine_gunner.Packet = battleFrameConverter peregrine_gunner.DestroyedModel = None - peregrine_gunner.destructionDelay = Some(4000L) + //peregrine_gunner.destructionDelay = Some(4000L) peregrine_gunner.JackingDuration = Array(0, 62, 60, 30) peregrine_gunner.RadiationShielding = 0.5f peregrine_gunner.DamageUsing = DamageCalculations.AgainstBfr diff --git a/src/main/scala/net/psforever/objects/vehicles/VehicleSubsystem.scala b/src/main/scala/net/psforever/objects/vehicles/VehicleSubsystem.scala index 29033e8b..364ce543 100644 --- a/src/main/scala/net/psforever/objects/vehicles/VehicleSubsystem.scala +++ b/src/main/scala/net/psforever/objects/vehicles/VehicleSubsystem.scala @@ -18,7 +18,7 @@ sealed abstract class VehicleSubsystemConditionModifier( ) extends IntEnumEntry object VehicleSubsystemConditionModifier extends IntEnum[VehicleSubsystemConditionModifier] { - val values = findValues + val values: IndexedSeq[VehicleSubsystemConditionModifier] = findValues case object Off extends VehicleSubsystemConditionModifier(value = 1065353216, multiplier = 0f, addend = 0) diff --git a/src/main/scala/net/psforever/objects/vehicles/control/BfrControl.scala b/src/main/scala/net/psforever/objects/vehicles/control/BfrControl.scala index 69d60d4b..9dbc3436 100644 --- a/src/main/scala/net/psforever/objects/vehicles/control/BfrControl.scala +++ b/src/main/scala/net/psforever/objects/vehicles/control/BfrControl.scala @@ -134,7 +134,7 @@ class BfrControl(vehicle: Vehicle) item.asInstanceOf[Tool], dimorph.transform(Handiness.Generic).asInstanceOf[ToolDefinition] ) - case _ => ; //no dimorphic entry; place as-is + case _ => () //no dimorphic entry; place as-is } val guid0 = PlanetSideGUID(0) //if the weapon arm is disabled, enable it for later (makes life easy) @@ -173,7 +173,8 @@ class BfrControl(vehicle: Vehicle) override def dismountCleanup(seatBeingDismounted: Int, player: Player): Unit = { super.dismountCleanup(seatBeingDismounted, player) if (!vehicle.Seats.values.exists(_.isOccupied)) { - vehicle.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator) match { + vehicle + .Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator) match { case Some(subsys) => if (vehicle.Shields > 0) { vehicleSubsystemMessages( @@ -190,7 +191,7 @@ class BfrControl(vehicle: Vehicle) } ) } - case _ => ; + case _ => () } } } @@ -203,7 +204,7 @@ class BfrControl(vehicle: Vehicle) if !subsys.Enabled && vehicle.Shields > 0 && subsys.Enabled_=(state = true) => //if the shield is damaged, it does not turn on until the damaged is cleared vehicleSubsystemMessages(subsys.changedMessages(vehicle)) - case _ => ; + case _ => () } } } @@ -425,9 +426,9 @@ class BfrControl(vehicle: Vehicle) zone.id, VehicleAction.GenericObjectAction(doNotSendTo, useThisGuid, action) ) - case _ => ; + case _ => () } - case _ => ; + case _ => () } } } @@ -602,7 +603,7 @@ class BfrControl(vehicle: Vehicle) ) ) } - case _ => ; + case _ => () } } } diff --git a/src/main/scala/net/psforever/objects/vehicles/control/BfrFlightControl.scala b/src/main/scala/net/psforever/objects/vehicles/control/BfrFlightControl.scala index 144ecb7d..78d9e638 100644 --- a/src/main/scala/net/psforever/objects/vehicles/control/BfrFlightControl.scala +++ b/src/main/scala/net/psforever/objects/vehicles/control/BfrFlightControl.scala @@ -58,20 +58,20 @@ class BfrFlightControl(vehicle: Vehicle) case Some(drain) if localFlyingValue.isEmpty => //shields off disableShield() - vehicle.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator).get.Enabled = false + vehicle.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator).map(s => s.Enabled = false) vehicle.Shields -= drain showShieldCharge() case None if localFlyingValue.isEmpty => //shields off disableShield() - vehicle.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator).get.Enabled = false + vehicle.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator).map(s => s.Enabled = false) case Some(drain) => vehicle.Shields -= drain showShieldCharge() case _ => ; } } - if (vehicle.Subsystems(VehicleSubsystemEntry.BattleframeFlightPod).get.Jammed) { + if (vehicle.Subsystems(VehicleSubsystemEntry.BattleframeFlightPod).exists(_.Jammed)) { } @@ -79,7 +79,7 @@ class BfrFlightControl(vehicle: Vehicle) if (flying.nonEmpty) { flying = None vehicle.Flying = None - vehicle.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator).get.Enabled = true + vehicle.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator).map(s => s.Enabled = true) if (vehicle.Shields > 0) { enableShield() } diff --git a/src/main/scala/net/psforever/objects/vital/resolution/ResolutionCalculations.scala b/src/main/scala/net/psforever/objects/vital/resolution/ResolutionCalculations.scala index e6112726..cc688407 100644 --- a/src/main/scala/net/psforever/objects/vital/resolution/ResolutionCalculations.scala +++ b/src/main/scala/net/psforever/objects/vital/resolution/ResolutionCalculations.scala @@ -354,7 +354,7 @@ object ResolutionCalculations { { data.cause.source.DamageToBattleframeOnly || data.cause.source.DamageToVehicleOnly || - !obj.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator).get.Enabled || + !obj.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator).exists(_.Enabled) || obj.Shields == 0 } ) From b8a47016dac7406541285169cd528a1b8fad5e0d Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Thu, 31 Jul 2025 01:12:28 -0400 Subject: [PATCH 22/25] aded an early test to determine if player account database is active (#1281) --- .../net/psforever/actors/net/LoginActor.scala | 371 +++++++++++------- 1 file changed, 228 insertions(+), 143 deletions(-) diff --git a/src/main/scala/net/psforever/actors/net/LoginActor.scala b/src/main/scala/net/psforever/actors/net/LoginActor.scala index 4b9a8741..5eb8bb14 100644 --- a/src/main/scala/net/psforever/actors/net/LoginActor.scala +++ b/src/main/scala/net/psforever/actors/net/LoginActor.scala @@ -24,7 +24,6 @@ import org.joda.time.LocalDateTime import scala.collection.mutable import scala.concurrent.Future import scala.concurrent.duration._ -import scala.util.matching.Regex import scala.util.{Failure, Success} object LoginActor { @@ -33,94 +32,31 @@ object LoginActor { private case object UpdateServerList extends Command final case class ReceptionistListing(listing: Receptionist.Listing) extends Command -} -class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], connectionId: String, sessionId: Long) - extends Actor - with MDCContextAware { - - import scala.concurrent.ExecutionContext.Implicits.global - - val usernameRegex: Regex = """[A-Za-z0-9]{3,}""".r - - var leftRef: ActorRef = Default.Actor - var rightRef: ActorRef = Default.Actor - var accountIntermediary: ActorRef = Default.Actor - var sockets: typed.ActorRef[SocketPane.Command] = Default.typed.Actor - - var updateServerListTask: Cancellable = Default.Cancellable - - var ipAddress: String = "" - var hostName: String = "" - var canonicalHostName: String = "" - var port: Int = 0 - - val serverName: String = Config.app.world.serverName - val gameTestServerAddress = new InetSocketAddress(InetAddress.getByName(Config.app.public), Config.app.world.port) - - private val bcryptRounds = 12 - - ServiceManager.serviceManager ! Lookup("accountIntermediary") - ServiceManager.receptionist ! Receptionist.Find(SocketPane.SocketPaneKey, context.self) - - override def postStop(): Unit = { - if (updateServerListTask != null) - updateServerListTask.cancel() - } - - def receive: Receive = { - case ServiceManager.LookupResult("accountIntermediary", endpoint) => - accountIntermediary = endpoint - - case SocketPane.SocketPaneKey.Listing(listings) => - sockets = listings.head - - case ReceiveIPAddress(address) => - ipAddress = address.Address - hostName = address.HostName - canonicalHostName = address.CanonicalHostName - port = address.Port - - case LoginActor.UpdateServerList => - updateServerList() - - case packet: PlanetSideGamePacket => - handleGamePkt(packet) - - case SocketPane.NextPort(_, _, portNum) => - val address = gameTestServerAddress.getAddress.getHostAddress - log.info(s"Connecting to ${address.toLowerCase}: $portNum ...") - val response = ConnectToWorldMessage(serverName, address, portNum) - middlewareActor ! MiddlewareActor.Send(response) - middlewareActor ! MiddlewareActor.Close() - - case default => - failWithError(s"Invalid packet class received: $default") - } - - def handleGamePkt(pkt: PlanetSideGamePacket): Unit = - pkt match { - case LoginMessage(majorVersion, minorVersion, buildDate, username, password, token, revision) => - // TODO: prevent multiple LoginMessages from being processed in a row!! We need a state machine - val clientVersion = s"Client Version: $majorVersion.$minorVersion.$revision, $buildDate" - if (token.isDefined) - log.debug(s"New login UN:$username Token:${token.get}. $clientVersion") - else { - log.debug(s"New login UN:$username. $clientVersion") - } - requestAccountLogin(username, password, token) - - case ConnectToWorldRequestMessage(name, _, _, _, _, _, _, _) => - log.info(s"Request to connect to world '$name' ...") - sockets ! SocketPane.GetNextPort("world", context.self) - - case _ => - log.warning(s"Unhandled GamePacket $pkt") + /** + * What does a token do? + * No one knows. + * @return a 32-bit ascii string + */ + private def generateToken(): String = { + val r = new scala.util.Random + val sb = new mutable.StringBuilder + for (_ <- 1 to 31) { + sb.append(r.nextPrintableChar()) } + sb.toString + } - // generates a password from username and password combination - // mimics the process the launcher follows and hashes the password salted by the username - def generateNewPassword(username: String, password: String): String = { + /** + * Generates a new password from username and password combination, + * hashing the initial password when salted by the username, + * mimicking the process the launcher follows. + * @param username part of the original details + * @param password part of the original details + * @param rounds number of times cryptographic mutation occurs + * @return new password + */ + private def generateNewPassword(username: String, password: String, rounds: Int): String = { // salt password hash with username (like the launcher does) (username + password) val saltedPassword = username.concat(password) // https://stackoverflow.com/a/46332228 @@ -129,20 +65,178 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne .digest(saltedPassword.getBytes("UTF-8")) .map("%02x".format(_)).mkString // bcrypt hash for DB storage - val bcryptedPassword = hashedPassword.bcryptBounded(bcryptRounds) + val bcryptedPassword = hashedPassword.bcryptBounded(rounds) bcryptedPassword } - def requestAccountLogin(username: String, passwordOpt: Option[String], tokenOpt: Option[String]): Unit = { - tokenOpt match { - case Some(token) => accountLoginWithToken(token) - case None => accountLogin(username, passwordOpt.getOrElse("")) + /** + * Remove flavor from the server name that should not show up in the log. + * @param name original name + * @return sanitized name + */ + private def sanitizeServerName(name: String): String = { + //remove color codes from the server name - look for '\\#' followed by six characters or numbers + name.replaceAll("\\\\#[\\da-fA-F]{6}","") + } +} + +class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], connectionId: String, sessionId: Long) + extends Actor + with MDCContextAware { + import scala.concurrent.ExecutionContext.Implicits.global + + //private val usernameRegex: Regex = """[A-Za-z\d]{3,}""".r might be useful one day + private var accountIntermediary: ActorRef = Default.Actor + private var sockets: typed.ActorRef[SocketPane.Command] = Default.typed.Actor + + private var updateServerListTask: Cancellable = Default.Cancellable + + private var ipAddress: String = "" + private var hostName: String = "" + private var canonicalHostName: String = "" + private var port: Int = 0 + + private val serverName: String = Config.app.world.serverName + private val gameTestServerAddress = new InetSocketAddress(InetAddress.getByName(Config.app.public), Config.app.world.port) + + private val bcryptRounds = 12 + + override def preStart(): Unit = { + super.preStart() + ServiceManager.serviceManager ! Lookup("accountIntermediary") + ServiceManager.receptionist ! Receptionist.Find(SocketPane.SocketPaneKey, context.self) + } + + override def postStop(): Unit = { + if (updateServerListTask != null) + updateServerListTask.cancel() + } + + def receive: Receive = beforeLoginBehavior + + private def persistentSetupMixinBehavior: Receive = { + case ServiceManager.LookupResult("accountIntermediary", endpoint) => + accountIntermediary = endpoint + + case SocketPane.SocketPaneKey.Listing(listings) => + sockets = listings.head + } + + private def idlingBehavior: Receive = persistentSetupMixinBehavior.orElse { + case _ => () + } + + private def beforeLoginBehavior: Receive = persistentSetupMixinBehavior.orElse { + case ReceiveIPAddress(address) => + ipAddress = address.Address + hostName = address.HostName + canonicalHostName = address.CanonicalHostName + port = address.Port + context.become(idlingBehavior) + runLoginTest() + + case _ => () + } + + private def accountLoginBehavior: Receive = persistentSetupMixinBehavior.orElse { + case packet: PlanetSideGamePacket => + handleGamePktDuringLogin(packet) + + case default => + failWithError(s"Invalid packet class received: $default") + } + + private def displayingServerListBehavior: Receive = persistentSetupMixinBehavior.orElse { + case packet: PlanetSideGamePacket => + handleGamePktDuringWorldSelect(packet) + + case LoginActor.UpdateServerList => + updateServerList() + + case SocketPane.NextPort(_, _, portNum) => + val address = gameTestServerAddress.getAddress.getHostAddress + log.info(s"Connecting to ${address.toLowerCase}: $portNum ...") + val response = ConnectToWorldMessage(serverName, address, portNum) + context.become(idlingBehavior) + middlewareActor ! MiddlewareActor.Send(response) + middlewareActor ! MiddlewareActor.Close() + + case default => + failWithError(s"Invalid packet class received: $default") + } + + private def waitingForServerTransferBehavior: Receive = persistentSetupMixinBehavior.orElse { + case SocketPane.NextPort(_, _, portNum) => + val address = gameTestServerAddress.getAddress.getHostAddress + log.info(s"Connecting to ${address.toLowerCase}: $portNum ...") + val response = ConnectToWorldMessage(serverName, address, portNum) + context.become(idlingBehavior) + middlewareActor ! MiddlewareActor.Send(response) + middlewareActor ! MiddlewareActor.Close() + + case _ => () + } + + private def handleGamePktDuringLogin(pkt: PlanetSideGamePacket): Unit = { + pkt match { + case LoginMessage(majorVersion, minorVersion, buildDate, username, _, Some(token), revision) => + val clientVersion = s"Client Version: $majorVersion.$minorVersion.$revision, $buildDate" + log.debug(s"New login UN:$username Token:$token. $clientVersion") + context.become(idlingBehavior) + accountLoginWithToken(token) + + case LoginMessage(majorVersion, minorVersion, buildDate, username, password, None, revision) => + val clientVersion = s"Client Version: $majorVersion.$minorVersion.$revision, $buildDate" + log.debug(s"New login UN:$username. $clientVersion") + context.become(idlingBehavior) + accountLogin(username, password.getOrElse("")) + + case _ => + log.warning(s"Unhandled GamePacket $pkt") } } - def accountLogin(username: String, password: String): Unit = { + private def handleGamePktDuringWorldSelect(pkt: PlanetSideGamePacket): Unit = { + pkt match { + case ConnectToWorldRequestMessage(name, _, _, _, _, _, _, _) => + val sanitizedName = LoginActor.sanitizeServerName(name) + log.info(s"Request to connect to world '$sanitizedName' ...") + context.become(waitingForServerTransferBehavior) + sockets ! SocketPane.GetNextPort("world", context.self) + + case _ => + log.warning(s"Unhandled GamePacket $pkt") + } + } + + private def runLoginTest(): Unit = { import ctx._ - val newToken = this.generateToken() + val result = for { + accountsExact <- ctx.run(query[persistence.Account].filter(_.username == lift("PSForever"))) + accountOption <- accountsExact.headOption match { + case Some(account) => + Future.successful(Some(account)) + case None => + Future.successful(None) + } + } yield accountOption + + result.onComplete { + case Success(Some(_)) => + context.become(accountLoginBehavior) // account found + case Success(None) => + middlewareActor ! MiddlewareActor.Send(DisconnectMessage("Character database not found; stopping ...")) + middlewareActor ! MiddlewareActor.Close() + case Failure(e) => + log.error(e.getMessage) + middlewareActor ! MiddlewareActor.Send(DisconnectMessage("Encountered login error; stopping ...")) + middlewareActor ! MiddlewareActor.Close() + } + } + + private def accountLogin(username: String, password: String): Unit = { + import ctx._ + val newToken = LoginActor.generateToken() val result = for { // backwards compatibility: prefer exact match first, then try lowercase accountsExact <- ctx.run(query[persistence.Account].filter(_.username == lift(username))) @@ -160,7 +254,7 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne case None => if (Config.app.login.createMissingAccounts) { // generate bcrypted passwords - val bcryptedPassword = generateNewPassword(username, password) + val bcryptedPassword = LoginActor.generateNewPassword(username, password, bcryptRounds) val passhash = password.bcryptBounded(bcryptRounds) // save bcrypted password hash to DB ctx.run( @@ -201,7 +295,7 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne if (account.password == "") { // generate bcrypted password // use username as provided by the user (db entry could be wrong), that is the way the launcher does it - val bcryptedPassword = generateNewPassword(username, password) + val bcryptedPassword = LoginActor.generateNewPassword(username, password, bcryptRounds) // update account, set password ctx.run( query[persistence.Account] @@ -210,31 +304,33 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne ) } loginSuccessfulResponse(username, newToken) + context.become(displayingServerListBehavior) updateServerListTask = context.system.scheduler.scheduleWithFixedDelay(0 seconds, 5 seconds, self, LoginActor.UpdateServerList) future case (_, false) => - loginPwdFailureResponse(username, newToken) - Future.successful(None) + loginFailurePasswordResponse(username, newToken) + loginFailureAction() case (true, _) => loginAccountFailureResponse(username, newToken) - Future.successful(None) + loginFailureAction() } - case None => Future.successful(None) + case None => + loginFailureAction() } } yield login result.onComplete { - case Success(_) => + case Success(_) => () case Failure(e) => log.error(e.getMessage) } } - def accountLoginWithToken(token: String): Unit = { + private def accountLoginWithToken(token: String): Unit = { import ctx._ - val newToken = this.generateToken() + val newToken = LoginActor.generateToken() val result = for { accountsExact <- ctx.run(query[persistence.Account].filter(_.token.getOrNull == lift(token))) accountOption <- accountsExact.headOption match { @@ -267,33 +363,35 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne _.port -> lift(port) ) ) - loginSuccessfulResponseToken(account.username, token, newToken) + loginSuccessfulResponseWithToken(account.username, token, newToken) + context.become(displayingServerListBehavior) updateServerListTask = context.system.scheduler.scheduleWithFixedDelay(0 seconds, 5 seconds, self, LoginActor.UpdateServerList) future case (_, false) => loginFailureResponseToken(account.username, token, newToken) - Future.successful(None) + loginFailureAction() case (true, _) => loginAccountFailureResponseToken(account.username, token, newToken) - Future.successful(None) + loginFailureAction() } - case None => Future.successful(None) + case None => + loginFailureAction() } } yield login result.onComplete { - case Success(_) => + case Success(_) => () case Failure(e) => log.error(e.getMessage) } } - def loginSuccessfulResponse(username: String, newToken: String): Unit = { + private def loginSuccessfulResponse(username: String, token: String): Unit = { middlewareActor ! MiddlewareActor.Send( LoginRespMessage( - newToken, + token, LoginError.Success, StationError.AccountActive, StationSubscriptionStatus.Active, @@ -304,26 +402,21 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne ) } - def loginSuccessfulResponseToken(username: String, token: String, newToken: String): Unit = { - log.info(s"User $username logged in unsing token $token") - middlewareActor ! MiddlewareActor.Send( - LoginRespMessage( - newToken, - LoginError.Success, - StationError.AccountActive, - StationSubscriptionStatus.Active, - 0, - username, - 10001 - ) - ) + private def loginSuccessfulResponseWithToken(username: String, token: String, newToken: String): Unit = { + log.info(s"User $username logged in using token $token") + loginSuccessfulResponse(username, newToken) } - def loginPwdFailureResponse(username: String, newToken: String): Unit = { + private def loginFailureAction(): Future[Any] = { + context.become(accountLoginBehavior) + Future.successful(None) + } + + private def loginFailurePasswordResponse(username: String, token: String): Unit = { log.warning(s"Failed login to account $username") middlewareActor ! MiddlewareActor.Send( LoginRespMessage( - newToken, + token, LoginError.BadUsernameOrPassword, StationError.AccountActive, StationSubscriptionStatus.Active, @@ -334,7 +427,7 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne ) } - def loginFailureResponseToken(token: String, newToken: String): Unit = { + private def loginFailureResponseToken(token: String, newToken: String): Unit = { log.warning(s"Failed login using unknown token $token") middlewareActor ! MiddlewareActor.Send( LoginRespMessage( @@ -349,7 +442,7 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne ) } - def loginFailureResponseTokenExpired(token: String, newToken: String): Unit = { + private def loginFailureResponseTokenExpired(token: String, newToken: String): Unit = { log.warning(s"Failed login using expired token $token") middlewareActor ! MiddlewareActor.Send( LoginRespMessage( @@ -364,11 +457,11 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne ) } - def loginFailureResponse(username: String, newToken: String): Unit = { + private def loginFailureResponse(username: String, token: String): Unit = { log.warning(s"DB problem username: $username") middlewareActor ! MiddlewareActor.Send( LoginRespMessage( - newToken, + token, LoginError.unk1, StationError.AccountActive, StationSubscriptionStatus.Active, @@ -379,7 +472,7 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne ) } - def loginFailureResponseToken(username: String, token: String, newToken: String): Unit = { + private def loginFailureResponseToken(username: String, token: String, newToken: String): Unit = { log.warning(s"DB problem username $username token: $token") middlewareActor ! MiddlewareActor.Send( LoginRespMessage( @@ -394,11 +487,11 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne ) } - def loginAccountFailureResponse(username: String, newToken: String): Unit = { + private def loginAccountFailureResponse(username: String, token: String): Unit = { log.warning(s"Account $username inactive") middlewareActor ! MiddlewareActor.Send( LoginRespMessage( - newToken, + token, LoginError.BadUsernameOrPassword, StationError.AccountClosed, StationSubscriptionStatus.Active, @@ -409,7 +502,7 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne ) } - def loginAccountFailureResponseToken(username: String, token: String, newToken: String): Unit = { + private def loginAccountFailureResponseToken(username: String, token: String, newToken: String): Unit = { log.warning(s"Account $username inactive token: $token ") middlewareActor ! MiddlewareActor.Send( LoginRespMessage( @@ -424,16 +517,8 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne ) } - def generateToken(): String = { - val r = new scala.util.Random - val sb = new mutable.StringBuilder - for (_ <- 1 to 31) { - sb.append(r.nextPrintableChar()) - } - sb.toString - } - - def updateServerList(): Unit = { + private def updateServerList(): Unit = { + //todo list of game servers from database, eventually, which is a separation of game server from login server middlewareActor ! MiddlewareActor.Send( VNLWorldStatusMessage( "Welcome to PlanetSide! ", @@ -450,7 +535,7 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne ) } - def failWithError(error: String): Unit = { + private def failWithError(error: String): Unit = { log.error(error) middlewareActor ! MiddlewareActor.Close() } From 8e2732681c53687d3d080aa25081c85ee96462db Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Thu, 31 Jul 2025 01:13:32 -0400 Subject: [PATCH 23/25] No Safe Spaces (#1283) * local zone maintains information about weapon fire capability per faction * map reload by faction to represent a change in weapons fire permissions via LMM --- .../actors/session/csr/ChatLogic.scala | 130 ++++++++++++++++++ .../session/normal/LocalHandlerLogic.scala | 23 +++- .../actors/session/spectator/ChatLogic.scala | 2 +- .../session/support/ChatOperations.scala | 24 +++- .../session/support/ZoningOperations.scala | 4 +- .../net/psforever/objects/zones/Zone.scala | 46 +++++++ .../services/local/LocalService.scala | 8 ++ .../services/local/LocalServiceMessage.scala | 1 + .../services/local/LocalServiceResponse.scala | 2 + 9 files changed, 234 insertions(+), 6 deletions(-) diff --git a/src/main/scala/net/psforever/actors/session/csr/ChatLogic.scala b/src/main/scala/net/psforever/actors/session/csr/ChatLogic.scala index 24c5c885..f90de34d 100644 --- a/src/main/scala/net/psforever/actors/session/csr/ChatLogic.scala +++ b/src/main/scala/net/psforever/actors/session/csr/ChatLogic.scala @@ -18,6 +18,7 @@ import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.chat.{ChatChannel, DefaultChannel, SpectatorChannel} import net.psforever.types.ChatMessageType.{CMT_TOGGLESPECTATORMODE, CMT_TOGGLE_GM} import net.psforever.types.{ChatMessageType, PlanetSideEmpire} +import net.psforever.zones.Zones import scala.util.Success @@ -225,6 +226,7 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext case "hidespectators" => customCommandHideSpectators() case "sayspectator" => customCommandSpeakAsSpectator(params, message) case "setempire" => customCommandSetEmpire(params) + case "weaponlock" => customCommandZoneWeaponUnlock(session, params) case _ => // command was not handled sendResponse( @@ -411,6 +413,134 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext } } + def customCommandZoneWeaponUnlock(session: Session, params: Seq[String]): Boolean = { + val usageMessage: Boolean = params.exists(_.matches("--help")) || params.exists(_.matches("-h")) + val formattedParams = ops.cliCommaSeparatedParams(params) + //handle params + val (zoneList, verifiedZones, factionList, verifiedFactions, stateOpt) = (formattedParams.headOption, formattedParams.lift(1), formattedParams.lift(2)) match { + case _ if usageMessage => + (Nil, Nil, Nil, Nil, None) + + case (None, None, None) => + ( + Seq(session.zone.id), + Seq(session.zone), + PlanetSideEmpire.values.map(_.toString()).toSeq, + PlanetSideEmpire.values.toSeq, + Some(true) + ) + + case (Some(zoneOrFaction), Some(factionOrZone), stateOpt) => + val factionOrZoneSplit = factionOrZone.split(",").toSeq + val zoneOrFactionSplit = zoneOrFaction.split(",").toSeq + val tryToFactions = factionOrZoneSplit.flatten(s => ops.captureBaseParamFaction(session, Some(s))) + if (tryToFactions.isEmpty) { + ( + factionOrZoneSplit, + customCommandZoneParse(factionOrZoneSplit), + zoneOrFactionSplit, + zoneOrFactionSplit.flatten(s => ops.captureBaseParamFaction(session, Some(s))), + customCommandOnOffStateOrNone(stateOpt) + ) + } else { + ( + zoneOrFactionSplit, + customCommandZoneParse(zoneOrFactionSplit), + factionOrZoneSplit, + tryToFactions, + customCommandOnOffStateOrNone(stateOpt) + ) + } + + case (Some(zoneOrFaction), stateOpt, None) => + val zoneOrFactionSplit = zoneOrFaction.split(",").toSeq + val tryToFactions = zoneOrFactionSplit.flatten(s => ops.captureBaseParamFaction(session, Some(s))) + if (tryToFactions.isEmpty) { + ( + zoneOrFactionSplit, + customCommandZoneParse(zoneOrFactionSplit), + PlanetSideEmpire.values.map(_.toString()).toSeq, + PlanetSideEmpire.values.toSeq, + customCommandOnOffStateOrNone(stateOpt) + ) + } else { + ( + Seq(session.zone.id), + Seq(session.zone), + zoneOrFactionSplit, + tryToFactions, + customCommandOnOffStateOrNone(stateOpt) + ) + } + + case (stateOpt, None, None) => + ( + Seq(session.zone.id), + Seq(session.zone), + PlanetSideEmpire.values.map(_.toString()).toSeq, + PlanetSideEmpire.values.toSeq, + customCommandOnOffState(stateOpt) + ) + } + //resolve + if (usageMessage) { + sendResponse(ChatMsg(ChatMessageType.UNK_227, "!weaponlock [zone[,...]] [faction[,...]] [o[n]|of[f]]")) + } else if (zoneList.isEmpty || verifiedZones.isEmpty || zoneList.size != verifiedZones.size) { + sendResponse(ChatMsg(ChatMessageType.UNK_227, "some zones can not be verified")) + } else if (factionList.isEmpty || verifiedFactions.isEmpty || factionList.size != verifiedFactions.size) { + sendResponse(ChatMsg(ChatMessageType.UNK_227, "some factions can not be verified")) + } else if (stateOpt.isEmpty) { + sendResponse(ChatMsg(ChatMessageType.UNK_227, "state must be on or off")) + } else { + val state = !stateOpt.get + verifiedZones.foreach { zone => + val events = zone.AvatarEvents + val zoneId = zone.id + //val reloadZoneMsg = AvatarAction.ReloadZone(zone) + zone + .UpdateLiveFireAllowed(state, verifiedFactions) + .foreach { + case (_, false, _) => () + case (faction, true, _) => + //events ! AvatarServiceMessage(s"$faction", reloadZoneMsg) + } + } + } + true + } + + private def customCommandOnOffStateOrNone(stateOpt: Option[String]): Option[Boolean] = { + stateOpt match { + case None => + Some(true) + case _ => + customCommandOnOffState(stateOpt) + } + } + + private def customCommandOnOffState(stateOpt: Option[String]): Option[Boolean] = { + stateOpt match { + case Some("o") | Some("on") => + Some(false) + case Some("of") | Some("off") => + Some(true) + case _ => + None + } + } + + def customCommandZoneParse(potentialZones: Seq[String]): Seq[Zone] = { + potentialZones.flatten { potentialZone => + if (potentialZone.toIntOption.nonEmpty) { + val xInt = potentialZone.toInt + Zones.zones.find(_.Number == xInt) + } else { + Zones.zones.find(z => z.id.equals(potentialZone)) + } + } + } + + override def stop(): Unit = { super.stop() seeSpectatorsIn.foreach(_ => customCommandHideSpectators()) diff --git a/src/main/scala/net/psforever/actors/session/normal/LocalHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/normal/LocalHandlerLogic.scala index 3b7538a5..1b176474 100644 --- a/src/main/scala/net/psforever/actors/session/normal/LocalHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/LocalHandlerLogic.scala @@ -8,9 +8,9 @@ import net.psforever.objects.serverobject.doors.Door import net.psforever.objects.vehicles.MountableWeapons import net.psforever.objects.{BoomerDeployable, ExplosiveDeployable, TelepadDeployable, Tool, TurretDeployable} import net.psforever.packet.game.{ChatMsg, DeployableObjectsInfoMessage, GenericActionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HackMessage, HackState, HackState1, InventoryStateMessage, ObjectAttachMessage, ObjectCreateMessage, ObjectDeleteMessage, ObjectDetachMessage, OrbitalShuttleTimeMsg, PadAndShuttlePair, PlanetsideAttributeMessage, ProximityTerminalUseMessage, SetEmpireMessage, TriggerEffectMessage, TriggerSoundMessage, TriggeredSound, VehicleStateMessage} -import net.psforever.services.Service +import net.psforever.services.{InterstellarClusterService, Service} import net.psforever.services.local.LocalResponse -import net.psforever.types.{ChatMessageType, PlanetSideGUID} +import net.psforever.types.{ChatMessageType, PlanetSideGUID, SpawnGroup} object LocalHandlerLogic { def apply(ops: SessionLocalHandlers): LocalHandlerLogic = { @@ -240,6 +240,25 @@ class LocalHandlerLogic(val ops: SessionLocalHandlers, implicit val context: Act sendResponse(InventoryStateMessage(weapon.AmmoSlot.Box.GUID, weapon.GUID, weapon.Magazine)) } + case LocalResponse.ForceZoneChange(zone) => + //todo we might be able to piggyback this for squad recalls later + if(session.zone eq zone) { + sessionLogic.zoning.zoneReload = true + zone.AvatarEvents ! Service.Leave() + zone.LocalEvents ! Service.Leave() + zone.VehicleEvents ! Service.Leave() + zone.AvatarEvents ! Service.Join(player.Name) //must manually restore this subscriptions + sessionLogic.zoning.spawn.handleNewPlayerLoaded(player) //will restart subscriptions and dispatch a LoadMapMessage + } else { + import akka.actor.typed.scaladsl.adapter._ + sessionLogic.cluster ! InterstellarClusterService.GetRandomSpawnPoint( + zone.Number, + player.Faction, + Seq(SpawnGroup.Facility, SpawnGroup.Tower, SpawnGroup.AMS), + context.self + ) + } + case _ => () } } diff --git a/src/main/scala/net/psforever/actors/session/spectator/ChatLogic.scala b/src/main/scala/net/psforever/actors/session/spectator/ChatLogic.scala index ae285706..c17b2fdc 100644 --- a/src/main/scala/net/psforever/actors/session/spectator/ChatLogic.scala +++ b/src/main/scala/net/psforever/actors/session/spectator/ChatLogic.scala @@ -140,7 +140,7 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext case _ => ("", Seq("")) } command match { - case "list" => ops.customCommandList(session, params, message) + case "list" => ops.customCommandList(session, params.toSeq, message) case "nearby" => ops.customCommandNearby(session) case "loc" => ops.customCommandLoc(session, message) case _ => 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 5aebd100..48a724db 100644 --- a/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala @@ -1046,7 +1046,7 @@ class ChatOperations( } } - private def captureBaseParamFaction( + def captureBaseParamFaction( @unused session: Session, token: Option[String] ): Option[PlanetSideEmpire.Value] = { @@ -1360,6 +1360,28 @@ class ChatOperations( str.replaceAll("\\s+", " ").trim.split("\\s").toList.filter(!_.equals("")) } + def cliCommaSeparatedParams(params: Seq[String]): Seq[String] = { + var len = 0 + var appendNext = false + var formattedParams: Seq[String] = Seq() + params.foreach { + case "," => + appendNext = true + case param if appendNext || param.startsWith(",") => + formattedParams = formattedParams.slice(0, len - 1) :+ formattedParams(len - 1) + "," + param.replaceAll(",", "") + appendNext = param.endsWith(",") + case param if param.endsWith(",") => + formattedParams = formattedParams :+ param.take(param.length-1) + len += 1 + appendNext = true + case param => + formattedParams = formattedParams :+ param + len += 1 + appendNext = false + } + formattedParams + } + def commandIncomingSend(message: ChatMsg): Unit = { sendResponse(message) } 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 ae62987a..68b5d140 100644 --- a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala @@ -2187,8 +2187,8 @@ class ZoningOperations( tplayer.avatar = avatar session = session.copy(player = tplayer) //LoadMapMessage causes the client to send BeginZoningMessage, eventually leading to SetCurrentAvatar - val weaponsEnabled = !(mapName.equals("map11") || mapName.equals("map12") || mapName.equals("map13")) - sendResponse(LoadMapMessage(mapName, id, 40100, 25, weaponsEnabled, map.checksum)) + //val weaponsEnabled = !(mapName.equals("map11") || mapName.equals("map12") || mapName.equals("map13")) + sendResponse(LoadMapMessage(mapName, id, 40100, 25, zone.LiveFireAllowed(tplayer.Faction), map.checksum)) if (isAcceptableNextSpawnPoint) { //important! the LoadMapMessage must be processed by the client before the avatar is created player.allowInteraction = true diff --git a/src/main/scala/net/psforever/objects/zones/Zone.scala b/src/main/scala/net/psforever/objects/zones/Zone.scala index cbf828d0..4deac170 100644 --- a/src/main/scala/net/psforever/objects/zones/Zone.scala +++ b/src/main/scala/net/psforever/objects/zones/Zone.scala @@ -182,6 +182,12 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) { */ private var vehicleEvents: ActorRef = Default.Actor + /** + * Is any player permitted to engage in weapons discharge in this zone? + */ + private var liveFireAllowed: mutable.HashMap[PlanetSideEmpire.Value, Boolean] = + mutable.HashMap.from(PlanetSideEmpire.values.map { f => (f, true) }) + /** * When the zone has completed initializing, fulfill this promise. * @see `init(ActorContext)` @@ -593,6 +599,46 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) { vehicleEvents = bus VehicleEvents } + + def LiveFireAllowed(): Boolean = liveFireAllowed.exists { case (_, v) => v } + + def LiveFireAllowed(faction: PlanetSideEmpire.Value): Boolean = liveFireAllowed.getOrElse(faction, false) + + def UpdateLiveFireAllowed(state: Boolean): List[(PlanetSideEmpire.Value, Boolean, Boolean)] = { + val output = liveFireAllowed.map { case (f, v) => + (f, v == state, state) + } + output.foreach { case (f, _, v) => + liveFireAllowed.update(f, v) + } + output.toList + } + + def UpdateLiveFireAllowed(state: Boolean, faction: PlanetSideEmpire.Value): List[(PlanetSideEmpire.Value, Boolean, Boolean)] = { + val output = liveFireAllowed.map { case (f, v) => + if (f == faction) { + (f, v == state, state) + } else { + (f, false, v) + } + } + liveFireAllowed.update(faction, state) + output.toList + } + + def UpdateLiveFireAllowed(state: Boolean, factions: Seq[PlanetSideEmpire.Value]): List[(PlanetSideEmpire.Value, Boolean, Boolean)] = { + val output = liveFireAllowed.map { case (f, v) => + if (factions.contains(f)) { + (f, v == state, state) + } else { + (f, false, v) + } + } + factions.foreach { f => + liveFireAllowed.update(f, state) + } + output.toList + } } /** diff --git a/src/main/scala/net/psforever/services/local/LocalService.scala b/src/main/scala/net/psforever/services/local/LocalService.scala index 91d900d3..751dce97 100644 --- a/src/main/scala/net/psforever/services/local/LocalService.scala +++ b/src/main/scala/net/psforever/services/local/LocalService.scala @@ -306,6 +306,14 @@ class LocalService(zone: Zone) extends Actor { LocalResponse.RechargeVehicleWeapon(vehicle_guid, weapon_guid) ) ) + case LocalAction.ForceZoneChange(zone) => + LocalEvents.publish( + LocalServiceResponse( + s"/$forChannel/Local", + Service.defaultPlayerGUID, + LocalResponse.ForceZoneChange(zone) + ) + ) case _ => ; } diff --git a/src/main/scala/net/psforever/services/local/LocalServiceMessage.scala b/src/main/scala/net/psforever/services/local/LocalServiceMessage.scala index de4545cc..ced62945 100644 --- a/src/main/scala/net/psforever/services/local/LocalServiceMessage.scala +++ b/src/main/scala/net/psforever/services/local/LocalServiceMessage.scala @@ -138,4 +138,5 @@ object LocalAction { mountable_guid: PlanetSideGUID, weapon_guid: PlanetSideGUID ) extends Action + final case class ForceZoneChange(zone: Zone) extends Action } diff --git a/src/main/scala/net/psforever/services/local/LocalServiceResponse.scala b/src/main/scala/net/psforever/services/local/LocalServiceResponse.scala index ffb1bb3f..8834ab19 100644 --- a/src/main/scala/net/psforever/services/local/LocalServiceResponse.scala +++ b/src/main/scala/net/psforever/services/local/LocalServiceResponse.scala @@ -6,6 +6,7 @@ import net.psforever.objects.{PlanetSideGameObject, TelepadDeployable, Vehicle} import net.psforever.objects.ce.{Deployable, DeployedItem} import net.psforever.objects.serverobject.terminals.{ProximityUnit, Terminal} import net.psforever.objects.vehicles.Utility +import net.psforever.objects.zones.Zone import net.psforever.packet.game.GenericObjectActionEnum.GenericObjectActionEnum import net.psforever.packet.game.PlanetsideAttributeEnum.PlanetsideAttributeEnum import net.psforever.packet.PlanetSideGamePacket @@ -86,4 +87,5 @@ object LocalResponse { final case class TriggerSound(sound: TriggeredSound.Value, pos: Vector3, unk: Int, volume: Float) extends Response final case class UpdateForceDomeStatus(building_guid: PlanetSideGUID, activated: Boolean) extends Response final case class RechargeVehicleWeapon(mountable_guid: PlanetSideGUID, weapon_guid: PlanetSideGUID) extends Response + final case class ForceZoneChange(zone: Zone) extends Response } From 48f6064cda04bf0275cab4638227f073ae8cdb13 Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Thu, 31 Jul 2025 01:14:40 -0400 Subject: [PATCH 24/25] Lukewarm Potato (#1285) * removing the cep reward for killing a flag carrier * log flag carrier kill to database as a facility capture event * need to acronym the exp event type, three characters only --- src/main/resources/application.conf | 2 +- .../actors/session/support/GeneralOperations.scala | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf index 27b1e4a5..20885b9e 100644 --- a/src/main/resources/application.conf +++ b/src/main/resources/application.conf @@ -469,7 +469,7 @@ game { # If a player died while carrying an lattice logic unit, # and satisfies the carrying duration, # award the player who is accredited with the kill command experience. - llu-slayer-credit = 200 + llu-slayer-credit = 0 # The maximum command experience that can be earned in a facility capture based on squad size maximum-per-squad-size = [990, 1980, 3466, 4950, 6436, 7920, 9406, 10890, 12376, 13860] # When the cep has to be capped for squad size, add a small value to the capped value diff --git a/src/main/scala/net/psforever/actors/session/support/GeneralOperations.scala b/src/main/scala/net/psforever/actors/session/support/GeneralOperations.scala index 181a1e74..e72e57a5 100644 --- a/src/main/scala/net/psforever/actors/session/support/GeneralOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/GeneralOperations.scala @@ -15,6 +15,7 @@ import net.psforever.objects.serverobject.turret.FacilityTurret import net.psforever.objects.sourcing.{DeployableSource, PlayerSource, VehicleSource} import net.psforever.objects.vehicles.Utility.InternalTelepad import net.psforever.objects.zones.blockmap.BlockMapEntity +import net.psforever.objects.zones.exp.ToDatabase import net.psforever.services.RemoverActor import net.psforever.services.local.{LocalAction, LocalServiceMessage} @@ -444,6 +445,13 @@ class GeneralOperations( attacker.Name, AvatarAction.AwardCep(attacker.CharId, Config.app.game.experience.cep.lluSlayerCredit) ) + ToDatabase.reportFacilityCapture( + attacker.CharId, + continent.Number, + llu.Owner.GUID.guid, + Config.app.game.experience.cep.lluSlayerCredit, + expType = "lsc" //three characters - "llu slayer credit" + ) } } if (!CaptureFlagManager.ReasonToLoseFlagViolently(continent, Some(guid), player)) { From 1bfe90ca84beaadd935328ac269c11514f4ee6b0 Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie <73139382+ScrawnyRonnie@users.noreply.github.com> Date: Thu, 31 Jul 2025 07:39:07 -0400 Subject: [PATCH 25/25] Uncomment handlers --- .../actors/session/normal/SquadHandlerLogic.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 6d8bfd8e..8a0ae9f3 100644 --- a/src/main/scala/net/psforever/actors/session/normal/SquadHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/SquadHandlerLogic.scala @@ -32,17 +32,17 @@ class SquadHandlerLogic(val ops: SessionSquadHandlers, implicit val context: Act /* packet */ def handleSquadDefinitionAction(pkt: SquadDefinitionActionMessage): Unit = { - /*val SquadDefinitionActionMessage(u1, u2, action) = pkt - squadService ! SquadServiceMessage(player, continent, SquadServiceAction.Definition(u1, u2, action))*/ + val SquadDefinitionActionMessage(u1, u2, action) = pkt + squadService ! SquadServiceMessage(player, continent, SquadServiceAction.Definition(u1, u2, action)) } def handleSquadMemberRequest(pkt: SquadMembershipRequest): Unit = { - /*val SquadMembershipRequest(request_type, char_id, unk3, player_name, unk5) = pkt + val SquadMembershipRequest(request_type, char_id, unk3, player_name, unk5) = pkt squadService ! SquadServiceMessage( player, continent, SquadServiceAction.Membership(request_type, char_id, unk3, player_name, unk5) - )*/ + ) } def handleSquadWaypointRequest(pkt: SquadWaypointRequest): Unit = {