Yet Another Squad Update (2)

Yet Another Squad Update (2) - not ready to go live
This commit is contained in:
ScrawnyRonnie 2025-08-01 12:51:53 -04:00 committed by GitHub
commit 749a611b87
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 3409 additions and 2249 deletions

View file

@ -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)

View file

@ -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(

View file

@ -100,6 +100,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)

View file

@ -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 {
@ -28,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 = {
@ -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) =>
@ -349,6 +361,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, messageType) =>
sendResponse(ChatMsg(messageType, comment))
case _ => ()
}
}

View file

@ -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)
}

View file

@ -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)

View file

@ -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)

View file

@ -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, messageType) =>
sendResponse(ChatMsg(messageType, comment))
case _ => ()
}
}

View file

@ -2,23 +2,30 @@
package net.psforever.actors.session.support
import akka.actor.Cancellable
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.{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.mutable
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
@ -54,6 +61,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 {
@ -74,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)
@ -1284,6 +1296,47 @@ class ChatOperations(
true
}
def customCommandSquad(params: Seq[String]): Boolean = {
params match {
case "invites" :: _ =>
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 =>
//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 =>
//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 _ => ()
}
true
}
def firstParam[T](
session: Session,
buffer: Iterable[String],

View file

@ -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))
}
}
@ -547,6 +554,7 @@ class SessionData(
if (avatar != null) {
accountPersistence ! AccountPersistenceService.Logout(avatar.name)
}
squad.cleanUpSquadCards()
middlewareActor ! MiddlewareActor.Teardown()
}

View file

@ -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
}
}

View file

@ -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
@ -22,7 +24,6 @@ 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.collection.mutable
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
@ -84,8 +85,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",
@ -186,6 +187,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],
@ -672,11 +681,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)
}
@ -1865,7 +1878,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
@ -1888,9 +1900,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 val initialActivityDelay: Int = 4
private var nextActivityDelay: Int = 0
private var statisticsPacketFunc: () => Unit = loginAvatarStatisticsFields
/* packets */
@ -1899,6 +1916,7 @@ class ZoningOperations(
val ReleaseAvatarRequestMessage() = pkt
log.info(s"${player.Name} on ${continent.id} has released")
reviveTimer.cancel()
avatarActive = false
GoToDeploymentMap()
HandleReleaseAvatar(player, continent)
}
@ -1986,16 +2004,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 =>
@ -2012,8 +2033,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
}
}
@ -2021,8 +2043,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
}
@ -2120,11 +2143,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)
}
@ -2926,6 +2953,7 @@ class ZoningOperations(
respawnTimer.cancel()
reviveTimer.cancel()
deadState = DeadState.RespawnTime
avatarActive = false
sendResponse(
AvatarDeadStateMessage(
DeadState.RespawnTime,
@ -3299,7 +3327,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))
}
}
avatarActor ! AvatarActor.RefreshPurchaseTimes()
@ -3561,8 +3589,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
}
@ -3773,6 +3799,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
}
@ -3791,6 +3818,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
@ -3815,7 +3843,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))
@ -3823,6 +3851,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(

View file

@ -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)
@ -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
@ -73,4 +83,6 @@ object SquadResponse {
unk2: Int,
zoneNumber: Int
) extends Response
final case class SquadRelatedComment(str: String, messageType: ChatMessageType = ChatMessageType.UNK_227) extends Response
}

View file

@ -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

View file

@ -0,0 +1,163 @@
// 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 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.
* 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(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, originalRequester, 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, 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 {
}
}
def handleRejection(
manager: SquadInvitationManager,
player: Player,
rejectingPlayer: Long,
@unused squadsToLeaders: List[(PlanetSideGUID, SquadFeatures)]
): Unit = {
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(
manager: SquadInvitationManager,
player: Player,
rejectingPlayer: Long
): Unit = {
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 = originalRequester
def appliesToPlayer(playerCharId: Long): Boolean = playerCharId == originalRequester.CharId
def appliesToSquad(guid: PlanetSideGUID): Boolean = features.Squad.GUID == guid
def appliesToSquadAndPosition(guid: PlanetSideGUID, squadPosition: Int): Boolean = false
}

View file

@ -0,0 +1,88 @@
// 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.<br>
* <br>
* 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
/**
* 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]
/**
* 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
}

View file

@ -0,0 +1,170 @@
// 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)
.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.SquadRelatedComment(s"Though a squad has been created, a member could not join it.")
)
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 _ =>
org.log4s.getLogger("InvitationToCreateASquad").error("could not create a squad when requested")
}
}
}
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 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
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
}

View file

@ -0,0 +1,167 @@
// 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
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
}
}
}
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 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)
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
}

View file

@ -0,0 +1,173 @@
// Copyright (c) 2024 PSForever
package net.psforever.services.teamwork.invitations
import net.psforever.objects.teamwork.{Member, SquadFeatures}
import net.psforever.objects.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 = {
val leaderCharId = squadLeader.CharId
if (
manager.notLimitedByEnrollmentInSquad(invitedPlayerSquadOpt, invitedPlayer) &&
SquadInvitationManager.canEnrollInSquad(features, invitedPlayer) &&
manager.joinSquad(player, 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.")
)
}
}
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 = {
val faction = player.Faction
manager.reloadSearchForRoleInvite(
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)
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
}

View file

@ -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
}

View file

@ -0,0 +1,149 @@
// 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 = {
val leaderCharId = squadLeader.CharId
//this cleanup activity always happens
features.ProxyInvites = features.ProxyInvites.filterNot { _ == invitedPlayer }
if (
manager.notLimitedByEnrollmentInSquad(invitedPlayerSquadOpt, invitedPlayer) &&
SquadInvitationManager.canEnrollInSquad(features, invitedPlayer) &&
manager.joinSquad(player, features, position)
) {
//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.")
)
}
(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(
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 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)
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
}

View file

@ -0,0 +1,164 @@
// 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"
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.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.")
)
}
}
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 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)
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
}