mirror of
https://github.com/psforever/PSF-LoginServer.git
synced 2026-01-19 18:44:45 +00:00
functioning spectator mode logic swap; initial packets for UplinkRequest, UplinkResponse, and UplinkPositionEvent
This commit is contained in:
parent
c229d50261
commit
79c0afa2c2
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import akka.actor.typed.{ActorRef, Behavior, PostStop, SupervisorStrategy}
|
|||
import akka.actor.typed.receptionist.Receptionist
|
||||
import akka.actor.typed.scaladsl.{ActorContext, Behaviors, StashBuffer}
|
||||
import akka.actor.typed.scaladsl.adapter._
|
||||
import net.psforever.actors.session.normal.NormalMode
|
||||
import net.psforever.actors.session.spectator.SpectatorMode
|
||||
import net.psforever.actors.zone.ZoneActor
|
||||
import net.psforever.objects.sourcing.PlayerSource
|
||||
import net.psforever.objects.zones.ZoneInfo
|
||||
|
|
@ -761,19 +763,14 @@ class ChatActor(
|
|||
sessionActor ! SessionActor.SendResponse(message.copy(contents = f"$speed%.3f"))
|
||||
|
||||
case (CMT_TOGGLESPECTATORMODE, _, contents) if gmCommandAllowed =>
|
||||
val spectator = contents match {
|
||||
case "on" => true
|
||||
case "off" => false
|
||||
case _ => !session.player.spectator
|
||||
val currentSpectatorActivation = session.player.spectator
|
||||
contents.toLowerCase() match {
|
||||
case "on" | "o" | "" if !currentSpectatorActivation =>
|
||||
sessionActor ! SessionActor.SetMode(SpectatorMode)
|
||||
case "off" | "of" if currentSpectatorActivation =>
|
||||
sessionActor ! SessionActor.SetMode(NormalMode)
|
||||
case _ => ()
|
||||
}
|
||||
sessionActor ! SessionActor.SetSpectator(spectator)
|
||||
sessionActor ! SessionActor.SendResponse(message.copy(contents = if (spectator) "on" else "off"))
|
||||
sessionActor ! SessionActor.SendResponse(
|
||||
message.copy(
|
||||
messageType = UNK_227,
|
||||
contents = if (spectator) "@SpectatorEnabled" else "@SpectatorDisabled"
|
||||
)
|
||||
)
|
||||
|
||||
case (CMT_RECALL, _, _) =>
|
||||
val errorMessage = session.zoningType match {
|
||||
|
|
|
|||
|
|
@ -2,12 +2,13 @@
|
|||
package net.psforever.actors.session
|
||||
|
||||
import akka.actor.{Actor, Cancellable, MDCContextAware, typed}
|
||||
import net.psforever.actors.session.normal.NormalMode
|
||||
import org.joda.time.LocalDateTime
|
||||
import org.log4s.MDC
|
||||
|
||||
import scala.collection.mutable
|
||||
//
|
||||
import net.psforever.actors.net.MiddlewareActor
|
||||
import net.psforever.actors.session.normal.NormalMode
|
||||
import net.psforever.actors.session.support.{ModeLogic, PlayerMode, SessionData}
|
||||
import net.psforever.objects.{Default, Player}
|
||||
import net.psforever.objects.avatar.Avatar
|
||||
|
|
@ -19,8 +20,6 @@ import net.psforever.types.Vector3
|
|||
object SessionActor {
|
||||
sealed trait Command
|
||||
|
||||
private[session] final case class PokeClient()
|
||||
|
||||
private[session] final case class ServerLoaded()
|
||||
|
||||
private[session] final case class NewPlayerLoaded(tplayer: Player)
|
||||
|
|
@ -69,6 +68,10 @@ object SessionActor {
|
|||
|
||||
private[session] case object CharSavedMsg extends Command
|
||||
|
||||
final case object StartHeartbeat extends Command
|
||||
|
||||
private final case object PokeClient extends Command
|
||||
|
||||
final case class SetMode(mode: PlayerMode) extends Command
|
||||
}
|
||||
|
||||
|
|
@ -96,12 +99,29 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
case _ if data.whenAllEventBusesLoaded() =>
|
||||
context.become(inTheGame)
|
||||
logic = mode.setup(data)
|
||||
startHeartbeat()
|
||||
buffer.foreach { self.tell(_, self) } //we forget the original sender, shouldn't be doing callbacks at this point
|
||||
buffer.clear()
|
||||
case _ => ()
|
||||
}
|
||||
|
||||
private def inTheGame: Receive = {
|
||||
/* used for the game's heartbeat */
|
||||
case SessionActor.StartHeartbeat =>
|
||||
startHeartbeat()
|
||||
|
||||
case SessionActor.PokeClient =>
|
||||
middlewareActor ! MiddlewareActor.Send(KeepAliveMessage())
|
||||
|
||||
case SessionActor.SetMode(newMode) =>
|
||||
logic.switchFrom(data.session)
|
||||
mode = newMode
|
||||
logic = mode.setup(data)
|
||||
logic.switchTo(data.session)
|
||||
|
||||
case packet =>
|
||||
logic.parse(sender())(packet)
|
||||
}
|
||||
|
||||
private def startHeartbeat(): Unit = {
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
|
|
@ -110,20 +130,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
initialDelay = 0.seconds,
|
||||
delay = 500.milliseconds,
|
||||
context.self,
|
||||
SessionActor.PokeClient()
|
||||
SessionActor.PokeClient
|
||||
)
|
||||
}
|
||||
|
||||
private def inTheGame: Receive = {
|
||||
/* used for the game's heartbeat */
|
||||
case SessionActor.PokeClient() =>
|
||||
middlewareActor ! MiddlewareActor.Send(KeepAliveMessage())
|
||||
|
||||
case SessionActor.SetMode(newMode) =>
|
||||
mode = newMode
|
||||
logic = mode.setup(data)
|
||||
|
||||
case packet =>
|
||||
logic.parse(sender())(packet)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,10 +23,14 @@ import net.psforever.services.Service
|
|||
import net.psforever.types.{ChatMessageType, PlanetSideGUID, TransactionType, Vector3}
|
||||
import net.psforever.util.Config
|
||||
|
||||
class AvatarHandlerLogic(val ops: SessionAvatarHandlers) extends AvatarHandlerFunctions {
|
||||
def sessionLogic: SessionData = ops.sessionLogic
|
||||
object AvatarHandlerLogic {
|
||||
def apply(ops: SessionAvatarHandlers): AvatarHandlerLogic = {
|
||||
new AvatarHandlerLogic(ops, ops.context)
|
||||
}
|
||||
}
|
||||
|
||||
implicit val context: ActorContext = ops.context
|
||||
class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: ActorContext) extends AvatarHandlerFunctions {
|
||||
def sessionLogic: SessionData = ops.sessionLogic
|
||||
|
||||
private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor
|
||||
|
||||
|
|
|
|||
|
|
@ -8,10 +8,14 @@ import net.psforever.packet.game.{BroadcastWarpgateUpdateMessage, FriendsRespons
|
|||
import net.psforever.services.galaxy.{GalaxyAction, GalaxyResponse, GalaxyServiceMessage}
|
||||
import net.psforever.types.{MemberAction, PlanetSideEmpire}
|
||||
|
||||
class GalaxyHandlerLogic(val ops: SessionGalaxyHandlers) extends GalaxyHandlerFunctions {
|
||||
def sessionLogic: SessionData = ops.sessionLogic
|
||||
object GalaxyHandlerLogic {
|
||||
def apply(ops: SessionGalaxyHandlers): GalaxyHandlerLogic = {
|
||||
new GalaxyHandlerLogic(ops, ops.context)
|
||||
}
|
||||
}
|
||||
|
||||
implicit val context: ActorContext = ops.context
|
||||
class GalaxyHandlerLogic(val ops: SessionGalaxyHandlers, implicit val context: ActorContext) extends GalaxyHandlerFunctions {
|
||||
def sessionLogic: SessionData = ops.sessionLogic
|
||||
|
||||
private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor
|
||||
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@
|
|||
package net.psforever.actors.session.normal
|
||||
|
||||
import akka.actor.typed.scaladsl.adapter._
|
||||
import akka.actor.{ActorContext, typed}
|
||||
import net.psforever.actors.session.{AvatarActor, ChatActor}
|
||||
import akka.actor.{ActorContext, Cancellable, typed}
|
||||
import net.psforever.actors.session.{AvatarActor, ChatActor, SessionActor}
|
||||
import net.psforever.actors.session.support.{GeneralFunctions, GeneralOperations, SessionData}
|
||||
import net.psforever.login.WorldSession.{CallBackForTask, ContainableMoveItem, DropEquipmentFromInventory, PickUpEquipmentFromGround, PutLoadoutEquipmentInInventory, RemoveOldEquipmentFromInventory}
|
||||
import net.psforever.objects.{Account, BoomerDeployable, BoomerTrigger, ConstructionItem, Deployables, GlobalDefinitions, Kit, LivePlayerList, PlanetSideGameObject, Player, SensorDeployable, ShieldGeneratorDeployable, SpecialEmp, TelepadDeployable, Tool, TrapDeployable, TurretDeployable, Vehicle}
|
||||
import net.psforever.login.WorldSession.{CallBackForTask, ContainableMoveItem, DropEquipmentFromInventory, PickUpEquipmentFromGround, RemoveOldEquipmentFromInventory}
|
||||
import net.psforever.objects.{Account, BoomerDeployable, BoomerTrigger, ConstructionItem, Default, Deployables, GlobalDefinitions, Kit, LivePlayerList, PlanetSideGameObject, Player, SensorDeployable, ShieldGeneratorDeployable, SpecialEmp, TelepadDeployable, Tool, TrapDeployable, TurretDeployable, Vehicle}
|
||||
import net.psforever.objects.avatar.{Avatar, PlayerControl, SpecialCarry}
|
||||
import net.psforever.objects.ballistics.Projectile
|
||||
import net.psforever.objects.ce.{Deployable, DeployedItem, TelepadLike}
|
||||
|
|
@ -14,7 +14,7 @@ import net.psforever.objects.definition.{BasicDefinition, KitDefinition, Special
|
|||
import net.psforever.objects.entity.WorldEntity
|
||||
import net.psforever.objects.equipment.Equipment
|
||||
import net.psforever.objects.guid.{GUIDTask, TaskBundle, TaskWorkflow}
|
||||
import net.psforever.objects.inventory.{Container, InventoryItem}
|
||||
import net.psforever.objects.inventory.Container
|
||||
import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject, ServerObject}
|
||||
import net.psforever.objects.serverobject.affinity.FactionAffinity
|
||||
import net.psforever.objects.serverobject.doors.Door
|
||||
|
|
@ -51,10 +51,14 @@ import net.psforever.util.Config
|
|||
|
||||
import scala.concurrent.duration._
|
||||
|
||||
class GeneralLogic(val ops: GeneralOperations) extends GeneralFunctions {
|
||||
def sessionLogic: SessionData = ops.sessionLogic
|
||||
object GeneralLogic {
|
||||
def apply(ops: GeneralOperations): GeneralLogic = {
|
||||
new GeneralLogic(ops, ops.context)
|
||||
}
|
||||
}
|
||||
|
||||
implicit val context: ActorContext = ops.context
|
||||
class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContext) extends GeneralFunctions {
|
||||
def sessionLogic: SessionData = ops.sessionLogic
|
||||
|
||||
private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor
|
||||
|
||||
|
|
@ -66,6 +70,7 @@ class GeneralLogic(val ops: GeneralOperations) extends GeneralFunctions {
|
|||
s"ConnectToWorldRequestMessage: client with versioning $majorVersion.$minorVersion.$revision, $buildDate has sent a token to the server"
|
||||
)
|
||||
sendResponse(ChatMsg(ChatMessageType.CMT_CULLWATERMARK, wideContents=false, "", "", None))
|
||||
context.self ! SessionActor.StartHeartbeat
|
||||
sessionLogic.accountIntermediary ! RetrieveAccountData(token)
|
||||
}
|
||||
|
||||
|
|
@ -989,7 +994,7 @@ class GeneralLogic(val ops: GeneralOperations) extends GeneralFunctions {
|
|||
}
|
||||
|
||||
def handleKick(player: Player, time: Option[Long]): Unit = {
|
||||
administrativeKick(player)
|
||||
ops.administrativeKick(player)
|
||||
sessionLogic.accountPersistence ! AccountPersistenceService.Kick(player.Name, time)
|
||||
}
|
||||
|
||||
|
|
@ -999,31 +1004,6 @@ class GeneralLogic(val ops: GeneralOperations) extends GeneralFunctions {
|
|||
|
||||
/* supporting functions */
|
||||
|
||||
/**
|
||||
* Enforce constraints on bulk purchases as determined by a given player's previous purchase times and hard acquisition delays.
|
||||
* Intended to assist in sanitizing loadout information from the perspective of the player, or target owner.
|
||||
* The equipment is expected to be unregistered and already fitted to their ultimate slot in the target container.
|
||||
* @param player the player whose purchasing constraints are to be tested
|
||||
* @param target the location in which the equipment will be stowed
|
||||
* @param slots the equipment, in the standard object-slot format container
|
||||
*/
|
||||
def applyPurchaseTimersBeforePackingLoadout(
|
||||
player: Player,
|
||||
target: PlanetSideServerObject with Container,
|
||||
slots: List[InventoryItem]
|
||||
): Unit = {
|
||||
slots.foreach { item =>
|
||||
player.avatar.purchaseCooldown(item.obj.Definition) match {
|
||||
case Some(_) => ()
|
||||
case None if Avatar.purchaseCooldowns.contains(item.obj.Definition) =>
|
||||
avatarActor ! AvatarActor.UpdatePurchaseTime(item.obj.Definition)
|
||||
TaskWorkflow.execute(PutLoadoutEquipmentInInventory(target)(item.obj, item.start))
|
||||
case None =>
|
||||
TaskWorkflow.execute(PutLoadoutEquipmentInInventory(target)(item.obj, item.start))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def handleUseDoor(door: Door, equipment: Option[Equipment]): Unit = {
|
||||
equipment match {
|
||||
case Some(tool: Tool) if tool.Definition == GlobalDefinitions.medicalapplicator =>
|
||||
|
|
@ -1493,23 +1473,6 @@ class GeneralLogic(val ops: GeneralOperations) extends GeneralFunctions {
|
|||
}
|
||||
}
|
||||
|
||||
def administrativeKick(tplayer: Player): Unit = {
|
||||
log.warn(s"${tplayer.Name} has been kicked by ${player.Name}")
|
||||
tplayer.death_by = -1
|
||||
sessionLogic.accountPersistence ! AccountPersistenceService.Kick(tplayer.Name)
|
||||
//get out of that vehicle
|
||||
sessionLogic.vehicles.GetMountableAndSeat(None, tplayer, continent) match {
|
||||
case (Some(obj), Some(seatNum)) =>
|
||||
tplayer.VehicleSeated = None
|
||||
obj.Seats(seatNum).unmount(tplayer)
|
||||
continent.VehicleEvents ! VehicleServiceMessage(
|
||||
continent.id,
|
||||
VehicleAction.KickPassenger(tplayer.GUID, seatNum, unk2=false, obj.GUID)
|
||||
)
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
|
||||
private def updateCollisionHistoryForTarget(
|
||||
target: PlanetSideServerObject with Vitality with FactionAffinity,
|
||||
curr: Long
|
||||
|
|
|
|||
|
|
@ -11,10 +11,14 @@ import net.psforever.services.Service
|
|||
import net.psforever.services.local.LocalResponse
|
||||
import net.psforever.types.{ChatMessageType, PlanetSideGUID, Vector3}
|
||||
|
||||
class LocalHandlerLogic(val ops: SessionLocalHandlers) extends LocalHandlerFunctions {
|
||||
def sessionLogic: SessionData = ops.sessionLogic
|
||||
object LocalHandlerLogic {
|
||||
def apply(ops: SessionLocalHandlers): LocalHandlerLogic = {
|
||||
new LocalHandlerLogic(ops, ops.context)
|
||||
}
|
||||
}
|
||||
|
||||
implicit val context: ActorContext = ops.context
|
||||
class LocalHandlerLogic(val ops: SessionLocalHandlers, implicit val context: ActorContext) extends LocalHandlerFunctions {
|
||||
def sessionLogic: SessionData = ops.sessionLogic
|
||||
|
||||
/* response handlers */
|
||||
|
||||
|
|
|
|||
|
|
@ -23,10 +23,14 @@ import net.psforever.types.{BailType, ChatMessageType, PlanetSideGUID, Vector3}
|
|||
|
||||
import scala.concurrent.duration._
|
||||
|
||||
class MountHandlerLogic(val ops: SessionMountHandlers) extends MountHandlerFunctions {
|
||||
def sessionLogic: SessionData = ops.sessionLogic
|
||||
object MountHandlerLogic {
|
||||
def apply(ops: SessionMountHandlers): MountHandlerLogic = {
|
||||
new MountHandlerLogic(ops, ops.context)
|
||||
}
|
||||
}
|
||||
|
||||
implicit val context: ActorContext = ops.context
|
||||
class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: ActorContext) extends MountHandlerFunctions {
|
||||
def sessionLogic: SessionData = ops.sessionLogic
|
||||
|
||||
private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor
|
||||
|
||||
|
|
@ -442,7 +446,7 @@ class MountHandlerLogic(val ops: SessionMountHandlers) extends MountHandlerFunct
|
|||
* @param obj the mountable object
|
||||
* @param seatNum the mount into which the player is mounting
|
||||
*/
|
||||
def MountingAction(tplayer: Player, obj: PlanetSideGameObject with FactionAffinity with InGameHistory, seatNum: Int): Unit = {
|
||||
private def MountingAction(tplayer: Player, obj: PlanetSideGameObject with FactionAffinity with InGameHistory, seatNum: Int): Unit = {
|
||||
val playerGuid: PlanetSideGUID = tplayer.GUID
|
||||
val objGuid: PlanetSideGUID = obj.GUID
|
||||
sessionLogic.actionsToCancel()
|
||||
|
|
@ -461,7 +465,7 @@ class MountHandlerLogic(val ops: SessionMountHandlers) extends MountHandlerFunct
|
|||
* @param obj the mountable object
|
||||
* @param seatNum the mount out of which which the player is disembarking
|
||||
*/
|
||||
def DismountVehicleAction(tplayer: Player, obj: PlanetSideGameObject with FactionAffinity with InGameHistory, seatNum: Int): Unit = {
|
||||
private def DismountVehicleAction(tplayer: Player, obj: PlanetSideGameObject with FactionAffinity with InGameHistory, seatNum: Int): Unit = {
|
||||
DismountAction(tplayer, obj, seatNum)
|
||||
//until vehicles maintain synchronized momentum without a driver
|
||||
obj match {
|
||||
|
|
@ -498,7 +502,7 @@ class MountHandlerLogic(val ops: SessionMountHandlers) extends MountHandlerFunct
|
|||
* @param obj the mountable object
|
||||
* @param seatNum the mount out of which which the player is disembarking
|
||||
*/
|
||||
def DismountAction(tplayer: Player, obj: PlanetSideGameObject with FactionAffinity with InGameHistory, seatNum: Int): Unit = {
|
||||
private def DismountAction(tplayer: Player, obj: PlanetSideGameObject with FactionAffinity with InGameHistory, seatNum: Int): Unit = {
|
||||
val playerGuid: PlanetSideGUID = tplayer.GUID
|
||||
tplayer.ContributionFrom(obj)
|
||||
sessionLogic.keepAliveFunc = sessionLogic.zoning.NormalKeepAlive
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ package net.psforever.actors.session.normal
|
|||
|
||||
import akka.actor.Actor.Receive
|
||||
import akka.actor.ActorRef
|
||||
import net.psforever.actors.session.support.{GeneralFunctions, LocalHandlerFunctions, MountHandlerFunctions, SquadHandlerFunctions, TerminalHandlerFunctions, VehicleFunctions, VehicleHandlerFunctions, WeaponAndProjectileFunctions}
|
||||
import net.psforever.packet.game.UplinkRequest
|
||||
//
|
||||
import net.psforever.actors.session.{AvatarActor, SessionActor}
|
||||
import net.psforever.actors.session.support.{ModeLogic, PlayerMode, SessionData, ZoningOperations}
|
||||
|
|
@ -29,16 +31,16 @@ import net.psforever.services.vehicle.VehicleServiceResponse
|
|||
import net.psforever.util.Config
|
||||
|
||||
class NormalModeLogic(data: SessionData) extends ModeLogic {
|
||||
val avatarResponse = new AvatarHandlerLogic(data.avatarResponse)
|
||||
val galaxy = new GalaxyHandlerLogic(data.galaxyResponseHandlers)
|
||||
val general = new GeneralLogic(data.general)
|
||||
val local = new LocalHandlerLogic(data.localResponse)
|
||||
val mountResponse = new MountHandlerLogic(data.mountResponse)
|
||||
val shooting = new WeaponAndProjectileLogic(data.shooting)
|
||||
val squad = new SquadHandlerLogic(data.squad)
|
||||
val terminals = new TerminalHandlerLogic(data.terminals)
|
||||
val vehicles = new VehicleLogic(data.vehicles)
|
||||
val vehicleResponse = new VehicleHandlerLogic(data.vehicleResponseOperations)
|
||||
val avatarResponse: AvatarHandlerLogic = AvatarHandlerLogic(data.avatarResponse)
|
||||
val galaxy: GalaxyHandlerLogic = GalaxyHandlerLogic(data.galaxyResponseHandlers)
|
||||
val general: GeneralFunctions = GeneralLogic(data.general)
|
||||
val local: LocalHandlerFunctions = LocalHandlerLogic(data.localResponse)
|
||||
val mountResponse: MountHandlerFunctions = MountHandlerLogic(data.mountResponse)
|
||||
val shooting: WeaponAndProjectileFunctions = WeaponAndProjectileLogic(data.shooting)
|
||||
val squad: SquadHandlerFunctions = SquadHandlerLogic(data.squad)
|
||||
val terminals: TerminalHandlerFunctions = TerminalHandlerLogic(data.terminals)
|
||||
val vehicles: VehicleFunctions = VehicleLogic(data.vehicles)
|
||||
val vehicleResponse: VehicleHandlerFunctions = VehicleHandlerLogic(data.vehicleResponseOperations)
|
||||
|
||||
def parse(sender: ActorRef): Receive = {
|
||||
/* really common messages (very frequently, every life) */
|
||||
|
|
@ -66,9 +68,6 @@ class NormalModeLogic(data: SessionData) extends ModeLogic {
|
|||
case VehicleServiceResponse(toChannel, guid, reply) =>
|
||||
vehicleResponse.handle(toChannel, guid, reply)
|
||||
|
||||
case SessionActor.PokeClient() =>
|
||||
data.sendResponse(KeepAliveMessage())
|
||||
|
||||
case SessionActor.SendResponse(packet) =>
|
||||
data.sendResponse(packet)
|
||||
|
||||
|
|
@ -407,6 +406,8 @@ class NormalModeLogic(data: SessionData) extends ModeLogic {
|
|||
case packet: WeaponLazeTargetPositionMessage =>
|
||||
shooting.handleWeaponLazeTargetPosition(packet)
|
||||
|
||||
case _: UplinkRequest => ()
|
||||
|
||||
case packet: HitMessage =>
|
||||
shooting.handleDirectHit(packet)
|
||||
|
||||
|
|
|
|||
|
|
@ -12,10 +12,14 @@ import net.psforever.services.chat.ChatService
|
|||
import net.psforever.services.teamwork.{SquadResponse, SquadServiceMessage, SquadAction => SquadServiceAction}
|
||||
import net.psforever.types.{ChatMessageType, PlanetSideGUID, SquadListDecoration, SquadResponseType, WaypointSubtype}
|
||||
|
||||
class SquadHandlerLogic(val ops: SessionSquadHandlers) extends SquadHandlerFunctions {
|
||||
def sessionLogic: SessionData = ops.sessionLogic
|
||||
object SquadHandlerLogic {
|
||||
def apply(ops: SessionSquadHandlers): SquadHandlerLogic = {
|
||||
new SquadHandlerLogic(ops, ops.context)
|
||||
}
|
||||
}
|
||||
|
||||
implicit val context: ActorContext = ops.context
|
||||
class SquadHandlerLogic(val ops: SessionSquadHandlers, implicit val context: ActorContext) extends SquadHandlerFunctions {
|
||||
def sessionLogic: SessionData = ops.sessionLogic
|
||||
|
||||
private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor
|
||||
|
||||
|
|
|
|||
|
|
@ -14,10 +14,14 @@ import net.psforever.objects.vital.TerminalUsedActivity
|
|||
import net.psforever.packet.game.{FavoritesAction, FavoritesRequest, ItemTransactionMessage, ItemTransactionResultMessage, ProximityTerminalUseMessage, UnuseItemMessage}
|
||||
import net.psforever.types.{TransactionType, Vector3}
|
||||
|
||||
class TerminalHandlerLogic(val ops: SessionTerminalHandlers) extends TerminalHandlerFunctions {
|
||||
def sessionLogic: SessionData = ops.sessionLogic
|
||||
object TerminalHandlerLogic {
|
||||
def apply(ops: SessionTerminalHandlers): TerminalHandlerLogic = {
|
||||
new TerminalHandlerLogic(ops, ops.context)
|
||||
}
|
||||
}
|
||||
|
||||
implicit val context: ActorContext = ops.context
|
||||
class TerminalHandlerLogic(val ops: SessionTerminalHandlers, implicit val context: ActorContext) extends TerminalHandlerFunctions {
|
||||
def sessionLogic: SessionData = ops.sessionLogic
|
||||
|
||||
private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor
|
||||
|
||||
|
|
|
|||
|
|
@ -15,10 +15,14 @@ import net.psforever.services.Service
|
|||
import net.psforever.services.vehicle.{VehicleResponse, VehicleServiceResponse}
|
||||
import net.psforever.types.{BailType, ChatMessageType, PlanetSideGUID, Vector3}
|
||||
|
||||
class VehicleHandlerLogic(val ops: SessionVehicleHandlers) extends VehicleHandlerFunctions {
|
||||
def sessionLogic: SessionData = ops.sessionLogic
|
||||
object VehicleHandlerLogic {
|
||||
def apply(ops: SessionVehicleHandlers): VehicleHandlerLogic = {
|
||||
new VehicleHandlerLogic(ops, ops.context)
|
||||
}
|
||||
}
|
||||
|
||||
implicit val context: ActorContext = ops.context
|
||||
class VehicleHandlerLogic(val ops: SessionVehicleHandlers, implicit val context: ActorContext) extends VehicleHandlerFunctions {
|
||||
def sessionLogic: SessionData = ops.sessionLogic
|
||||
|
||||
private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor
|
||||
|
||||
|
|
|
|||
|
|
@ -14,10 +14,14 @@ import net.psforever.packet.game.{ChildObjectStateMessage, DeployRequestMessage,
|
|||
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
|
||||
import net.psforever.types.{DriveState, Vector3}
|
||||
|
||||
class VehicleLogic(val ops: VehicleOperations) extends VehicleFunctions {
|
||||
def sessionLogic: SessionData = ops.sessionLogic
|
||||
object VehicleLogic {
|
||||
def apply(ops: VehicleOperations): VehicleLogic = {
|
||||
new VehicleLogic(ops, ops.context)
|
||||
}
|
||||
}
|
||||
|
||||
implicit val context: ActorContext = ops.context
|
||||
class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContext) extends VehicleFunctions {
|
||||
def sessionLogic: SessionData = ops.sessionLogic
|
||||
|
||||
private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor
|
||||
|
||||
|
|
@ -330,11 +334,11 @@ class VehicleLogic(val ops: VehicleOperations) extends VehicleFunctions {
|
|||
* if and only if the vehicle is known to this client and the `WorldSessioNActor`-global `player` occupies it;
|
||||
* `(None, None)`, otherwise (even if the vehicle can be determined)
|
||||
*/
|
||||
def GetMountableAndSeat(
|
||||
direct: Option[PlanetSideGameObject with Mountable],
|
||||
occupant: Player,
|
||||
zone: Zone
|
||||
): (Option[PlanetSideGameObject with Mountable], Option[Int]) =
|
||||
private def GetMountableAndSeat(
|
||||
direct: Option[PlanetSideGameObject with Mountable],
|
||||
occupant: Player,
|
||||
zone: Zone
|
||||
): (Option[PlanetSideGameObject with Mountable], Option[Int]) =
|
||||
direct.orElse(zone.GUID(occupant.VehicleSeated)) match {
|
||||
case Some(obj: PlanetSideGameObject with Mountable) =>
|
||||
obj.PassengerInSeat(occupant) match {
|
||||
|
|
@ -347,26 +351,6 @@ class VehicleLogic(val ops: VehicleOperations) extends VehicleFunctions {
|
|||
(None, None)
|
||||
}
|
||||
|
||||
/**
|
||||
* If the player is seated in a vehicle, find that vehicle and get the mount index number at which the player is sat.<br>
|
||||
* <br>
|
||||
* For special purposes involved in zone transfers,
|
||||
* where the vehicle may or may not exist in either of the zones (yet),
|
||||
* the value of `interstellarFerry` is also polled.
|
||||
* Making certain this field is blanked after the transfer is completed is important
|
||||
* to avoid inspecting the wrong vehicle and failing simple vehicle checks where this function may be employed.
|
||||
* @see `GetMountableAndSeat`
|
||||
* @see `interstellarFerry`
|
||||
* @return a tuple consisting of a vehicle reference and a mount index
|
||||
* if and only if the vehicle is known to this client and the `WorldSessioNActor`-global `player` occupies it;
|
||||
* `(None, None)`, otherwise (even if the vehicle can be determined)
|
||||
*/
|
||||
def GetKnownVehicleAndSeat(): (Option[Vehicle], Option[Int]) =
|
||||
GetMountableAndSeat(sessionLogic.zoning.interstellarFerry, player, continent) match {
|
||||
case (Some(v: Vehicle), Some(seat)) => (Some(v), Some(seat))
|
||||
case _ => (None, None)
|
||||
}
|
||||
|
||||
/**
|
||||
* If the player is seated in a vehicle, find that vehicle and get the mount index number at which the player is sat.
|
||||
* @see `GetMountableAndSeat`
|
||||
|
|
@ -374,7 +358,7 @@ class VehicleLogic(val ops: VehicleOperations) extends VehicleFunctions {
|
|||
* if and only if the vehicle is known to this client and the `WorldSessioNActor`-global `player` occupies it;
|
||||
* `(None, None)`, otherwise (even if the vehicle can be determined)
|
||||
*/
|
||||
def GetVehicleAndSeat(): (Option[Vehicle], Option[Int]) =
|
||||
private def GetVehicleAndSeat(): (Option[Vehicle], Option[Int]) =
|
||||
GetMountableAndSeat(None, player, continent) match {
|
||||
case (Some(v: Vehicle), Some(seat)) => (Some(v), Some(seat))
|
||||
case _ => (None, None)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
package net.psforever.actors.session.normal
|
||||
|
||||
import akka.actor.{ActorContext, typed}
|
||||
import net.psforever.actors.session.{AvatarActor, ChatActor}
|
||||
import net.psforever.actors.session.AvatarActor
|
||||
import net.psforever.actors.session.support.{SessionData, WeaponAndProjectileFunctions, WeaponAndProjectileOperations}
|
||||
import net.psforever.login.WorldSession.{CountAmmunition, CountGrenades, FindAmmoBoxThatUses, FindEquipmentStock, FindToolThatUses, PutEquipmentInInventoryOrDrop, PutNewEquipmentInInventoryOrDrop, RemoveOldEquipmentFromInventory}
|
||||
import net.psforever.objects.ballistics.{Projectile, ProjectileQuality}
|
||||
|
|
@ -26,7 +26,7 @@ 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.{Zone, ZoneProjectile}
|
||||
import net.psforever.packet.game.{AIDamage, AvatarGrenadeStateMessage, ChainLashMessage, ChangeAmmoMessage, ChangeFireModeMessage, ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, HitMessage, InventoryStateMessage, LashMessage, LongRangeProjectileInfoMessage, ObjectAttachMessage, ObjectDeleteMessage, ObjectDetachMessage, ProjectileStateMessage, QuantityUpdateMessage, ReloadMessage, SplashHitMessage, WeaponDelayFireMessage, WeaponDryFireMessage, WeaponFireMessage, WeaponLazeTargetPositionMessage}
|
||||
import net.psforever.packet.game.{AIDamage, AvatarGrenadeStateMessage, ChainLashMessage, ChangeAmmoMessage, ChangeFireModeMessage, ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, HitMessage, InventoryStateMessage, LashMessage, LongRangeProjectileInfoMessage, ObjectAttachMessage, ObjectDeleteMessage, ObjectDetachMessage, ProjectileStateMessage, QuantityUpdateMessage, ReloadMessage, SplashHitMessage, UplinkRequest, WeaponDelayFireMessage, WeaponDryFireMessage, WeaponFireMessage, WeaponLazeTargetPositionMessage}
|
||||
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
|
||||
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
|
||||
import net.psforever.types.{PlanetSideGUID, Vector3}
|
||||
|
|
@ -36,15 +36,109 @@ import scala.concurrent.ExecutionContext.Implicits.global
|
|||
import scala.concurrent.Future
|
||||
import scala.concurrent.duration._
|
||||
|
||||
class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations) extends WeaponAndProjectileFunctions {
|
||||
object WeaponAndProjectileLogic {
|
||||
def apply(ops: WeaponAndProjectileOperations): WeaponAndProjectileLogic = {
|
||||
new WeaponAndProjectileLogic(ops, ops.context)
|
||||
}
|
||||
|
||||
/**
|
||||
* Does a line segment line intersect with a sphere?<br>
|
||||
* This most likely belongs in `Geometry` or `GeometryForm` or somehow in association with the `\objects\geometry\` package.
|
||||
* @param start first point of the line segment
|
||||
* @param end second point of the line segment
|
||||
* @param center center of the sphere
|
||||
* @param radius radius of the sphere
|
||||
* @return list of all points of intersection, if any
|
||||
* @see `Vector3.DistanceSquared`
|
||||
* @see `Vector3.MagnitudeSquared`
|
||||
*/
|
||||
private def quickLineSphereIntersectionPoints(
|
||||
start: Vector3,
|
||||
end: Vector3,
|
||||
center: Vector3,
|
||||
radius: Float
|
||||
): Iterable[Vector3] = {
|
||||
/*
|
||||
Algorithm adapted from code found on https://paulbourke.net/geometry/circlesphere/index.html#linesphere,
|
||||
because I kept messing up proper substitution of the line formula and the circle formula into the quadratic equation.
|
||||
*/
|
||||
val Vector3(cx, cy, cz) = center
|
||||
val Vector3(sx, sy, sz) = start
|
||||
val vector = end - start
|
||||
//speed our way through a quadratic equation
|
||||
val (a, b) = {
|
||||
val Vector3(dx, dy, dz) = vector
|
||||
(
|
||||
dx * dx + dy * dy + dz * dz,
|
||||
2f * (dx * (sx - cx) + dy * (sy - cy) + dz * (sz - cz))
|
||||
)
|
||||
}
|
||||
val c = Vector3.MagnitudeSquared(center) + Vector3.MagnitudeSquared(start) - 2f * (cx * sx + cy * sy + cz * sz) - radius * radius
|
||||
val result = b * b - 4 * a * c
|
||||
if (result < 0f) {
|
||||
//negative, no intersection
|
||||
Seq()
|
||||
} else if (result < 0.00001f) {
|
||||
//zero-ish, one intersection point
|
||||
Seq(start - vector * (b / (2f * a)))
|
||||
} else {
|
||||
//positive, two intersection points
|
||||
val sqrt = math.sqrt(result).toFloat
|
||||
val endStart = vector / (2f * a)
|
||||
Seq(start + endStart * (sqrt - b), start + endStart * (b + sqrt) * -1f)
|
||||
}.filter(p => Vector3.DistanceSquared(start, p) <= a)
|
||||
}
|
||||
/**
|
||||
* Preparation for explosion damage that utilizes the Scorpion's little buddy sub-projectiles.
|
||||
* The main difference from "normal" server-side explosion
|
||||
* is that the owner of the projectile must be clarified explicitly.
|
||||
* @see `Zone::serverSideDamage`
|
||||
* @param zone where the explosion is taking place
|
||||
* (`source` contains the coordinate location)
|
||||
* @param source a game object that represents the source of the explosion
|
||||
* @param owner who or what to accredit damage from the explosion to;
|
||||
* clarifies a normal `SourceEntry(source)` accreditation
|
||||
*/
|
||||
private def detonateLittleBuddy(
|
||||
zone: Zone,
|
||||
source: PlanetSideGameObject with FactionAffinity with Vitality,
|
||||
proxy: Projectile,
|
||||
owner: SourceEntry
|
||||
)(): Unit = {
|
||||
Zone.serverSideDamage(zone, source, littleBuddyExplosionDamage(owner, proxy.id, source.Position))
|
||||
}
|
||||
|
||||
/**
|
||||
* Preparation for explosion damage that utilizes the Scorpion's little buddy sub-projectiles.
|
||||
* The main difference from "normal" server-side explosion
|
||||
* is that the owner of the projectile must be clarified explicitly.
|
||||
* The sub-projectiles will be the product of a normal projectile rather than a standard game object
|
||||
* so a custom `source` entity must wrap around it and fulfill the requirements of the field.
|
||||
* @see `Zone::explosionDamage`
|
||||
* @param owner who or what to accredit damage from the explosion to
|
||||
* @param explosionPosition where the explosion will be positioned in the game world
|
||||
* @param source a game object that represents the source of the explosion
|
||||
* @param target a game object that is affected by the explosion
|
||||
* @return a `DamageInteraction` object
|
||||
*/
|
||||
private def littleBuddyExplosionDamage(
|
||||
owner: SourceEntry,
|
||||
projectileId: Long,
|
||||
explosionPosition: Vector3
|
||||
)
|
||||
(
|
||||
source: PlanetSideGameObject with FactionAffinity with Vitality,
|
||||
target: PlanetSideGameObject with FactionAffinity with Vitality
|
||||
): DamageInteraction = {
|
||||
DamageInteraction(SourceEntry(target), OicwLilBuddyReason(owner, projectileId, target.DamageModel), explosionPosition)
|
||||
}
|
||||
}
|
||||
|
||||
class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit val context: ActorContext) extends WeaponAndProjectileFunctions {
|
||||
def sessionLogic: SessionData = ops.sessionLogic
|
||||
|
||||
implicit val context: ActorContext = ops.context
|
||||
|
||||
private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor
|
||||
|
||||
private val chatActor: typed.ActorRef[ChatActor.Command] = ops.chatActor
|
||||
|
||||
/* packets */
|
||||
|
||||
def handleWeaponFire(pkt: WeaponFireMessage): Unit = {
|
||||
|
|
@ -107,6 +201,10 @@ class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations) extends W
|
|||
log.info(s"${player.Name} is lazing a position$purpose")
|
||||
}
|
||||
|
||||
def handleUplinkRequest(packet: UplinkRequest): Unit = {
|
||||
sessionLogic.administrativeKick(player)
|
||||
}
|
||||
|
||||
def handleAvatarGrenadeState(pkt: AvatarGrenadeStateMessage): Unit = {
|
||||
val AvatarGrenadeStateMessage(_, state) = pkt
|
||||
//TODO I thought I had this working?
|
||||
|
|
@ -1242,97 +1340,3 @@ class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations) extends W
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
object WeaponAndProjectileLogic {
|
||||
/**
|
||||
* Does a line segment line intersect with a sphere?<br>
|
||||
* This most likely belongs in `Geometry` or `GeometryForm` or somehow in association with the `\objects\geometry\` package.
|
||||
* @param start first point of the line segment
|
||||
* @param end second point of the line segment
|
||||
* @param center center of the sphere
|
||||
* @param radius radius of the sphere
|
||||
* @return list of all points of intersection, if any
|
||||
* @see `Vector3.DistanceSquared`
|
||||
* @see `Vector3.MagnitudeSquared`
|
||||
*/
|
||||
private def quickLineSphereIntersectionPoints(
|
||||
start: Vector3,
|
||||
end: Vector3,
|
||||
center: Vector3,
|
||||
radius: Float
|
||||
): Iterable[Vector3] = {
|
||||
/*
|
||||
Algorithm adapted from code found on https://paulbourke.net/geometry/circlesphere/index.html#linesphere,
|
||||
because I kept messing up proper substitution of the line formula and the circle formula into the quadratic equation.
|
||||
*/
|
||||
val Vector3(cx, cy, cz) = center
|
||||
val Vector3(sx, sy, sz) = start
|
||||
val vector = end - start
|
||||
//speed our way through a quadratic equation
|
||||
val (a, b) = {
|
||||
val Vector3(dx, dy, dz) = vector
|
||||
(
|
||||
dx * dx + dy * dy + dz * dz,
|
||||
2f * (dx * (sx - cx) + dy * (sy - cy) + dz * (sz - cz))
|
||||
)
|
||||
}
|
||||
val c = Vector3.MagnitudeSquared(center) + Vector3.MagnitudeSquared(start) - 2f * (cx * sx + cy * sy + cz * sz) - radius * radius
|
||||
val result = b * b - 4 * a * c
|
||||
if (result < 0f) {
|
||||
//negative, no intersection
|
||||
Seq()
|
||||
} else if (result < 0.00001f) {
|
||||
//zero-ish, one intersection point
|
||||
Seq(start - vector * (b / (2f * a)))
|
||||
} else {
|
||||
//positive, two intersection points
|
||||
val sqrt = math.sqrt(result).toFloat
|
||||
val endStart = vector / (2f * a)
|
||||
Seq(start + endStart * (sqrt - b), start + endStart * (b + sqrt) * -1f)
|
||||
}.filter(p => Vector3.DistanceSquared(start, p) <= a)
|
||||
}
|
||||
/**
|
||||
* Preparation for explosion damage that utilizes the Scorpion's little buddy sub-projectiles.
|
||||
* The main difference from "normal" server-side explosion
|
||||
* is that the owner of the projectile must be clarified explicitly.
|
||||
* @see `Zone::serverSideDamage`
|
||||
* @param zone where the explosion is taking place
|
||||
* (`source` contains the coordinate location)
|
||||
* @param source a game object that represents the source of the explosion
|
||||
* @param owner who or what to accredit damage from the explosion to;
|
||||
* clarifies a normal `SourceEntry(source)` accreditation
|
||||
*/
|
||||
private def detonateLittleBuddy(
|
||||
zone: Zone,
|
||||
source: PlanetSideGameObject with FactionAffinity with Vitality,
|
||||
proxy: Projectile,
|
||||
owner: SourceEntry
|
||||
)(): Unit = {
|
||||
Zone.serverSideDamage(zone, source, littleBuddyExplosionDamage(owner, proxy.id, source.Position))
|
||||
}
|
||||
|
||||
/**
|
||||
* Preparation for explosion damage that utilizes the Scorpion's little buddy sub-projectiles.
|
||||
* The main difference from "normal" server-side explosion
|
||||
* is that the owner of the projectile must be clarified explicitly.
|
||||
* The sub-projectiles will be the product of a normal projectile rather than a standard game object
|
||||
* so a custom `source` entity must wrap around it and fulfill the requirements of the field.
|
||||
* @see `Zone::explosionDamage`
|
||||
* @param owner who or what to accredit damage from the explosion to
|
||||
* @param explosionPosition where the explosion will be positioned in the game world
|
||||
* @param source a game object that represents the source of the explosion
|
||||
* @param target a game object that is affected by the explosion
|
||||
* @return a `DamageInteraction` object
|
||||
*/
|
||||
private def littleBuddyExplosionDamage(
|
||||
owner: SourceEntry,
|
||||
projectileId: Long,
|
||||
explosionPosition: Vector3
|
||||
)
|
||||
(
|
||||
source: PlanetSideGameObject with FactionAffinity with Vitality,
|
||||
target: PlanetSideGameObject with FactionAffinity with Vitality
|
||||
): DamageInteraction = {
|
||||
DamageInteraction(SourceEntry(target), OicwLilBuddyReason(owner, projectileId, target.DamageModel), explosionPosition)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,573 @@
|
|||
// Copyright (c) 2024 PSForever
|
||||
package net.psforever.actors.session.spectator
|
||||
|
||||
import akka.actor.{ActorContext, typed}
|
||||
import net.psforever.actors.session.support.AvatarHandlerFunctions
|
||||
|
||||
import scala.concurrent.duration._
|
||||
//
|
||||
import net.psforever.actors.session.AvatarActor
|
||||
import net.psforever.actors.session.support.{SessionAvatarHandlers, SessionData}
|
||||
import net.psforever.login.WorldSession.{DropEquipmentFromInventory, DropLeftovers, HoldNewEquipmentUp}
|
||||
import net.psforever.objects.{GlobalDefinitions, Player, Tool, Vehicle}
|
||||
import net.psforever.objects.guid.{GUIDTask, TaskWorkflow}
|
||||
import net.psforever.objects.inventory.InventoryItem
|
||||
import net.psforever.objects.serverobject.pad.VehicleSpawnPad
|
||||
import net.psforever.objects.serverobject.terminals.{ProximityUnit, Terminal}
|
||||
import net.psforever.objects.vital.etc.ExplodingEntityReason
|
||||
import net.psforever.objects.zones.Zoning
|
||||
import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent
|
||||
import net.psforever.packet.game.{ArmorChangedMessage, AvatarDeadStateMessage, ChangeAmmoMessage, ChangeFireModeMessage, ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, ChatMsg, DeadState, DestroyMessage, DrowningTarget, GenericActionMessage, GenericObjectActionMessage, HitHint, ItemTransactionResultMessage, ObjectCreateDetailedMessage, ObjectCreateMessage, ObjectDeleteMessage, ObjectHeldMessage, OxygenStateMessage, PlanetsideAttributeMessage, PlayerStateMessage, ProjectileStateMessage, ReloadMessage, SetEmpireMessage, UseItemMessage, WeaponDryFireMessage}
|
||||
import net.psforever.services.avatar.{AvatarAction, AvatarResponse, AvatarServiceMessage}
|
||||
import net.psforever.services.Service
|
||||
import net.psforever.types.{ChatMessageType, PlanetSideGUID, TransactionType, Vector3}
|
||||
import net.psforever.util.Config
|
||||
|
||||
object AvatarHandlerLogic {
|
||||
def apply(ops: SessionAvatarHandlers): AvatarHandlerLogic = {
|
||||
new AvatarHandlerLogic(ops, ops.context)
|
||||
}
|
||||
}
|
||||
|
||||
class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: ActorContext) extends AvatarHandlerFunctions {
|
||||
def sessionLogic: SessionData = ops.sessionLogic
|
||||
|
||||
private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor
|
||||
|
||||
/**
|
||||
* na
|
||||
* @param toChannel na
|
||||
* @param guid na
|
||||
* @param reply na
|
||||
*/
|
||||
def handle(toChannel: String, guid: PlanetSideGUID, reply: AvatarResponse.Response): Unit = {
|
||||
val resolvedPlayerGuid = if (player != null && player.HasGUID) {
|
||||
player.GUID
|
||||
} else {
|
||||
Service.defaultPlayerGUID
|
||||
}
|
||||
val isNotSameTarget = resolvedPlayerGuid != guid
|
||||
val isSameTarget = !isNotSameTarget
|
||||
reply match {
|
||||
/* special messages */
|
||||
case AvatarResponse.TeardownConnection() =>
|
||||
log.trace(s"ending ${player.Name}'s old session by event system request (relog)")
|
||||
context.stop(context.self)
|
||||
|
||||
/* really common messages (very frequently, every life) */
|
||||
case pstate @ AvatarResponse.PlayerState(
|
||||
pos,
|
||||
vel,
|
||||
yaw,
|
||||
pitch,
|
||||
yawUpper,
|
||||
_,
|
||||
isCrouching,
|
||||
isJumping,
|
||||
jumpThrust,
|
||||
isCloaking,
|
||||
isNotRendered,
|
||||
canSeeReallyFar
|
||||
) if isNotSameTarget =>
|
||||
val pstateToSave = pstate.copy(timestamp = 0)
|
||||
val (lastMsg, lastTime, lastPosition, wasVisible, wasShooting) = ops.lastSeenStreamMessage.get(guid.guid) match {
|
||||
case Some(SessionAvatarHandlers.LastUpstream(Some(msg), visible, shooting, time)) => (Some(msg), time, msg.pos, visible, shooting)
|
||||
case _ => (None, 0L, Vector3.Zero, false, None)
|
||||
}
|
||||
val drawConfig = Config.app.game.playerDraw //m
|
||||
val maxRange = drawConfig.rangeMax * drawConfig.rangeMax //sq.m
|
||||
val ourPosition = player.Position //xyz
|
||||
val currentDistance = Vector3.DistanceSquared(ourPosition, pos) //sq.m
|
||||
val inDrawableRange = currentDistance <= maxRange
|
||||
val now = System.currentTimeMillis() //ms
|
||||
if (
|
||||
sessionLogic.zoning.zoningStatus != Zoning.Status.Deconstructing &&
|
||||
!isNotRendered && inDrawableRange
|
||||
) {
|
||||
//conditions where visibility is assured
|
||||
val durationSince = now - lastTime //ms
|
||||
lazy val previouslyInDrawableRange = Vector3.DistanceSquared(ourPosition, lastPosition) <= maxRange
|
||||
lazy val targetDelay = {
|
||||
val populationOver = math.max(
|
||||
0,
|
||||
sessionLogic.localSector.livePlayerList.size - drawConfig.populationThreshold
|
||||
)
|
||||
val distanceAdjustment = math.pow(populationOver / drawConfig.populationStep * drawConfig.rangeStep, 2) //sq.m
|
||||
val adjustedDistance = currentDistance + distanceAdjustment //sq.m
|
||||
drawConfig.ranges.lastIndexWhere { dist => adjustedDistance > dist * dist } match {
|
||||
case -1 => 1
|
||||
case index => drawConfig.delays(index)
|
||||
}
|
||||
} //ms
|
||||
if (!wasVisible ||
|
||||
!previouslyInDrawableRange ||
|
||||
durationSince > drawConfig.delayMax ||
|
||||
(!lastMsg.contains(pstateToSave) &&
|
||||
(canSeeReallyFar ||
|
||||
currentDistance < drawConfig.rangeMin * drawConfig.rangeMin ||
|
||||
sessionLogic.general.canSeeReallyFar ||
|
||||
durationSince > targetDelay
|
||||
)
|
||||
)
|
||||
) {
|
||||
//must draw
|
||||
sendResponse(
|
||||
PlayerStateMessage(
|
||||
guid,
|
||||
pos,
|
||||
vel,
|
||||
yaw,
|
||||
pitch,
|
||||
yawUpper,
|
||||
timestamp = 0, //is this okay?
|
||||
isCrouching,
|
||||
isJumping,
|
||||
jumpThrust,
|
||||
isCloaking
|
||||
)
|
||||
)
|
||||
ops.lastSeenStreamMessage.put(guid.guid, SessionAvatarHandlers.LastUpstream(Some(pstateToSave), visible=true, wasShooting, now))
|
||||
} else {
|
||||
//is visible, but skip reinforcement
|
||||
ops.lastSeenStreamMessage.put(guid.guid, SessionAvatarHandlers.LastUpstream(Some(pstateToSave), visible=true, wasShooting, lastTime))
|
||||
}
|
||||
} else {
|
||||
//conditions where the target is not currently visible
|
||||
if (wasVisible) {
|
||||
//the target was JUST PREVIOUSLY visible; one last draw to move target beyond a renderable distance
|
||||
val lat = (1 + ops.hidingPlayerRandomizer.nextInt(continent.map.scale.height.toInt)).toFloat
|
||||
sendResponse(
|
||||
PlayerStateMessage(
|
||||
guid,
|
||||
Vector3(1f, lat, 1f),
|
||||
vel=None,
|
||||
facingYaw=0f,
|
||||
facingPitch=0f,
|
||||
facingYawUpper=0f,
|
||||
timestamp=0, //is this okay?
|
||||
is_cloaked = isCloaking
|
||||
)
|
||||
)
|
||||
ops.lastSeenStreamMessage.put(guid.guid, SessionAvatarHandlers.LastUpstream(Some(pstateToSave), visible=false, wasShooting, now))
|
||||
} else {
|
||||
//skip drawing altogether
|
||||
ops.lastSeenStreamMessage.put(guid.guid, SessionAvatarHandlers.LastUpstream(Some(pstateToSave), visible=false, wasShooting, lastTime))
|
||||
}
|
||||
}
|
||||
|
||||
case AvatarResponse.ObjectHeld(slot, _)
|
||||
if isSameTarget && player.VisibleSlots.contains(slot) =>
|
||||
sendResponse(ObjectHeldMessage(guid, slot, unk1=true))
|
||||
//Stop using proximity terminals if player unholsters a weapon
|
||||
continent.GUID(sessionLogic.terminals.usingMedicalTerminal).collect {
|
||||
case term: Terminal with ProximityUnit => sessionLogic.terminals.StopUsingProximityUnit(term)
|
||||
}
|
||||
if (sessionLogic.zoning.zoningStatus == Zoning.Status.Deconstructing) {
|
||||
sessionLogic.zoning.spawn.stopDeconstructing()
|
||||
}
|
||||
|
||||
case AvatarResponse.ObjectHeld(slot, _)
|
||||
if isSameTarget && slot > -1 =>
|
||||
sendResponse(ObjectHeldMessage(guid, slot, unk1=true))
|
||||
|
||||
case AvatarResponse.ObjectHeld(_, _)
|
||||
if isSameTarget => ()
|
||||
|
||||
case AvatarResponse.ObjectHeld(_, previousSlot) =>
|
||||
sendResponse(ObjectHeldMessage(guid, previousSlot, unk1=false))
|
||||
|
||||
case AvatarResponse.ChangeFireState_Start(weaponGuid)
|
||||
if isNotSameTarget && ops.lastSeenStreamMessage.get(guid.guid).exists { _.visible } =>
|
||||
sendResponse(ChangeFireStateMessage_Start(weaponGuid))
|
||||
val entry = ops.lastSeenStreamMessage(guid.guid)
|
||||
ops.lastSeenStreamMessage.put(guid.guid, entry.copy(shooting = Some(weaponGuid)))
|
||||
|
||||
case AvatarResponse.ChangeFireState_Start(weaponGuid)
|
||||
if isNotSameTarget =>
|
||||
sendResponse(ChangeFireStateMessage_Start(weaponGuid))
|
||||
|
||||
case AvatarResponse.ChangeFireState_Stop(weaponGuid)
|
||||
if isNotSameTarget && ops.lastSeenStreamMessage.get(guid.guid).exists { msg => msg.visible || msg.shooting.nonEmpty } =>
|
||||
sendResponse(ChangeFireStateMessage_Stop(weaponGuid))
|
||||
val entry = ops.lastSeenStreamMessage(guid.guid)
|
||||
ops.lastSeenStreamMessage.put(guid.guid, entry.copy(shooting = None))
|
||||
|
||||
case AvatarResponse.ChangeFireState_Stop(weaponGuid)
|
||||
if isNotSameTarget =>
|
||||
sendResponse(ChangeFireStateMessage_Stop(weaponGuid))
|
||||
|
||||
case AvatarResponse.LoadPlayer(pkt) if isNotSameTarget =>
|
||||
sendResponse(pkt)
|
||||
|
||||
case AvatarResponse.EquipmentInHand(pkt) if isNotSameTarget =>
|
||||
sendResponse(pkt)
|
||||
|
||||
case AvatarResponse.PlanetsideAttribute(attributeType, attributeValue) if isNotSameTarget =>
|
||||
sendResponse(PlanetsideAttributeMessage(guid, attributeType, attributeValue))
|
||||
|
||||
case AvatarResponse.PlanetsideAttributeToAll(attributeType, attributeValue) =>
|
||||
sendResponse(PlanetsideAttributeMessage(guid, attributeType, attributeValue))
|
||||
|
||||
case AvatarResponse.PlanetsideAttributeSelf(attributeType, attributeValue) if isSameTarget =>
|
||||
sendResponse(PlanetsideAttributeMessage(guid, attributeType, attributeValue))
|
||||
|
||||
case AvatarResponse.GenericObjectAction(objectGuid, actionCode) if isNotSameTarget =>
|
||||
sendResponse(GenericObjectActionMessage(objectGuid, actionCode))
|
||||
|
||||
case AvatarResponse.HitHint(sourceGuid) if player.isAlive =>
|
||||
sendResponse(HitHint(sourceGuid, guid))
|
||||
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_dmg")
|
||||
|
||||
case AvatarResponse.Destroy(victim, killer, weapon, pos) =>
|
||||
// guid = victim // killer = killer
|
||||
sendResponse(DestroyMessage(victim, killer, weapon, pos))
|
||||
|
||||
case AvatarResponse.DestroyDisplay(killer, victim, method, unk) =>
|
||||
sendResponse(ops.destroyDisplayMessage(killer, victim, method, unk))
|
||||
|
||||
case AvatarResponse.TerminalOrderResult(terminalGuid, action, result)
|
||||
if result && (action == TransactionType.Buy || action == TransactionType.Loadout) =>
|
||||
sendResponse(ItemTransactionResultMessage(terminalGuid, action, result))
|
||||
sessionLogic.terminals.lastTerminalOrderFulfillment = true
|
||||
AvatarActor.savePlayerData(player)
|
||||
sessionLogic.general.renewCharSavedTimer(
|
||||
Config.app.game.savedMsg.interruptedByAction.fixed,
|
||||
Config.app.game.savedMsg.interruptedByAction.variable
|
||||
)
|
||||
|
||||
case AvatarResponse.TerminalOrderResult(terminalGuid, action, result) =>
|
||||
sendResponse(ItemTransactionResultMessage(terminalGuid, action, result))
|
||||
sessionLogic.terminals.lastTerminalOrderFulfillment = true
|
||||
|
||||
case AvatarResponse.ChangeExosuit(
|
||||
target,
|
||||
armor,
|
||||
exosuit,
|
||||
subtype,
|
||||
_,
|
||||
maxhand,
|
||||
oldHolsters,
|
||||
holsters,
|
||||
oldInventory,
|
||||
inventory,
|
||||
drop,
|
||||
delete
|
||||
) if resolvedPlayerGuid == target =>
|
||||
sendResponse(ArmorChangedMessage(target, exosuit, subtype))
|
||||
sendResponse(PlanetsideAttributeMessage(target, attribute_type=4, armor))
|
||||
//happening to this player
|
||||
//cleanup
|
||||
sendResponse(ObjectHeldMessage(target, Player.HandsDownSlot, unk1=false))
|
||||
(oldHolsters ++ oldInventory ++ delete).foreach {
|
||||
case (_, dguid) => sendResponse(ObjectDeleteMessage(dguid, unk1=0))
|
||||
}
|
||||
//functionally delete
|
||||
delete.foreach { case (obj, _) => TaskWorkflow.execute(GUIDTask.unregisterEquipment(continent.GUID, obj)) }
|
||||
//redraw
|
||||
if (maxhand) {
|
||||
TaskWorkflow.execute(HoldNewEquipmentUp(player)(
|
||||
Tool(GlobalDefinitions.MAXArms(subtype, player.Faction)),
|
||||
0
|
||||
))
|
||||
}
|
||||
//draw free hand
|
||||
player.FreeHand.Equipment.foreach { obj =>
|
||||
val definition = obj.Definition
|
||||
sendResponse(
|
||||
ObjectCreateDetailedMessage(
|
||||
definition.ObjectId,
|
||||
obj.GUID,
|
||||
ObjectCreateMessageParent(target, Player.FreeHandSlot),
|
||||
definition.Packet.DetailedConstructorData(obj).get
|
||||
)
|
||||
)
|
||||
}
|
||||
//draw holsters and inventory
|
||||
(holsters ++ inventory).foreach {
|
||||
case InventoryItem(obj, index) =>
|
||||
val definition = obj.Definition
|
||||
sendResponse(
|
||||
ObjectCreateDetailedMessage(
|
||||
definition.ObjectId,
|
||||
obj.GUID,
|
||||
ObjectCreateMessageParent(target, index),
|
||||
definition.Packet.DetailedConstructorData(obj).get
|
||||
)
|
||||
)
|
||||
}
|
||||
DropLeftovers(player)(drop)
|
||||
|
||||
case AvatarResponse.ChangeExosuit(target, armor, exosuit, subtype, slot, _, oldHolsters, holsters, _, _, _, delete) =>
|
||||
sendResponse(ArmorChangedMessage(target, exosuit, subtype))
|
||||
sendResponse(PlanetsideAttributeMessage(target, attribute_type=4, armor))
|
||||
//happening to some other player
|
||||
sendResponse(ObjectHeldMessage(target, slot, unk1 = false))
|
||||
//cleanup
|
||||
(oldHolsters ++ delete).foreach { case (_, guid) => sendResponse(ObjectDeleteMessage(guid, unk1=0)) }
|
||||
//draw holsters
|
||||
holsters.foreach {
|
||||
case InventoryItem(obj, index) =>
|
||||
val definition = obj.Definition
|
||||
sendResponse(
|
||||
ObjectCreateMessage(
|
||||
definition.ObjectId,
|
||||
obj.GUID,
|
||||
ObjectCreateMessageParent(target, index),
|
||||
definition.Packet.ConstructorData(obj).get
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
case AvatarResponse.ChangeLoadout(
|
||||
target,
|
||||
armor,
|
||||
exosuit,
|
||||
subtype,
|
||||
_,
|
||||
maxhand,
|
||||
oldHolsters,
|
||||
holsters,
|
||||
oldInventory,
|
||||
inventory,
|
||||
drops
|
||||
) if resolvedPlayerGuid == target =>
|
||||
sendResponse(ArmorChangedMessage(target, exosuit, subtype))
|
||||
sendResponse(PlanetsideAttributeMessage(target, attribute_type = 4, armor))
|
||||
//happening to this player
|
||||
sendResponse(ObjectHeldMessage(target, Player.HandsDownSlot, unk1=true))
|
||||
//cleanup
|
||||
(oldHolsters ++ oldInventory).foreach {
|
||||
case (obj, objGuid) =>
|
||||
sendResponse(ObjectDeleteMessage(objGuid, unk1=0))
|
||||
TaskWorkflow.execute(GUIDTask.unregisterEquipment(continent.GUID, obj))
|
||||
}
|
||||
drops.foreach(item => sendResponse(ObjectDeleteMessage(item.obj.GUID, unk1=0)))
|
||||
//redraw
|
||||
if (maxhand) {
|
||||
TaskWorkflow.execute(HoldNewEquipmentUp(player)(
|
||||
Tool(GlobalDefinitions.MAXArms(subtype, player.Faction)),
|
||||
slot = 0
|
||||
))
|
||||
}
|
||||
sessionLogic.general.applyPurchaseTimersBeforePackingLoadout(player, player, holsters ++ inventory)
|
||||
DropLeftovers(player)(drops)
|
||||
|
||||
case AvatarResponse.ChangeLoadout(target, armor, exosuit, subtype, slot, _, oldHolsters, _, _, _, _) =>
|
||||
//redraw handled by callbacks
|
||||
sendResponse(ArmorChangedMessage(target, exosuit, subtype))
|
||||
sendResponse(PlanetsideAttributeMessage(target, attribute_type=4, armor))
|
||||
//happening to some other player
|
||||
sendResponse(ObjectHeldMessage(target, slot, unk1=false))
|
||||
//cleanup
|
||||
oldHolsters.foreach { case (_, guid) => sendResponse(ObjectDeleteMessage(guid, unk1=0)) }
|
||||
|
||||
case AvatarResponse.UseKit(kguid, kObjId) =>
|
||||
sendResponse(
|
||||
UseItemMessage(
|
||||
resolvedPlayerGuid,
|
||||
kguid,
|
||||
resolvedPlayerGuid,
|
||||
unk2 = 4294967295L,
|
||||
unk3 = false,
|
||||
unk4 = Vector3.Zero,
|
||||
unk5 = Vector3.Zero,
|
||||
unk6 = 126,
|
||||
unk7 = 0, //sequence time?
|
||||
unk8 = 137,
|
||||
kObjId
|
||||
)
|
||||
)
|
||||
sendResponse(ObjectDeleteMessage(kguid, unk1=0))
|
||||
|
||||
case AvatarResponse.KitNotUsed(_, "") =>
|
||||
sessionLogic.general.kitToBeUsed = None
|
||||
|
||||
case AvatarResponse.KitNotUsed(_, msg) =>
|
||||
sessionLogic.general.kitToBeUsed = None
|
||||
sendResponse(ChatMsg(ChatMessageType.UNK_225, msg))
|
||||
|
||||
case AvatarResponse.UpdateKillsDeathsAssists(_, kda) =>
|
||||
avatarActor ! AvatarActor.UpdateKillsDeathsAssists(kda)
|
||||
|
||||
case AvatarResponse.AwardBep(charId, bep, expType) =>
|
||||
//if the target player, always award (some) BEP
|
||||
if (charId == player.CharId) {
|
||||
avatarActor ! AvatarActor.AwardBep(bep, expType)
|
||||
}
|
||||
|
||||
case AvatarResponse.AwardCep(charId, cep) =>
|
||||
//if the target player, always award (some) CEP
|
||||
if (charId == player.CharId) {
|
||||
avatarActor ! AvatarActor.AwardCep(cep)
|
||||
}
|
||||
|
||||
case AvatarResponse.FacilityCaptureRewards(buildingId, zoneNumber, cep) =>
|
||||
ops.facilityCaptureRewards(buildingId, zoneNumber, cep)
|
||||
|
||||
case AvatarResponse.SendResponse(msg) =>
|
||||
sendResponse(msg)
|
||||
|
||||
case AvatarResponse.SendResponseTargeted(targetGuid, msg) if resolvedPlayerGuid == targetGuid =>
|
||||
sendResponse(msg)
|
||||
|
||||
/* common messages (maybe once every respawn) */
|
||||
case AvatarResponse.Reload(itemGuid)
|
||||
if isNotSameTarget && ops.lastSeenStreamMessage.get(guid.guid).exists { _.visible } =>
|
||||
sendResponse(ReloadMessage(itemGuid, ammo_clip=1, unk1=0))
|
||||
|
||||
case AvatarResponse.Killed(mount) =>
|
||||
//log and chat messages
|
||||
val cause = player.LastDamage.flatMap { damage =>
|
||||
val interaction = damage.interaction
|
||||
val reason = interaction.cause
|
||||
val adversarial = interaction.adversarial.map { _.attacker }
|
||||
reason match {
|
||||
case r: ExplodingEntityReason if r.entity.isInstanceOf[VehicleSpawnPad] =>
|
||||
//also, @SVCP_Killed_TooCloseToPadOnCreate^n~ or "... within n meters of pad ..."
|
||||
sendResponse(ChatMsg(ChatMessageType.UNK_227, "@SVCP_Killed_OnPadOnCreate"))
|
||||
case _ => ()
|
||||
}
|
||||
adversarial.map {_.Name }.orElse { Some(s"a ${reason.getClass.getSimpleName}") }
|
||||
}.getOrElse { s"an unfortunate circumstance (probably ${player.Sex.pronounObject} own fault)" }
|
||||
log.info(s"${player.Name} has died, killed by $cause")
|
||||
if (sessionLogic.shooting.shotsWhileDead > 0) {
|
||||
log.warn(
|
||||
s"SHOTS_WHILE_DEAD: client of ${avatar.name} fired ${sessionLogic.shooting.shotsWhileDead} rounds while character was dead on server"
|
||||
)
|
||||
sessionLogic.shooting.shotsWhileDead = 0
|
||||
}
|
||||
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason(msg = "cancel")
|
||||
sessionLogic.general.renewCharSavedTimer(fixedLen = 1800L, varLen = 0L)
|
||||
|
||||
//player state changes
|
||||
AvatarActor.updateToolDischargeFor(avatar)
|
||||
player.FreeHand.Equipment.foreach { item =>
|
||||
DropEquipmentFromInventory(player)(item)
|
||||
}
|
||||
sessionLogic.general.dropSpecialSlotItem()
|
||||
sessionLogic.general.toggleMaxSpecialState(enable = false)
|
||||
sessionLogic.keepAliveFunc = sessionLogic.zoning.NormalKeepAlive
|
||||
sessionLogic.zoning.zoningStatus = Zoning.Status.None
|
||||
sessionLogic.zoning.spawn.deadState = DeadState.Dead
|
||||
continent.GUID(mount).collect { case obj: Vehicle =>
|
||||
sessionLogic.vehicles.ConditionalDriverVehicleControl(obj)
|
||||
sessionLogic.general.unaccessContainer(obj)
|
||||
}
|
||||
sessionLogic.actionsToCancel()
|
||||
sessionLogic.terminals.CancelAllProximityUnits()
|
||||
AvatarActor.savePlayerLocation(player)
|
||||
sessionLogic.zoning.spawn.shiftPosition = Some(player.Position)
|
||||
|
||||
//respawn
|
||||
sessionLogic.zoning.spawn.reviveTimer.cancel()
|
||||
if (player.death_by == 0) {
|
||||
sessionLogic.zoning.spawn.randomRespawn(300.seconds)
|
||||
} else {
|
||||
sessionLogic.zoning.spawn.HandleReleaseAvatar(player, continent)
|
||||
}
|
||||
|
||||
case AvatarResponse.Release(tplayer) if isNotSameTarget =>
|
||||
sessionLogic.zoning.spawn.DepictPlayerAsCorpse(tplayer)
|
||||
|
||||
case AvatarResponse.Revive(revivalTargetGuid) if resolvedPlayerGuid == revivalTargetGuid =>
|
||||
log.info(s"No time for rest, ${player.Name}. Back on your feet!")
|
||||
sessionLogic.zoning.spawn.reviveTimer.cancel()
|
||||
sessionLogic.zoning.spawn.deadState = DeadState.Alive
|
||||
player.Revive
|
||||
val health = player.Health
|
||||
sendResponse(PlanetsideAttributeMessage(revivalTargetGuid, attribute_type=0, health))
|
||||
sendResponse(AvatarDeadStateMessage(DeadState.Alive, timer_max=0, timer=0, player.Position, player.Faction, unk5=true))
|
||||
continent.AvatarEvents ! AvatarServiceMessage(
|
||||
continent.id,
|
||||
AvatarAction.PlanetsideAttributeToAll(revivalTargetGuid, attribute_type=0, health)
|
||||
)
|
||||
|
||||
/* uncommon messages (utility, or once in a while) */
|
||||
case AvatarResponse.ChangeAmmo(weapon_guid, weapon_slot, previous_guid, ammo_id, ammo_guid, ammo_data)
|
||||
if isNotSameTarget && ops.lastSeenStreamMessage.get(guid.guid).exists { _.visible } =>
|
||||
ops.changeAmmoProcedures(weapon_guid, previous_guid, ammo_id, ammo_guid, weapon_slot, ammo_data)
|
||||
sendResponse(ChangeAmmoMessage(weapon_guid, 1))
|
||||
|
||||
case AvatarResponse.ChangeAmmo(weapon_guid, weapon_slot, previous_guid, ammo_id, ammo_guid, ammo_data)
|
||||
if isNotSameTarget =>
|
||||
ops.changeAmmoProcedures(weapon_guid, previous_guid, ammo_id, ammo_guid, weapon_slot, ammo_data)
|
||||
|
||||
case AvatarResponse.ChangeFireMode(itemGuid, mode) if isNotSameTarget =>
|
||||
sendResponse(ChangeFireModeMessage(itemGuid, mode))
|
||||
|
||||
case AvatarResponse.ConcealPlayer() =>
|
||||
sendResponse(GenericObjectActionMessage(guid, code=9))
|
||||
|
||||
case AvatarResponse.EnvironmentalDamage(_, _, _) =>
|
||||
//TODO damage marker?
|
||||
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_dmg")
|
||||
|
||||
case AvatarResponse.DropItem(pkt) if isNotSameTarget =>
|
||||
sendResponse(pkt)
|
||||
|
||||
case AvatarResponse.ObjectDelete(itemGuid, unk) if isNotSameTarget =>
|
||||
sendResponse(ObjectDeleteMessage(itemGuid, unk))
|
||||
|
||||
/* rare messages */
|
||||
case AvatarResponse.SetEmpire(objectGuid, faction) if isNotSameTarget =>
|
||||
sendResponse(SetEmpireMessage(objectGuid, faction))
|
||||
|
||||
case AvatarResponse.DropSpecialItem() =>
|
||||
sessionLogic.general.dropSpecialSlotItem()
|
||||
|
||||
case AvatarResponse.OxygenState(player, vehicle) =>
|
||||
sendResponse(OxygenStateMessage(
|
||||
DrowningTarget(player.guid, player.progress, player.state),
|
||||
vehicle.flatMap { vinfo => Some(DrowningTarget(vinfo.guid, vinfo.progress, vinfo.state)) }
|
||||
))
|
||||
|
||||
case AvatarResponse.LoadProjectile(pkt) if isNotSameTarget =>
|
||||
sendResponse(pkt)
|
||||
|
||||
case AvatarResponse.ProjectileState(projectileGuid, shotPos, shotVel, shotOrient, seq, end, targetGuid) if isNotSameTarget =>
|
||||
sendResponse(ProjectileStateMessage(projectileGuid, shotPos, shotVel, shotOrient, seq, end, targetGuid))
|
||||
|
||||
case AvatarResponse.ProjectileExplodes(projectileGuid, projectile) =>
|
||||
sendResponse(
|
||||
ProjectileStateMessage(
|
||||
projectileGuid,
|
||||
projectile.Position,
|
||||
shot_vel = Vector3.Zero,
|
||||
projectile.Orientation,
|
||||
sequence_num=0,
|
||||
end=true,
|
||||
hit_target_guid=PlanetSideGUID(0)
|
||||
)
|
||||
)
|
||||
sendResponse(ObjectDeleteMessage(projectileGuid, unk1=2))
|
||||
|
||||
case AvatarResponse.ProjectileAutoLockAwareness(mode) =>
|
||||
sendResponse(GenericActionMessage(mode))
|
||||
|
||||
case AvatarResponse.PutDownFDU(target) if isNotSameTarget =>
|
||||
sendResponse(GenericObjectActionMessage(target, code=53))
|
||||
|
||||
case AvatarResponse.StowEquipment(target, slot, item) if isNotSameTarget =>
|
||||
val definition = item.Definition
|
||||
sendResponse(
|
||||
ObjectCreateDetailedMessage(
|
||||
definition.ObjectId,
|
||||
item.GUID,
|
||||
ObjectCreateMessageParent(target, slot),
|
||||
definition.Packet.DetailedConstructorData(item).get
|
||||
)
|
||||
)
|
||||
|
||||
case AvatarResponse.WeaponDryFire(weaponGuid)
|
||||
if isNotSameTarget && ops.lastSeenStreamMessage.get(guid.guid).exists { _.visible } =>
|
||||
continent.GUID(weaponGuid).collect {
|
||||
case tool: Tool if tool.Magazine == 0 =>
|
||||
// check that the magazine is still empty before sending WeaponDryFireMessage
|
||||
// if it has been reloaded since then, other clients will not see it firing
|
||||
sendResponse(WeaponDryFireMessage(weaponGuid))
|
||||
}
|
||||
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
// Copyright (c) 2024 PSForever
|
||||
package net.psforever.actors.session.spectator
|
||||
|
||||
import akka.actor.{ActorContext, ActorRef, typed}
|
||||
import net.psforever.actors.session.AvatarActor
|
||||
import net.psforever.actors.session.support.{GalaxyHandlerFunctions, SessionGalaxyHandlers, SessionData}
|
||||
import net.psforever.packet.game.{BroadcastWarpgateUpdateMessage, FriendsResponse, HotSpotUpdateMessage, ZoneInfoMessage, ZonePopulationUpdateMessage, HotSpotInfo => PacketHotSpotInfo}
|
||||
import net.psforever.services.galaxy.{GalaxyAction, GalaxyResponse, GalaxyServiceMessage}
|
||||
import net.psforever.types.{MemberAction, PlanetSideEmpire}
|
||||
|
||||
object GalaxyHandlerLogic {
|
||||
def apply(ops: SessionGalaxyHandlers): GalaxyHandlerLogic = {
|
||||
new GalaxyHandlerLogic(ops, ops.context)
|
||||
}
|
||||
}
|
||||
|
||||
class GalaxyHandlerLogic(val ops: SessionGalaxyHandlers, implicit val context: ActorContext) extends GalaxyHandlerFunctions {
|
||||
def sessionLogic: SessionData = ops.sessionLogic
|
||||
|
||||
private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor
|
||||
|
||||
private val galaxyService: ActorRef = ops.galaxyService
|
||||
|
||||
/* packets */
|
||||
|
||||
def handleUpdateIgnoredPlayers(pkt: FriendsResponse): Unit = {
|
||||
sendResponse(pkt)
|
||||
pkt.friends.foreach { f =>
|
||||
galaxyService ! GalaxyServiceMessage(GalaxyAction.LogStatusChange(f.name))
|
||||
}
|
||||
}
|
||||
|
||||
/* response handlers */
|
||||
|
||||
def handle(reply: GalaxyResponse.Response): Unit = {
|
||||
reply match {
|
||||
case GalaxyResponse.HotSpotUpdate(zone_index, priority, hot_spot_info) =>
|
||||
sendResponse(
|
||||
HotSpotUpdateMessage(
|
||||
zone_index,
|
||||
priority,
|
||||
hot_spot_info.map { spot => PacketHotSpotInfo(spot.DisplayLocation.x, spot.DisplayLocation.y, 40) }
|
||||
)
|
||||
)
|
||||
|
||||
case GalaxyResponse.MapUpdate(msg) =>
|
||||
sendResponse(msg)
|
||||
|
||||
case GalaxyResponse.UpdateBroadcastPrivileges(zoneId, gateMapId, fromFactions, toFactions) =>
|
||||
val faction = player.Faction
|
||||
val from = fromFactions.contains(faction)
|
||||
val to = toFactions.contains(faction)
|
||||
if (from && !to) {
|
||||
sendResponse(BroadcastWarpgateUpdateMessage(zoneId, gateMapId, PlanetSideEmpire.NEUTRAL))
|
||||
} else if (!from && to) {
|
||||
sendResponse(BroadcastWarpgateUpdateMessage(zoneId, gateMapId, faction))
|
||||
}
|
||||
|
||||
case GalaxyResponse.FlagMapUpdate(msg) =>
|
||||
sendResponse(msg)
|
||||
|
||||
case GalaxyResponse.TransferPassenger(temp_channel, vehicle, _, manifest) =>
|
||||
sessionLogic.zoning.handleTransferPassenger(temp_channel, vehicle, manifest)
|
||||
|
||||
case GalaxyResponse.LockedZoneUpdate(zone, time) =>
|
||||
sendResponse(ZoneInfoMessage(zone.Number, empire_status=false, lock_time=time))
|
||||
|
||||
case GalaxyResponse.UnlockedZoneUpdate(zone) => ;
|
||||
sendResponse(ZoneInfoMessage(zone.Number, empire_status=true, lock_time=0L))
|
||||
val popBO = 0
|
||||
val popTR = zone.Players.count(_.faction == PlanetSideEmpire.TR)
|
||||
val popNC = zone.Players.count(_.faction == PlanetSideEmpire.NC)
|
||||
val popVS = zone.Players.count(_.faction == PlanetSideEmpire.VS)
|
||||
sendResponse(ZonePopulationUpdateMessage(zone.Number, 414, 138, popTR, 138, popNC, 138, popVS, 138, popBO))
|
||||
|
||||
case GalaxyResponse.LogStatusChange(name) if avatar.people.friend.exists(_.name.equals(name)) =>
|
||||
avatarActor ! AvatarActor.MemberListRequest(MemberAction.UpdateFriend, name)
|
||||
|
||||
case GalaxyResponse.SendResponse(msg) =>
|
||||
sendResponse(msg)
|
||||
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,640 @@
|
|||
// Copyright (c) 2024 PSForever
|
||||
package net.psforever.actors.session.spectator
|
||||
|
||||
import akka.actor.{ActorContext, typed}
|
||||
import net.psforever.actors.session.{AvatarActor, ChatActor}
|
||||
import net.psforever.actors.session.support.{GeneralFunctions, GeneralOperations, SessionData}
|
||||
import net.psforever.login.WorldSession.RemoveOldEquipmentFromInventory
|
||||
import net.psforever.objects.{Account, BoomerDeployable, BoomerTrigger, GlobalDefinitions, LivePlayerList, PlanetSideGameObject, Player, TelepadDeployable, Tool, Vehicle}
|
||||
import net.psforever.objects.avatar.Avatar
|
||||
import net.psforever.objects.ballistics.Projectile
|
||||
import net.psforever.objects.ce.{Deployable, TelepadLike}
|
||||
import net.psforever.objects.definition.{BasicDefinition, KitDefinition, SpecialExoSuitDefinition}
|
||||
import net.psforever.objects.equipment.Equipment
|
||||
import net.psforever.objects.guid.{GUIDTask, TaskWorkflow}
|
||||
import net.psforever.objects.inventory.Container
|
||||
import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject}
|
||||
import net.psforever.objects.serverobject.doors.Door
|
||||
import net.psforever.objects.sourcing.{PlayerSource, VehicleSource}
|
||||
import net.psforever.objects.vehicles.{Utility, UtilityType}
|
||||
import net.psforever.objects.vehicles.Utility.InternalTelepad
|
||||
import net.psforever.objects.vital.{VehicleDismountActivity, VehicleMountActivity}
|
||||
import net.psforever.objects.zones.{Zone, ZoneProjectile}
|
||||
import net.psforever.packet.PlanetSideGamePacket
|
||||
import net.psforever.packet.game.{ActionCancelMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, BugReportMessage, ChangeFireModeMessage, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestMessage, ChatMsg, ConnectToWorldRequestMessage, CreateShortcutMessage, DeployObjectMessage, DisplayedAwardMessage, DropItemMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FriendsRequest, GenericAction, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, ImplantAction, InvalidTerrainMessage, LootItemMessage, MoveItemMessage, ObjectDeleteMessage, ObjectDetectedMessage, ObjectHeldMessage, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, PlayerStateShiftMessage, RequestDestroyMessage, SetChatFilterMessage, ShiftState, TargetInfo, TargetingImplantRequest, TargetingInfoMessage, TradeMessage, UnuseItemMessage, UseItemMessage, VoiceHostInfo, VoiceHostKill, VoiceHostRequest, ZipLineMessage}
|
||||
import net.psforever.services.RemoverActor
|
||||
import net.psforever.services.account.AccountPersistenceService
|
||||
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
|
||||
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
|
||||
import net.psforever.types.{ChatMessageType, DriveState, ExoSuitType, PlanetSideGUID, Vector3}
|
||||
import net.psforever.util.Config
|
||||
|
||||
object GeneralLogic {
|
||||
def apply(ops: GeneralOperations): GeneralLogic = {
|
||||
new GeneralLogic(ops, ops.context)
|
||||
}
|
||||
}
|
||||
|
||||
class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContext) extends GeneralFunctions {
|
||||
def sessionLogic: SessionData = ops.sessionLogic
|
||||
|
||||
private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor
|
||||
|
||||
private val chatActor: typed.ActorRef[ChatActor.Command] = ops.chatActor
|
||||
|
||||
private var customImplants = SpectatorModeLogic.SpectatorImplants.map(_.get)
|
||||
|
||||
def handleConnectToWorldRequest(pkt: ConnectToWorldRequestMessage): Unit = { /* intentionally blank */ }
|
||||
|
||||
def handleCharacterCreateRequest(pkt: CharacterCreateRequestMessage): Unit = { /* intentionally blank */ }
|
||||
|
||||
def handleCharacterRequest(pkt: CharacterRequestMessage): Unit = { /* intentionally blank */ }
|
||||
|
||||
def handlePlayerStateUpstream(pkt: PlayerStateMessageUpstream): Unit = {
|
||||
val PlayerStateMessageUpstream(
|
||||
avatarGuid,
|
||||
pos,
|
||||
vel,
|
||||
yaw,
|
||||
pitch,
|
||||
yawUpper,
|
||||
_/*seqTime*/,
|
||||
_,
|
||||
isCrouching,
|
||||
isJumping,
|
||||
_/*jumpThrust*/,
|
||||
isCloaking,
|
||||
_,
|
||||
_
|
||||
)= pkt
|
||||
sessionLogic.persist()
|
||||
sessionLogic.turnCounterFunc(avatarGuid)
|
||||
ops.fallHeightTracker(pos.z)
|
||||
// if (isCrouching && !player.Crouching) {
|
||||
// //dev stuff goes here
|
||||
// }
|
||||
player.Position = pos
|
||||
player.Velocity = vel
|
||||
player.Orientation = Vector3(player.Orientation.x, pitch, yaw)
|
||||
player.FacingYawUpper = yawUpper
|
||||
player.Crouching = isCrouching
|
||||
player.Jumping = isJumping
|
||||
player.Cloaked = player.ExoSuit == ExoSuitType.Infiltration && isCloaking
|
||||
if (player.death_by == -1) {
|
||||
sessionLogic.kickedByAdministration()
|
||||
}
|
||||
}
|
||||
|
||||
def handleChat(pkt: ChatMsg): Unit = {
|
||||
chatActor ! ChatActor.Message(pkt)
|
||||
}
|
||||
|
||||
def handleChatFilter(pkt: SetChatFilterMessage): Unit = { /* intentionally blank */ }
|
||||
|
||||
def handleVoiceHostRequest(pkt: VoiceHostRequest): Unit = {
|
||||
log.debug(s"$pkt")
|
||||
sendResponse(VoiceHostKill())
|
||||
sendResponse(
|
||||
ChatMsg(ChatMessageType.CMT_OPEN, wideContents=false, "", "Try our Discord at https://discord.gg/0nRe5TNbTYoUruA4", None)
|
||||
)
|
||||
}
|
||||
|
||||
def handleVoiceHostInfo(pkt: VoiceHostInfo): Unit = {
|
||||
log.debug(s"$pkt")
|
||||
sendResponse(VoiceHostKill())
|
||||
sendResponse(
|
||||
ChatMsg(ChatMessageType.CMT_OPEN, wideContents=false, "", "Try our Discord at https://discord.gg/0nRe5TNbTYoUruA4", None)
|
||||
)
|
||||
}
|
||||
|
||||
def handleEmote(pkt: EmoteMsg): Unit = {
|
||||
val EmoteMsg(avatarGuid, emote) = pkt
|
||||
sendResponse(EmoteMsg(avatarGuid, emote))
|
||||
}
|
||||
|
||||
def handleDropItem(pkt: DropItemMessage): Unit = { /* intentionally blank */ }
|
||||
|
||||
def handlePickupItem(pkt: PickupItemMessage): Unit = { /* intentionally blank */ }
|
||||
|
||||
def handleObjectHeld(pkt: ObjectHeldMessage): Unit = {
|
||||
val ObjectHeldMessage(_, heldHolsters, _) = pkt
|
||||
if (heldHolsters != Player.HandsDownSlot && heldHolsters != 4) {
|
||||
sendResponse(ObjectHeldMessage(player.GUID, Player.HandsDownSlot, unk1=true))
|
||||
}
|
||||
}
|
||||
|
||||
def handleAvatarJump(pkt: AvatarJumpMessage): Unit = { /* intentionally blank */ }
|
||||
|
||||
def handleZipLine(pkt: ZipLineMessage): Unit = {
|
||||
val ZipLineMessage(playerGuid, forwards, action, pathId, pos) = pkt
|
||||
continent.zipLinePaths.find(x => x.PathId == pathId) match {
|
||||
case Some(path) if path.IsTeleporter =>
|
||||
val endPoint = path.ZipLinePoints.last
|
||||
sendResponse(ZipLineMessage(PlanetSideGUID(0), forwards, 0, pathId, pos))
|
||||
//todo: send to zone to show teleport animation to all clients
|
||||
sendResponse(PlayerStateShiftMessage(ShiftState(0, endPoint, (player.Orientation.z + player.FacingYawUpper) % 360f, None)))
|
||||
case Some(_) =>
|
||||
action match {
|
||||
case 0 =>
|
||||
//travel along the zipline in the direction specified
|
||||
sendResponse(ZipLineMessage(playerGuid, forwards, action, pathId, pos))
|
||||
case 1 =>
|
||||
//disembark from zipline at destination!
|
||||
sendResponse(ZipLineMessage(playerGuid, forwards, action, 0, pos))
|
||||
case 2 =>
|
||||
//get off by force
|
||||
sendResponse(ZipLineMessage(playerGuid, forwards, action, 0, pos))
|
||||
case _ =>
|
||||
log.warn(
|
||||
s"${player.Name} tried to do something with a zipline but can't handle it. forwards: $forwards action: $action pathId: $pathId zone: ${continent.Number} / ${continent.id}"
|
||||
)
|
||||
}
|
||||
case _ =>
|
||||
log.warn(s"${player.Name} couldn't find a zipline path $pathId in zone ${continent.id}")
|
||||
}
|
||||
}
|
||||
|
||||
def handleRequestDestroy(pkt: RequestDestroyMessage): Unit = {
|
||||
val RequestDestroyMessage(objectGuid) = pkt
|
||||
//make sure this is the correct response for all cases
|
||||
sessionLogic.validObject(objectGuid, decorator = "RequestDestroy") match {
|
||||
case Some(obj: Projectile) =>
|
||||
if (!obj.isResolved) {
|
||||
obj.Miss()
|
||||
}
|
||||
continent.Projectile ! ZoneProjectile.Remove(objectGuid)
|
||||
|
||||
case Some(obj: BoomerTrigger) =>
|
||||
if (findEquipmentToDelete(objectGuid, obj)) {
|
||||
continent.GUID(obj.Companion) match {
|
||||
case Some(boomer: BoomerDeployable) =>
|
||||
boomer.Trigger = None
|
||||
boomer.Actor ! Deployable.Deconstruct()
|
||||
case Some(thing) =>
|
||||
log.warn(s"RequestDestroy: BoomerTrigger object connected to wrong object - $thing")
|
||||
case None => ()
|
||||
}
|
||||
}
|
||||
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
|
||||
def handleMoveItem(pkt: MoveItemMessage): Unit = { /* intentionally blank */ }
|
||||
|
||||
def handleLootItem(pkt: LootItemMessage): Unit = { /* intentionally blank */ }
|
||||
|
||||
def handleAvatarImplant(pkt: AvatarImplantMessage): Unit = {
|
||||
val AvatarImplantMessage(_, _, slot, _) = pkt
|
||||
customImplants.lift(slot)
|
||||
.collect {
|
||||
case implant if implant.active =>
|
||||
customImplants = customImplants.updated(slot, implant.copy(active = false))
|
||||
sendResponse(AvatarImplantMessage(player.GUID, ImplantAction.Activation, slot, 0))
|
||||
sendResponse(PlanetsideAttributeMessage(player.GUID, 28, implant.definition.implantType.value * 2))
|
||||
case implant =>
|
||||
customImplants = customImplants.updated(slot, implant.copy(active = true))
|
||||
sendResponse(AvatarImplantMessage(player.GUID, ImplantAction.Activation, slot, 1))
|
||||
sendResponse(PlanetsideAttributeMessage(player.GUID, 28, implant.definition.implantType.value * 2 + 1))
|
||||
}
|
||||
}
|
||||
|
||||
def handleUseItem(pkt: UseItemMessage): Unit = {
|
||||
sessionLogic.validObject(pkt.object_guid, decorator = "UseItem") match {
|
||||
case Some(door: Door) =>
|
||||
handleUseDoor(door, None)
|
||||
case Some(obj: TelepadDeployable) =>
|
||||
handleUseTelepadDeployable(obj, None, pkt)
|
||||
case Some(obj: Utility.InternalTelepad) =>
|
||||
handleUseInternalTelepad(obj, pkt)
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
|
||||
def handleUnuseItem(pkt: UnuseItemMessage): Unit = { /* intentionally blank */ }
|
||||
|
||||
def handleDeployObject(pkt: DeployObjectMessage): Unit = { /* intentionally blank */ }
|
||||
|
||||
def handlePlanetsideAttribute(pkt: PlanetsideAttributeMessage): Unit = {
|
||||
val PlanetsideAttributeMessage(objectGuid, attributeType, _/*attributeValue*/) = pkt
|
||||
sessionLogic.validObject(objectGuid, decorator = "PlanetsideAttribute") match {
|
||||
case Some(_: Vehicle) => ()
|
||||
case Some(_: Player) if attributeType == 106 => ()
|
||||
case Some(obj) =>
|
||||
log.trace(s"PlanetsideAttribute: ${player.Name} does not know how to apply unknown attributes behavior $attributeType to ${obj.Definition.Name}")
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
|
||||
def handleGenericObjectAction(pkt: GenericObjectActionMessage): Unit = {
|
||||
val GenericObjectActionMessage(objectGuid, _/*code*/) = pkt
|
||||
sessionLogic.validObject(objectGuid, decorator = "GenericObjectAction") match {
|
||||
case Some(_: Vehicle) => ()
|
||||
case Some(_: Tool) => ()
|
||||
case _ => log.info(s"${player.Name} - $pkt")
|
||||
}
|
||||
}
|
||||
|
||||
def handleGenericObjectActionAtPosition(pkt: GenericObjectActionAtPositionMessage): Unit = {
|
||||
val GenericObjectActionAtPositionMessage(objectGuid, _, _) = pkt
|
||||
sessionLogic.validObject(objectGuid, decorator = "GenericObjectActionAtPosition") match {
|
||||
case Some(tool: Tool) if GlobalDefinitions.isBattleFrameNTUSiphon(tool.Definition) => ()
|
||||
case _ => log.info(s"${player.Name} - $pkt")
|
||||
}
|
||||
}
|
||||
|
||||
def handleGenericObjectState(pkt: GenericObjectStateMsg): Unit = {
|
||||
val GenericObjectStateMsg(_, _) = pkt
|
||||
log.info(s"${player.Name} - $pkt")
|
||||
}
|
||||
|
||||
def handleGenericAction(pkt: GenericActionMessage): Unit = {
|
||||
val GenericActionMessage(action) = pkt
|
||||
val (toolOpt, definition) = player.Slot(0).Equipment match {
|
||||
case Some(tool: Tool) =>
|
||||
(Some(tool), tool.Definition)
|
||||
case _ =>
|
||||
(None, GlobalDefinitions.bullet_9mm)
|
||||
}
|
||||
action match {
|
||||
case GenericAction.MaxAnchorsExtend_RCV =>
|
||||
log.info(s"${player.Name} has anchored ${player.Sex.pronounObject}self to the ground")
|
||||
player.UsingSpecial = SpecialExoSuitDefinition.Mode.Anchored
|
||||
continent.AvatarEvents ! AvatarServiceMessage(
|
||||
continent.id,
|
||||
AvatarAction.PlanetsideAttribute(player.GUID, 19, 1)
|
||||
)
|
||||
definition match {
|
||||
case GlobalDefinitions.trhev_dualcycler | GlobalDefinitions.trhev_burster =>
|
||||
val tool = toolOpt.get
|
||||
tool.ToFireMode = 1
|
||||
sendResponse(ChangeFireModeMessage(tool.GUID, 1))
|
||||
case GlobalDefinitions.trhev_pounder =>
|
||||
val tool = toolOpt.get
|
||||
val convertFireModeIndex = if (tool.FireModeIndex == 0) { 1 }
|
||||
else { 4 }
|
||||
tool.ToFireMode = convertFireModeIndex
|
||||
sendResponse(ChangeFireModeMessage(tool.GUID, convertFireModeIndex))
|
||||
case _ =>
|
||||
log.warn(s"GenericObject: ${player.Name} is a MAX with an unexpected attachment - ${definition.Name}")
|
||||
}
|
||||
case GenericAction.MaxAnchorsRelease_RCV =>
|
||||
log.info(s"${player.Name} has released the anchors")
|
||||
player.UsingSpecial = SpecialExoSuitDefinition.Mode.Normal
|
||||
continent.AvatarEvents ! AvatarServiceMessage(
|
||||
continent.id,
|
||||
AvatarAction.PlanetsideAttribute(player.GUID, 19, 0)
|
||||
)
|
||||
definition match {
|
||||
case GlobalDefinitions.trhev_dualcycler | GlobalDefinitions.trhev_burster =>
|
||||
val tool = toolOpt.get
|
||||
tool.ToFireMode = 0
|
||||
sendResponse(ChangeFireModeMessage(tool.GUID, 0))
|
||||
case GlobalDefinitions.trhev_pounder =>
|
||||
val tool = toolOpt.get
|
||||
val convertFireModeIndex = if (tool.FireModeIndex == 1) { 0 } else { 3 }
|
||||
tool.ToFireMode = convertFireModeIndex
|
||||
sendResponse(ChangeFireModeMessage(tool.GUID, convertFireModeIndex))
|
||||
case _ =>
|
||||
log.warn(s"GenericObject: $player is MAX with an unexpected attachment - ${definition.Name}")
|
||||
}
|
||||
case GenericAction.AwayFromKeyboard_RCV =>
|
||||
log.info(s"${player.Name} is AFK")
|
||||
AvatarActor.savePlayerLocation(player)
|
||||
ops.displayCharSavedMsgThenRenewTimer(fixedLen=1800L, varLen=0L) //~30min
|
||||
player.AwayFromKeyboard = true
|
||||
case GenericAction.BackInGame_RCV =>
|
||||
log.info(s"${player.Name} is back")
|
||||
player.AwayFromKeyboard = false
|
||||
ops.renewCharSavedTimer(
|
||||
Config.app.game.savedMsg.renewal.fixed,
|
||||
Config.app.game.savedMsg.renewal.variable
|
||||
)
|
||||
case GenericAction.LookingForSquad_RCV => //Looking For Squad ON
|
||||
if (!avatar.lookingForSquad && (sessionLogic.squad.squadUI.isEmpty || sessionLogic.squad.squadUI(player.CharId).index == 0)) {
|
||||
avatarActor ! AvatarActor.SetLookingForSquad(true)
|
||||
}
|
||||
case GenericAction.NotLookingForSquad_RCV => //Looking For Squad OFF
|
||||
if (avatar.lookingForSquad && (sessionLogic.squad.squadUI.isEmpty || sessionLogic.squad.squadUI(player.CharId).index == 0)) {
|
||||
avatarActor ! AvatarActor.SetLookingForSquad(false)
|
||||
}
|
||||
case _ =>
|
||||
log.warn(s"GenericActionMessage: ${player.Name} can't handle $action")
|
||||
}
|
||||
}
|
||||
|
||||
def handleGenericCollision(pkt: GenericCollisionMsg): Unit = { /* intentionally blank */ }
|
||||
|
||||
def handleAvatarFirstTimeEvent(pkt: AvatarFirstTimeEventMessage): Unit = { /* intentionally blank */ }
|
||||
|
||||
def handleBugReport(pkt: PlanetSideGamePacket): Unit = {
|
||||
val BugReportMessage(
|
||||
_/*versionMajor*/,
|
||||
_/*versionMinor*/,
|
||||
_/*versionDate*/,
|
||||
_/*bugType*/,
|
||||
_/*repeatable*/,
|
||||
_/*location*/,
|
||||
_/*zone*/,
|
||||
_/*pos*/,
|
||||
_/*summary*/,
|
||||
_/*desc*/
|
||||
) = pkt
|
||||
log.warn(s"${player.Name} filed a bug report - it might be something important")
|
||||
log.debug(s"$pkt")
|
||||
}
|
||||
|
||||
def handleFacilityBenefitShieldChargeRequest(pkt: FacilityBenefitShieldChargeRequestMessage): Unit = { /* intentionally blank */ }
|
||||
|
||||
def handleBattleplan(pkt: BattleplanMessage): Unit = {
|
||||
val BattleplanMessage(_, name, _, _) = pkt
|
||||
val lament: String = s"$name has a brilliant idea that no one will ever see"
|
||||
log.info(lament)
|
||||
log.debug(s"Battleplan: $lament - $pkt")
|
||||
}
|
||||
|
||||
def handleBindPlayer(pkt: BindPlayerMessage): Unit = {
|
||||
val BindPlayerMessage(_, _, _, _, _, _, _, _) = pkt
|
||||
}
|
||||
|
||||
def handleCreateShortcut(pkt: CreateShortcutMessage): Unit = { /* intentionally blank */ }
|
||||
|
||||
def handleChangeShortcutBank(pkt: ChangeShortcutBankMessage): Unit = { /* intentionally blank */ }
|
||||
|
||||
def handleFriendRequest(pkt: FriendsRequest): Unit = {
|
||||
val FriendsRequest(action, name) = pkt
|
||||
avatarActor ! AvatarActor.MemberListRequest(action, name)
|
||||
}
|
||||
|
||||
def handleInvalidTerrain(pkt: InvalidTerrainMessage): Unit = { /* intentionally blank */ }
|
||||
|
||||
def handleActionCancel(pkt: ActionCancelMessage): Unit = {
|
||||
val ActionCancelMessage(_, _, _) = pkt
|
||||
ops.progressBarUpdate.cancel()
|
||||
ops.progressBarValue = None
|
||||
}
|
||||
|
||||
def handleTrade(pkt: TradeMessage): Unit = { /* intentionally blank */ }
|
||||
|
||||
def handleDisplayedAward(pkt: DisplayedAwardMessage): Unit = {
|
||||
val DisplayedAwardMessage(_, ribbon, bar) = pkt
|
||||
log.trace(s"${player.Name} changed the $bar displayed award ribbon to $ribbon")
|
||||
avatarActor ! AvatarActor.SetRibbon(ribbon, bar)
|
||||
}
|
||||
|
||||
def handleObjectDetected(pkt: ObjectDetectedMessage): Unit = { /* intentionally blank */ }
|
||||
|
||||
def handleTargetingImplantRequest(pkt: TargetingImplantRequest): Unit = {
|
||||
val TargetingImplantRequest(list) = pkt
|
||||
val targetInfo: List[TargetInfo] = list.flatMap { x =>
|
||||
continent.GUID(x.target_guid) match {
|
||||
case Some(player: Player) =>
|
||||
val health = player.Health.toFloat / player.MaxHealth
|
||||
val armor = if (player.MaxArmor > 0) {
|
||||
player.Armor.toFloat / player.MaxArmor
|
||||
} else {
|
||||
0
|
||||
}
|
||||
Some(TargetInfo(player.GUID, health, armor))
|
||||
case _ =>
|
||||
log.warn(
|
||||
s"TargetingImplantRequest: the info that ${player.Name} requested for target ${x.target_guid} is not for a player"
|
||||
)
|
||||
None
|
||||
}
|
||||
}
|
||||
sendResponse(TargetingInfoMessage(targetInfo))
|
||||
}
|
||||
|
||||
def handleHitHint(pkt: HitHint): Unit = { /* intentionally blank */ }
|
||||
|
||||
/* messages */
|
||||
|
||||
def handleSetAvatar(avatar: Avatar): Unit = {
|
||||
session = session.copy(avatar = avatar)
|
||||
if (session.player != null) {
|
||||
session.player.avatar = avatar
|
||||
}
|
||||
LivePlayerList.Update(avatar.id, avatar)
|
||||
}
|
||||
|
||||
def handleReceiveAccountData(account: Account): Unit = {
|
||||
log.trace(s"ReceiveAccountData $account")
|
||||
session = session.copy(account = account)
|
||||
avatarActor ! AvatarActor.SetAccount(account)
|
||||
}
|
||||
|
||||
def handleUseCooldownRenew: BasicDefinition => Unit = {
|
||||
case _: KitDefinition => ops.kitToBeUsed = None
|
||||
case _ => ()
|
||||
}
|
||||
|
||||
def handleAvatarResponse(avatar: Avatar): Unit = {
|
||||
session = session.copy(avatar = avatar)
|
||||
sessionLogic.accountPersistence ! AccountPersistenceService.Login(avatar.name, avatar.id)
|
||||
}
|
||||
|
||||
def handleSetSpeed(speed: Float): Unit = {
|
||||
session = session.copy(speed = speed)
|
||||
}
|
||||
|
||||
def handleSetFlying(flying: Boolean): Unit = {
|
||||
session = session.copy(flying = flying)
|
||||
}
|
||||
|
||||
def handleSetSpectator(spectator: Boolean): Unit = {
|
||||
session.player.spectator = spectator
|
||||
}
|
||||
|
||||
def handleKick(player: Player, time: Option[Long]): Unit = {
|
||||
administrativeKick(player)
|
||||
sessionLogic.accountPersistence ! AccountPersistenceService.Kick(player.Name, time)
|
||||
}
|
||||
|
||||
def handleSilenced(isSilenced: Boolean): Unit = {
|
||||
player.silenced = isSilenced
|
||||
}
|
||||
|
||||
/* supporting functions */
|
||||
|
||||
private def handleUseDoor(door: Door, equipment: Option[Equipment]): Unit = {
|
||||
equipment match {
|
||||
case Some(tool: Tool) if tool.Definition == GlobalDefinitions.medicalapplicator =>
|
||||
val distance: Float = math.max(
|
||||
Config.app.game.doorsCanBeOpenedByMedAppFromThisDistance,
|
||||
door.Definition.initialOpeningDistance
|
||||
)
|
||||
door.Actor ! CommonMessages.Use(player, Some(distance))
|
||||
case _ =>
|
||||
door.Actor ! CommonMessages.Use(player)
|
||||
}
|
||||
}
|
||||
|
||||
private def handleUseTelepadDeployable(obj: TelepadDeployable, equipment: Option[Equipment], msg: UseItemMessage): Unit = {
|
||||
if (equipment.isEmpty) {
|
||||
(continent.GUID(obj.Router) match {
|
||||
case Some(vehicle: Vehicle) => Some((vehicle, vehicle.Utility(UtilityType.internal_router_telepad_deployable)))
|
||||
case Some(vehicle) => Some(vehicle, None)
|
||||
case None => None
|
||||
}) match {
|
||||
case Some((vehicle: Vehicle, Some(util: Utility.InternalTelepad))) =>
|
||||
player.WhichSide = vehicle.WhichSide
|
||||
useRouterTelepadSystem(
|
||||
router = vehicle,
|
||||
internalTelepad = util,
|
||||
remoteTelepad = obj,
|
||||
src = obj,
|
||||
dest = util
|
||||
)
|
||||
case Some((vehicle: Vehicle, None)) =>
|
||||
log.error(
|
||||
s"telepad@${msg.object_guid.guid} is not linked to a router - ${vehicle.Definition.Name}"
|
||||
)
|
||||
case Some((o, _)) =>
|
||||
log.error(
|
||||
s"telepad@${msg.object_guid.guid} is linked to wrong kind of object - ${o.Definition.Name}, ${obj.Router}"
|
||||
)
|
||||
obj.Actor ! Deployable.Deconstruct()
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def handleUseInternalTelepad(obj: InternalTelepad, msg: UseItemMessage): Unit = {
|
||||
continent.GUID(obj.Telepad) match {
|
||||
case Some(pad: TelepadDeployable) =>
|
||||
player.WhichSide = pad.WhichSide
|
||||
useRouterTelepadSystem(
|
||||
router = obj.Owner.asInstanceOf[Vehicle],
|
||||
internalTelepad = obj,
|
||||
remoteTelepad = pad,
|
||||
src = obj,
|
||||
dest = pad
|
||||
)
|
||||
case Some(o) =>
|
||||
log.error(
|
||||
s"internal telepad@${msg.object_guid.guid} is not linked to a remote telepad - ${o.Definition.Name}@${o.GUID.guid}"
|
||||
)
|
||||
case None => ()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current `Vehicle` object that the player is riding/driving.
|
||||
* The vehicle must be found solely through use of `player.VehicleSeated`.
|
||||
* @return the vehicle
|
||||
*/
|
||||
private def findLocalVehicle: Option[Vehicle] = {
|
||||
continent.GUID(player.VehicleSeated) match {
|
||||
case Some(obj: Vehicle) => Some(obj)
|
||||
case _ => None
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple object searching algorithm that is limited to containers currently known and accessible by the player.
|
||||
* If all relatively local containers are checked and the object is not found,
|
||||
* the player's locker inventory will be checked, and then
|
||||
* the game environment (items on the ground) will be checked too.
|
||||
* If the target object is discovered, it is removed from its current location and is completely destroyed.
|
||||
* @see `RequestDestroyMessage`
|
||||
* @see `Zone.ItemIs.Where`
|
||||
* @param objectGuid the target object's globally unique identifier;
|
||||
* it is not expected that the object will be unregistered, but it is also not gauranteed
|
||||
* @param obj the target object
|
||||
* @return `true`, if the target object was discovered and removed;
|
||||
* `false`, otherwise
|
||||
*/
|
||||
private def findEquipmentToDelete(objectGuid: PlanetSideGUID, obj: Equipment): Boolean = {
|
||||
val findFunc
|
||||
: PlanetSideServerObject with Container => Option[(PlanetSideServerObject with Container, Option[Int])] =
|
||||
ops.findInLocalContainer(objectGuid)
|
||||
|
||||
findFunc(player)
|
||||
.orElse(ops.accessedContainer match {
|
||||
case Some(parent: PlanetSideServerObject) =>
|
||||
findFunc(parent)
|
||||
case _ =>
|
||||
None
|
||||
})
|
||||
.orElse(findLocalVehicle match {
|
||||
case Some(parent: PlanetSideServerObject) =>
|
||||
findFunc(parent)
|
||||
case _ =>
|
||||
None
|
||||
}) match {
|
||||
case Some((parent, Some(_))) =>
|
||||
obj.Position = Vector3.Zero
|
||||
RemoveOldEquipmentFromInventory(parent)(obj)
|
||||
true
|
||||
case _ if player.avatar.locker.Inventory.Remove(objectGuid) =>
|
||||
sendResponse(ObjectDeleteMessage(objectGuid, 0))
|
||||
true
|
||||
case _ if continent.EquipmentOnGround.contains(obj) =>
|
||||
obj.Position = Vector3.Zero
|
||||
continent.Ground ! Zone.Ground.RemoveItem(objectGuid)
|
||||
continent.AvatarEvents ! AvatarServiceMessage.Ground(RemoverActor.ClearSpecific(List(obj), continent))
|
||||
true
|
||||
case _ =>
|
||||
Zone.EquipmentIs.Where(obj, objectGuid, continent) match {
|
||||
case None =>
|
||||
true
|
||||
case Some(Zone.EquipmentIs.Orphaned()) if obj.HasGUID =>
|
||||
TaskWorkflow.execute(GUIDTask.unregisterEquipment(continent.GUID, obj))
|
||||
true
|
||||
case Some(Zone.EquipmentIs.Orphaned()) =>
|
||||
true
|
||||
case _ =>
|
||||
log.warn(s"RequestDestroy: equipment $obj exists, but ${player.Name} can not reach it to dispose of it")
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A player uses a fully-linked Router teleportation system.
|
||||
* @param router the Router vehicle
|
||||
* @param internalTelepad the internal telepad within the Router vehicle
|
||||
* @param remoteTelepad the remote telepad that is currently associated with this Router
|
||||
* @param src the origin of the teleportation (where the player starts)
|
||||
* @param dest the destination of the teleportation (where the player is going)
|
||||
*/
|
||||
private def useRouterTelepadSystem(
|
||||
router: Vehicle,
|
||||
internalTelepad: InternalTelepad,
|
||||
remoteTelepad: TelepadDeployable,
|
||||
src: PlanetSideGameObject with TelepadLike,
|
||||
dest: PlanetSideGameObject with TelepadLike
|
||||
): Unit = {
|
||||
val time = System.currentTimeMillis()
|
||||
if (
|
||||
time - ops.recentTeleportAttempt > 2000L && router.DeploymentState == DriveState.Deployed &&
|
||||
internalTelepad.Active &&
|
||||
remoteTelepad.Active
|
||||
) {
|
||||
val pguid = player.GUID
|
||||
val sguid = src.GUID
|
||||
val dguid = dest.GUID
|
||||
sendResponse(PlayerStateShiftMessage(ShiftState(0, dest.Position, player.Orientation.z)))
|
||||
ops.useRouterTelepadEffect(pguid, sguid, dguid)
|
||||
continent.LocalEvents ! LocalServiceMessage(
|
||||
continent.id,
|
||||
LocalAction.RouterTelepadTransport(pguid, pguid, sguid, dguid)
|
||||
)
|
||||
val vSource = VehicleSource(router)
|
||||
val zoneNumber = continent.Number
|
||||
player.LogActivity(VehicleMountActivity(vSource, PlayerSource(player), zoneNumber))
|
||||
player.Position = dest.Position
|
||||
player.LogActivity(VehicleDismountActivity(vSource, PlayerSource(player), zoneNumber))
|
||||
} else {
|
||||
log.warn(s"UseRouterTelepadSystem: ${player.Name} can not teleport")
|
||||
}
|
||||
ops.recentTeleportAttempt = time
|
||||
}
|
||||
|
||||
private def administrativeKick(tplayer: Player): Unit = {
|
||||
log.warn(s"${tplayer.Name} has been kicked by ${player.Name}")
|
||||
tplayer.death_by = -1
|
||||
sessionLogic.accountPersistence ! AccountPersistenceService.Kick(tplayer.Name)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,248 @@
|
|||
// Copyright (c) 2024 PSForever
|
||||
package net.psforever.actors.session.spectator
|
||||
|
||||
import akka.actor.ActorContext
|
||||
import net.psforever.actors.session.support.{LocalHandlerFunctions, SessionData, SessionLocalHandlers}
|
||||
import net.psforever.objects.ce.Deployable
|
||||
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, InventoryStateMessage, ObjectAttachMessage, ObjectCreateMessage, ObjectDeleteMessage, ObjectDetachMessage, OrbitalShuttleTimeMsg, PadAndShuttlePair, PlanetsideAttributeMessage, ProximityTerminalUseMessage, SetEmpireMessage, TriggerEffectMessage, TriggerSoundMessage, TriggeredSound, VehicleStateMessage}
|
||||
import net.psforever.services.Service
|
||||
import net.psforever.services.local.LocalResponse
|
||||
import net.psforever.types.{ChatMessageType, PlanetSideGUID, Vector3}
|
||||
|
||||
object LocalHandlerLogic {
|
||||
def apply(ops: SessionLocalHandlers): LocalHandlerLogic = {
|
||||
new LocalHandlerLogic(ops, ops.context)
|
||||
}
|
||||
}
|
||||
|
||||
class LocalHandlerLogic(val ops: SessionLocalHandlers, implicit val context: ActorContext) extends LocalHandlerFunctions {
|
||||
def sessionLogic: SessionData = ops.sessionLogic
|
||||
|
||||
/* response handlers */
|
||||
|
||||
/**
|
||||
* na
|
||||
* @param toChannel na
|
||||
* @param guid na
|
||||
* @param reply na
|
||||
*/
|
||||
def handle(toChannel: String, guid: PlanetSideGUID, reply: LocalResponse.Response): Unit = {
|
||||
val resolvedPlayerGuid = if (player.HasGUID) {
|
||||
player.GUID
|
||||
} else {
|
||||
Service.defaultPlayerGUID
|
||||
}
|
||||
val isNotSameTarget = resolvedPlayerGuid != guid
|
||||
reply match {
|
||||
case LocalResponse.DeployableMapIcon(behavior, deployInfo) if isNotSameTarget =>
|
||||
sendResponse(DeployableObjectsInfoMessage(behavior, deployInfo))
|
||||
|
||||
case LocalResponse.DeployableUIFor(item) =>
|
||||
sessionLogic.general.updateDeployableUIElements(avatar.deployables.UpdateUIElement(item))
|
||||
|
||||
case LocalResponse.Detonate(dguid, _: BoomerDeployable) =>
|
||||
sendResponse(TriggerEffectMessage(dguid, "detonate_boomer"))
|
||||
sendResponse(PlanetsideAttributeMessage(dguid, attribute_type=29, attribute_value=1))
|
||||
sendResponse(ObjectDeleteMessage(dguid, unk1=0))
|
||||
|
||||
case LocalResponse.Detonate(dguid, _: ExplosiveDeployable) =>
|
||||
sendResponse(GenericObjectActionMessage(dguid, code=19))
|
||||
sendResponse(PlanetsideAttributeMessage(dguid, attribute_type=29, attribute_value=1))
|
||||
sendResponse(ObjectDeleteMessage(dguid, unk1=0))
|
||||
|
||||
case LocalResponse.Detonate(_, obj) =>
|
||||
log.warn(s"LocalResponse.Detonate: ${obj.Definition.Name} not configured to explode correctly")
|
||||
|
||||
case LocalResponse.DoorOpens(doorGuid) if isNotSameTarget =>
|
||||
sendResponse(GenericObjectStateMsg(doorGuid, state=16))
|
||||
|
||||
case LocalResponse.DoorCloses(doorGuid) => //door closes for everyone
|
||||
sendResponse(GenericObjectStateMsg(doorGuid, state=17))
|
||||
|
||||
case LocalResponse.EliminateDeployable(obj: TurretDeployable, dguid, _, _) if obj.Destroyed =>
|
||||
sendResponse(ObjectDeleteMessage(dguid, unk1=0))
|
||||
|
||||
case LocalResponse.EliminateDeployable(obj: TurretDeployable, dguid, pos, _) =>
|
||||
obj.Destroyed = true
|
||||
DeconstructDeployable(
|
||||
obj,
|
||||
dguid,
|
||||
pos,
|
||||
obj.Orientation,
|
||||
deletionType= if (obj.MountPoints.isEmpty) { 2 } else { 1 }
|
||||
)
|
||||
|
||||
case LocalResponse.EliminateDeployable(obj: ExplosiveDeployable, dguid, _, _)
|
||||
if obj.Destroyed || obj.Jammed || obj.Health == 0 =>
|
||||
sendResponse(ObjectDeleteMessage(dguid, unk1=0))
|
||||
|
||||
case LocalResponse.EliminateDeployable(obj: ExplosiveDeployable, dguid, pos, effect) =>
|
||||
obj.Destroyed = true
|
||||
DeconstructDeployable(obj, dguid, pos, obj.Orientation, effect)
|
||||
|
||||
case LocalResponse.EliminateDeployable(obj: TelepadDeployable, dguid, _, _) if obj.Active && obj.Destroyed =>
|
||||
//if active, deactivate
|
||||
obj.Active = false
|
||||
sendResponse(GenericObjectActionMessage(dguid, code=29))
|
||||
sendResponse(GenericObjectActionMessage(dguid, code=30))
|
||||
//standard deployable elimination behavior
|
||||
sendResponse(ObjectDeleteMessage(dguid, unk1=0))
|
||||
|
||||
case LocalResponse.EliminateDeployable(obj: TelepadDeployable, dguid, pos, _) if obj.Active =>
|
||||
//if active, deactivate
|
||||
obj.Active = false
|
||||
sendResponse(GenericObjectActionMessage(dguid, code=29))
|
||||
sendResponse(GenericObjectActionMessage(dguid, code=30))
|
||||
//standard deployable elimination behavior
|
||||
obj.Destroyed = true
|
||||
DeconstructDeployable(obj, dguid, pos, obj.Orientation, deletionType=2)
|
||||
|
||||
case LocalResponse.EliminateDeployable(obj: TelepadDeployable, dguid, _, _) if obj.Destroyed =>
|
||||
//standard deployable elimination behavior
|
||||
sendResponse(ObjectDeleteMessage(dguid, unk1=0))
|
||||
|
||||
case LocalResponse.EliminateDeployable(obj: TelepadDeployable, dguid, pos, _) =>
|
||||
//standard deployable elimination behavior
|
||||
obj.Destroyed = true
|
||||
DeconstructDeployable(obj, dguid, pos, obj.Orientation, deletionType=2)
|
||||
|
||||
case LocalResponse.EliminateDeployable(obj, dguid, _, _) if obj.Destroyed =>
|
||||
sendResponse(ObjectDeleteMessage(dguid, unk1=0))
|
||||
|
||||
case LocalResponse.EliminateDeployable(obj, dguid, pos, effect) =>
|
||||
obj.Destroyed = true
|
||||
DeconstructDeployable(obj, dguid, pos, obj.Orientation, effect)
|
||||
|
||||
case LocalResponse.SendHackMessageHackCleared(targetGuid, unk1, unk2) =>
|
||||
sendResponse(HackMessage(unk1=0, targetGuid, guid, progress=0, unk1, HackState.HackCleared, unk2))
|
||||
|
||||
case LocalResponse.HackObject(targetGuid, unk1, unk2) =>
|
||||
sessionLogic.general.hackObject(targetGuid, unk1, unk2)
|
||||
|
||||
case LocalResponse.PlanetsideAttribute(targetGuid, attributeType, attributeValue) =>
|
||||
sessionLogic.general.sendPlanetsideAttributeMessage(targetGuid, attributeType, attributeValue)
|
||||
|
||||
case LocalResponse.GenericObjectAction(targetGuid, actionNumber) =>
|
||||
sendResponse(GenericObjectActionMessage(targetGuid, actionNumber))
|
||||
|
||||
case LocalResponse.GenericActionMessage(actionNumber) =>
|
||||
sendResponse(GenericActionMessage(actionNumber))
|
||||
|
||||
case LocalResponse.ChatMessage(msg) =>
|
||||
sendResponse(msg)
|
||||
|
||||
case LocalResponse.SendPacket(packet) =>
|
||||
sendResponse(packet)
|
||||
|
||||
case LocalResponse.LluSpawned(llu) =>
|
||||
// Create LLU on client
|
||||
sendResponse(ObjectCreateMessage(
|
||||
llu.Definition.ObjectId,
|
||||
llu.GUID,
|
||||
llu.Definition.Packet.ConstructorData(llu).get
|
||||
))
|
||||
sendResponse(TriggerSoundMessage(TriggeredSound.LLUMaterialize, llu.Position, unk=20, volume=0.8000001f))
|
||||
|
||||
case LocalResponse.LluDespawned(lluGuid, position) =>
|
||||
sendResponse(TriggerSoundMessage(TriggeredSound.LLUDeconstruct, position, unk=20, volume=0.8000001f))
|
||||
sendResponse(ObjectDeleteMessage(lluGuid, unk1=0))
|
||||
// If the player was holding the LLU, remove it from their tracked special item slot
|
||||
sessionLogic.general.specialItemSlotGuid.collect { case guid if guid == lluGuid =>
|
||||
sessionLogic.general.specialItemSlotGuid = None
|
||||
player.Carrying = None
|
||||
}
|
||||
|
||||
case LocalResponse.ObjectDelete(objectGuid, unk) if isNotSameTarget =>
|
||||
sendResponse(ObjectDeleteMessage(objectGuid, unk))
|
||||
|
||||
case LocalResponse.ProximityTerminalEffect(object_guid, true) =>
|
||||
sendResponse(ProximityTerminalUseMessage(Service.defaultPlayerGUID, object_guid, unk=true))
|
||||
|
||||
case LocalResponse.ProximityTerminalEffect(objectGuid, false) =>
|
||||
sendResponse(ProximityTerminalUseMessage(Service.defaultPlayerGUID, objectGuid, unk=false))
|
||||
sessionLogic.terminals.ForgetAllProximityTerminals(objectGuid)
|
||||
|
||||
case LocalResponse.RouterTelepadMessage(msg) =>
|
||||
sendResponse(ChatMsg(ChatMessageType.UNK_229, wideContents=false, recipient="", msg, note=None))
|
||||
|
||||
case LocalResponse.RouterTelepadTransport(passengerGuid, srcGuid, destGuid) =>
|
||||
sessionLogic.general.useRouterTelepadEffect(passengerGuid, srcGuid, destGuid)
|
||||
|
||||
case LocalResponse.SendResponse(msg) =>
|
||||
sendResponse(msg)
|
||||
|
||||
case LocalResponse.SetEmpire(objectGuid, empire) =>
|
||||
sendResponse(SetEmpireMessage(objectGuid, empire))
|
||||
|
||||
case LocalResponse.ShuttleEvent(ev) =>
|
||||
val msg = OrbitalShuttleTimeMsg(
|
||||
ev.u1,
|
||||
ev.u2,
|
||||
ev.t1,
|
||||
ev.t2,
|
||||
ev.t3,
|
||||
pairs=ev.pairs.map { case ((a, b), c) => PadAndShuttlePair(a, b, c) }
|
||||
)
|
||||
sendResponse(msg)
|
||||
|
||||
case LocalResponse.ShuttleDock(pguid, sguid, slot) =>
|
||||
sendResponse(ObjectAttachMessage(pguid, sguid, slot))
|
||||
|
||||
case LocalResponse.ShuttleUndock(pguid, sguid, pos, orient) =>
|
||||
sendResponse(ObjectDetachMessage(pguid, sguid, pos, orient))
|
||||
|
||||
case LocalResponse.ShuttleState(sguid, pos, orient, state) =>
|
||||
sendResponse(VehicleStateMessage(sguid, unk1=0, pos, orient, vel=None, Some(state), unk3=0, unk4=0, wheel_direction=15, is_decelerating=false, is_cloaked=false))
|
||||
|
||||
case LocalResponse.ToggleTeleportSystem(router, systemPlan) =>
|
||||
sessionLogic.general.toggleTeleportSystem(router, systemPlan)
|
||||
|
||||
case LocalResponse.TriggerEffect(targetGuid, effect, effectInfo, triggerLocation) =>
|
||||
sendResponse(TriggerEffectMessage(targetGuid, effect, effectInfo, triggerLocation))
|
||||
|
||||
case LocalResponse.TriggerSound(sound, pos, unk, volume) =>
|
||||
sendResponse(TriggerSoundMessage(sound, pos, unk, volume))
|
||||
|
||||
case LocalResponse.UpdateForceDomeStatus(buildingGuid, true) =>
|
||||
sendResponse(GenericObjectActionMessage(buildingGuid, 11))
|
||||
|
||||
case LocalResponse.UpdateForceDomeStatus(buildingGuid, false) =>
|
||||
sendResponse(GenericObjectActionMessage(buildingGuid, 12))
|
||||
|
||||
case LocalResponse.RechargeVehicleWeapon(vehicleGuid, weaponGuid) if resolvedPlayerGuid == guid =>
|
||||
continent.GUID(vehicleGuid)
|
||||
.collect { case vehicle: MountableWeapons => (vehicle, vehicle.PassengerInSeat(player)) }
|
||||
.collect { case (vehicle, Some(seat_num)) => vehicle.WeaponControlledFromSeat(seat_num) }
|
||||
.getOrElse(Set.empty)
|
||||
.collect { case weapon: Tool if weapon.GUID == weaponGuid =>
|
||||
sendResponse(InventoryStateMessage(weapon.AmmoSlot.Box.GUID, weapon.GUID, weapon.Magazine))
|
||||
}
|
||||
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
|
||||
/* support functions */
|
||||
|
||||
/**
|
||||
* Common behavior for deconstructing deployables in the game environment.
|
||||
* @param obj the deployable
|
||||
* @param guid the globally unique identifier for the deployable
|
||||
* @param pos the previous position of the deployable
|
||||
* @param orient the previous orientation of the deployable
|
||||
* @param deletionType the value passed to `ObjectDeleteMessage` concerning the deconstruction animation
|
||||
*/
|
||||
def DeconstructDeployable(
|
||||
obj: Deployable,
|
||||
guid: PlanetSideGUID,
|
||||
pos: Vector3,
|
||||
orient: Vector3,
|
||||
deletionType: Int
|
||||
): Unit = {
|
||||
sendResponse(TriggerEffectMessage("spawn_object_failed_effect", pos, orient))
|
||||
sendResponse(PlanetsideAttributeMessage(guid, 29, 1)) //make deployable vanish
|
||||
sendResponse(ObjectDeleteMessage(guid, deletionType))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,302 @@
|
|||
// Copyright (c) 2024 PSForever
|
||||
package net.psforever.actors.session.spectator
|
||||
|
||||
import akka.actor.ActorContext
|
||||
import net.psforever.actors.session.SessionActor
|
||||
import net.psforever.actors.session.normal.NormalMode
|
||||
import net.psforever.actors.session.support.{MountHandlerFunctions, SessionData, SessionMountHandlers}
|
||||
import net.psforever.actors.zone.ZoneActor
|
||||
import net.psforever.objects.{GlobalDefinitions, PlanetSideGameObject, Player, Vehicle, Vehicles}
|
||||
import net.psforever.objects.serverobject.affinity.FactionAffinity
|
||||
import net.psforever.objects.serverobject.mount.Mountable
|
||||
import net.psforever.objects.serverobject.terminals.implant.ImplantTerminalMech
|
||||
import net.psforever.objects.vehicles.{AccessPermissionGroup, CargoBehavior}
|
||||
import net.psforever.objects.vital.InGameHistory
|
||||
import net.psforever.packet.game.{DelayedPathMountMsg, DismountVehicleCargoMsg, DismountVehicleMsg, GenericObjectActionMessage, MountVehicleCargoMsg, MountVehicleMsg, ObjectDetachMessage, PlayerStasisMessage, PlayerStateShiftMessage, ShiftState}
|
||||
import net.psforever.services.Service
|
||||
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
|
||||
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
|
||||
import net.psforever.types.{BailType, PlanetSideGUID, Vector3}
|
||||
|
||||
object MountHandlerLogic {
|
||||
def apply(ops: SessionMountHandlers): MountHandlerLogic = {
|
||||
new MountHandlerLogic(ops, ops.context)
|
||||
}
|
||||
}
|
||||
|
||||
class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: ActorContext) extends MountHandlerFunctions {
|
||||
def sessionLogic: SessionData = ops.sessionLogic
|
||||
|
||||
/* packets */
|
||||
|
||||
def handleMountVehicle(pkt: MountVehicleMsg): Unit = { /* intentionally blank */ }
|
||||
|
||||
def handleDismountVehicle(pkt: DismountVehicleMsg): Unit = {
|
||||
val DismountVehicleMsg(player_guid, bailType, wasKickedByDriver) = pkt
|
||||
val dError: (String, Player)=> Unit = dismountError(bailType, wasKickedByDriver)
|
||||
//TODO optimize this later
|
||||
//common warning for this section
|
||||
if (player.GUID == player_guid) {
|
||||
//normally disembarking from a mount
|
||||
(sessionLogic.zoning.interstellarFerry.orElse(continent.GUID(player.VehicleSeated)) match {
|
||||
case out @ Some(obj: Vehicle) =>
|
||||
continent.GUID(obj.MountedIn) match {
|
||||
case Some(_: Vehicle) => None //cargo vehicle
|
||||
case _ => out //arrangement "may" be permissible
|
||||
}
|
||||
case out @ Some(_: Mountable) =>
|
||||
out
|
||||
case _ =>
|
||||
dError(s"DismountVehicleMsg: player ${player.Name} not considered seated in a mountable entity", player)
|
||||
None
|
||||
}) match {
|
||||
case Some(obj: Mountable) =>
|
||||
obj.PassengerInSeat(player) match {
|
||||
case Some(seat_num) =>
|
||||
obj.Actor ! Mountable.TryDismount(player, seat_num, bailType)
|
||||
//short-circuit the temporary channel for transferring between zones, the player is no longer doing that
|
||||
sessionLogic.zoning.interstellarFerry = None
|
||||
// Deconstruct the vehicle if the driver has bailed out and the vehicle is capable of flight
|
||||
//todo: implement auto landing procedure if the pilot bails but passengers are still present instead of deconstructing the vehicle
|
||||
//todo: continue flight path until aircraft crashes if no passengers present (or no passenger seats), then deconstruct.
|
||||
//todo: kick cargo passengers out. To be added after PR #216 is merged
|
||||
obj match {
|
||||
case v: Vehicle
|
||||
if bailType == BailType.Bailed &&
|
||||
v.SeatPermissionGroup(seat_num).contains(AccessPermissionGroup.Driver) &&
|
||||
v.isFlying =>
|
||||
v.Actor ! Vehicle.Deconstruct(None) //immediate deconstruction
|
||||
case _ => ;
|
||||
}
|
||||
|
||||
case None =>
|
||||
dError(s"DismountVehicleMsg: can not find where player ${player.Name}_guid is seated in mountable ${player.VehicleSeated}", player)
|
||||
}
|
||||
case _ =>
|
||||
dError(s"DismountVehicleMsg: can not find mountable entity ${player.VehicleSeated}", player)
|
||||
}
|
||||
} else {
|
||||
//kicking someone else out of a mount; need to own that mount/mountable
|
||||
val dWarn: (String, Player)=> Unit = dismountWarning(bailType, wasKickedByDriver)
|
||||
player.avatar.vehicle match {
|
||||
case Some(obj_guid) =>
|
||||
(
|
||||
(
|
||||
sessionLogic.validObject(obj_guid, decorator = "DismountVehicle/Vehicle"),
|
||||
sessionLogic.validObject(player_guid, decorator = "DismountVehicle/Player")
|
||||
) match {
|
||||
case (vehicle @ Some(obj: Vehicle), tplayer) =>
|
||||
if (obj.MountedIn.isEmpty) (vehicle, tplayer) else (None, None)
|
||||
case (mount @ Some(_: Mountable), tplayer) =>
|
||||
(mount, tplayer)
|
||||
case _ =>
|
||||
(None, None)
|
||||
}) match {
|
||||
case (Some(obj: Mountable), Some(tplayer: Player)) =>
|
||||
obj.PassengerInSeat(tplayer) match {
|
||||
case Some(seat_num) =>
|
||||
obj.Actor ! Mountable.TryDismount(tplayer, seat_num, bailType)
|
||||
case None =>
|
||||
dError(s"DismountVehicleMsg: can not find where other player ${tplayer.Name} is seated in mountable $obj_guid", tplayer)
|
||||
}
|
||||
case (None, _) =>
|
||||
dWarn(s"DismountVehicleMsg: ${player.Name} can not find his vehicle", player)
|
||||
case (_, None) =>
|
||||
dWarn(s"DismountVehicleMsg: player $player_guid could not be found to kick, ${player.Name}", player)
|
||||
case _ =>
|
||||
dWarn(s"DismountVehicleMsg: object is either not a Mountable or not a Player", player)
|
||||
}
|
||||
case None =>
|
||||
dWarn(s"DismountVehicleMsg: ${player.Name} does not own a vehicle", player)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def handleMountVehicleCargo(pkt: MountVehicleCargoMsg): Unit = { /* intentionally blank */ }
|
||||
|
||||
def handleDismountVehicleCargo(pkt: DismountVehicleCargoMsg): Unit = {
|
||||
val DismountVehicleCargoMsg(_, cargo_guid, bailed, _, kicked) = pkt
|
||||
continent.GUID(cargo_guid) match {
|
||||
case Some(cargo: Vehicle) =>
|
||||
cargo.Actor ! CargoBehavior.StartCargoDismounting(bailed || kicked)
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
|
||||
/* response handlers */
|
||||
|
||||
/**
|
||||
* na
|
||||
*
|
||||
* @param tplayer na
|
||||
* @param reply na
|
||||
*/
|
||||
def handle(tplayer: Player, reply: Mountable.Exchange): Unit = {
|
||||
reply match {
|
||||
case Mountable.CanDismount(obj: ImplantTerminalMech, seatNum, _) =>
|
||||
DismountAction(tplayer, obj, seatNum)
|
||||
obj.Zone.actor ! ZoneActor.RemoveFromBlockMap(player)
|
||||
|
||||
case Mountable.CanDismount(obj: Vehicle, _, mountPoint)
|
||||
if obj.Definition == GlobalDefinitions.orbital_shuttle && obj.MountedIn.nonEmpty =>
|
||||
//dismount to hart lobby
|
||||
val pguid = player.GUID
|
||||
val sguid = obj.GUID
|
||||
val (pos, zang) = Vehicles.dismountShuttle(obj, mountPoint)
|
||||
tplayer.Position = pos
|
||||
sendResponse(DelayedPathMountMsg(pguid, sguid, u1=60, u2=true))
|
||||
continent.LocalEvents ! LocalServiceMessage(
|
||||
continent.id,
|
||||
LocalAction.SendResponse(ObjectDetachMessage(sguid, pguid, pos, roll=0, pitch=0, zang))
|
||||
)
|
||||
obj.Zone.actor ! ZoneActor.RemoveFromBlockMap(player)
|
||||
sessionLogic.keepAliveFunc = sessionLogic.zoning.NormalKeepAlive
|
||||
|
||||
case Mountable.CanDismount(obj: Vehicle, seatNum, _)
|
||||
if obj.Definition == GlobalDefinitions.orbital_shuttle =>
|
||||
//get ready for orbital drop
|
||||
val pguid = player.GUID
|
||||
val events = continent.VehicleEvents
|
||||
DismountAction(tplayer, obj, seatNum)
|
||||
continent.actor ! ZoneActor.RemoveFromBlockMap(player) //character doesn't need it
|
||||
//DismountAction(...) uses vehicle service, so use that service to coordinate the remainder of the messages
|
||||
events ! VehicleServiceMessage(
|
||||
player.Name,
|
||||
VehicleAction.SendResponse(Service.defaultPlayerGUID, PlayerStasisMessage(pguid)) //the stasis message
|
||||
)
|
||||
//when the player dismounts, they will be positioned where the shuttle was when it disappeared in the sky
|
||||
//the player will fall to the ground and is perfectly vulnerable in this state
|
||||
//additionally, our player must exist in the current zone
|
||||
//having no in-game avatar target will throw us out of the map screen when deploying and cause softlock
|
||||
events ! VehicleServiceMessage(
|
||||
player.Name,
|
||||
VehicleAction.SendResponse(
|
||||
Service.defaultPlayerGUID,
|
||||
PlayerStateShiftMessage(ShiftState(unk=0, obj.Position, obj.Orientation.z, vel=None)) //cower in the shuttle bay
|
||||
)
|
||||
)
|
||||
events ! VehicleServiceMessage(
|
||||
continent.id,
|
||||
VehicleAction.SendResponse(pguid, GenericObjectActionMessage(pguid, code=9)) //conceal the player
|
||||
)
|
||||
context.self ! SessionActor.SetMode(NormalMode)
|
||||
sessionLogic.keepAliveFunc = sessionLogic.zoning.NormalKeepAlive
|
||||
|
||||
case Mountable.CanDismount(obj: Vehicle, seatNum, _)
|
||||
if obj.Definition == GlobalDefinitions.droppod =>
|
||||
sessionLogic.general.unaccessContainer(obj)
|
||||
DismountAction(tplayer, obj, seatNum)
|
||||
obj.Actor ! Vehicle.Deconstruct()
|
||||
|
||||
case Mountable.CanDismount(obj: Vehicle, seatNum, _)
|
||||
if tplayer.GUID == player.GUID =>
|
||||
sessionLogic.vehicles.ConditionalDriverVehicleControl(obj)
|
||||
sessionLogic.general.unaccessContainer(obj)
|
||||
DismountVehicleAction(tplayer, obj, seatNum)
|
||||
|
||||
case Mountable.CanDismount(obj: Vehicle, seat_num, _) =>
|
||||
continent.VehicleEvents ! VehicleServiceMessage(
|
||||
continent.id,
|
||||
VehicleAction.KickPassenger(tplayer.GUID, seat_num, unk2=true, obj.GUID)
|
||||
)
|
||||
|
||||
case Mountable.CanDismount(obj: PlanetSideGameObject with PlanetSideGameObject with Mountable with FactionAffinity with InGameHistory, seatNum, _) =>
|
||||
DismountAction(tplayer, obj, seatNum)
|
||||
|
||||
case Mountable.CanDismount(obj: Mountable, _, _) => ()
|
||||
|
||||
case Mountable.CanNotDismount(obj: Vehicle, seatNum) =>
|
||||
obj.Actor ! Vehicle.Deconstruct()
|
||||
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
|
||||
/* support functions */
|
||||
|
||||
private def dismountWarning(
|
||||
bailAs: BailType.Value,
|
||||
kickedByDriver: Boolean
|
||||
)
|
||||
(
|
||||
note: String,
|
||||
player: Player
|
||||
): Unit = {
|
||||
log.warn(note)
|
||||
player.VehicleSeated = None
|
||||
sendResponse(DismountVehicleMsg(player.GUID, bailAs, kickedByDriver))
|
||||
}
|
||||
|
||||
private def dismountError(
|
||||
bailAs: BailType.Value,
|
||||
kickedByDriver: Boolean
|
||||
)
|
||||
(
|
||||
note: String,
|
||||
player: Player
|
||||
): Unit = {
|
||||
log.error(s"$note; some vehicle might not know that ${player.Name} is no longer sitting in it")
|
||||
player.VehicleSeated = None
|
||||
sendResponse(DismountVehicleMsg(player.GUID, bailAs, kickedByDriver))
|
||||
}
|
||||
|
||||
/**
|
||||
* Common activities/procedure when a player dismounts a valid mountable object.
|
||||
* @param tplayer the player
|
||||
* @param obj the mountable object
|
||||
* @param seatNum the mount out of which which the player is disembarking
|
||||
*/
|
||||
private def DismountVehicleAction(tplayer: Player, obj: PlanetSideGameObject with FactionAffinity with InGameHistory, seatNum: Int): Unit = {
|
||||
DismountAction(tplayer, obj, seatNum)
|
||||
//until vehicles maintain synchronized momentum without a driver
|
||||
obj match {
|
||||
case v: Vehicle
|
||||
if seatNum == 0 && Vector3.MagnitudeSquared(v.Velocity.getOrElse(Vector3.Zero)) > 0f =>
|
||||
sessionLogic.vehicles.serverVehicleControlVelocity.collect { _ =>
|
||||
sessionLogic.vehicles.ServerVehicleOverrideStop(v)
|
||||
}
|
||||
v.Velocity = Vector3.Zero
|
||||
continent.VehicleEvents ! VehicleServiceMessage(
|
||||
continent.id,
|
||||
VehicleAction.VehicleState(
|
||||
tplayer.GUID,
|
||||
v.GUID,
|
||||
unk1 = 0,
|
||||
v.Position,
|
||||
v.Orientation,
|
||||
vel = None,
|
||||
v.Flying,
|
||||
unk3 = 0,
|
||||
unk4 = 0,
|
||||
wheel_direction = 15,
|
||||
unk5 = false,
|
||||
unk6 = v.Cloaked
|
||||
)
|
||||
)
|
||||
v.Zone.actor ! ZoneActor.RemoveFromBlockMap(player)
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Common activities/procedure when a player dismounts a valid mountable object.
|
||||
* @param tplayer the player
|
||||
* @param obj the mountable object
|
||||
* @param seatNum the mount out of which which the player is disembarking
|
||||
*/
|
||||
private def DismountAction(tplayer: Player, obj: PlanetSideGameObject with FactionAffinity with InGameHistory, seatNum: Int): Unit = {
|
||||
val playerGuid: PlanetSideGUID = tplayer.GUID
|
||||
tplayer.ContributionFrom(obj)
|
||||
sessionLogic.keepAliveFunc = sessionLogic.zoning.NormalKeepAlive
|
||||
val bailType = if (tplayer.BailProtection) {
|
||||
BailType.Bailed
|
||||
} else {
|
||||
BailType.Normal
|
||||
}
|
||||
sendResponse(DismountVehicleMsg(playerGuid, bailType, wasKickedByDriver = false))
|
||||
continent.VehicleEvents ! VehicleServiceMessage(
|
||||
continent.id,
|
||||
VehicleAction.DismountVehicle(playerGuid, bailType, unk2 = false)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,661 @@
|
|||
// Copyright (c) 2024 PSForever
|
||||
package net.psforever.actors.session.spectator
|
||||
|
||||
import akka.actor.Actor.Receive
|
||||
import akka.actor.ActorRef
|
||||
import net.psforever.actors.session.support.{AvatarHandlerFunctions, GalaxyHandlerFunctions, GeneralFunctions, LocalHandlerFunctions, MountHandlerFunctions, SquadHandlerFunctions, TerminalHandlerFunctions, VehicleFunctions, VehicleHandlerFunctions, WeaponAndProjectileFunctions}
|
||||
import net.psforever.actors.zone.ZoneActor
|
||||
import net.psforever.objects.avatar.{BattleRank, CommandRank, DeployableToolbox, FirstTimeEvents, Implant, ProgressDecoration, Shortcut => AvatarShortcut}
|
||||
import net.psforever.objects.serverobject.ServerObject
|
||||
import net.psforever.objects.{GlobalDefinitions, Player, Session, SimpleItem, Vehicle}
|
||||
import net.psforever.packet.PlanetSidePacket
|
||||
import net.psforever.packet.game.{ObjectCreateDetailedMessage, ObjectDeleteMessage}
|
||||
import net.psforever.packet.game.objectcreate.{ObjectClass, ObjectCreateMessageParent, RibbonBars}
|
||||
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
|
||||
import net.psforever.services.teamwork.{SquadAction, SquadServiceMessage}
|
||||
import net.psforever.types.{CapacitorStateType, ChatMessageType, ExoSuitType, MeritCommendation, SquadRequestType}
|
||||
//
|
||||
import net.psforever.actors.session.{AvatarActor, SessionActor}
|
||||
import net.psforever.actors.session.support.{ModeLogic, PlayerMode, SessionData, ZoningOperations}
|
||||
import net.psforever.objects.TurretDeployable
|
||||
import net.psforever.objects.guid.{GUIDTask, TaskWorkflow}
|
||||
import net.psforever.objects.serverobject.CommonMessages
|
||||
import net.psforever.objects.serverobject.containable.Containable
|
||||
import net.psforever.objects.serverobject.deploy.Deployment
|
||||
import net.psforever.objects.serverobject.mount.Mountable
|
||||
import net.psforever.objects.serverobject.terminals.{ProximityUnit, Terminal}
|
||||
import net.psforever.objects.zones.Zone
|
||||
import net.psforever.packet.PlanetSideGamePacket
|
||||
import net.psforever.packet.game.{AIDamage, ActionCancelMessage, AvatarFirstTimeEventMessage, AvatarGrenadeStateMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BeginZoningMessage, BindPlayerMessage, BugReportMessage, ChangeAmmoMessage, ChangeFireModeMessage, ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestMessage, ChatMsg, ChildObjectStateMessage, ConnectToWorldRequestMessage, CreateShortcutMessage, DeployObjectMessage, DeployRequestMessage, DismountVehicleCargoMsg, DismountVehicleMsg, DisplayedAwardMessage, DropItemMessage, DroppodLaunchRequestMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FavoritesRequest, FrameVehicleStateMessage, FriendsRequest, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, HitMessage, InvalidTerrainMessage, ItemTransactionMessage, KeepAliveMessage, LashMessage, LongRangeProjectileInfoMessage, LootItemMessage, MountVehicleCargoMsg, MountVehicleMsg, MoveItemMessage, ObjectDetectedMessage, ObjectHeldMessage, OutfitRequest, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, ProjectileStateMessage, ProximityTerminalUseMessage, ReleaseAvatarRequestMessage, ReloadMessage, RequestDestroyMessage, SetChatFilterMessage, SpawnRequestMessage, SplashHitMessage, SquadDefinitionActionMessage, SquadMembershipRequest, SquadWaypointRequest, TargetingImplantRequest, TradeMessage, UnuseItemMessage, UplinkRequest, UseItemMessage, VehicleStateMessage, VehicleSubStateMessage, VoiceHostInfo, VoiceHostRequest, WarpgateRequest, WeaponDelayFireMessage, WeaponDryFireMessage, WeaponFireMessage, WeaponLazeTargetPositionMessage, ZipLineMessage}
|
||||
import net.psforever.services.{InterstellarClusterService => ICS}
|
||||
import net.psforever.services.CavernRotationService
|
||||
import net.psforever.services.CavernRotationService.SendCavernRotationUpdates
|
||||
import net.psforever.services.ServiceManager.LookupResult
|
||||
import net.psforever.services.account.{PlayerToken, ReceiveAccountData}
|
||||
import net.psforever.services.avatar.AvatarServiceResponse
|
||||
import net.psforever.services.galaxy.GalaxyServiceResponse
|
||||
import net.psforever.services.local.LocalServiceResponse
|
||||
import net.psforever.services.teamwork.SquadServiceResponse
|
||||
import net.psforever.services.vehicle.VehicleServiceResponse
|
||||
|
||||
class SpectatorModeLogic(data: SessionData) extends ModeLogic {
|
||||
val avatarResponse: AvatarHandlerFunctions = AvatarHandlerLogic(data.avatarResponse)
|
||||
val galaxy: GalaxyHandlerFunctions = GalaxyHandlerLogic(data.galaxyResponseHandlers)
|
||||
val general: GeneralFunctions = GeneralLogic(data.general)
|
||||
val local: LocalHandlerFunctions = LocalHandlerLogic(data.localResponse)
|
||||
val mountResponse: MountHandlerFunctions = MountHandlerLogic(data.mountResponse)
|
||||
val shooting: WeaponAndProjectileFunctions = WeaponAndProjectileLogic(data.shooting)
|
||||
val squad: SquadHandlerFunctions = SquadHandlerLogic(data.squad)
|
||||
val terminals: TerminalHandlerFunctions = TerminalHandlerLogic(data.terminals)
|
||||
val vehicles: VehicleFunctions = VehicleLogic(data.vehicles)
|
||||
val vehicleResponse: VehicleHandlerFunctions = VehicleHandlerLogic(data.vehicleResponseOperations)
|
||||
|
||||
override def switchTo(session: Session): Unit = {
|
||||
val player = session.player
|
||||
val continent = session.zone
|
||||
val pguid = player.GUID
|
||||
val sendResponse: PlanetSidePacket=>Unit = data.sendResponse
|
||||
//
|
||||
sendResponse(ChatMsg(ChatMessageType.CMT_TOGGLESPECTATORMODE, "on"))
|
||||
sendResponse(ChatMsg(ChatMessageType.UNK_227, "@SpectatorEnabled"))
|
||||
continent.actor ! ZoneActor.RemoveFromBlockMap(player)
|
||||
data.general.avatarActor ! AvatarActor.DeactivateActiveImplants()
|
||||
continent
|
||||
.GUID(data.terminals.usingMedicalTerminal)
|
||||
.foreach { case term: Terminal with ProximityUnit =>
|
||||
data.terminals.StopUsingProximityUnit(term)
|
||||
}
|
||||
data.general.accessedContainer
|
||||
.collect {
|
||||
case veh: Vehicle if player.VehicleSeated.isEmpty || player.VehicleSeated.get != veh.GUID =>
|
||||
sendResponse(UnuseItemMessage(pguid, veh.GUID))
|
||||
sendResponse(UnuseItemMessage(pguid, pguid))
|
||||
data.general.unaccessContainer(veh)
|
||||
case container => //just in case
|
||||
if (player.VehicleSeated.isEmpty || player.VehicleSeated.get != container.GUID) {
|
||||
// Ensure we don't close the container if the player is seated in it
|
||||
// If the container is a corpse and gets removed just as this runs it can cause a client disconnect, so we'll check the container has a GUID first.
|
||||
if (container.HasGUID) {
|
||||
sendResponse(UnuseItemMessage(pguid, container.GUID))
|
||||
}
|
||||
sendResponse(UnuseItemMessage(pguid, pguid))
|
||||
data.general.unaccessContainer(container)
|
||||
}
|
||||
}
|
||||
player.CapacitorState = CapacitorStateType.Idle
|
||||
player.Capacitor = 0f
|
||||
player.Inventory.Items
|
||||
.foreach { entry => sendResponse(ObjectDeleteMessage(entry.GUID, 0)) }
|
||||
sendResponse(ObjectDeleteMessage(player.avatar.locker.GUID, 0))
|
||||
continent.AvatarEvents ! AvatarServiceMessage(continent.id, AvatarAction.ObjectDelete(pguid, pguid))
|
||||
player.Holsters()
|
||||
.collect { case slot if slot.Equipment.nonEmpty => sendResponse(ObjectDeleteMessage(slot.Equipment.get.GUID, 0)) }
|
||||
val vehicleAndSeat = data.vehicles.GetMountableAndSeat(None, player, continent) match {
|
||||
case (Some(obj: Vehicle), Some(seatNum)) if seatNum == 0 =>
|
||||
data.vehicles.ServerVehicleOverrideStop(obj)
|
||||
obj.Actor ! ServerObject.AttributeMsg(10, 3) //faction-accessible driver seat
|
||||
obj.Actor ! Mountable.TryDismount(player, seatNum)
|
||||
Some(ObjectCreateMessageParent(obj.GUID, seatNum))
|
||||
case (Some(obj), Some(seatNum)) =>
|
||||
obj.Actor ! Mountable.TryDismount(player, seatNum)
|
||||
Some(ObjectCreateMessageParent(obj.GUID, seatNum))
|
||||
case _ =>
|
||||
None
|
||||
}
|
||||
data.general.dropSpecialSlotItem()
|
||||
data.general.toggleMaxSpecialState(enable = false)
|
||||
data.terminals.CancelAllProximityUnits()
|
||||
data.terminals.lastTerminalOrderFulfillment = true
|
||||
data.squadService ! SquadServiceMessage(
|
||||
player,
|
||||
continent,
|
||||
SquadAction.Membership(SquadRequestType.Leave, player.CharId, Some(player.CharId), player.Name, None)
|
||||
)
|
||||
val originalEvent = player.History.headOption
|
||||
player.ClearHistory()
|
||||
player.LogActivity(originalEvent)
|
||||
player.spectator = true
|
||||
//
|
||||
val newPlayer = SpectatorModeLogic.spectatorCharacter(player)
|
||||
val cud = new SimpleItem(GlobalDefinitions.command_detonater)
|
||||
cud.GUID = player.avatar.locker.GUID
|
||||
sendResponse(ObjectCreateDetailedMessage(
|
||||
0L,
|
||||
ObjectClass.avatar,
|
||||
pguid,
|
||||
vehicleAndSeat,
|
||||
newPlayer.Definition.Packet.DetailedConstructorData(newPlayer).get
|
||||
))
|
||||
sendResponse(ObjectCreateDetailedMessage(
|
||||
0L,
|
||||
ObjectClass.command_detonater,
|
||||
cud.GUID,
|
||||
Some(ObjectCreateMessageParent(pguid, 4)),
|
||||
cud.Definition.Packet.DetailedConstructorData(cud).get
|
||||
))
|
||||
data.zoning.spawn.HandleSetCurrentAvatar(newPlayer)
|
||||
data.session = session.copy(player = player)
|
||||
}
|
||||
|
||||
override def switchFrom(session: Session): Unit = {
|
||||
import scala.concurrent.duration._
|
||||
val player = data.player
|
||||
val zoning = data.zoning
|
||||
val sendResponse: PlanetSidePacket => Unit = data.sendResponse
|
||||
//
|
||||
player.spectator = false
|
||||
sendResponse(ObjectDeleteMessage(player.avatar.locker.GUID, 0)) //free up the slot (from cud)
|
||||
sendResponse(ChatMsg(ChatMessageType.CMT_TOGGLESPECTATORMODE, "off"))
|
||||
sendResponse(ChatMsg(ChatMessageType.UNK_227, "@SpectatorDisabled"))
|
||||
zoning.spawn.randomRespawn(0.seconds) //to sanctuary
|
||||
}
|
||||
|
||||
def parse(sender: ActorRef): Receive = {
|
||||
/* really common messages (very frequently, every life) */
|
||||
case packet: PlanetSideGamePacket =>
|
||||
handleGamePkt(packet)
|
||||
|
||||
case AvatarServiceResponse(toChannel, guid, reply) =>
|
||||
avatarResponse.handle(toChannel, guid, reply)
|
||||
|
||||
case GalaxyServiceResponse(_, reply) =>
|
||||
galaxy.handle(reply)
|
||||
|
||||
case LocalServiceResponse(toChannel, guid, reply) =>
|
||||
local.handle(toChannel, guid, reply)
|
||||
|
||||
case Mountable.MountMessages(tplayer, reply) =>
|
||||
mountResponse.handle(tplayer, reply)
|
||||
|
||||
case SquadServiceResponse(_, excluded, response) =>
|
||||
squad.handle(response, excluded)
|
||||
|
||||
case Terminal.TerminalMessage(tplayer, msg, order) =>
|
||||
terminals.handle(tplayer, msg, order)
|
||||
|
||||
case VehicleServiceResponse(toChannel, guid, reply) =>
|
||||
vehicleResponse.handle(toChannel, guid, reply)
|
||||
|
||||
case SessionActor.SendResponse(packet) =>
|
||||
data.sendResponse(packet)
|
||||
|
||||
case SessionActor.CharSaved => ()
|
||||
|
||||
case SessionActor.CharSavedMsg => ()
|
||||
|
||||
/* common messages (maybe once every respawn) */
|
||||
case ICS.SpawnPointResponse(response) =>
|
||||
data.zoning.handleSpawnPointResponse(response)
|
||||
|
||||
case SessionActor.NewPlayerLoaded(tplayer) =>
|
||||
data.zoning.spawn.handleNewPlayerLoaded(tplayer)
|
||||
|
||||
case SessionActor.PlayerLoaded(tplayer) =>
|
||||
data.zoning.spawn.handlePlayerLoaded(tplayer)
|
||||
|
||||
case Zone.Population.PlayerHasLeft(zone, None) =>
|
||||
data.log.debug(s"PlayerHasLeft: ${data.player.Name} does not have a body on ${zone.id}")
|
||||
|
||||
case Zone.Population.PlayerHasLeft(zone, Some(tplayer)) =>
|
||||
if (tplayer.isAlive) {
|
||||
data.log.info(s"${tplayer.Name} has left zone ${zone.id}")
|
||||
}
|
||||
|
||||
case Zone.Population.PlayerCanNotSpawn(zone, tplayer) =>
|
||||
data.log.warn(s"${tplayer.Name} can not spawn in zone ${zone.id}; why?")
|
||||
|
||||
case Zone.Population.PlayerAlreadySpawned(zone, tplayer) =>
|
||||
data.log.warn(s"${tplayer.Name} is already spawned on zone ${zone.id}; is this a clerical error?")
|
||||
|
||||
case Zone.Vehicle.CanNotSpawn(zone, vehicle, reason) =>
|
||||
data.log.warn(
|
||||
s"${data.player.Name}'s ${vehicle.Definition.Name} can not spawn in ${zone.id} because $reason"
|
||||
)
|
||||
|
||||
case Zone.Vehicle.CanNotDespawn(zone, vehicle, reason) =>
|
||||
data.log.warn(
|
||||
s"${data.player.Name}'s ${vehicle.Definition.Name} can not deconstruct in ${zone.id} because $reason"
|
||||
)
|
||||
|
||||
case ICS.ZoneResponse(Some(zone)) =>
|
||||
data.zoning.handleZoneResponse(zone)
|
||||
|
||||
/* uncommon messages (once a session) */
|
||||
case ICS.ZonesResponse(zones) =>
|
||||
data.zoning.handleZonesResponse(zones)
|
||||
|
||||
case SessionActor.SetAvatar(avatar) =>
|
||||
general.handleSetAvatar(avatar)
|
||||
|
||||
case PlayerToken.LoginInfo(name, Zone.Nowhere, _) =>
|
||||
data.zoning.spawn.handleLoginInfoNowhere(name, sender)
|
||||
|
||||
case PlayerToken.LoginInfo(name, inZone, optionalSavedData) =>
|
||||
data.zoning.spawn.handleLoginInfoSomewhere(name, inZone, optionalSavedData, sender)
|
||||
|
||||
case PlayerToken.RestoreInfo(playerName, inZone, pos) =>
|
||||
data.zoning.spawn.handleLoginInfoRestore(playerName, inZone, pos, sender)
|
||||
|
||||
case PlayerToken.CanNotLogin(playerName, reason) =>
|
||||
data.zoning.spawn.handleLoginCanNot(playerName, reason)
|
||||
|
||||
case ReceiveAccountData(account) =>
|
||||
general.handleReceiveAccountData(account)
|
||||
|
||||
case AvatarActor.AvatarResponse(avatar) =>
|
||||
general.handleAvatarResponse(avatar)
|
||||
|
||||
case AvatarActor.AvatarLoginResponse(avatar) =>
|
||||
data.zoning.spawn.avatarLoginResponse(avatar)
|
||||
|
||||
case SessionActor.SetCurrentAvatar(tplayer, max_attempts, attempt) =>
|
||||
data.zoning.spawn.ReadyToSetCurrentAvatar(tplayer, max_attempts, attempt)
|
||||
|
||||
case SessionActor.SetConnectionState(state) =>
|
||||
data.connectionState = state
|
||||
|
||||
case SessionActor.AvatarLoadingSync(state) =>
|
||||
data.zoning.spawn.handleAvatarLoadingSync(state)
|
||||
|
||||
/* uncommon messages (utility, or once in a while) */
|
||||
case ZoningOperations.AvatarAwardMessageBundle(pkts, delay) =>
|
||||
data.zoning.spawn.performAvatarAwardMessageDelivery(pkts, delay)
|
||||
|
||||
case CommonMessages.ProgressEvent(delta, finishedAction, stepAction, tick) =>
|
||||
general.ops.handleProgressChange(delta, finishedAction, stepAction, tick)
|
||||
|
||||
case CommonMessages.Progress(rate, finishedAction, stepAction) =>
|
||||
general.ops.setupProgressChange(rate, finishedAction, stepAction)
|
||||
|
||||
case CavernRotationService.CavernRotationServiceKey.Listing(listings) =>
|
||||
listings.head ! SendCavernRotationUpdates(data.context.self)
|
||||
|
||||
case LookupResult("propertyOverrideManager", endpoint) =>
|
||||
data.zoning.propertyOverrideManagerLoadOverrides(endpoint)
|
||||
|
||||
case SessionActor.UpdateIgnoredPlayers(msg) =>
|
||||
galaxy.handleUpdateIgnoredPlayers(msg)
|
||||
|
||||
case SessionActor.UseCooldownRenewed(definition, _) =>
|
||||
general.handleUseCooldownRenew(definition)
|
||||
|
||||
case Deployment.CanDeploy(obj, state) =>
|
||||
vehicles.handleCanDeploy(obj, state)
|
||||
|
||||
case Deployment.CanUndeploy(obj, state) =>
|
||||
vehicles.handleCanUndeploy(obj, state)
|
||||
|
||||
case Deployment.CanNotChangeDeployment(obj, state, reason) =>
|
||||
vehicles.handleCanNotChangeDeployment(obj, state, reason)
|
||||
|
||||
/* rare messages */
|
||||
case ProximityUnit.StopAction(term, _) =>
|
||||
terminals.ops.LocalStopUsingProximityUnit(term)
|
||||
|
||||
case SessionActor.Suicide() =>
|
||||
general.ops.suicide(data.player)
|
||||
|
||||
case SessionActor.Recall() =>
|
||||
data.zoning.handleRecall()
|
||||
|
||||
case SessionActor.InstantAction() =>
|
||||
data.zoning.handleInstantAction()
|
||||
|
||||
case SessionActor.Quit() =>
|
||||
data.zoning.handleQuit()
|
||||
|
||||
case ICS.DroppodLaunchDenial(errorCode, _) =>
|
||||
data.zoning.handleDroppodLaunchDenial(errorCode)
|
||||
|
||||
case ICS.DroppodLaunchConfirmation(zone, position) =>
|
||||
data.zoning.LoadZoneLaunchDroppod(zone, position)
|
||||
|
||||
case SessionActor.PlayerFailedToLoad(tplayer) =>
|
||||
data.failWithError(s"${tplayer.Name} failed to load anywhere")
|
||||
|
||||
/* csr only */
|
||||
case SessionActor.SetSpeed(speed) =>
|
||||
general.handleSetSpeed(speed)
|
||||
|
||||
case SessionActor.SetFlying(isFlying) =>
|
||||
general.handleSetFlying(isFlying)
|
||||
|
||||
case SessionActor.SetSpectator(isSpectator) =>
|
||||
general.handleSetSpectator(isSpectator)
|
||||
|
||||
case SessionActor.Kick(player, time) =>
|
||||
general.handleKick(player, time)
|
||||
|
||||
case SessionActor.SetZone(zoneId, position) =>
|
||||
data.zoning.handleSetZone(zoneId, position)
|
||||
|
||||
case SessionActor.SetPosition(position) =>
|
||||
data.zoning.spawn.handleSetPosition(position)
|
||||
|
||||
case SessionActor.SetSilenced(silenced) =>
|
||||
general.handleSilenced(silenced)
|
||||
|
||||
/* catch these messages */
|
||||
case _: ProximityUnit.Action => ;
|
||||
|
||||
case _: Zone.Vehicle.HasSpawned => ;
|
||||
|
||||
case _: Zone.Vehicle.HasDespawned => ;
|
||||
|
||||
case Zone.Deployable.IsDismissed(obj: TurretDeployable) => //only if target deployable was never fully introduced
|
||||
TaskWorkflow.execute(GUIDTask.unregisterDeployableTurret(data.continent.GUID, obj))
|
||||
|
||||
case Zone.Deployable.IsDismissed(obj) => //only if target deployable was never fully introduced
|
||||
TaskWorkflow.execute(GUIDTask.unregisterObject(data.continent.GUID, obj))
|
||||
|
||||
case msg: Containable.ItemPutInSlot =>
|
||||
data.log.debug(s"ItemPutInSlot: $msg")
|
||||
|
||||
case msg: Containable.CanNotPutItemInSlot =>
|
||||
data.log.debug(s"CanNotPutItemInSlot: $msg")
|
||||
|
||||
case _ => ()
|
||||
}
|
||||
|
||||
private def handleGamePkt: PlanetSideGamePacket => Unit = {
|
||||
case packet: ConnectToWorldRequestMessage =>
|
||||
general.handleConnectToWorldRequest(packet)
|
||||
|
||||
case packet: MountVehicleCargoMsg =>
|
||||
mountResponse.handleMountVehicleCargo(packet)
|
||||
|
||||
case packet: DismountVehicleCargoMsg =>
|
||||
mountResponse.handleDismountVehicleCargo(packet)
|
||||
|
||||
case packet: CharacterCreateRequestMessage =>
|
||||
general.handleCharacterCreateRequest(packet)
|
||||
|
||||
case packet: CharacterRequestMessage =>
|
||||
general.handleCharacterRequest(packet)
|
||||
|
||||
case _: KeepAliveMessage =>
|
||||
data.keepAliveFunc()
|
||||
|
||||
case packet: BeginZoningMessage =>
|
||||
data.zoning.handleBeginZoning(packet)
|
||||
|
||||
case packet: PlayerStateMessageUpstream =>
|
||||
general.handlePlayerStateUpstream(packet)
|
||||
|
||||
case packet: ChildObjectStateMessage =>
|
||||
vehicles.handleChildObjectState(packet)
|
||||
|
||||
case packet: VehicleStateMessage =>
|
||||
vehicles.handleVehicleState(packet)
|
||||
|
||||
case packet: VehicleSubStateMessage =>
|
||||
vehicles.handleVehicleSubState(packet)
|
||||
|
||||
case packet: FrameVehicleStateMessage =>
|
||||
vehicles.handleFrameVehicleState(packet)
|
||||
|
||||
case packet: ProjectileStateMessage =>
|
||||
shooting.handleProjectileState(packet)
|
||||
|
||||
case packet: LongRangeProjectileInfoMessage =>
|
||||
shooting.handleLongRangeProjectileState(packet)
|
||||
|
||||
case packet: ReleaseAvatarRequestMessage =>
|
||||
data.zoning.spawn.handleReleaseAvatarRequest(packet)
|
||||
|
||||
case packet: SpawnRequestMessage =>
|
||||
data.zoning.spawn.handleSpawnRequest(packet)
|
||||
|
||||
case packet: ChatMsg =>
|
||||
general.handleChat(packet)
|
||||
|
||||
case packet: SetChatFilterMessage =>
|
||||
general.handleChatFilter(packet)
|
||||
|
||||
case packet: VoiceHostRequest =>
|
||||
general.handleVoiceHostRequest(packet)
|
||||
|
||||
case packet: VoiceHostInfo =>
|
||||
general.handleVoiceHostInfo(packet)
|
||||
|
||||
case packet: ChangeAmmoMessage =>
|
||||
shooting.handleChangeAmmo(packet)
|
||||
|
||||
case packet: ChangeFireModeMessage =>
|
||||
shooting.handleChangeFireMode(packet)
|
||||
|
||||
case packet: ChangeFireStateMessage_Start =>
|
||||
shooting.handleChangeFireStateStart(packet)
|
||||
|
||||
case packet: ChangeFireStateMessage_Stop =>
|
||||
shooting.handleChangeFireStateStop(packet)
|
||||
|
||||
case packet: EmoteMsg =>
|
||||
general.handleEmote(packet)
|
||||
|
||||
case packet: DropItemMessage =>
|
||||
general.handleDropItem(packet)
|
||||
|
||||
case packet: PickupItemMessage =>
|
||||
general.handlePickupItem(packet)
|
||||
|
||||
case packet: ReloadMessage =>
|
||||
shooting.handleReload(packet)
|
||||
|
||||
case packet: ObjectHeldMessage =>
|
||||
general.handleObjectHeld(packet)
|
||||
|
||||
case packet: AvatarJumpMessage =>
|
||||
general.handleAvatarJump(packet)
|
||||
|
||||
case packet: ZipLineMessage =>
|
||||
general.handleZipLine(packet)
|
||||
|
||||
case packet: RequestDestroyMessage =>
|
||||
general.handleRequestDestroy(packet)
|
||||
|
||||
case packet: MoveItemMessage =>
|
||||
general.handleMoveItem(packet)
|
||||
|
||||
case packet: LootItemMessage =>
|
||||
general.handleLootItem(packet)
|
||||
|
||||
case packet: AvatarImplantMessage =>
|
||||
general.handleAvatarImplant(packet)
|
||||
|
||||
case packet: UseItemMessage =>
|
||||
general.handleUseItem(packet)
|
||||
|
||||
case packet: UnuseItemMessage =>
|
||||
general.handleUnuseItem(packet)
|
||||
|
||||
case packet: ProximityTerminalUseMessage =>
|
||||
terminals.handleProximityTerminalUse(packet)
|
||||
|
||||
case packet: DeployObjectMessage =>
|
||||
general.handleDeployObject(packet)
|
||||
|
||||
case packet: GenericObjectActionMessage =>
|
||||
general.handleGenericObjectAction(packet)
|
||||
|
||||
case packet: GenericObjectActionAtPositionMessage =>
|
||||
general.handleGenericObjectActionAtPosition(packet)
|
||||
|
||||
case packet: GenericObjectStateMsg =>
|
||||
general.handleGenericObjectState(packet)
|
||||
|
||||
case packet: GenericActionMessage =>
|
||||
general.handleGenericAction(packet)
|
||||
|
||||
case packet: ItemTransactionMessage =>
|
||||
terminals.handleItemTransaction(packet)
|
||||
|
||||
case packet: FavoritesRequest =>
|
||||
terminals.handleFavoritesRequest(packet)
|
||||
|
||||
case packet: WeaponDelayFireMessage =>
|
||||
shooting.handleWeaponDelayFire(packet)
|
||||
|
||||
case packet: WeaponDryFireMessage =>
|
||||
shooting.handleWeaponDryFire(packet)
|
||||
|
||||
case packet: WeaponFireMessage =>
|
||||
shooting.handleWeaponFire(packet)
|
||||
|
||||
case packet: WeaponLazeTargetPositionMessage =>
|
||||
shooting.handleWeaponLazeTargetPosition(packet)
|
||||
|
||||
case packet: UplinkRequest =>
|
||||
shooting.handleUplinkRequest(packet)
|
||||
|
||||
case packet: HitMessage =>
|
||||
shooting.handleDirectHit(packet)
|
||||
|
||||
case packet: SplashHitMessage =>
|
||||
shooting.handleSplashHit(packet)
|
||||
|
||||
case packet: LashMessage =>
|
||||
shooting.handleLashHit(packet)
|
||||
|
||||
case packet: AIDamage =>
|
||||
shooting.handleAIDamage(packet)
|
||||
|
||||
case packet: AvatarFirstTimeEventMessage =>
|
||||
general.handleAvatarFirstTimeEvent(packet)
|
||||
|
||||
case packet: WarpgateRequest =>
|
||||
data.zoning.handleWarpgateRequest(packet)
|
||||
|
||||
case packet: MountVehicleMsg =>
|
||||
mountResponse.handleMountVehicle(packet)
|
||||
|
||||
case packet: DismountVehicleMsg =>
|
||||
mountResponse.handleDismountVehicle(packet)
|
||||
|
||||
case packet: DeployRequestMessage =>
|
||||
vehicles.handleDeployRequest(packet)
|
||||
|
||||
case packet: AvatarGrenadeStateMessage =>
|
||||
shooting.handleAvatarGrenadeState(packet)
|
||||
|
||||
case packet: SquadDefinitionActionMessage =>
|
||||
squad.handleSquadDefinitionAction(packet)
|
||||
|
||||
case packet: SquadMembershipRequest =>
|
||||
squad.handleSquadMemberRequest(packet)
|
||||
|
||||
case packet: SquadWaypointRequest =>
|
||||
squad.handleSquadWaypointRequest(packet)
|
||||
|
||||
case packet: GenericCollisionMsg =>
|
||||
general.handleGenericCollision(packet)
|
||||
|
||||
case packet: BugReportMessage =>
|
||||
general.handleBugReport(packet)
|
||||
|
||||
case packet: BindPlayerMessage =>
|
||||
general.handleBindPlayer(packet)
|
||||
|
||||
case packet: PlanetsideAttributeMessage =>
|
||||
general.handlePlanetsideAttribute(packet)
|
||||
|
||||
case packet: FacilityBenefitShieldChargeRequestMessage =>
|
||||
general.handleFacilityBenefitShieldChargeRequest(packet)
|
||||
|
||||
case packet: BattleplanMessage =>
|
||||
general.handleBattleplan(packet)
|
||||
|
||||
case packet: CreateShortcutMessage =>
|
||||
general.handleCreateShortcut(packet)
|
||||
|
||||
case packet: ChangeShortcutBankMessage =>
|
||||
general.handleChangeShortcutBank(packet)
|
||||
|
||||
case packet: FriendsRequest =>
|
||||
general.handleFriendRequest(packet)
|
||||
|
||||
case packet: DroppodLaunchRequestMessage =>
|
||||
data.zoning.handleDroppodLaunchRequest(packet)
|
||||
|
||||
case packet: InvalidTerrainMessage =>
|
||||
general.handleInvalidTerrain(packet)
|
||||
|
||||
case packet: ActionCancelMessage =>
|
||||
general.handleActionCancel(packet)
|
||||
|
||||
case packet: TradeMessage =>
|
||||
general.handleTrade(packet)
|
||||
|
||||
case packet: DisplayedAwardMessage =>
|
||||
general.handleDisplayedAward(packet)
|
||||
|
||||
case packet: ObjectDetectedMessage =>
|
||||
general.handleObjectDetected(packet)
|
||||
|
||||
case packet: TargetingImplantRequest =>
|
||||
general.handleTargetingImplantRequest(packet)
|
||||
|
||||
case packet: HitHint =>
|
||||
general.handleHitHint(packet)
|
||||
|
||||
case _: OutfitRequest => ()
|
||||
|
||||
case pkt =>
|
||||
data.log.warn(s"Unhandled GamePacket $pkt")
|
||||
}
|
||||
}
|
||||
|
||||
object SpectatorModeLogic {
|
||||
final val SpectatorImplants: Seq[Option[Implant]] = Seq(
|
||||
Some(Implant(GlobalDefinitions.targeting, initialized = true)),
|
||||
Some(Implant(GlobalDefinitions.darklight_vision, initialized = true)),
|
||||
Some(Implant(GlobalDefinitions.range_magnifier, initialized = true))
|
||||
)
|
||||
|
||||
private def spectatorCharacter(player: Player): Player = {
|
||||
val avatar = player.avatar
|
||||
val newAvatar = avatar.copy(
|
||||
basic = avatar.basic.copy(name = "spectator"),
|
||||
bep = BattleRank.BR18.experience,
|
||||
cep = CommandRank.CR5.experience,
|
||||
certifications = Set(),
|
||||
decoration = ProgressDecoration(
|
||||
ribbonBars = RibbonBars(
|
||||
MeritCommendation.BendingMovieActor,
|
||||
MeritCommendation.BendingMovieActor,
|
||||
MeritCommendation.BendingMovieActor,
|
||||
MeritCommendation.BendingMovieActor
|
||||
),
|
||||
firstTimeEvents = FirstTimeEvents.All
|
||||
),
|
||||
deployables = {
|
||||
val dt = new DeployableToolbox()
|
||||
dt.Initialize(Set())
|
||||
dt
|
||||
},
|
||||
implants = SpectatorImplants,
|
||||
lookingForSquad = false,
|
||||
shortcuts = {
|
||||
val allShortcuts: Array[Option[AvatarShortcut]] = Array.fill[Option[AvatarShortcut]](64)(None)
|
||||
SpectatorImplants.zipWithIndex.collect { case (Some(implant), slot) =>
|
||||
allShortcuts.update(slot + 1, Some(AvatarShortcut(2, implant.definition.Name)))
|
||||
}
|
||||
allShortcuts
|
||||
}
|
||||
)
|
||||
val newPlayer = Player(newAvatar)
|
||||
newPlayer.GUID = player.GUID
|
||||
newPlayer.ExoSuit = ExoSuitType.Infiltration
|
||||
newPlayer.Position = player.Position
|
||||
newPlayer.Orientation = player.Orientation
|
||||
newPlayer.spectator = true
|
||||
newPlayer.Spawn()
|
||||
newPlayer
|
||||
}
|
||||
}
|
||||
|
||||
case object SpectatorMode extends PlayerMode {
|
||||
def setup(data: SessionData): ModeLogic = {
|
||||
new SpectatorModeLogic(data)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,184 @@
|
|||
// Copyright (c) 2024 PSForever
|
||||
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, ChatActor}
|
||||
import net.psforever.actors.session.support.{SessionData, SessionSquadHandlers, 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.services.chat.ChatService
|
||||
import net.psforever.services.teamwork.SquadResponse
|
||||
import net.psforever.types.{PlanetSideGUID, SquadListDecoration, SquadResponseType}
|
||||
|
||||
object SquadHandlerLogic {
|
||||
def apply(ops: SessionSquadHandlers): SquadHandlerLogic = {
|
||||
new SquadHandlerLogic(ops, ops.context)
|
||||
}
|
||||
}
|
||||
|
||||
class SquadHandlerLogic(val ops: SessionSquadHandlers, implicit val context: ActorContext) extends SquadHandlerFunctions {
|
||||
def sessionLogic: SessionData = ops.sessionLogic
|
||||
|
||||
private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor
|
||||
|
||||
private val chatActor: typed.ActorRef[ChatActor.Command] = ops.chatActor
|
||||
|
||||
//private val squadService: ActorRef = ops.squadService
|
||||
|
||||
/* packet */
|
||||
|
||||
def handleSquadDefinitionAction(pkt: SquadDefinitionActionMessage): Unit = { /* intentionally blank */ }
|
||||
|
||||
def handleSquadMemberRequest(pkt: SquadMembershipRequest): Unit = { /* intentionally blank */ }
|
||||
|
||||
def handleSquadWaypointRequest(pkt: SquadWaypointRequest): Unit = { /* intentionally blank */ }
|
||||
|
||||
/* response handlers */
|
||||
|
||||
def handle(response: SquadResponse.Response, excluded: Iterable[Long]): Unit = {
|
||||
if (!excluded.exists(_ == avatar.id)) {
|
||||
response match {
|
||||
case SquadResponse.InitList(infos) =>
|
||||
sendResponse(ReplicationStreamMessage(infos))
|
||||
|
||||
case SquadResponse.UpdateList(infos) if infos.nonEmpty =>
|
||||
sendResponse(
|
||||
ReplicationStreamMessage(
|
||||
6,
|
||||
None,
|
||||
infos.map {
|
||||
case (index, squadInfo) =>
|
||||
SquadListing(index, squadInfo)
|
||||
}.toVector
|
||||
)
|
||||
)
|
||||
|
||||
case SquadResponse.RemoveFromList(infos) if infos.nonEmpty =>
|
||||
sendResponse(
|
||||
ReplicationStreamMessage(
|
||||
1,
|
||||
None,
|
||||
infos.map { index =>
|
||||
SquadListing(index, None)
|
||||
}.toVector
|
||||
)
|
||||
)
|
||||
|
||||
case SquadResponse.SquadDecoration(guid, squad) =>
|
||||
val decoration = if (
|
||||
ops.squadUI.nonEmpty ||
|
||||
squad.Size == squad.Capacity ||
|
||||
{
|
||||
val offer = avatar.certifications
|
||||
!squad.Membership.exists { _.isAvailable(offer) }
|
||||
}
|
||||
) {
|
||||
SquadListDecoration.NotAvailable
|
||||
} else {
|
||||
SquadListDecoration.Available
|
||||
}
|
||||
sendResponse(SquadDefinitionActionMessage(guid, 0, SquadAction.SquadListDecorator(decoration)))
|
||||
|
||||
case SquadResponse.Detail(guid, detail) =>
|
||||
sendResponse(SquadDetailDefinitionUpdateMessage(guid, detail))
|
||||
|
||||
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 =>
|
||||
//the name of the player indicated by unk3 is needed
|
||||
LivePlayerList.WorldPopulation({ case (_, a: Avatar) => charId == a.id }).headOption match {
|
||||
case Some(player) =>
|
||||
player.name
|
||||
case None =>
|
||||
player_name
|
||||
}
|
||||
case _ =>
|
||||
player_name
|
||||
}
|
||||
sendResponse(SquadMembershipResponse(request_type, unk1, unk2, charId, opt_char_id, name, unk5, unk6))
|
||||
|
||||
case SquadResponse.Leave(squad, positionsToUpdate) =>
|
||||
positionsToUpdate.find({ case (member, _) => member == avatar.id }) match {
|
||||
case Some((ourMember, ourIndex)) =>
|
||||
//we are leaving the squad
|
||||
//remove each member's entry (our own too)
|
||||
ops.updateSquadRef = Default.Actor
|
||||
positionsToUpdate.foreach {
|
||||
case (member, index) =>
|
||||
sendResponse(SquadMemberEvent.Remove(ops.squad_supplement_id, member, index))
|
||||
ops.squadUI.remove(member)
|
||||
}
|
||||
//uninitialize
|
||||
val playerGuid = player.GUID
|
||||
sendResponse(SquadMemberEvent.Remove(ops.squad_supplement_id, ourMember, ourIndex)) //repeat of our entry
|
||||
ops.GiveSquadColorsToSelf(value = 0)
|
||||
sendResponse(PlanetsideAttributeMessage(playerGuid, 32, 0)) //disassociate with member position in squad?
|
||||
sendResponse(PlanetsideAttributeMessage(playerGuid, 34, 4294967295L)) //unknown, perhaps unrelated?
|
||||
avatarActor ! AvatarActor.SetLookingForSquad(false)
|
||||
//a finalization? what does this do?
|
||||
sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(18)))
|
||||
ops.squad_supplement_id = 0
|
||||
ops.squadUpdateCounter = 0
|
||||
ops.updateSquad = ops.NoSquadUpdates
|
||||
chatActor ! ChatActor.LeaveChannel(ChatService.ChatChannel.Squad(squad.GUID))
|
||||
case _ =>
|
||||
//remove each member's entry
|
||||
ops.GiveSquadColorsToMembers(
|
||||
positionsToUpdate.map {
|
||||
case (member, index) =>
|
||||
sendResponse(SquadMemberEvent.Remove(ops.squad_supplement_id, member, index))
|
||||
ops.squadUI.remove(member)
|
||||
member
|
||||
},
|
||||
value = 0
|
||||
)
|
||||
}
|
||||
|
||||
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 _ => ()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
// Copyright (c) 2024 PSForever
|
||||
package net.psforever.actors.session.spectator
|
||||
|
||||
import akka.actor.ActorContext
|
||||
import net.psforever.actors.session.support.{SessionData, SessionTerminalHandlers, TerminalHandlerFunctions}
|
||||
import net.psforever.login.WorldSession.SellEquipmentFromInventory
|
||||
import net.psforever.objects.Player
|
||||
import net.psforever.objects.serverobject.terminals.Terminal
|
||||
import net.psforever.packet.game.{FavoritesRequest, ItemTransactionMessage, ItemTransactionResultMessage, ProximityTerminalUseMessage}
|
||||
|
||||
object TerminalHandlerLogic {
|
||||
def apply(ops: SessionTerminalHandlers): TerminalHandlerLogic = {
|
||||
new TerminalHandlerLogic(ops, ops.context)
|
||||
}
|
||||
}
|
||||
|
||||
class TerminalHandlerLogic(val ops: SessionTerminalHandlers, implicit val context: ActorContext) extends TerminalHandlerFunctions {
|
||||
def sessionLogic: SessionData = ops.sessionLogic
|
||||
|
||||
def handleItemTransaction(pkt: ItemTransactionMessage): Unit = { /* intentionally blank */ }
|
||||
|
||||
def handleProximityTerminalUse(pkt: ProximityTerminalUseMessage): Unit = { /* intentionally blank */ }
|
||||
|
||||
def handleFavoritesRequest(pkt: FavoritesRequest): Unit = { /* intentionally blank */ }
|
||||
|
||||
/**
|
||||
* na
|
||||
* @param tplayer na
|
||||
* @param msg na
|
||||
* @param order na
|
||||
*/
|
||||
def handle(tplayer: Player, msg: ItemTransactionMessage, order: Terminal.Exchange): Unit = {
|
||||
order match {
|
||||
case Terminal.SellEquipment() =>
|
||||
SellEquipmentFromInventory(tplayer, tplayer, msg.terminal_guid)(Player.FreeHandSlot)
|
||||
|
||||
case _ if msg != null =>
|
||||
sendResponse(ItemTransactionResultMessage(msg.terminal_guid, msg.transaction_type, success = false))
|
||||
ops.lastTerminalOrderFulfillment = true
|
||||
|
||||
case _ =>
|
||||
ops.lastTerminalOrderFulfillment = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,399 @@
|
|||
// Copyright (c) 2024 PSForever
|
||||
package net.psforever.actors.session.spectator
|
||||
|
||||
import akka.actor.{ActorContext, ActorRef, typed}
|
||||
import net.psforever.actors.session.AvatarActor
|
||||
import net.psforever.actors.session.support.{SessionData, SessionVehicleHandlers, VehicleHandlerFunctions}
|
||||
import net.psforever.objects.{GlobalDefinitions, Player, Tool, Vehicle, Vehicles}
|
||||
import net.psforever.objects.equipment.{Equipment, JammableMountedWeapons, JammableUnit}
|
||||
import net.psforever.objects.guid.{GUIDTask, TaskWorkflow}
|
||||
import net.psforever.objects.serverobject.mount.Mountable
|
||||
import net.psforever.objects.serverobject.pad.VehicleSpawnPad
|
||||
import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent
|
||||
import net.psforever.packet.game.{ChangeAmmoMessage, ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, ChatMsg, ChildObjectStateMessage, DeadState, DeployRequestMessage, DismountVehicleMsg, FrameVehicleStateMessage, GenericObjectActionMessage, HitHint, InventoryStateMessage, ObjectAttachMessage, ObjectCreateDetailedMessage, ObjectCreateMessage, ObjectDeleteMessage, ObjectDetachMessage, PlanetsideAttributeMessage, ReloadMessage, ServerVehicleOverrideMsg, VehicleStateMessage, WeaponDryFireMessage}
|
||||
import net.psforever.services.Service
|
||||
import net.psforever.services.vehicle.{VehicleResponse, VehicleServiceResponse}
|
||||
import net.psforever.types.{BailType, ChatMessageType, PlanetSideGUID, Vector3}
|
||||
|
||||
object VehicleHandlerLogic {
|
||||
def apply(ops: SessionVehicleHandlers): VehicleHandlerLogic = {
|
||||
new VehicleHandlerLogic(ops, ops.context)
|
||||
}
|
||||
}
|
||||
|
||||
class VehicleHandlerLogic(val ops: SessionVehicleHandlers, implicit val context: ActorContext) extends VehicleHandlerFunctions {
|
||||
def sessionLogic: SessionData = ops.sessionLogic
|
||||
|
||||
private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor
|
||||
|
||||
private val galaxyService: ActorRef = ops.galaxyService
|
||||
|
||||
/**
|
||||
* na
|
||||
*
|
||||
* @param toChannel na
|
||||
* @param guid na
|
||||
* @param reply na
|
||||
*/
|
||||
def handle(toChannel: String, guid: PlanetSideGUID, reply: VehicleResponse.Response): Unit = {
|
||||
val resolvedPlayerGuid = if (player.HasGUID) {
|
||||
player.GUID
|
||||
} else {
|
||||
PlanetSideGUID(-1)
|
||||
}
|
||||
val isNotSameTarget = resolvedPlayerGuid != guid
|
||||
reply match {
|
||||
case VehicleResponse.VehicleState(
|
||||
vehicleGuid,
|
||||
unk1,
|
||||
pos,
|
||||
orient,
|
||||
vel,
|
||||
unk2,
|
||||
unk3,
|
||||
unk4,
|
||||
wheelDirection,
|
||||
unk5,
|
||||
unk6
|
||||
) if isNotSameTarget && player.VehicleSeated.contains(vehicleGuid) =>
|
||||
//player who is also in the vehicle (not driver)
|
||||
sendResponse(VehicleStateMessage(vehicleGuid, unk1, pos, orient, vel, unk2, unk3, unk4, wheelDirection, unk5, unk6))
|
||||
player.Position = pos
|
||||
player.Orientation = orient
|
||||
player.Velocity = vel
|
||||
sessionLogic.updateLocalBlockMap(pos)
|
||||
|
||||
case VehicleResponse.VehicleState(
|
||||
vehicleGuid,
|
||||
unk1,
|
||||
pos,
|
||||
ang,
|
||||
vel,
|
||||
unk2,
|
||||
unk3,
|
||||
unk4,
|
||||
wheelDirection,
|
||||
unk5,
|
||||
unk6
|
||||
) if isNotSameTarget =>
|
||||
//player who is watching the vehicle from the outside
|
||||
sendResponse(VehicleStateMessage(vehicleGuid, unk1, pos, ang, vel, unk2, unk3, unk4, wheelDirection, unk5, unk6))
|
||||
|
||||
case VehicleResponse.ChildObjectState(objectGuid, pitch, yaw) if isNotSameTarget =>
|
||||
sendResponse(ChildObjectStateMessage(objectGuid, pitch, yaw))
|
||||
|
||||
case VehicleResponse.FrameVehicleState(vguid, u1, pos, oient, vel, u2, u3, u4, is_crouched, u6, u7, u8, u9, uA)
|
||||
if isNotSameTarget =>
|
||||
sendResponse(FrameVehicleStateMessage(vguid, u1, pos, oient, vel, u2, u3, u4, is_crouched, u6, u7, u8, u9, uA))
|
||||
|
||||
case VehicleResponse.ChangeFireState_Start(weaponGuid) if isNotSameTarget =>
|
||||
sendResponse(ChangeFireStateMessage_Start(weaponGuid))
|
||||
|
||||
case VehicleResponse.ChangeFireState_Stop(weaponGuid) if isNotSameTarget =>
|
||||
sendResponse(ChangeFireStateMessage_Stop(weaponGuid))
|
||||
|
||||
case VehicleResponse.Reload(itemGuid) if isNotSameTarget =>
|
||||
sendResponse(ReloadMessage(itemGuid, ammo_clip=1, unk1=0))
|
||||
|
||||
case VehicleResponse.ChangeAmmo(weapon_guid, weapon_slot, previous_guid, ammo_id, ammo_guid, ammo_data) if isNotSameTarget =>
|
||||
sendResponse(ObjectDetachMessage(weapon_guid, previous_guid, Vector3.Zero, 0))
|
||||
//TODO? sendResponse(ObjectDeleteMessage(previousAmmoGuid, 0))
|
||||
sendResponse(
|
||||
ObjectCreateMessage(
|
||||
ammo_id,
|
||||
ammo_guid,
|
||||
ObjectCreateMessageParent(weapon_guid, weapon_slot),
|
||||
ammo_data
|
||||
)
|
||||
)
|
||||
sendResponse(ChangeAmmoMessage(weapon_guid, 1))
|
||||
|
||||
case VehicleResponse.WeaponDryFire(weaponGuid) if isNotSameTarget =>
|
||||
continent.GUID(weaponGuid).collect {
|
||||
case tool: Tool if tool.Magazine == 0 =>
|
||||
// check that the magazine is still empty before sending WeaponDryFireMessage
|
||||
// if it has been reloaded since then, other clients will not see it firing
|
||||
sendResponse(WeaponDryFireMessage(weaponGuid))
|
||||
}
|
||||
|
||||
case VehicleResponse.DismountVehicle(bailType, wasKickedByDriver) if isNotSameTarget =>
|
||||
sendResponse(DismountVehicleMsg(guid, bailType, wasKickedByDriver))
|
||||
|
||||
case VehicleResponse.MountVehicle(vehicleGuid, seat) if isNotSameTarget =>
|
||||
sendResponse(ObjectAttachMessage(vehicleGuid, guid, seat))
|
||||
|
||||
case VehicleResponse.DeployRequest(objectGuid, state, unk1, unk2, pos) if isNotSameTarget =>
|
||||
sendResponse(DeployRequestMessage(guid, objectGuid, state, unk1, unk2, pos))
|
||||
|
||||
case VehicleResponse.SendResponse(msg) =>
|
||||
sendResponse(msg)
|
||||
|
||||
case VehicleResponse.AttachToRails(vehicleGuid, padGuid) =>
|
||||
sendResponse(ObjectAttachMessage(padGuid, vehicleGuid, slot=3))
|
||||
|
||||
case VehicleResponse.ConcealPlayer(playerGuid) =>
|
||||
sendResponse(GenericObjectActionMessage(playerGuid, code=9))
|
||||
|
||||
case VehicleResponse.DetachFromRails(vehicleGuid, padGuid, padPosition, padOrientationZ) =>
|
||||
val pad = continent.GUID(padGuid).get.asInstanceOf[VehicleSpawnPad].Definition
|
||||
sendResponse(
|
||||
ObjectDetachMessage(
|
||||
padGuid,
|
||||
vehicleGuid,
|
||||
padPosition + Vector3.z(pad.VehicleCreationZOffset),
|
||||
padOrientationZ + pad.VehicleCreationZOrientOffset
|
||||
)
|
||||
)
|
||||
|
||||
case VehicleResponse.EquipmentInSlot(pkt) if isNotSameTarget =>
|
||||
sendResponse(pkt)
|
||||
|
||||
case VehicleResponse.GenericObjectAction(objectGuid, action) if isNotSameTarget =>
|
||||
sendResponse(GenericObjectActionMessage(objectGuid, action))
|
||||
|
||||
case VehicleResponse.HitHint(sourceGuid) if player.isAlive =>
|
||||
sendResponse(HitHint(sourceGuid, player.GUID))
|
||||
|
||||
case VehicleResponse.InventoryState(obj, parentGuid, start, conData) if isNotSameTarget =>
|
||||
//TODO prefer ObjectDetachMessage, but how to force ammo pools to update properly?
|
||||
val objGuid = obj.GUID
|
||||
sendResponse(ObjectDeleteMessage(objGuid, unk1=0))
|
||||
sendResponse(ObjectCreateDetailedMessage(
|
||||
obj.Definition.ObjectId,
|
||||
objGuid,
|
||||
ObjectCreateMessageParent(parentGuid, start),
|
||||
conData
|
||||
))
|
||||
|
||||
case VehicleResponse.KickPassenger(_, wasKickedByDriver, vehicleGuid) if resolvedPlayerGuid == guid =>
|
||||
//seat number (first field) seems to be correct if passenger is kicked manually by driver
|
||||
//but always seems to return 4 if user is kicked by mount permissions changing
|
||||
sendResponse(DismountVehicleMsg(guid, BailType.Kicked, wasKickedByDriver))
|
||||
val typeOfRide = continent.GUID(vehicleGuid) match {
|
||||
case Some(obj: Vehicle) =>
|
||||
sessionLogic.general.unaccessContainer(obj)
|
||||
s"the ${obj.Definition.Name}'s seat by ${obj.OwnerName.getOrElse("the pilot")}"
|
||||
case _ =>
|
||||
s"${player.Sex.possessive} ride"
|
||||
}
|
||||
log.info(s"${player.Name} has been kicked from $typeOfRide!")
|
||||
|
||||
case VehicleResponse.KickPassenger(_, wasKickedByDriver, _) =>
|
||||
//seat number (first field) seems to be correct if passenger is kicked manually by driver
|
||||
//but always seems to return 4 if user is kicked by mount permissions changing
|
||||
sendResponse(DismountVehicleMsg(guid, BailType.Kicked, wasKickedByDriver))
|
||||
|
||||
case VehicleResponse.InventoryState2(objGuid, parentGuid, value) if isNotSameTarget =>
|
||||
sendResponse(InventoryStateMessage(objGuid, unk=0, parentGuid, value))
|
||||
|
||||
case VehicleResponse.LoadVehicle(vehicle, vtype, vguid, vdata) if isNotSameTarget =>
|
||||
//this is not be suitable for vehicles with people who are seated in it before it spawns (if that is possible)
|
||||
sendResponse(ObjectCreateMessage(vtype, vguid, vdata))
|
||||
Vehicles.ReloadAccessPermissions(vehicle, player.Name)
|
||||
|
||||
case VehicleResponse.ObjectDelete(itemGuid) if isNotSameTarget =>
|
||||
sendResponse(ObjectDeleteMessage(itemGuid, unk1=0))
|
||||
|
||||
case VehicleResponse.Ownership(vehicleGuid) if resolvedPlayerGuid == guid =>
|
||||
//Only the player that owns this vehicle needs the ownership packet
|
||||
avatarActor ! AvatarActor.SetVehicle(Some(vehicleGuid))
|
||||
sendResponse(PlanetsideAttributeMessage(resolvedPlayerGuid, attribute_type=21, vehicleGuid))
|
||||
|
||||
case VehicleResponse.PlanetsideAttribute(vehicleGuid, attributeType, attributeValue) if isNotSameTarget =>
|
||||
sendResponse(PlanetsideAttributeMessage(vehicleGuid, attributeType, attributeValue))
|
||||
|
||||
case VehicleResponse.ResetSpawnPad(padGuid) =>
|
||||
sendResponse(GenericObjectActionMessage(padGuid, code=23))
|
||||
|
||||
case VehicleResponse.RevealPlayer(playerGuid) =>
|
||||
sendResponse(GenericObjectActionMessage(playerGuid, code=10))
|
||||
|
||||
case VehicleResponse.SeatPermissions(vehicleGuid, seatGroup, permission) if isNotSameTarget =>
|
||||
sendResponse(PlanetsideAttributeMessage(vehicleGuid, seatGroup, permission))
|
||||
|
||||
case VehicleResponse.StowEquipment(vehicleGuid, slot, itemType, itemGuid, itemData) if isNotSameTarget =>
|
||||
//TODO prefer ObjectAttachMessage, but how to force ammo pools to update properly?
|
||||
sendResponse(ObjectCreateDetailedMessage(itemType, itemGuid, ObjectCreateMessageParent(vehicleGuid, slot), itemData))
|
||||
|
||||
case VehicleResponse.UnloadVehicle(_, vehicleGuid) =>
|
||||
sendResponse(ObjectDeleteMessage(vehicleGuid, unk1=0))
|
||||
|
||||
case VehicleResponse.UnstowEquipment(itemGuid) if isNotSameTarget =>
|
||||
//TODO prefer ObjectDetachMessage, but how to force ammo pools to update properly?
|
||||
sendResponse(ObjectDeleteMessage(itemGuid, unk1=0))
|
||||
|
||||
case VehicleResponse.UpdateAmsSpawnPoint(list) =>
|
||||
sessionLogic.zoning.spawn.amsSpawnPoints = list.filter(tube => tube.Faction == player.Faction)
|
||||
sessionLogic.zoning.spawn.DrawCurrentAmsSpawnPoint()
|
||||
|
||||
case VehicleResponse.TransferPassengerChannel(oldChannel, tempChannel, vehicle, vehicleToDelete) if isNotSameTarget =>
|
||||
sessionLogic.zoning.interstellarFerry = Some(vehicle)
|
||||
sessionLogic.zoning.interstellarFerryTopLevelGUID = Some(vehicleToDelete)
|
||||
continent.VehicleEvents ! Service.Leave(Some(oldChannel)) //old vehicle-specific channel (was s"${vehicle.Actor}")
|
||||
galaxyService ! Service.Join(tempChannel) //temporary vehicle-specific channel
|
||||
log.debug(s"TransferPassengerChannel: ${player.Name} now subscribed to $tempChannel for vehicle gating")
|
||||
|
||||
case VehicleResponse.KickCargo(vehicle, speed, delay)
|
||||
if player.VehicleSeated.nonEmpty && sessionLogic.zoning.spawn.deadState == DeadState.Alive && speed > 0 =>
|
||||
val strafe = 1 + Vehicles.CargoOrientation(vehicle)
|
||||
val reverseSpeed = if (strafe > 1) { 0 } else { speed }
|
||||
//strafe or reverse, not both
|
||||
sessionLogic.vehicles.ServerVehicleOverrideWithPacket(
|
||||
vehicle,
|
||||
ServerVehicleOverrideMsg(
|
||||
lock_accelerator=true,
|
||||
lock_wheel=true,
|
||||
reverse=true,
|
||||
unk4=false,
|
||||
lock_vthrust=0,
|
||||
strafe,
|
||||
reverseSpeed,
|
||||
unk8=Some(0)
|
||||
)
|
||||
)
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.concurrent.duration._
|
||||
context.system.scheduler.scheduleOnce(
|
||||
delay milliseconds,
|
||||
context.self,
|
||||
VehicleServiceResponse(toChannel, PlanetSideGUID(0), VehicleResponse.KickCargo(vehicle, speed=0, delay))
|
||||
)
|
||||
|
||||
case VehicleResponse.KickCargo(cargo, _, _)
|
||||
if player.VehicleSeated.nonEmpty && sessionLogic.zoning.spawn.deadState == DeadState.Alive =>
|
||||
sessionLogic.vehicles.TotalDriverVehicleControl(cargo)
|
||||
|
||||
case VehicleResponse.StartPlayerSeatedInVehicle(vehicle, _)
|
||||
if player.VisibleSlots.contains(player.DrawnSlot) =>
|
||||
player.DrawnSlot = Player.HandsDownSlot
|
||||
startPlayerSeatedInVehicle(vehicle)
|
||||
|
||||
case VehicleResponse.StartPlayerSeatedInVehicle(vehicle, _) =>
|
||||
startPlayerSeatedInVehicle(vehicle)
|
||||
|
||||
case VehicleResponse.PlayerSeatedInVehicle(vehicle, _) =>
|
||||
Vehicles.ReloadAccessPermissions(vehicle, player.Name)
|
||||
sessionLogic.vehicles.ServerVehicleOverrideWithPacket(
|
||||
vehicle,
|
||||
ServerVehicleOverrideMsg(
|
||||
lock_accelerator=true,
|
||||
lock_wheel=true,
|
||||
reverse=true,
|
||||
unk4=false,
|
||||
lock_vthrust=1,
|
||||
lock_strafe=0,
|
||||
movement_speed=0,
|
||||
unk8=Some(0)
|
||||
)
|
||||
)
|
||||
sessionLogic.vehicles.serverVehicleControlVelocity = Some(0)
|
||||
|
||||
case VehicleResponse.ServerVehicleOverrideStart(vehicle, _) =>
|
||||
val vdef = vehicle.Definition
|
||||
sessionLogic.vehicles.ServerVehicleOverrideWithPacket(
|
||||
vehicle,
|
||||
ServerVehicleOverrideMsg(
|
||||
lock_accelerator=true,
|
||||
lock_wheel=true,
|
||||
reverse=false,
|
||||
unk4=false,
|
||||
lock_vthrust=if (GlobalDefinitions.isFlightVehicle(vdef)) { 1 } else { 0 },
|
||||
lock_strafe=0,
|
||||
movement_speed=vdef.AutoPilotSpeed1,
|
||||
unk8=Some(0)
|
||||
)
|
||||
)
|
||||
|
||||
case VehicleResponse.ServerVehicleOverrideEnd(vehicle, _) =>
|
||||
sessionLogic.vehicles.ServerVehicleOverrideStop(vehicle)
|
||||
|
||||
case VehicleResponse.PeriodicReminder(VehicleSpawnPad.Reminders.Blocked, data) =>
|
||||
sendResponse(ChatMsg(
|
||||
ChatMessageType.CMT_OPEN,
|
||||
wideContents=true,
|
||||
recipient="",
|
||||
s"The vehicle spawn where you placed your order is blocked. ${data.getOrElse("")}",
|
||||
note=None
|
||||
))
|
||||
|
||||
case VehicleResponse.PeriodicReminder(_, data) =>
|
||||
val (isType, flag, msg): (ChatMessageType, Boolean, String) = data match {
|
||||
case Some(msg: String) if msg.startsWith("@") => (ChatMessageType.UNK_227, false, msg)
|
||||
case Some(msg: String) => (ChatMessageType.CMT_OPEN, true, msg)
|
||||
case _ => (ChatMessageType.CMT_OPEN, true, "Your vehicle order has been cancelled.")
|
||||
}
|
||||
sendResponse(ChatMsg(isType, flag, recipient="", msg, None))
|
||||
|
||||
case VehicleResponse.ChangeLoadout(target, oldWeapons, addedWeapons, oldInventory, newInventory)
|
||||
if player.avatar.vehicle.contains(target) =>
|
||||
//TODO when vehicle weapons can be changed without visual glitches, rewrite this
|
||||
continent.GUID(target).collect { case vehicle: Vehicle =>
|
||||
import net.psforever.login.WorldSession.boolToInt
|
||||
//owner: must unregister old equipment, and register and install new equipment
|
||||
(oldWeapons ++ oldInventory).foreach {
|
||||
case (obj, eguid) =>
|
||||
sendResponse(ObjectDeleteMessage(eguid, unk1=0))
|
||||
TaskWorkflow.execute(GUIDTask.unregisterEquipment(continent.GUID, obj))
|
||||
}
|
||||
sessionLogic.general.applyPurchaseTimersBeforePackingLoadout(player, vehicle, addedWeapons ++ newInventory)
|
||||
//jammer or unjamm new weapons based on vehicle status
|
||||
val vehicleJammered = vehicle.Jammed
|
||||
addedWeapons
|
||||
.map { _.obj }
|
||||
.collect {
|
||||
case jamItem: JammableUnit if jamItem.Jammed != vehicleJammered =>
|
||||
jamItem.Jammed = vehicleJammered
|
||||
JammableMountedWeapons.JammedWeaponStatus(vehicle.Zone, jamItem, vehicleJammered)
|
||||
}
|
||||
changeLoadoutDeleteOldEquipment(vehicle, oldWeapons, oldInventory)
|
||||
}
|
||||
|
||||
case VehicleResponse.ChangeLoadout(target, oldWeapons, _, oldInventory, _)
|
||||
if sessionLogic.general.accessedContainer.map(_.GUID).contains(target) =>
|
||||
//TODO when vehicle weapons can be changed without visual glitches, rewrite this
|
||||
continent.GUID(target).collect { case vehicle: Vehicle =>
|
||||
//external participant: observe changes to equipment
|
||||
(oldWeapons ++ oldInventory).foreach { case (_, eguid) => sendResponse(ObjectDeleteMessage(eguid, unk1=0)) }
|
||||
changeLoadoutDeleteOldEquipment(vehicle, oldWeapons, oldInventory)
|
||||
}
|
||||
|
||||
case VehicleResponse.ChangeLoadout(target, oldWeapons, _, oldInventory, _) =>
|
||||
//TODO when vehicle weapons can be changed without visual glitches, rewrite this
|
||||
continent.GUID(target).collect { case vehicle: Vehicle =>
|
||||
changeLoadoutDeleteOldEquipment(vehicle, oldWeapons, oldInventory)
|
||||
}
|
||||
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
|
||||
private def changeLoadoutDeleteOldEquipment(
|
||||
vehicle: Vehicle,
|
||||
oldWeapons: Iterable[(Equipment, PlanetSideGUID)],
|
||||
oldInventory: Iterable[(Equipment, PlanetSideGUID)]
|
||||
): Unit = {
|
||||
vehicle.PassengerInSeat(player) match {
|
||||
case Some(seatNum) =>
|
||||
//participant: observe changes to equipment
|
||||
(oldWeapons ++ oldInventory).foreach {
|
||||
case (_, eguid) => sendResponse(ObjectDeleteMessage(eguid, unk1=0))
|
||||
}
|
||||
sessionLogic.mountResponse.updateWeaponAtSeatPosition(vehicle, seatNum)
|
||||
case None =>
|
||||
//observer: observe changes to external equipment
|
||||
oldWeapons.foreach { case (_, eguid) => sendResponse(ObjectDeleteMessage(eguid, unk1=0)) }
|
||||
}
|
||||
}
|
||||
|
||||
private def startPlayerSeatedInVehicle(vehicle: Vehicle): Unit = {
|
||||
val vehicle_guid = vehicle.GUID
|
||||
sessionLogic.actionsToCancel()
|
||||
sessionLogic.terminals.CancelAllProximityUnits()
|
||||
sessionLogic.vehicles.serverVehicleControlVelocity = Some(0)
|
||||
sendResponse(PlanetsideAttributeMessage(vehicle_guid, attribute_type=22, attribute_value=1L)) //mount points off
|
||||
sendResponse(PlanetsideAttributeMessage(player.GUID, attribute_type=21, vehicle_guid)) //ownership
|
||||
vehicle.MountPoints.find { case (_, mp) => mp.seatIndex == 0 }.collect {
|
||||
case (mountPoint, _) => vehicle.Actor ! Mountable.TryMount(player, mountPoint)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,363 @@
|
|||
// Copyright (c) 2024 PSForever
|
||||
package net.psforever.actors.session.spectator
|
||||
|
||||
import akka.actor.{ActorContext, typed}
|
||||
import net.psforever.actors.session.AvatarActor
|
||||
import net.psforever.actors.session.support.{SessionData, VehicleFunctions, VehicleOperations}
|
||||
import net.psforever.objects.serverobject.PlanetSideServerObject
|
||||
import net.psforever.objects.{PlanetSideGameObject, Player, Vehicle, Vehicles}
|
||||
import net.psforever.objects.serverobject.deploy.Deployment
|
||||
import net.psforever.objects.serverobject.mount.Mountable
|
||||
import net.psforever.objects.vehicles.control.BfrFlight
|
||||
import net.psforever.objects.zones.Zone
|
||||
import net.psforever.packet.game.{ChildObjectStateMessage, DeployRequestMessage, FrameVehicleStateMessage, VehicleStateMessage, VehicleSubStateMessage}
|
||||
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
|
||||
import net.psforever.types.{DriveState, Vector3}
|
||||
|
||||
object VehicleLogic {
|
||||
def apply(ops: VehicleOperations): VehicleLogic = {
|
||||
new VehicleLogic(ops, ops.context)
|
||||
}
|
||||
}
|
||||
|
||||
class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContext) extends VehicleFunctions {
|
||||
def sessionLogic: SessionData = ops.sessionLogic
|
||||
|
||||
private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor
|
||||
|
||||
/* packets */
|
||||
|
||||
def handleVehicleState(pkt: VehicleStateMessage): Unit = {
|
||||
val VehicleStateMessage(
|
||||
vehicle_guid,
|
||||
unk1,
|
||||
pos,
|
||||
ang,
|
||||
vel,
|
||||
is_flying,
|
||||
unk6,
|
||||
unk7,
|
||||
wheels,
|
||||
is_decelerating,
|
||||
is_cloaked
|
||||
) = pkt
|
||||
GetVehicleAndSeat() match {
|
||||
case (Some(obj), Some(0)) =>
|
||||
//we're driving the vehicle
|
||||
sessionLogic.persist()
|
||||
sessionLogic.turnCounterFunc(player.GUID)
|
||||
sessionLogic.general.fallHeightTracker(pos.z)
|
||||
if (obj.MountedIn.isEmpty) {
|
||||
sessionLogic.updateBlockMap(obj, pos)
|
||||
}
|
||||
player.Position = pos //convenient
|
||||
if (obj.WeaponControlledFromSeat(0).isEmpty) {
|
||||
player.Orientation = Vector3.z(ang.z) //convenient
|
||||
}
|
||||
obj.Position = pos
|
||||
obj.Orientation = ang
|
||||
if (obj.MountedIn.isEmpty) {
|
||||
if (obj.DeploymentState != DriveState.Deployed) {
|
||||
obj.Velocity = vel
|
||||
} else {
|
||||
obj.Velocity = Some(Vector3.Zero)
|
||||
}
|
||||
if (obj.Definition.CanFly) {
|
||||
obj.Flying = is_flying //usually Some(7)
|
||||
}
|
||||
obj.Cloaked = obj.Definition.CanCloak && is_cloaked
|
||||
} else {
|
||||
obj.Velocity = None
|
||||
obj.Flying = None
|
||||
}
|
||||
continent.VehicleEvents ! VehicleServiceMessage(
|
||||
continent.id,
|
||||
VehicleAction.VehicleState(
|
||||
player.GUID,
|
||||
vehicle_guid,
|
||||
unk1,
|
||||
obj.Position,
|
||||
ang,
|
||||
obj.Velocity,
|
||||
if (obj.isFlying) {
|
||||
is_flying
|
||||
} else {
|
||||
None
|
||||
},
|
||||
unk6,
|
||||
unk7,
|
||||
wheels,
|
||||
is_decelerating,
|
||||
obj.Cloaked
|
||||
)
|
||||
)
|
||||
sessionLogic.squad.updateSquad()
|
||||
obj.zoneInteractions()
|
||||
case (None, _) =>
|
||||
//log.error(s"VehicleState: no vehicle $vehicle_guid found in zone")
|
||||
//TODO placing a "not driving" warning here may trigger as we are disembarking the vehicle
|
||||
case (_, Some(index)) =>
|
||||
log.error(
|
||||
s"VehicleState: ${player.Name} should not be dispatching this kind of packet from vehicle ${vehicle_guid.guid} when not the driver (actually, seat $index)"
|
||||
)
|
||||
case _ => ;
|
||||
}
|
||||
if (player.death_by == -1) {
|
||||
sessionLogic.kickedByAdministration()
|
||||
}
|
||||
}
|
||||
|
||||
def handleFrameVehicleState(pkt: FrameVehicleStateMessage): Unit = {
|
||||
val FrameVehicleStateMessage(
|
||||
vehicle_guid,
|
||||
unk1,
|
||||
pos,
|
||||
ang,
|
||||
vel,
|
||||
unk2,
|
||||
unk3,
|
||||
unk4,
|
||||
is_crouched,
|
||||
is_airborne,
|
||||
ascending_flight,
|
||||
flight_time,
|
||||
unk9,
|
||||
unkA
|
||||
) = pkt
|
||||
GetVehicleAndSeat() match {
|
||||
case (Some(obj), Some(0)) =>
|
||||
//we're driving the vehicle
|
||||
sessionLogic.persist()
|
||||
sessionLogic.turnCounterFunc(player.GUID)
|
||||
val (position, angle, velocity, notMountedState) = continent.GUID(obj.MountedIn) match {
|
||||
case Some(v: Vehicle) =>
|
||||
sessionLogic.updateBlockMap(obj, pos)
|
||||
(pos, v.Orientation - Vector3.z(value = 90f) * Vehicles.CargoOrientation(obj).toFloat, v.Velocity, false)
|
||||
case _ =>
|
||||
(pos, ang, vel, true)
|
||||
}
|
||||
player.Position = position //convenient
|
||||
if (obj.WeaponControlledFromSeat(seatNumber = 0).isEmpty) {
|
||||
player.Orientation = Vector3.z(ang.z) //convenient
|
||||
}
|
||||
obj.Position = position
|
||||
obj.Orientation = angle
|
||||
obj.Velocity = velocity
|
||||
// if (is_crouched && obj.DeploymentState != DriveState.Kneeling) {
|
||||
// //dev stuff goes here
|
||||
// }
|
||||
// else
|
||||
// if (!is_crouched && obj.DeploymentState == DriveState.Kneeling) {
|
||||
// //dev stuff goes here
|
||||
// }
|
||||
obj.DeploymentState = if (is_crouched || !notMountedState) DriveState.Kneeling else DriveState.Mobile
|
||||
if (notMountedState) {
|
||||
if (obj.DeploymentState != DriveState.Kneeling) {
|
||||
if (is_airborne) {
|
||||
val flight = if (ascending_flight) flight_time else -flight_time
|
||||
obj.Flying = Some(flight)
|
||||
obj.Actor ! BfrFlight.Soaring(flight)
|
||||
} else if (obj.Flying.nonEmpty) {
|
||||
obj.Flying = None
|
||||
obj.Actor ! BfrFlight.Landed
|
||||
}
|
||||
} else {
|
||||
obj.Velocity = None
|
||||
obj.Flying = None
|
||||
}
|
||||
obj.zoneInteractions()
|
||||
} else {
|
||||
obj.Velocity = None
|
||||
obj.Flying = None
|
||||
}
|
||||
continent.VehicleEvents ! VehicleServiceMessage(
|
||||
continent.id,
|
||||
VehicleAction.FrameVehicleState(
|
||||
player.GUID,
|
||||
vehicle_guid,
|
||||
unk1,
|
||||
position,
|
||||
angle,
|
||||
velocity,
|
||||
unk2,
|
||||
unk3,
|
||||
unk4,
|
||||
is_crouched,
|
||||
is_airborne,
|
||||
ascending_flight,
|
||||
flight_time,
|
||||
unk9,
|
||||
unkA
|
||||
)
|
||||
)
|
||||
sessionLogic.squad.updateSquad()
|
||||
case (None, _) =>
|
||||
//log.error(s"VehicleState: no vehicle $vehicle_guid found in zone")
|
||||
//TODO placing a "not driving" warning here may trigger as we are disembarking the vehicle
|
||||
case (_, Some(index)) =>
|
||||
log.error(
|
||||
s"VehicleState: ${player.Name} should not be dispatching this kind of packet from vehicle ${vehicle_guid.guid} when not the driver (actually, seat $index)"
|
||||
)
|
||||
case _ => ()
|
||||
}
|
||||
if (player.death_by == -1) {
|
||||
sessionLogic.kickedByAdministration()
|
||||
}
|
||||
}
|
||||
|
||||
def handleChildObjectState(pkt: ChildObjectStateMessage): Unit = {
|
||||
val ChildObjectStateMessage(object_guid, pitch, yaw) = pkt
|
||||
val (o, tools) = sessionLogic.shooting.FindContainedWeapon
|
||||
//is COSM our primary upstream packet?
|
||||
(o match {
|
||||
case Some(mount: Mountable) => (o, mount.PassengerInSeat(player))
|
||||
case _ => (None, None)
|
||||
}) match {
|
||||
case (None, None) | (_, None) | (Some(_: Vehicle), Some(0)) => ()
|
||||
case _ =>
|
||||
sessionLogic.persist()
|
||||
sessionLogic.turnCounterFunc(player.GUID)
|
||||
}
|
||||
//the majority of the following check retrieves information to determine if we are in control of the child
|
||||
tools.find { _.GUID == object_guid } match {
|
||||
case None => ()
|
||||
case Some(_) => player.Orientation = Vector3(0f, pitch, yaw)
|
||||
}
|
||||
if (player.death_by == -1) {
|
||||
sessionLogic.kickedByAdministration()
|
||||
}
|
||||
}
|
||||
|
||||
def handleVehicleSubState(pkt: VehicleSubStateMessage): Unit = {
|
||||
val VehicleSubStateMessage(vehicle_guid, _, pos, ang, vel, unk1, _) = pkt
|
||||
sessionLogic.validObject(vehicle_guid, decorator = "VehicleSubState") match {
|
||||
case Some(obj: Vehicle) =>
|
||||
import net.psforever.login.WorldSession.boolToInt
|
||||
obj.Position = pos
|
||||
obj.Orientation = ang
|
||||
obj.Velocity = vel
|
||||
sessionLogic.updateBlockMap(obj, pos)
|
||||
obj.zoneInteractions()
|
||||
continent.VehicleEvents ! VehicleServiceMessage(
|
||||
continent.id,
|
||||
VehicleAction.VehicleState(
|
||||
player.GUID,
|
||||
vehicle_guid,
|
||||
unk1,
|
||||
pos,
|
||||
ang,
|
||||
obj.Velocity,
|
||||
obj.Flying,
|
||||
0,
|
||||
0,
|
||||
15,
|
||||
unk5 = false,
|
||||
obj.Cloaked
|
||||
)
|
||||
)
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
|
||||
def handleDeployRequest(pkt: DeployRequestMessage): Unit = {
|
||||
val DeployRequestMessage(_, vehicle_guid, deploy_state, _, _, _) = pkt
|
||||
val vehicle = player.avatar.vehicle
|
||||
if (vehicle.contains(vehicle_guid)) {
|
||||
if (vehicle == player.VehicleSeated) {
|
||||
continent.GUID(vehicle_guid) match {
|
||||
case Some(obj: Vehicle) =>
|
||||
if (obj.DeploymentState == DriveState.Deployed) {
|
||||
obj.Actor ! Deployment.TryDeploymentChange(deploy_state)
|
||||
}
|
||||
case _ => ()
|
||||
avatarActor ! AvatarActor.SetVehicle(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* messages */
|
||||
|
||||
def handleCanDeploy(obj: Deployment.DeploymentObject, state: DriveState.Value): Unit = { /* intentionally blank */ }
|
||||
|
||||
def handleCanUndeploy(obj: Deployment.DeploymentObject, state: DriveState.Value): Unit = {
|
||||
if (state != DriveState.Undeploying && state != DriveState.Mobile) {
|
||||
CanNotChangeDeployment(obj, state, "incorrect undeploy state")
|
||||
}
|
||||
}
|
||||
|
||||
def handleCanNotChangeDeployment(obj: Deployment.DeploymentObject, state: DriveState.Value, reason: String): Unit = {
|
||||
if (Deployment.CheckForDeployState(state) && !Deployment.AngleCheck(obj)) {
|
||||
CanNotChangeDeployment(obj, state, reason = "ground too steep")
|
||||
} else {
|
||||
CanNotChangeDeployment(obj, state, reason)
|
||||
}
|
||||
}
|
||||
|
||||
/* support functions */
|
||||
|
||||
/**
|
||||
* If the player is mounted in some entity, find that entity and get the mount index number at which the player is sat.
|
||||
* The priority of object confirmation is `direct` then `occupant.VehicleSeated`.
|
||||
* Once an object is found, the remainder are ignored.
|
||||
* @param direct a game object in which the player may be sat
|
||||
* @param occupant the player who is sat and may have specified the game object in which mounted
|
||||
* @return a tuple consisting of a vehicle reference and a mount index
|
||||
* if and only if the vehicle is known to this client and the `WorldSessioNActor`-global `player` occupies it;
|
||||
* `(None, None)`, otherwise (even if the vehicle can be determined)
|
||||
*/
|
||||
private def GetMountableAndSeat(
|
||||
direct: Option[PlanetSideGameObject with Mountable],
|
||||
occupant: Player,
|
||||
zone: Zone
|
||||
): (Option[PlanetSideGameObject with Mountable], Option[Int]) =
|
||||
direct.orElse(zone.GUID(occupant.VehicleSeated)) match {
|
||||
case Some(obj: PlanetSideGameObject with Mountable) =>
|
||||
obj.PassengerInSeat(occupant) match {
|
||||
case index @ Some(_) =>
|
||||
(Some(obj), index)
|
||||
case None =>
|
||||
(None, None)
|
||||
}
|
||||
case _ =>
|
||||
(None, None)
|
||||
}
|
||||
|
||||
/**
|
||||
* If the player is seated in a vehicle, find that vehicle and get the mount index number at which the player is sat.
|
||||
* @see `GetMountableAndSeat`
|
||||
* @return a tuple consisting of a vehicle reference and a mount index
|
||||
* if and only if the vehicle is known to this client and the `WorldSessioNActor`-global `player` occupies it;
|
||||
* `(None, None)`, otherwise (even if the vehicle can be determined)
|
||||
*/
|
||||
private def GetVehicleAndSeat(): (Option[Vehicle], Option[Int]) =
|
||||
GetMountableAndSeat(None, player, continent) match {
|
||||
case (Some(v: Vehicle), Some(seat)) => (Some(v), Some(seat))
|
||||
case _ => (None, None)
|
||||
}
|
||||
|
||||
/**
|
||||
* Common reporting behavior when a `Deployment` object fails to properly transition between states.
|
||||
* @param obj the game object that could not
|
||||
* @param state the `DriveState` that could not be promoted
|
||||
* @param reason a string explaining why the state can not or will not change
|
||||
*/
|
||||
private def CanNotChangeDeployment(
|
||||
obj: PlanetSideServerObject with Deployment,
|
||||
state: DriveState.Value,
|
||||
reason: String
|
||||
): Unit = {
|
||||
val mobileShift: String = if (obj.DeploymentState != DriveState.Mobile) {
|
||||
obj.DeploymentState = DriveState.Mobile
|
||||
sendResponse(DeployRequestMessage(player.GUID, obj.GUID, DriveState.Mobile, 0, unk3=false, Vector3.Zero))
|
||||
continent.VehicleEvents ! VehicleServiceMessage(
|
||||
continent.id,
|
||||
VehicleAction.DeployRequest(player.GUID, obj.GUID, DriveState.Mobile, 0, unk2=false, Vector3.Zero)
|
||||
)
|
||||
"; enforcing Mobile deployment state"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
log.error(s"DeployRequest: ${player.Name} can not transition $obj to $state - $reason$mobileShift")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,680 @@
|
|||
// Copyright (c) 2024 PSForever
|
||||
package net.psforever.actors.session.spectator
|
||||
|
||||
import akka.actor.{ActorContext, typed}
|
||||
import net.psforever.actors.session.AvatarActor
|
||||
import net.psforever.actors.session.support.{SessionData, WeaponAndProjectileFunctions, WeaponAndProjectileOperations}
|
||||
import net.psforever.login.WorldSession.{CountGrenades, FindEquipmentStock, FindToolThatUses, RemoveOldEquipmentFromInventory}
|
||||
import net.psforever.objects.ballistics.{Projectile, ProjectileQuality}
|
||||
import net.psforever.objects.definition.ProjectileDefinition
|
||||
import net.psforever.objects.equipment.{ChargeFireModeDefinition, EquipmentSize}
|
||||
import net.psforever.objects.inventory.Container
|
||||
import net.psforever.objects.serverobject.affinity.FactionAffinity
|
||||
import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject}
|
||||
import net.psforever.objects.serverobject.doors.InteriorDoorPassage
|
||||
import net.psforever.objects.{AmmoBox, BoomerDeployable, BoomerTrigger, DummyExplodingEntity, GlobalDefinitions, OwnableByPlayer, PlanetSideGameObject, SpecialEmp, Tool}
|
||||
import net.psforever.objects.serverobject.interior.Sidedness
|
||||
import net.psforever.objects.serverobject.mount.Mountable
|
||||
import net.psforever.objects.serverobject.turret.auto.{AutomatedTurret, AutomatedTurretBehavior}
|
||||
import net.psforever.objects.sourcing.SourceEntry
|
||||
import net.psforever.objects.vital.Vitality
|
||||
import net.psforever.objects.vital.base.{DamageResolution, DamageType}
|
||||
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.{Zone, ZoneProjectile}
|
||||
import net.psforever.packet.game.{AIDamage, AvatarGrenadeStateMessage, ChainLashMessage, ChangeAmmoMessage, ChangeFireModeMessage, ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, HitMessage, InventoryStateMessage, LashMessage, LongRangeProjectileInfoMessage, ProjectileStateMessage, QuantityUpdateMessage, ReloadMessage, SplashHitMessage, UplinkRequest, UplinkRequestType, UplinkResponse, WeaponDelayFireMessage, WeaponDryFireMessage, WeaponFireMessage, WeaponLazeTargetPositionMessage}
|
||||
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
|
||||
import net.psforever.types.{PlanetSideGUID, Vector3}
|
||||
import net.psforever.util.Config
|
||||
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.concurrent.duration._
|
||||
|
||||
object WeaponAndProjectileLogic {
|
||||
def apply(ops: WeaponAndProjectileOperations): WeaponAndProjectileLogic = {
|
||||
new WeaponAndProjectileLogic(ops, ops.context)
|
||||
}
|
||||
|
||||
/**
|
||||
* Does a line segment line intersect with a sphere?<br>
|
||||
* This most likely belongs in `Geometry` or `GeometryForm` or somehow in association with the `\objects\geometry\` package.
|
||||
* @param start first point of the line segment
|
||||
* @param end second point of the line segment
|
||||
* @param center center of the sphere
|
||||
* @param radius radius of the sphere
|
||||
* @return list of all points of intersection, if any
|
||||
* @see `Vector3.DistanceSquared`
|
||||
* @see `Vector3.MagnitudeSquared`
|
||||
*/
|
||||
private def quickLineSphereIntersectionPoints(
|
||||
start: Vector3,
|
||||
end: Vector3,
|
||||
center: Vector3,
|
||||
radius: Float
|
||||
): Iterable[Vector3] = {
|
||||
/*
|
||||
Algorithm adapted from code found on https://paulbourke.net/geometry/circlesphere/index.html#linesphere,
|
||||
because I kept messing up proper substitution of the line formula and the circle formula into the quadratic equation.
|
||||
*/
|
||||
val Vector3(cx, cy, cz) = center
|
||||
val Vector3(sx, sy, sz) = start
|
||||
val vector = end - start
|
||||
//speed our way through a quadratic equation
|
||||
val (a, b) = {
|
||||
val Vector3(dx, dy, dz) = vector
|
||||
(
|
||||
dx * dx + dy * dy + dz * dz,
|
||||
2f * (dx * (sx - cx) + dy * (sy - cy) + dz * (sz - cz))
|
||||
)
|
||||
}
|
||||
val c = Vector3.MagnitudeSquared(center) + Vector3.MagnitudeSquared(start) - 2f * (cx * sx + cy * sy + cz * sz) - radius * radius
|
||||
val result = b * b - 4 * a * c
|
||||
if (result < 0f) {
|
||||
//negative, no intersection
|
||||
Seq()
|
||||
} else if (result < 0.00001f) {
|
||||
//zero-ish, one intersection point
|
||||
Seq(start - vector * (b / (2f * a)))
|
||||
} else {
|
||||
//positive, two intersection points
|
||||
val sqrt = math.sqrt(result).toFloat
|
||||
val endStart = vector / (2f * a)
|
||||
Seq(start + endStart * (sqrt - b), start + endStart * (b + sqrt) * -1f)
|
||||
}.filter(p => Vector3.DistanceSquared(start, p) <= a)
|
||||
}
|
||||
/**
|
||||
* Preparation for explosion damage that utilizes the Scorpion's little buddy sub-projectiles.
|
||||
* The main difference from "normal" server-side explosion
|
||||
* is that the owner of the projectile must be clarified explicitly.
|
||||
* @see `Zone::serverSideDamage`
|
||||
* @param zone where the explosion is taking place
|
||||
* (`source` contains the coordinate location)
|
||||
* @param source a game object that represents the source of the explosion
|
||||
* @param owner who or what to accredit damage from the explosion to;
|
||||
* clarifies a normal `SourceEntry(source)` accreditation
|
||||
*/
|
||||
private def detonateLittleBuddy(
|
||||
zone: Zone,
|
||||
source: PlanetSideGameObject with FactionAffinity with Vitality,
|
||||
proxy: Projectile,
|
||||
owner: SourceEntry
|
||||
)(): Unit = {
|
||||
Zone.serverSideDamage(zone, source, littleBuddyExplosionDamage(owner, proxy.id, source.Position))
|
||||
}
|
||||
|
||||
/**
|
||||
* Preparation for explosion damage that utilizes the Scorpion's little buddy sub-projectiles.
|
||||
* The main difference from "normal" server-side explosion
|
||||
* is that the owner of the projectile must be clarified explicitly.
|
||||
* The sub-projectiles will be the product of a normal projectile rather than a standard game object
|
||||
* so a custom `source` entity must wrap around it and fulfill the requirements of the field.
|
||||
* @see `Zone::explosionDamage`
|
||||
* @param owner who or what to accredit damage from the explosion to
|
||||
* @param explosionPosition where the explosion will be positioned in the game world
|
||||
* @param source a game object that represents the source of the explosion
|
||||
* @param target a game object that is affected by the explosion
|
||||
* @return a `DamageInteraction` object
|
||||
*/
|
||||
private def littleBuddyExplosionDamage(
|
||||
owner: SourceEntry,
|
||||
projectileId: Long,
|
||||
explosionPosition: Vector3
|
||||
)
|
||||
(
|
||||
source: PlanetSideGameObject with FactionAffinity with Vitality,
|
||||
target: PlanetSideGameObject with FactionAffinity with Vitality
|
||||
): DamageInteraction = {
|
||||
DamageInteraction(SourceEntry(target), OicwLilBuddyReason(owner, projectileId, target.DamageModel), explosionPosition)
|
||||
}
|
||||
}
|
||||
|
||||
class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit val context: ActorContext) extends WeaponAndProjectileFunctions {
|
||||
def sessionLogic: SessionData = ops.sessionLogic
|
||||
|
||||
private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor
|
||||
|
||||
/* packets */
|
||||
|
||||
def handleWeaponFire(pkt: WeaponFireMessage): Unit = { /* intentionally blank */ }
|
||||
|
||||
def handleWeaponDelayFire(pkt: WeaponDelayFireMessage): Unit = { /* intentionally blank */ }
|
||||
|
||||
def handleWeaponDryFire(pkt: WeaponDryFireMessage): Unit = { /* intentionally blank */ }
|
||||
|
||||
def handleWeaponLazeTargetPosition(pkt: WeaponLazeTargetPositionMessage): Unit = { /* intentionally blank */ }
|
||||
|
||||
def handleUplinkRequest(packet: UplinkRequest): Unit = {
|
||||
val UplinkRequest(code, _, _) = packet
|
||||
val playerFaction = player.Faction
|
||||
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 handleAvatarGrenadeState(pkt: AvatarGrenadeStateMessage): Unit = { /* intentionally blank */ }
|
||||
|
||||
def handleChangeFireStateStart(pkt: ChangeFireStateMessage_Start): Unit = { /* intentionally blank */ }
|
||||
|
||||
def handleChangeFireStateStop(pkt: ChangeFireStateMessage_Stop): Unit = {
|
||||
val ChangeFireStateMessage_Stop(item_guid) = pkt
|
||||
val now = System.currentTimeMillis()
|
||||
ops.prefire -= item_guid
|
||||
ops.shootingStop += item_guid -> now
|
||||
ops.shooting -= item_guid
|
||||
sessionLogic.findEquipment(item_guid) match {
|
||||
case Some(tool: Tool) if player.VehicleSeated.isEmpty =>
|
||||
fireStateStopWhenPlayer(tool, item_guid)
|
||||
case Some(tool: Tool) =>
|
||||
fireStateStopWhenMounted(tool, item_guid)
|
||||
case Some(trigger: BoomerTrigger) =>
|
||||
ops.fireStateStopPlayerMessages(item_guid)
|
||||
continent.GUID(trigger.Companion).collect {
|
||||
case boomer: BoomerDeployable =>
|
||||
boomer.Actor ! CommonMessages.Use(player, Some(trigger))
|
||||
}
|
||||
case Some(_) if player.VehicleSeated.isEmpty =>
|
||||
ops.fireStateStopPlayerMessages(item_guid)
|
||||
case Some(_) =>
|
||||
ops.fireStateStopMountedMessages(item_guid)
|
||||
case _ => ()
|
||||
}
|
||||
sessionLogic.general.progressBarUpdate.cancel()
|
||||
sessionLogic.general.progressBarValue = None
|
||||
}
|
||||
|
||||
def handleReload(pkt: ReloadMessage): Unit = { /* intentionally blank */ }
|
||||
|
||||
def handleChangeAmmo(pkt: ChangeAmmoMessage): Unit = { /* intentionally blank */ }
|
||||
|
||||
def handleChangeFireMode(pkt: ChangeFireModeMessage): Unit = { /* intentionally blank */ }
|
||||
|
||||
def handleProjectileState(pkt: ProjectileStateMessage): Unit = {
|
||||
val ProjectileStateMessage(projectile_guid, shot_pos, shot_vel, shot_orient, seq, end, target_guid) = pkt
|
||||
val index = projectile_guid.guid - Projectile.baseUID
|
||||
ops.projectiles(index) match {
|
||||
case Some(projectile) if projectile.HasGUID =>
|
||||
val projectileGlobalUID = projectile.GUID
|
||||
projectile.Position = shot_pos
|
||||
projectile.Orientation = shot_orient
|
||||
projectile.Velocity = shot_vel
|
||||
continent.AvatarEvents ! AvatarServiceMessage(
|
||||
continent.id,
|
||||
AvatarAction.ProjectileState(
|
||||
player.GUID,
|
||||
projectileGlobalUID,
|
||||
shot_pos,
|
||||
shot_vel,
|
||||
shot_orient,
|
||||
seq,
|
||||
end,
|
||||
target_guid
|
||||
)
|
||||
)
|
||||
case _ if seq == 0 =>
|
||||
/* missing the first packet in the sequence is permissible */
|
||||
case _ =>
|
||||
log.warn(s"ProjectileState: constructed projectile ${projectile_guid.guid} can not be found")
|
||||
}
|
||||
}
|
||||
|
||||
def handleLongRangeProjectileState(pkt: LongRangeProjectileInfoMessage): Unit = { /* intentionally blank */ }
|
||||
|
||||
def handleDirectHit(pkt: HitMessage): Unit = { /* intentionally blank */ }
|
||||
|
||||
def handleSplashHit(pkt: SplashHitMessage): Unit = {
|
||||
val SplashHitMessage(
|
||||
_,
|
||||
projectile_guid,
|
||||
explosion_pos,
|
||||
direct_victim_uid,
|
||||
_,
|
||||
projectile_vel,
|
||||
_,
|
||||
targets
|
||||
) = pkt
|
||||
ops.FindProjectileEntry(projectile_guid) match {
|
||||
case Some(projectile) =>
|
||||
val profile = projectile.profile
|
||||
projectile.Velocity = projectile_vel
|
||||
val (resolution1, resolution2) = profile.Aggravated match {
|
||||
case Some(_) if profile.ProjectileDamageTypes.contains(DamageType.Aggravated) =>
|
||||
(DamageResolution.AggravatedDirect, DamageResolution.AggravatedSplash)
|
||||
case _ =>
|
||||
(DamageResolution.Splash, DamageResolution.Splash)
|
||||
}
|
||||
//direct_victim_uid
|
||||
sessionLogic.validObject(direct_victim_uid, decorator = "SplashHit/direct_victim") match {
|
||||
case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) =>
|
||||
CheckForHitPositionDiscrepancy(projectile_guid, explosion_pos, target)
|
||||
ResolveProjectileInteraction(projectile, resolution1, target, target.Position).collect { resprojectile =>
|
||||
addShotsLanded(resprojectile.cause.attribution, shots = 1)
|
||||
sessionLogic.handleDealingDamage(target, resprojectile)
|
||||
}
|
||||
case _ => ()
|
||||
}
|
||||
//other victims
|
||||
targets.foreach(elem => {
|
||||
sessionLogic.validObject(elem.uid, decorator = "SplashHit/other_victims") match {
|
||||
case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) =>
|
||||
CheckForHitPositionDiscrepancy(projectile_guid, explosion_pos, target)
|
||||
ResolveProjectileInteraction(projectile, resolution2, target, explosion_pos).collect { resprojectile =>
|
||||
addShotsLanded(resprojectile.cause.attribution, shots = 1)
|
||||
sessionLogic.handleDealingDamage(target, resprojectile)
|
||||
}
|
||||
case _ => ()
|
||||
}
|
||||
})
|
||||
//...
|
||||
HandleDamageProxy(projectile, projectile_guid, explosion_pos)
|
||||
if (
|
||||
projectile.profile.HasJammedEffectDuration ||
|
||||
projectile.profile.JammerProjectile ||
|
||||
projectile.profile.SympatheticExplosion
|
||||
) {
|
||||
//can also substitute 'projectile.profile' for 'SpecialEmp.emp'
|
||||
Zone.serverSideDamage(
|
||||
continent,
|
||||
player,
|
||||
SpecialEmp.emp,
|
||||
SpecialEmp.createEmpInteraction(SpecialEmp.emp, explosion_pos),
|
||||
SpecialEmp.prepareDistanceCheck(player, explosion_pos, player.Faction),
|
||||
SpecialEmp.findAllBoomers(profile.DamageRadius)
|
||||
)
|
||||
}
|
||||
if (profile.ExistsOnRemoteClients && projectile.HasGUID) {
|
||||
//cleanup
|
||||
if (projectile.HasGUID) {
|
||||
continent.Projectile ! ZoneProjectile.Remove(projectile.GUID)
|
||||
}
|
||||
}
|
||||
case None => ()
|
||||
}
|
||||
}
|
||||
|
||||
def handleLashHit(pkt: LashMessage): Unit = { /* intentionally blank */ }
|
||||
|
||||
def handleAIDamage(pkt: AIDamage): Unit = {
|
||||
val AIDamage(targetGuid, attackerGuid, projectileTypeId, _, _) = pkt
|
||||
(continent.GUID(player.VehicleSeated) match {
|
||||
case Some(tobj: PlanetSideServerObject with FactionAffinity with Vitality with OwnableByPlayer)
|
||||
if tobj.GUID == targetGuid &&
|
||||
tobj.OwnerGuid.contains(player.GUID) =>
|
||||
//deployable turrets
|
||||
Some(tobj)
|
||||
case Some(tobj: PlanetSideServerObject with FactionAffinity with Vitality with Mountable)
|
||||
if tobj.GUID == targetGuid &&
|
||||
tobj.Seats.values.flatMap(_.occupants.map(_.GUID)).toSeq.contains(player.GUID) =>
|
||||
//facility turrets, etc.
|
||||
Some(tobj)
|
||||
case _
|
||||
if player.GUID == targetGuid =>
|
||||
//player avatars
|
||||
Some(player)
|
||||
case _ =>
|
||||
None
|
||||
}).collect {
|
||||
case target: AutomatedTurret.Target =>
|
||||
sessionLogic.validObject(attackerGuid, decorator = "AIDamage/AutomatedTurret")
|
||||
.collect {
|
||||
case turret: AutomatedTurret if turret.Target.isEmpty =>
|
||||
turret.Actor ! AutomatedTurretBehavior.ConfirmShot(target)
|
||||
Some(target)
|
||||
|
||||
case turret: AutomatedTurret =>
|
||||
turret.Actor ! AutomatedTurretBehavior.ConfirmShot(target)
|
||||
HandleAIDamage(target, CompileAutomatedTurretDamageData(turret, turret.TurretOwner, projectileTypeId))
|
||||
Some(target)
|
||||
}
|
||||
}
|
||||
.orElse {
|
||||
//occasionally, something that is not technically a turret's natural target may be attacked
|
||||
sessionLogic.validObject(targetGuid, decorator = "AIDamage/Target")
|
||||
.collect {
|
||||
case target: PlanetSideServerObject with FactionAffinity with Vitality =>
|
||||
sessionLogic.validObject(attackerGuid, decorator = "AIDamage/Attacker")
|
||||
.collect {
|
||||
case turret: AutomatedTurret if turret.Target.nonEmpty =>
|
||||
//the turret must be shooting at something (else) first
|
||||
HandleAIDamage(target, CompileAutomatedTurretDamageData(turret, turret.TurretOwner, projectileTypeId))
|
||||
}
|
||||
Some(target)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* support code */
|
||||
|
||||
/**
|
||||
* After a weapon has finished shooting, determine if it needs to be sorted in a special way.
|
||||
* @param tool a weapon
|
||||
*/
|
||||
private def FireCycleCleanup(tool: Tool): Unit = {
|
||||
//TODO replaced by more appropriate functionality in the future
|
||||
val tdef = tool.Definition
|
||||
if (GlobalDefinitions.isGrenade(tdef)) {
|
||||
val ammoType = tool.AmmoType
|
||||
FindEquipmentStock(player, FindToolThatUses(ammoType), 3, CountGrenades).reverse match { //do not search sidearm holsters
|
||||
case Nil =>
|
||||
log.info(s"${player.Name} has no more $ammoType grenades to throw")
|
||||
RemoveOldEquipmentFromInventory(player)(tool)
|
||||
|
||||
case x :: xs => //this is similar to ReloadMessage
|
||||
val box = x.obj.asInstanceOf[Tool]
|
||||
val tailReloadValue: Int = if (xs.isEmpty) { 0 }
|
||||
else { xs.map(_.obj.asInstanceOf[Tool].Magazine).sum }
|
||||
val sumReloadValue: Int = box.Magazine + tailReloadValue
|
||||
val actualReloadValue = if (sumReloadValue <= 3) {
|
||||
RemoveOldEquipmentFromInventory(player)(x.obj)
|
||||
sumReloadValue
|
||||
} else {
|
||||
ModifyAmmunition(player)(box.AmmoSlot.Box, 3 - tailReloadValue)
|
||||
3
|
||||
}
|
||||
log.info(s"${player.Name} found $actualReloadValue more $ammoType grenades to throw")
|
||||
ModifyAmmunition(player)(
|
||||
tool.AmmoSlot.Box,
|
||||
-actualReloadValue
|
||||
) //grenade item already in holster (negative because empty)
|
||||
xs.foreach(item => { RemoveOldEquipmentFromInventory(player)(item.obj) })
|
||||
}
|
||||
} else if (tdef == GlobalDefinitions.phoenix) {
|
||||
RemoveOldEquipmentFromInventory(player)(tool)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an object that contains a box of amunition in its `Inventory` at a certain location,
|
||||
* change the amount of ammunition within that box.
|
||||
* @param obj the `Container`
|
||||
* @param box an `AmmoBox` to modify
|
||||
* @param reloadValue the value to modify the `AmmoBox`;
|
||||
* subtracted from the current `Capacity` of `Box`
|
||||
*/
|
||||
private def ModifyAmmunition(obj: PlanetSideGameObject with Container)(box: AmmoBox, reloadValue: Int): Unit = {
|
||||
val capacity = box.Capacity - reloadValue
|
||||
box.Capacity = capacity
|
||||
sendResponse(InventoryStateMessage(box.GUID, obj.GUID, capacity))
|
||||
}
|
||||
|
||||
private def CheckForHitPositionDiscrepancy(
|
||||
projectile_guid: PlanetSideGUID,
|
||||
hitPos: Vector3,
|
||||
target: PlanetSideGameObject with FactionAffinity with Vitality
|
||||
): Unit = {
|
||||
val hitPositionDiscrepancy = Vector3.DistanceSquared(hitPos, target.Position)
|
||||
if (hitPositionDiscrepancy > Config.app.antiCheat.hitPositionDiscrepancyThreshold) {
|
||||
// If the target position on the server does not match the position where the projectile landed within reason there may be foul play
|
||||
log.warn(
|
||||
s"${player.Name}'s shot #${projectile_guid.guid} has hit discrepancy with target. Target: ${target.Position}, Reported: $hitPos, Distance: $hitPositionDiscrepancy / ${math.sqrt(hitPositionDiscrepancy).toFloat}; suspect"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* na
|
||||
* @param projectile the projectile object
|
||||
* @param resolution the resolution status to promote the projectile
|
||||
* @return a copy of the projectile
|
||||
*/
|
||||
private def ResolveProjectileInteraction(
|
||||
projectile: Projectile,
|
||||
resolution: DamageResolution.Value,
|
||||
target: PlanetSideGameObject with FactionAffinity with Vitality,
|
||||
pos: Vector3
|
||||
): Option[DamageInteraction] = {
|
||||
if (projectile.isMiss) {
|
||||
log.warn("expected projectile was already counted as a missed shot; can not resolve any further")
|
||||
None
|
||||
} else {
|
||||
val outProjectile = ProjectileQuality.modifiers(projectile, resolution, target, pos, Some(player))
|
||||
if (projectile.tool_def.Size == EquipmentSize.Melee && outProjectile.quality == ProjectileQuality.Modified(25)) {
|
||||
avatarActor ! AvatarActor.ConsumeStamina(10)
|
||||
}
|
||||
Some(DamageInteraction(SourceEntry(target), ProjectileReason(resolution, outProjectile, target.DamageModel), pos))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a projectile that was introduced into the game world and
|
||||
* determine if it generates a secondary damage projectile or
|
||||
* an method of damage causation that requires additional management.
|
||||
* @param projectile the projectile
|
||||
* @param pguid the client-local projectile identifier
|
||||
* @param hitPos the game world position where the projectile is being recorded
|
||||
* @return a for all affected targets, a combination of projectiles, projectile location, and the target's location;
|
||||
* nothing if no targets were affected
|
||||
*/
|
||||
private def HandleDamageProxy(
|
||||
projectile: Projectile,
|
||||
pguid: PlanetSideGUID,
|
||||
hitPos: Vector3
|
||||
): List[(PlanetSideGameObject with FactionAffinity with Vitality, Projectile, Vector3, Vector3)] = {
|
||||
GlobalDefinitions.getDamageProxy(projectile, hitPos) match {
|
||||
case Nil =>
|
||||
Nil
|
||||
case list if list.isEmpty =>
|
||||
Nil
|
||||
case list =>
|
||||
HandleDamageProxySetupLittleBuddy(list, hitPos)
|
||||
UpdateProjectileSidednessAfterHit(projectile, hitPos)
|
||||
val projectileSide = projectile.WhichSide
|
||||
list.flatMap { proxy =>
|
||||
if (proxy.profile.ExistsOnRemoteClients) {
|
||||
proxy.Position = hitPos
|
||||
proxy.WhichSide = projectileSide
|
||||
continent.Projectile ! ZoneProjectile.Add(player.GUID, proxy)
|
||||
Nil
|
||||
} else if (proxy.tool_def == GlobalDefinitions.maelstrom) {
|
||||
//server-side maelstrom grenade target selection
|
||||
val radius = proxy.profile.LashRadius * proxy.profile.LashRadius
|
||||
val targets = Zone.findAllTargets(continent, hitPos, proxy.profile.LashRadius, { _.livePlayerList })
|
||||
.filter { target =>
|
||||
Vector3.DistanceSquared(target.Position, hitPos) <= radius
|
||||
}
|
||||
//chainlash is separated from the actual damage application for convenience
|
||||
continent.AvatarEvents ! AvatarServiceMessage(
|
||||
continent.id,
|
||||
AvatarAction.SendResponse(
|
||||
PlanetSideGUID(0),
|
||||
ChainLashMessage(
|
||||
hitPos,
|
||||
projectile.profile.ObjectId,
|
||||
targets.map { _.GUID }
|
||||
)
|
||||
)
|
||||
)
|
||||
targets.map { target =>
|
||||
CheckForHitPositionDiscrepancy(pguid, hitPos, target)
|
||||
(target, proxy, hitPos, target.Position)
|
||||
}
|
||||
} else {
|
||||
Nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def HandleDamageProxySetupLittleBuddy(listOfProjectiles: List[Projectile], detonationPosition: Vector3): Boolean = {
|
||||
val listOfLittleBuddies: List[Projectile] = listOfProjectiles.filter { _.tool_def == GlobalDefinitions.oicw }
|
||||
val size: Int = listOfLittleBuddies.size
|
||||
if (size > 0) {
|
||||
val desiredDownwardsProjectiles: Int = 2
|
||||
val firstHalf: Int = math.min(size, desiredDownwardsProjectiles) //number that fly straight down
|
||||
val secondHalf: Int = math.max(size - firstHalf, 0) //number that are flared out
|
||||
val z: Float = player.Orientation.z //player's standing direction
|
||||
val north: Vector3 = Vector3(0,1,0) //map North
|
||||
val speed: Float = 144f //speed (packet discovered)
|
||||
val dist: Float = 25 //distance (client defined)
|
||||
val downwardsAngle: Float = -85f
|
||||
val flaredAngle: Float = -70f
|
||||
//angle of separation for downwards, degrees from vertical for flared out
|
||||
val (smallStep, smallAngle): (Float, Float) = if (firstHalf > 1) {
|
||||
(360f / firstHalf, downwardsAngle)
|
||||
} else {
|
||||
(0f, 0f)
|
||||
}
|
||||
val (largeStep, largeAngle): (Float, Float) = if (secondHalf > 1) {
|
||||
(360f / secondHalf, flaredAngle)
|
||||
} else {
|
||||
(0f, 0f)
|
||||
}
|
||||
val smallRotOffset: Float = z + 90f
|
||||
val largeRotOffset: Float = z + math.random().toFloat * 45f
|
||||
val verticalCorrection = Vector3.z(dist - dist * math.sin(math.toRadians(90 - smallAngle + largeAngle)).toFloat)
|
||||
//downwards projectiles
|
||||
var i: Int = 0
|
||||
listOfLittleBuddies.take(firstHalf).foreach { proxy =>
|
||||
val facing = (smallRotOffset + smallStep * i.toFloat) % 360
|
||||
val dir = north.Rx(smallAngle).Rz(facing)
|
||||
proxy.Position = detonationPosition + dir.xy + verticalCorrection
|
||||
proxy.Velocity = dir * speed
|
||||
proxy.Orientation = Vector3(0, (360f + smallAngle) % 360, facing)
|
||||
HandleDamageProxyLittleBuddyExplosion(proxy, dir, dist)
|
||||
i += 1
|
||||
}
|
||||
//flared out projectiles
|
||||
i = 0
|
||||
listOfLittleBuddies.drop(firstHalf).foreach { proxy =>
|
||||
val facing = (largeRotOffset + largeStep * i.toFloat) % 360
|
||||
val dir = north.Rx(largeAngle).Rz(facing)
|
||||
proxy.Position = detonationPosition + dir
|
||||
proxy.Velocity = dir * speed
|
||||
proxy.Orientation = Vector3(0, (360f + largeAngle) % 360, facing)
|
||||
HandleDamageProxyLittleBuddyExplosion(proxy, dir, dist)
|
||||
i += 1
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private def HandleDamageProxyLittleBuddyExplosion(proxy: Projectile, orientation: Vector3, distance: Float): Unit = {
|
||||
//explosion
|
||||
val obj = new DummyExplodingEntity(proxy, proxy.owner.Faction)
|
||||
obj.Position = obj.Position + orientation * distance
|
||||
val explosionFunc: ()=>Unit = WeaponAndProjectileLogic.detonateLittleBuddy(continent, obj, proxy, proxy.owner)
|
||||
context.system.scheduler.scheduleOnce(500.milliseconds) { explosionFunc() }
|
||||
}
|
||||
|
||||
private def fireStateStartPlayerMessages(itemGuid: PlanetSideGUID): Unit = {
|
||||
continent.AvatarEvents ! AvatarServiceMessage(
|
||||
continent.id,
|
||||
AvatarAction.ChangeFireState_Start(player.GUID, itemGuid)
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
used by ChangeFireStateMessage_Stop handling
|
||||
*/
|
||||
private def fireStateStopUpdateChargeAndCleanup(tool: Tool): Unit = {
|
||||
tool.FireMode match {
|
||||
case _: ChargeFireModeDefinition =>
|
||||
sendResponse(QuantityUpdateMessage(tool.AmmoSlot.Box.GUID, tool.Magazine))
|
||||
case _ => ()
|
||||
}
|
||||
if (tool.Magazine == 0) {
|
||||
FireCycleCleanup(tool)
|
||||
}
|
||||
}
|
||||
|
||||
private def fireStateStopWhenPlayer(tool: Tool, itemGuid: PlanetSideGUID): Unit = {
|
||||
//the decimator does not send a ChangeFireState_Start on the last shot; heaven knows why
|
||||
//suppress the decimator's alternate fire mode, however
|
||||
if (
|
||||
tool.Definition == GlobalDefinitions.phoenix &&
|
||||
tool.Projectile != GlobalDefinitions.phoenix_missile_guided_projectile
|
||||
) {
|
||||
fireStateStartPlayerMessages(itemGuid)
|
||||
}
|
||||
fireStateStopUpdateChargeAndCleanup(tool)
|
||||
ops.fireStateStopPlayerMessages(itemGuid)
|
||||
}
|
||||
|
||||
private def fireStateStopWhenMounted(tool: Tool, itemGuid: PlanetSideGUID): Unit = {
|
||||
fireStateStopUpdateChargeAndCleanup(tool)
|
||||
ops.fireStateStopMountedMessages(itemGuid)
|
||||
}
|
||||
|
||||
//noinspection SameParameterValue
|
||||
private def addShotsLanded(weaponId: Int, shots: Int): Unit = {
|
||||
ops.addShotsToMap(ops.shotsLanded, weaponId, shots)
|
||||
}
|
||||
|
||||
private def CompileAutomatedTurretDamageData(
|
||||
turret: AutomatedTurret,
|
||||
owner: SourceEntry,
|
||||
projectileTypeId: Long
|
||||
): Option[(AutomatedTurret, Tool, SourceEntry, ProjectileDefinition)] = {
|
||||
turret.Weapons
|
||||
.values
|
||||
.flatMap { _.Equipment }
|
||||
.collect { case weapon: Tool => (turret, weapon, owner, weapon.Projectile) }
|
||||
.find { case (_, _, _, p) => p.ObjectId == projectileTypeId }
|
||||
}
|
||||
|
||||
private def HandleAIDamage(
|
||||
target: PlanetSideServerObject with FactionAffinity with Vitality,
|
||||
results: Option[(AutomatedTurret, Tool, SourceEntry, ProjectileDefinition)]
|
||||
): Unit = {
|
||||
results.collect {
|
||||
case (obj, tool, owner, projectileInfo) =>
|
||||
val angle = Vector3.Unit(target.Position - obj.Position)
|
||||
val proj = new Projectile(
|
||||
projectileInfo,
|
||||
tool.Definition,
|
||||
tool.FireMode,
|
||||
None,
|
||||
owner,
|
||||
obj.Definition.ObjectId,
|
||||
obj.Position + Vector3.z(value = 1f),
|
||||
angle,
|
||||
Some(angle * projectileInfo.FinalVelocity)
|
||||
)
|
||||
val hitPos = target.Position + Vector3.z(value = 1f)
|
||||
ResolveProjectileInteraction(proj, DamageResolution.Hit, target, hitPos).collect { resprojectile =>
|
||||
addShotsLanded(resprojectile.cause.attribution, shots = 1)
|
||||
sessionLogic.handleDealingDamage(target, resprojectile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def UpdateProjectileSidednessAfterHit(projectile: Projectile, hitPosition: Vector3): Unit = {
|
||||
val origin = projectile.Position
|
||||
val distance = Vector3.Magnitude(hitPosition - origin)
|
||||
continent.blockMap
|
||||
.sector(hitPosition, distance)
|
||||
.environmentList
|
||||
.collect { case o: InteriorDoorPassage =>
|
||||
val door = o.door
|
||||
val intersectTest = WeaponAndProjectileLogic.quickLineSphereIntersectionPoints(
|
||||
origin,
|
||||
hitPosition,
|
||||
door.Position,
|
||||
door.Definition.UseRadius + 0.1f
|
||||
)
|
||||
(door, intersectTest)
|
||||
}
|
||||
.collect { case (door, intersectionTest) if intersectionTest.nonEmpty =>
|
||||
(door, Vector3.Magnitude(hitPosition - door.Position), intersectionTest)
|
||||
}
|
||||
.minByOption { case (_, dist, _) => dist }
|
||||
.foreach { case (door, _, intersects) =>
|
||||
val strictly = if (Vector3.DotProduct(Vector3.Unit(hitPosition - door.Position), door.Outwards) > 0f) {
|
||||
Sidedness.OutsideOf
|
||||
} else {
|
||||
Sidedness.InsideOf
|
||||
}
|
||||
projectile.WhichSide = if (intersects.size == 1) {
|
||||
Sidedness.InBetweenSides(door, strictly)
|
||||
} else {
|
||||
strictly
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package net.psforever.actors.session.support
|
|||
|
||||
import akka.actor.Actor.Receive
|
||||
import akka.actor.ActorRef
|
||||
import net.psforever.objects.Session
|
||||
|
||||
trait ModeLogic {
|
||||
def avatarResponse: AvatarHandlerFunctions
|
||||
|
|
@ -16,6 +17,10 @@ trait ModeLogic {
|
|||
def vehicles: VehicleFunctions
|
||||
def vehicleResponse: VehicleHandlerFunctions
|
||||
|
||||
def switchTo(session: Session): Unit = { /* to override */ }
|
||||
|
||||
def switchFrom(session: Session): Unit = { /* to override */ }
|
||||
|
||||
def parse(sender: ActorRef): Receive
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@ trait WeaponAndProjectileFunctions extends CommonSessionInterfacingFunctionality
|
|||
|
||||
def handleWeaponLazeTargetPosition(pkt: WeaponLazeTargetPositionMessage): Unit
|
||||
|
||||
def handleUplinkRequest(pkt: UplinkRequest): Unit
|
||||
|
||||
def handleAvatarGrenadeState(pkt: AvatarGrenadeStateMessage): Unit
|
||||
|
||||
def handleChangeFireStateStart(pkt: ChangeFireStateMessage_Start): Unit
|
||||
|
|
|
|||
|
|
@ -1930,7 +1930,7 @@ class ZoningOperations(
|
|||
//find and reload previous player
|
||||
(
|
||||
inZone.Players.find(p => p.name.equals(name)),
|
||||
inZone.LivePlayers.find(p => p.Name.equals(name))
|
||||
inZone.AllPlayers.find(p => p.Name.equals(name))
|
||||
) match {
|
||||
case (_, Some(p)) if p.death_by == -1 =>
|
||||
//player is not allowed
|
||||
|
|
@ -2905,36 +2905,34 @@ class ZoningOperations(
|
|||
def HandleSetCurrentAvatar(tplayer: Player): Unit = {
|
||||
log.trace(s"HandleSetCurrentAvatar - ${tplayer.Name}")
|
||||
session = session.copy(player = tplayer)
|
||||
val tavatar = tplayer.avatar
|
||||
val guid = tplayer.GUID
|
||||
sessionLogic.general.updateDeployableUIElements(Deployables.InitializeDeployableUIElements(avatar))
|
||||
sessionLogic.general.updateDeployableUIElements(Deployables.InitializeDeployableUIElements(tavatar))
|
||||
sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(0), 75, 0))
|
||||
sendResponse(SetCurrentAvatarMessage(guid, 0, 0))
|
||||
sendResponse(ChatMsg(ChatMessageType.CMT_EXPANSIONS, wideContents=true, "", "1 on", None)) //CC on //TODO once per respawn?
|
||||
val pos = player.Position = shiftPosition.getOrElse(tplayer.Position)
|
||||
val orient = player.Orientation = shiftOrientation.getOrElse(tplayer.Orientation)
|
||||
val pos = tplayer.Position = shiftPosition.getOrElse(tplayer.Position)
|
||||
val orient = tplayer.Orientation = shiftOrientation.getOrElse(tplayer.Orientation)
|
||||
sendResponse(PlayerStateShiftMessage(ShiftState(1, pos, orient.z)))
|
||||
shiftPosition = None
|
||||
shiftOrientation = None
|
||||
if (player.spectator) {
|
||||
sendResponse(ChatMsg(ChatMessageType.CMT_TOGGLESPECTATORMODE, wideContents=false, "", "on", None))
|
||||
}
|
||||
if (player.Jammed) {
|
||||
if (tplayer.Jammed) {
|
||||
//TODO something better than just canceling?
|
||||
player.Actor ! JammableUnit.ClearJammeredStatus()
|
||||
player.Actor ! JammableUnit.ClearJammeredSound()
|
||||
tplayer.Actor ! JammableUnit.ClearJammeredStatus()
|
||||
tplayer.Actor ! JammableUnit.ClearJammeredSound()
|
||||
}
|
||||
val originalDeadState = deadState
|
||||
deadState = DeadState.Alive
|
||||
avatarActor ! AvatarActor.ResetImplants()
|
||||
sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(0), 82, 0))
|
||||
initializeShortcutsAndBank(guid)
|
||||
initializeShortcutsAndBank(guid, tavatar.shortcuts)
|
||||
//Favorites lists
|
||||
avatarActor ! AvatarActor.InitialRefreshLoadouts()
|
||||
|
||||
sendResponse(
|
||||
SetChatFilterMessage(ChatChannel.Platoon, origin = false, ChatChannel.values.toList)
|
||||
) //TODO will not always be "on" like this
|
||||
sendResponse(AvatarDeadStateMessage(DeadState.Alive, 0, 0, tplayer.Position, player.Faction, unk5 = true))
|
||||
sendResponse(AvatarDeadStateMessage(DeadState.Alive, 0, 0, tplayer.Position, tplayer.Faction, unk5 = true))
|
||||
//looking for squad (members)
|
||||
if (tplayer.avatar.lookingForSquad) {
|
||||
sendResponse(PlanetsideAttributeMessage(guid, 53, 1))
|
||||
|
|
@ -2979,7 +2977,7 @@ class ZoningOperations(
|
|||
drawDeloyableIcon = DontRedrawIcons
|
||||
|
||||
//assert or transfer vehicle ownership
|
||||
continent.GUID(player.avatar.vehicle) match {
|
||||
continent.GUID(tplayer.avatar.vehicle) match {
|
||||
case Some(vehicle: Vehicle) if vehicle.OwnerName.contains(tplayer.Name) =>
|
||||
vehicle.OwnerGuid = guid
|
||||
continent.VehicleEvents ! VehicleServiceMessage(
|
||||
|
|
@ -3033,7 +3031,7 @@ class ZoningOperations(
|
|||
)
|
||||
case (Some(vehicle), _) =>
|
||||
//passenger
|
||||
vehicle.Actor ! Vehicle.UpdateZoneInteractionProgressUI(player)
|
||||
vehicle.Actor ! Vehicle.UpdateZoneInteractionProgressUI(tplayer)
|
||||
case _ => ;
|
||||
}
|
||||
interstellarFerryTopLevelGUID = None
|
||||
|
|
@ -3042,12 +3040,12 @@ class ZoningOperations(
|
|||
loadConfZone = false
|
||||
}
|
||||
if (noSpawnPointHere) {
|
||||
RequestSanctuaryZoneSpawn(player, continent.Number)
|
||||
} else if (originalDeadState == DeadState.Dead || player.Health == 0) {
|
||||
RequestSanctuaryZoneSpawn(tplayer, continent.Number)
|
||||
} else if (originalDeadState == DeadState.Dead || tplayer.Health == 0) {
|
||||
//killed during spawn setup or possibly a relog into a corpse (by accident?)
|
||||
player.Actor ! Player.Die()
|
||||
tplayer.Actor ! Player.Die()
|
||||
} else {
|
||||
AvatarActor.savePlayerData(player)
|
||||
AvatarActor.savePlayerData(tplayer)
|
||||
sessionLogic.general.displayCharSavedMsgThenRenewTimer(
|
||||
Config.app.game.savedMsg.short.fixed,
|
||||
Config.app.game.savedMsg.short.variable
|
||||
|
|
@ -3061,14 +3059,14 @@ class ZoningOperations(
|
|||
}
|
||||
.collect { case Some(thing: PlanetSideGameObject with FactionAffinity) => Some(SourceEntry(thing)) }
|
||||
.flatten
|
||||
val lastEntryOpt = player.History.lastOption
|
||||
val lastEntryOpt = tplayer.History.lastOption
|
||||
if (lastEntryOpt.exists { !_.isInstanceOf[IncarnationActivity] }) {
|
||||
player.LogActivity({
|
||||
tplayer.LogActivity({
|
||||
lastEntryOpt match {
|
||||
case Some(_) =>
|
||||
ReconstructionActivity(PlayerSource(player), continent.Number, effortBy)
|
||||
ReconstructionActivity(PlayerSource(tplayer), continent.Number, effortBy)
|
||||
case None =>
|
||||
SpawningActivity(PlayerSource(player), continent.Number, effortBy)
|
||||
SpawningActivity(PlayerSource(tplayer), continent.Number, effortBy)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -3079,9 +3077,9 @@ class ZoningOperations(
|
|||
!account.gm && /* gm's are excluded */
|
||||
Config.app.game.promotion.active && /* play versus progress system must be active */
|
||||
BattleRank.withExperience(tplayer.avatar.bep).value <= Config.app.game.promotion.broadcastBattleRank && /* must be below a certain battle rank */
|
||||
avatar.scorecard.Lives.isEmpty && /* first life after login */
|
||||
avatar.scorecard.CurrentLife.prior.isEmpty && /* no revives */
|
||||
player.History.size == 1 /* did nothing but come into existence */
|
||||
tavatar.scorecard.Lives.isEmpty && /* first life after login */
|
||||
tavatar.scorecard.CurrentLife.prior.isEmpty && /* no revives */
|
||||
tplayer.History.size == 1 /* did nothing but come into existence */
|
||||
) {
|
||||
ZoningOperations.reportProgressionSystem(context.self)
|
||||
}
|
||||
|
|
@ -3149,6 +3147,18 @@ class ZoningOperations(
|
|||
}
|
||||
sendResponse(ChangeShortcutBankMessage(guid, 0))
|
||||
}
|
||||
def initializeShortcutsAndBank(guid: PlanetSideGUID, shortcuts: Array[Option[AvatarShortcut]]): Unit = {
|
||||
shortcuts
|
||||
.zipWithIndex
|
||||
.collect { case (Some(shortcut), index) =>
|
||||
sendResponse(CreateShortcutMessage(
|
||||
guid,
|
||||
index + 1,
|
||||
Some(AvatarShortcut.convert(shortcut))
|
||||
))
|
||||
}
|
||||
sendResponse(ChangeShortcutBankMessage(guid, 0))
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw the icon for this deployable object.<br>
|
||||
|
|
|
|||
|
|
@ -463,4 +463,9 @@ object FirstTimeEvents {
|
|||
"xpe_th_flail",
|
||||
"xpe_th_bfr"
|
||||
)
|
||||
|
||||
val All: Set[String] = NC.All ++ TR.All ++ VS.All ++
|
||||
Standard.All ++ Cavern.All ++
|
||||
Maps ++ Monoliths ++ Gingerman ++ Sled ++ Snowman ++ Charlie ++ BattleRanks ++ CommandRanks ++
|
||||
Training ++ OldTraining ++ Generic
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ object AvatarConverter {
|
|||
obj.avatar.basic,
|
||||
CommonFieldData(
|
||||
obj.Faction,
|
||||
bops = false,
|
||||
bops = obj.spectator,
|
||||
alt_model_flag,
|
||||
v1 = false,
|
||||
None,
|
||||
|
|
|
|||
|
|
@ -204,7 +204,7 @@ object EquipmentTerminalDefinition {
|
|||
"advanced_ace" -> MakeConstructionItem(advanced_ace),
|
||||
"remote_electronics_kit" -> MakeSimpleItem(remote_electronics_kit),
|
||||
"trek" -> MakeTool(trek),
|
||||
"command_detonater" -> MakeSimpleItem(command_detonater),
|
||||
//"command_detonater" -> MakeSimpleItem(command_detonater),
|
||||
"flail_targeting_laser" -> MakeSimpleItem(flail_targeting_laser)
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -566,9 +566,13 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) {
|
|||
|
||||
def Vehicles: List[Vehicle] = vehicles.toList
|
||||
|
||||
def Players: List[Avatar] = players.values.flatten.map(_.avatar).toList
|
||||
def AllPlayers: List[Player] = players.values.flatten.toList
|
||||
|
||||
def LivePlayers: List[Player] = players.values.flatten.toList
|
||||
def Players: List[Avatar] = AllPlayers.map(_.avatar)
|
||||
|
||||
def LivePlayers: List[Player] = AllPlayers.filterNot(_.spectator)
|
||||
|
||||
def Spectator: List[Player] = AllPlayers.filter(_.spectator)
|
||||
|
||||
def Corpses: List[Player] = corpses.toList
|
||||
|
||||
|
|
|
|||
|
|
@ -489,14 +489,14 @@ object GamePacketOpcode extends Enumeration {
|
|||
case 0x9b => noDecoder(SyncMessage)
|
||||
case 0x9c => game.DebugDrawMessage.decode
|
||||
case 0x9d => noDecoder(SoulMarkMessage)
|
||||
case 0x9e => noDecoder(UplinkPositionEvent)
|
||||
case 0x9e => game.UplinkPositionEvent.decode
|
||||
case 0x9f => game.HotSpotUpdateMessage.decode
|
||||
|
||||
// OPCODES 0xa0-af
|
||||
case 0xa0 => game.BuildingInfoUpdateMessage.decode
|
||||
case 0xa1 => game.FireHintMessage.decode
|
||||
case 0xa2 => noDecoder(UplinkRequest)
|
||||
case 0xa3 => noDecoder(UplinkResponse)
|
||||
case 0xa2 => game.UplinkRequest.decode
|
||||
case 0xa3 => game.UplinkResponse.decode
|
||||
case 0xa4 => game.WarpgateRequest.decode
|
||||
case 0xa5 => noDecoder(WarpgateResponse)
|
||||
case 0xa6 => game.DamageWithPositionMessage.decode
|
||||
|
|
|
|||
|
|
@ -0,0 +1,114 @@
|
|||
// Copyright (c) 2024 PSForever
|
||||
package net.psforever.packet.game
|
||||
|
||||
import net.psforever.packet.GamePacketOpcode.Type
|
||||
import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
|
||||
import shapeless.{::, HNil}
|
||||
import net.psforever.types.Vector3
|
||||
import scodec.bits.BitVector
|
||||
import scodec.{Attempt, Codec}
|
||||
import scodec.codecs._
|
||||
|
||||
import scala.annotation.switch
|
||||
|
||||
trait UplinkEvent {
|
||||
def code: Int
|
||||
}
|
||||
|
||||
final case class Event0(code: Int) extends UplinkEvent
|
||||
|
||||
final case class Event1(code: Int, unk1: Int) extends UplinkEvent
|
||||
|
||||
final case class Event2(
|
||||
code: Int,
|
||||
unk1: Vector3,
|
||||
unk2: Int,
|
||||
unk3: Int,
|
||||
unk4: Long,
|
||||
unk5: Long,
|
||||
unk6: Long,
|
||||
unk7: Long,
|
||||
unk8: Option[Boolean]
|
||||
) extends UplinkEvent
|
||||
|
||||
final case class UplinkPositionEvent(
|
||||
code: Int,
|
||||
event: UplinkEvent
|
||||
) extends PlanetSideGamePacket {
|
||||
type Packet = UplinkPositionEvent
|
||||
def opcode: Type = GamePacketOpcode.UplinkPositionEvent
|
||||
def encode: Attempt[BitVector] = UplinkPositionEvent.encode(this)
|
||||
}
|
||||
|
||||
object UplinkPositionEvent extends Marshallable[UplinkPositionEvent] {
|
||||
private def event0Codec(code: Int): Codec[Event0] = conditional(included = false, bool).xmap[Event0](
|
||||
_ => Event0(code),
|
||||
{
|
||||
case Event0(_) => None
|
||||
}
|
||||
)
|
||||
|
||||
private def event1Codec(code: Int): Codec[Event1] =
|
||||
("unk1" | uint8L).hlist.xmap[Event1](
|
||||
{
|
||||
case unk1 :: HNil => Event1(code, unk1)
|
||||
},
|
||||
{
|
||||
case Event1(_, unk1) => unk1 :: HNil
|
||||
}
|
||||
)
|
||||
|
||||
private def event2NoBoolCodec(code: Int): Codec[Event2] = (
|
||||
("unk1" | Vector3.codec_pos) ::
|
||||
("unk2" | uint8) ::
|
||||
("unk3" | uint16L) ::
|
||||
("unk4" | uint32L) ::
|
||||
("unk5" | uint32L) ::
|
||||
("unk6" | uint32L) ::
|
||||
("unk7" | uint32L)
|
||||
).xmap[Event2](
|
||||
{
|
||||
case u1 :: u2 :: u3 :: u4 :: u5 :: u6 :: u7 :: HNil =>
|
||||
Event2(code, u1, u2, u3, u4, u5, u6, u7, None)
|
||||
},
|
||||
{
|
||||
case Event2(_, u1, u2, u3, u4, u5, u6, u7, _) =>
|
||||
u1 :: u2 :: u3 :: u4 :: u5 :: u6 :: u7 :: HNil
|
||||
}
|
||||
)
|
||||
|
||||
private def event2WithBoolCodec(code: Int): Codec[Event2] = (
|
||||
("unk1" | Vector3.codec_pos) ::
|
||||
("unk2" | uint8) ::
|
||||
("unk3" | uint16L) ::
|
||||
("unk4" | uint32L) ::
|
||||
("unk5" | uint32L) ::
|
||||
("unk6" | uint32L) ::
|
||||
("unk7" | uint32L) ::
|
||||
("unk8" | bool)
|
||||
).xmap[Event2](
|
||||
{
|
||||
case u1 :: u2 :: u3 :: u4 :: u5 :: u6 :: u7 :: u8 :: HNil =>
|
||||
Event2(code, u1, u2, u3, u4, u5, u6, u7, Some(u8))
|
||||
},
|
||||
{
|
||||
case Event2(_, u1, u2, u3, u4, u5, u6, u7, Some(u8)) =>
|
||||
u1 :: u2 :: u3 :: u4 :: u5 :: u6 :: u7 :: u8 :: HNil
|
||||
}
|
||||
)
|
||||
|
||||
private def switchUplinkEvent(code: Int): Codec[UplinkEvent] = {
|
||||
((code: @switch) match {
|
||||
case 0 => event2NoBoolCodec(code)
|
||||
case 1 | 2 => event2WithBoolCodec(code)
|
||||
case 3 | 4 => event1Codec(code)
|
||||
case _ => event0Codec(code)
|
||||
}).asInstanceOf[Codec[UplinkEvent]]
|
||||
}
|
||||
|
||||
implicit val codec: Codec[UplinkPositionEvent] = (
|
||||
("code" | uint(bits = 3)) >>:~ { code =>
|
||||
("event" | switchUplinkEvent(code)).hlist
|
||||
}
|
||||
).as[UplinkPositionEvent]
|
||||
}
|
||||
59
src/main/scala/net/psforever/packet/game/UplinkRequest.scala
Normal file
59
src/main/scala/net/psforever/packet/game/UplinkRequest.scala
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
// Copyright (c) 2024 PSForever
|
||||
package net.psforever.packet.game
|
||||
|
||||
import enumeratum.values.{IntEnum, IntEnumEntry}
|
||||
import shapeless.{::, HNil}
|
||||
import net.psforever.newcodecs.newcodecs
|
||||
import net.psforever.packet.GamePacketOpcode.Type
|
||||
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
|
||||
import net.psforever.types.Vector3
|
||||
import scodec.bits.BitVector
|
||||
import scodec.{Attempt, Codec}
|
||||
import scodec.codecs._
|
||||
|
||||
sealed abstract class UplinkRequestType(val value: Int) extends IntEnumEntry
|
||||
|
||||
object UplinkRequestType extends IntEnum[UplinkRequestType] {
|
||||
val values: IndexedSeq[UplinkRequestType] = findValues
|
||||
|
||||
case object RevealFriendlies extends UplinkRequestType(value = 0)
|
||||
|
||||
case object RevealEnemies extends UplinkRequestType(value = 1)
|
||||
|
||||
case object Unknown2 extends UplinkRequestType(value = 2)
|
||||
|
||||
case object ElectroMagneticPulse extends UplinkRequestType(value = 3)
|
||||
|
||||
case object OrbitalStrike extends UplinkRequestType(value = 4)
|
||||
|
||||
implicit val codec: Codec[UplinkRequestType] = PacketHelpers.createIntEnumCodec(this, uint4)
|
||||
}
|
||||
|
||||
final case class UplinkRequest(
|
||||
uplinkType: UplinkRequestType,
|
||||
pos: Option[Vector3],
|
||||
unk: Boolean
|
||||
) extends PlanetSideGamePacket {
|
||||
type Packet = UplinkRequest
|
||||
def opcode: Type = GamePacketOpcode.UplinkRequest
|
||||
def encode: Attempt[BitVector] = UplinkRequest.encode(this)
|
||||
}
|
||||
|
||||
object UplinkRequest extends Marshallable[UplinkRequest] {
|
||||
private val xyToVector3: Codec[Vector3] =
|
||||
(newcodecs.q_float(0.0, 8192.0, 20) ::
|
||||
newcodecs.q_float(0.0, 8192.0, 20)).xmap[Vector3](
|
||||
{
|
||||
case x :: y :: HNil => Vector3(x, y, 0f)
|
||||
},
|
||||
{
|
||||
case Vector3(x, y, _) => x :: y :: HNil
|
||||
}
|
||||
)
|
||||
|
||||
implicit val codec: Codec[UplinkRequest] = (
|
||||
("uplinkType" | UplinkRequestType.codec) >>:~ { uplinkType =>
|
||||
conditional(uplinkType == UplinkRequestType.OrbitalStrike, xyToVector3) ::
|
||||
("unk" | bool)
|
||||
}).as[UplinkRequest]
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
// Copyright (c) 2024 PSForever
|
||||
package net.psforever.packet.game
|
||||
|
||||
import net.psforever.packet.GamePacketOpcode.Type
|
||||
import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
|
||||
import scodec.bits.BitVector
|
||||
import scodec.{Attempt, Codec}
|
||||
import scodec.codecs._
|
||||
|
||||
final case class UplinkResponse(
|
||||
unk1: Int,
|
||||
unk2: Int
|
||||
) extends PlanetSideGamePacket {
|
||||
type Packet = UplinkResponse
|
||||
def opcode: Type = GamePacketOpcode.UplinkResponse
|
||||
def encode: Attempt[BitVector] = UplinkResponse.encode(this)
|
||||
}
|
||||
|
||||
object UplinkResponse extends Marshallable[UplinkResponse] {
|
||||
implicit val codec: Codec[UplinkResponse] = (
|
||||
("unk1" | uint(bits = 3)) ::
|
||||
("unk2" | uint4)
|
||||
).as[UplinkResponse]
|
||||
}
|
||||
|
|
@ -363,7 +363,7 @@ class PersistenceMonitor(
|
|||
* but should be uncommon.
|
||||
*/
|
||||
def PerformLogout(): Unit = {
|
||||
(inZone.Players.find(p => p.name == name), inZone.LivePlayers.find(p => p.Name == name)) match {
|
||||
(inZone.Players.find(p => p.name == name), inZone.AllPlayers.find(p => p.Name == name)) match {
|
||||
case (Some(avatar), Some(player)) if player.VehicleSeated.nonEmpty =>
|
||||
//in case the player is holding the llu and disconnects
|
||||
player.Zone.AvatarEvents ! AvatarServiceMessage(player.Name, AvatarAction.DropSpecialItem())
|
||||
|
|
|
|||
Loading…
Reference in a new issue