mirror of
https://github.com/2revoemag/PSF-BotServer.git
synced 2026-02-24 09:03:35 +00:00
Spectator Role (#1200)
* reorganized files and methods for session actor in preparation for custom spectator implementation * split existing code between data and functions, and entry points; parsing and logic management is now handled within the current game mode, which should reduce the need to ask explicits * should fix players not performing damage after being broken for an unknown amount of time, maybe never working correctly * functioning spectator mode logic swap; initial packets for UplinkRequest, UplinkResponse, and UplinkPositionEvent * available chat prompts can now be different based on player mode without testing flags * modified ChatActor to be replaced by a function-data logic pair, and modified ChatService to be able to accommodate the new chat channel; chat packet handling moved from general operations to the new chat operations * resolved issues with spectator implants, at least enough that implants should be stable; created an exclusive permission for spectator mode; database changes to persist permissions for different modes * command detonater is no longer allowed; spectators now hold a laze pointer * for the purposes of testing, anyone can be a spectator * spectator mode is important to confirm; removed deprecated chat actor; dismount quietly * oops; allowed again * restored commands setbr and setcr; deployables are erased from spectator map; projectiles destruction only just in case * role only for those who are permitted
This commit is contained in:
parent
21637108c2
commit
426ab84f0a
64 changed files with 14058 additions and 8917 deletions
|
|
@ -7,11 +7,11 @@ import akka.actor.typed.{ActorRef, Behavior, PostStop, SupervisorStrategy}
|
|||
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import net.psforever.actors.zone.ZoneActor
|
||||
import net.psforever.objects.Session
|
||||
import net.psforever.objects.avatar.ModePermissions
|
||||
import net.psforever.objects.avatar.scoring.{Assist, Death, EquipmentStat, KDAStat, Kill, Life, ScoreCard, SupportActivity}
|
||||
import net.psforever.objects.serverobject.affinity.FactionAffinity
|
||||
import net.psforever.objects.sourcing.{TurretSource, VehicleSource}
|
||||
import net.psforever.objects.vital.{InGameHistory, ReconstructionActivity}
|
||||
import net.psforever.objects.vehicles.MountedWeapons
|
||||
import net.psforever.objects.vital.ReconstructionActivity
|
||||
import net.psforever.types.{ChatMessageType, StatisticalCategory, StatisticalElement}
|
||||
import org.joda.time.{LocalDateTime, Seconds}
|
||||
|
||||
|
|
@ -42,7 +42,6 @@ import net.psforever.objects.inventory.{Container, InventoryItem}
|
|||
import net.psforever.objects.loadouts.{InfantryLoadout, Loadout, VehicleLoadout}
|
||||
import net.psforever.objects.locker.LockerContainer
|
||||
import net.psforever.objects.sourcing.{PlayerSource,SourceWithHealthEntry}
|
||||
import net.psforever.objects.vital.projectile.ProjectileReason
|
||||
import net.psforever.objects.vital.{DamagingActivity, HealFromImplant, HealingActivity, SpawningActivity}
|
||||
import net.psforever.packet.game.objectcreate.{BasicCharacterData, ObjectClass, RibbonBars}
|
||||
import net.psforever.packet.game.{Friend => GameFriend, _}
|
||||
|
|
@ -958,6 +957,28 @@ object AvatarActor {
|
|||
out.future
|
||||
}
|
||||
|
||||
def loadSpectatorModePermissions(avatarId: Long): Future[ModePermissions] = {
|
||||
import ctx._
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
val out: Promise[ModePermissions] = Promise()
|
||||
val result = ctx.run(query[persistence.Avatarmodepermission].filter(_.avatarId == lift(avatarId)))
|
||||
result.onComplete {
|
||||
case Success(res) =>
|
||||
res.headOption
|
||||
.collect {
|
||||
case perms: persistence.Avatarmodepermission =>
|
||||
out.completeWith(Future(ModePermissions(perms.canSpectate, perms.canGm)))
|
||||
}
|
||||
.orElse {
|
||||
out.completeWith(Future(ModePermissions()))
|
||||
None
|
||||
}
|
||||
case _ =>
|
||||
out.completeWith(Future(ModePermissions()))
|
||||
}
|
||||
out.future
|
||||
}
|
||||
|
||||
def toAvatar(avatar: persistence.Avatar): Avatar = {
|
||||
val bep = avatar.bep
|
||||
val convertedCosmetics = if (BattleRank.showCosmetics(bep)) {
|
||||
|
|
@ -2046,9 +2067,10 @@ class AvatarActor(
|
|||
shortcuts <- loadShortcuts(avatarId)
|
||||
saved <- AvatarActor.loadSavedAvatarData(avatarId)
|
||||
card <- AvatarActor.loadCampaignKdaData(avatarId)
|
||||
} yield (loadouts, friends, ignored, shortcuts, saved, card)
|
||||
perms <- AvatarActor.loadSpectatorModePermissions(avatarId)
|
||||
} yield (loadouts, friends, ignored, shortcuts, saved, card, perms)
|
||||
result.onComplete {
|
||||
case Success((loadoutList, friendsList, ignoredList, shortcutList, saved, card)) =>
|
||||
case Success((loadoutList, friendsList, ignoredList, shortcutList, saved, card, perms)) =>
|
||||
avatarCopy(
|
||||
avatar.copy(
|
||||
loadouts = avatar.loadouts.copy(suit = loadoutList),
|
||||
|
|
@ -2058,7 +2080,8 @@ class AvatarActor(
|
|||
purchase = AvatarActor.buildCooldownsFromClob(saved.purchaseCooldowns, Avatar.purchaseCooldowns, log),
|
||||
use = AvatarActor.buildCooldownsFromClob(saved.useCooldowns, Avatar.useCooldowns, log)
|
||||
),
|
||||
scorecard = card
|
||||
scorecard = card,
|
||||
permissions = perms
|
||||
)
|
||||
)
|
||||
sessionActor ! SessionActor.AvatarLoadingSync(step = 2)
|
||||
|
|
@ -2239,13 +2262,15 @@ class AvatarActor(
|
|||
if (implant.active) {
|
||||
deactivateImplant(implant.definition.implantType)
|
||||
}
|
||||
session.get.zone.AvatarEvents ! AvatarServiceMessage(
|
||||
session.get.zone.id,
|
||||
AvatarAction.SendResponse(
|
||||
Service.defaultPlayerGUID,
|
||||
AvatarImplantMessage(session.get.player.GUID, ImplantAction.Initialization, slot, 0)
|
||||
if (implant.initialized) {
|
||||
session.get.zone.AvatarEvents ! AvatarServiceMessage(
|
||||
session.get.zone.id,
|
||||
AvatarAction.SendResponse(
|
||||
Service.defaultPlayerGUID,
|
||||
AvatarImplantMessage(session.get.player.GUID, ImplantAction.Initialization, slot, 0)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
Some(implant.copy(initialized = false, active = false))
|
||||
case (None, _) => None
|
||||
}))
|
||||
|
|
@ -3177,16 +3202,7 @@ class AvatarActor(
|
|||
val zone = _session.zone
|
||||
val player = _session.player
|
||||
val playerSource = PlayerSource(player)
|
||||
val historyTranscript = {
|
||||
(killStat.info.interaction.cause match {
|
||||
case pr: ProjectileReason => pr.projectile.mounted_in.flatMap { a => zone.GUID(a._1) } //what fired the projectile
|
||||
case _ => None
|
||||
}).collect {
|
||||
case mount: PlanetSideGameObject with FactionAffinity with InGameHistory with MountedWeapons =>
|
||||
player.ContributionFrom(mount)
|
||||
}
|
||||
player.HistoryAndContributions()
|
||||
}
|
||||
val historyTranscript = Players.produceContributionTranscriptFromKill(zone, player, killStat)
|
||||
val target = killStat.info.targetAfter.asInstanceOf[PlayerSource]
|
||||
val targetMounted = target.seatedIn
|
||||
.collect {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,44 +1,25 @@
|
|||
// Copyright (c) 2016, 2020 PSForever
|
||||
// Copyright (c) 2016, 2020, 2024 PSForever
|
||||
package net.psforever.actors.session
|
||||
|
||||
import akka.actor.typed.receptionist.Receptionist
|
||||
import akka.actor.typed.scaladsl.adapter._
|
||||
import akka.actor.{Actor, MDCContextAware, typed}
|
||||
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.support.SessionData
|
||||
import net.psforever.objects._
|
||||
import net.psforever.objects.avatar._
|
||||
import net.psforever.objects.definition._
|
||||
import net.psforever.objects.guid._
|
||||
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._
|
||||
import net.psforever.objects.serverobject.CommonMessages
|
||||
import net.psforever.objects.zones._
|
||||
import net.psforever.packet._
|
||||
import net.psforever.packet.game._
|
||||
import net.psforever.services.CavernRotationService.SendCavernRotationUpdates
|
||||
import net.psforever.services.ServiceManager.{Lookup, 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
|
||||
import net.psforever.services.{CavernRotationService, ServiceManager, InterstellarClusterService => ICS}
|
||||
import net.psforever.types._
|
||||
import net.psforever.util.Config
|
||||
import net.psforever.actors.session.support.{ModeLogic, PlayerMode, SessionData}
|
||||
import net.psforever.objects.{Default, Player}
|
||||
import net.psforever.objects.avatar.Avatar
|
||||
import net.psforever.objects.definition.BasicDefinition
|
||||
import net.psforever.packet.PlanetSidePacket
|
||||
import net.psforever.packet.game.{FriendsResponse, KeepAliveMessage}
|
||||
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)
|
||||
|
|
@ -87,521 +68,71 @@ object SessionActor {
|
|||
|
||||
private[session] case object CharSavedMsg extends Command
|
||||
|
||||
/**
|
||||
* The message that progresses some form of user-driven activity with a certain eventual outcome
|
||||
* and potential feedback per cycle.
|
||||
* @param delta how much the progress value changes each tick, which will be treated as a percentage;
|
||||
* must be a positive value
|
||||
* @param completionAction a finalizing action performed once the progress reaches 100(%)
|
||||
* @param tickAction an action that is performed for each increase of progress
|
||||
* @param tickTime how long between each `tickAction` (ms);
|
||||
* defaults to 250 milliseconds
|
||||
*/
|
||||
private[session] final case class ProgressEvent(
|
||||
delta: Float,
|
||||
completionAction: () => Unit,
|
||||
tickAction: Float => Boolean,
|
||||
tickTime: Long = 250L
|
||||
)
|
||||
final case object StartHeartbeat extends Command
|
||||
|
||||
private[session] final case class AvatarAwardMessageBundle(
|
||||
bundle: Iterable[Iterable[PlanetSideGamePacket]],
|
||||
delay: Long
|
||||
)
|
||||
private final case object PokeClient extends Command
|
||||
|
||||
final case class SetMode(mode: PlayerMode) extends Command
|
||||
}
|
||||
|
||||
class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], connectionId: String, sessionId: Long)
|
||||
extends Actor
|
||||
extends Actor
|
||||
with MDCContextAware {
|
||||
MDC("connectionId") = connectionId
|
||||
|
||||
private var clientKeepAlive: Cancellable = Default.Cancellable
|
||||
private[this] val buffer: mutable.ListBuffer[Any] = new mutable.ListBuffer[Any]()
|
||||
private[this] val sessionFuncs = new SessionData(middlewareActor, context)
|
||||
|
||||
ServiceManager.serviceManager ! Lookup("accountIntermediary")
|
||||
ServiceManager.serviceManager ! Lookup("accountPersistence")
|
||||
ServiceManager.serviceManager ! Lookup("galaxy")
|
||||
ServiceManager.serviceManager ! Lookup("squad")
|
||||
ServiceManager.receptionist ! Receptionist.Find(ICS.InterstellarClusterServiceKey, context.self)
|
||||
private[this] val data = new SessionData(middlewareActor, context)
|
||||
private[this] var mode: PlayerMode = NormalMode
|
||||
private[this] var logic: ModeLogic = _
|
||||
|
||||
override def postStop(): Unit = {
|
||||
//normally, the player avatar persists a minute or so after disconnect; we are subject to the SessionReaper
|
||||
//TODO put any temporary values back into the avatar
|
||||
sessionFuncs.stop()
|
||||
clientKeepAlive.cancel()
|
||||
data.stop()
|
||||
}
|
||||
|
||||
def receive: Receive = startup
|
||||
|
||||
def startup: Receive = {
|
||||
case msg if !sessionFuncs.assignEventBus(msg) =>
|
||||
private def startup: Receive = {
|
||||
case msg if !data.assignEventBus(msg) =>
|
||||
buffer.addOne(msg)
|
||||
case _ if sessionFuncs.whenAllEventBusesLoaded() =>
|
||||
case _ if data.whenAllEventBusesLoaded() =>
|
||||
context.become(inTheGame)
|
||||
logic = mode.setup(data)
|
||||
buffer.foreach { self.tell(_, self) } //we forget the original sender, shouldn't be doing callbacks at this point
|
||||
buffer.clear()
|
||||
case _ => ()
|
||||
}
|
||||
|
||||
def inTheGame: Receive = {
|
||||
/* really common messages (very frequently, every life) */
|
||||
case packet: PlanetSideGamePacket =>
|
||||
handleGamePkt(packet)
|
||||
private def inTheGame: Receive = {
|
||||
/* used for the game's heartbeat */
|
||||
case SessionActor.StartHeartbeat =>
|
||||
startHeartbeat()
|
||||
|
||||
case AvatarServiceResponse(toChannel, guid, reply) =>
|
||||
sessionFuncs.avatarResponse.handle(toChannel, guid, reply)
|
||||
case SessionActor.PokeClient =>
|
||||
middlewareActor ! MiddlewareActor.Send(KeepAliveMessage())
|
||||
|
||||
case GalaxyServiceResponse(_, reply) =>
|
||||
sessionFuncs.galaxyResponseHanders.handle(reply)
|
||||
|
||||
case LocalServiceResponse(toChannel, guid, reply) =>
|
||||
sessionFuncs.localResponse.handle(toChannel, guid, reply)
|
||||
|
||||
case Mountable.MountMessages(tplayer, reply) =>
|
||||
sessionFuncs.mountResponse.handle(tplayer, reply)
|
||||
|
||||
case SquadServiceResponse(_, excluded, response) =>
|
||||
sessionFuncs.squad.handle(response, excluded)
|
||||
|
||||
case Terminal.TerminalMessage(tplayer, msg, order) =>
|
||||
sessionFuncs.terminals.handle(tplayer, msg, order)
|
||||
|
||||
case VehicleServiceResponse(toChannel, guid, reply) =>
|
||||
sessionFuncs.vehicleResponseOperations.handle(toChannel, guid, reply)
|
||||
|
||||
case SessionActor.PokeClient() =>
|
||||
sessionFuncs.sendResponse(KeepAliveMessage())
|
||||
|
||||
case SessionActor.SendResponse(packet) =>
|
||||
sessionFuncs.sendResponse(packet)
|
||||
|
||||
case SessionActor.CharSaved =>
|
||||
sessionFuncs.renewCharSavedTimer(
|
||||
Config.app.game.savedMsg.interruptedByAction.fixed,
|
||||
Config.app.game.savedMsg.interruptedByAction.variable
|
||||
)
|
||||
|
||||
case SessionActor.CharSavedMsg =>
|
||||
sessionFuncs.displayCharSavedMsgThenRenewTimer(
|
||||
Config.app.game.savedMsg.renewal.fixed,
|
||||
Config.app.game.savedMsg.renewal.variable
|
||||
)
|
||||
|
||||
/* common messages (maybe once every respawn) */
|
||||
case ICS.SpawnPointResponse(response) =>
|
||||
sessionFuncs.zoning.handleSpawnPointResponse(response)
|
||||
|
||||
case SessionActor.NewPlayerLoaded(tplayer) =>
|
||||
sessionFuncs.zoning.spawn.handleNewPlayerLoaded(tplayer)
|
||||
|
||||
case SessionActor.PlayerLoaded(tplayer) =>
|
||||
sessionFuncs.zoning.spawn.handlePlayerLoaded(tplayer)
|
||||
|
||||
case Zone.Population.PlayerHasLeft(zone, None) =>
|
||||
log.debug(s"PlayerHasLeft: ${sessionFuncs.player.Name} does not have a body on ${zone.id}")
|
||||
|
||||
case Zone.Population.PlayerHasLeft(zone, Some(tplayer)) =>
|
||||
if (tplayer.isAlive) {
|
||||
log.info(s"${tplayer.Name} has left zone ${zone.id}")
|
||||
case SessionActor.SetMode(newMode) =>
|
||||
if (mode != newMode) {
|
||||
logic.switchFrom(data.session)
|
||||
}
|
||||
mode = newMode
|
||||
logic = mode.setup(data)
|
||||
logic.switchTo(data.session)
|
||||
|
||||
case Zone.Population.PlayerCanNotSpawn(zone, tplayer) =>
|
||||
log.warning(s"${tplayer.Name} can not spawn in zone ${zone.id}; why?")
|
||||
|
||||
case Zone.Population.PlayerAlreadySpawned(zone, tplayer) =>
|
||||
log.warning(s"${tplayer.Name} is already spawned on zone ${zone.id}; is this a clerical error?")
|
||||
|
||||
case Zone.Vehicle.CanNotSpawn(zone, vehicle, reason) =>
|
||||
log.warning(
|
||||
s"${sessionFuncs.player.Name}'s ${vehicle.Definition.Name} can not spawn in ${zone.id} because $reason"
|
||||
)
|
||||
|
||||
case Zone.Vehicle.CanNotDespawn(zone, vehicle, reason) =>
|
||||
log.warning(
|
||||
s"${sessionFuncs.player.Name}'s ${vehicle.Definition.Name} can not deconstruct in ${zone.id} because $reason"
|
||||
)
|
||||
|
||||
case ICS.ZoneResponse(Some(zone)) =>
|
||||
sessionFuncs.zoning.handleZoneResponse(zone)
|
||||
|
||||
/* uncommon messages (once a session) */
|
||||
case ICS.ZonesResponse(zones) =>
|
||||
sessionFuncs.zoning.handleZonesResponse(zones)
|
||||
|
||||
case SessionActor.SetAvatar(avatar) =>
|
||||
sessionFuncs.handleSetAvatar(avatar)
|
||||
|
||||
case PlayerToken.LoginInfo(name, Zone.Nowhere, _) =>
|
||||
sessionFuncs.zoning.spawn.handleLoginInfoNowhere(name, sender())
|
||||
|
||||
case PlayerToken.LoginInfo(name, inZone, optionalSavedData) =>
|
||||
sessionFuncs.zoning.spawn.handleLoginInfoSomewhere(name, inZone, optionalSavedData, sender())
|
||||
|
||||
case PlayerToken.RestoreInfo(playerName, inZone, pos) =>
|
||||
sessionFuncs.zoning.spawn.handleLoginInfoRestore(playerName, inZone, pos, sender())
|
||||
|
||||
case PlayerToken.CanNotLogin(playerName, reason) =>
|
||||
sessionFuncs.zoning.spawn.handleLoginCanNot(playerName, reason)
|
||||
|
||||
case ReceiveAccountData(account) =>
|
||||
sessionFuncs.handleReceiveAccountData(account)
|
||||
|
||||
case AvatarActor.AvatarResponse(avatar) =>
|
||||
sessionFuncs.handleAvatarResponse(avatar)
|
||||
|
||||
case AvatarActor.AvatarLoginResponse(avatar) =>
|
||||
sessionFuncs.zoning.spawn.avatarLoginResponse(avatar)
|
||||
|
||||
case SessionActor.SetCurrentAvatar(tplayer, max_attempts, attempt) =>
|
||||
sessionFuncs.zoning.spawn.ReadyToSetCurrentAvatar(tplayer, max_attempts, attempt)
|
||||
|
||||
case SessionActor.SetConnectionState(state) =>
|
||||
sessionFuncs.connectionState = state
|
||||
|
||||
case SessionActor.AvatarLoadingSync(state) =>
|
||||
sessionFuncs.zoning.spawn.handleAvatarLoadingSync(state)
|
||||
|
||||
/* uncommon messages (utility, or once in a while) */
|
||||
case SessionActor.AvatarAwardMessageBundle(pkts, delay) =>
|
||||
sessionFuncs.zoning.spawn.performAvatarAwardMessageDelivery(pkts, delay)
|
||||
|
||||
case CommonMessages.Progress(rate, finishedAction, stepAction) =>
|
||||
sessionFuncs.setupProgressChange(rate, finishedAction, stepAction)
|
||||
|
||||
case SessionActor.ProgressEvent(delta, finishedAction, stepAction, tick) =>
|
||||
sessionFuncs.handleProgressChange(delta, finishedAction, stepAction, tick)
|
||||
|
||||
case CavernRotationService.CavernRotationServiceKey.Listing(listings) =>
|
||||
listings.head ! SendCavernRotationUpdates(context.self)
|
||||
|
||||
case LookupResult("propertyOverrideManager", endpoint) =>
|
||||
sessionFuncs.zoning.propertyOverrideManagerLoadOverrides(endpoint)
|
||||
|
||||
case SessionActor.UpdateIgnoredPlayers(msg) =>
|
||||
sessionFuncs.handleUpdateIgnoredPlayers(msg)
|
||||
|
||||
case SessionActor.UseCooldownRenewed(definition, _) =>
|
||||
sessionFuncs.handleUseCooldownRenew(definition)
|
||||
|
||||
case Deployment.CanDeploy(obj, state) =>
|
||||
sessionFuncs.vehicles.handleCanDeploy(obj, state)
|
||||
|
||||
case Deployment.CanUndeploy(obj, state) =>
|
||||
sessionFuncs.vehicles.handleCanUndeploy(obj, state)
|
||||
|
||||
case Deployment.CanNotChangeDeployment(obj, state, reason) =>
|
||||
sessionFuncs.vehicles.handleCanNotChangeDeployment(obj, state, reason)
|
||||
|
||||
/* rare messages */
|
||||
case ProximityUnit.StopAction(term, _) =>
|
||||
sessionFuncs.terminals.LocalStopUsingProximityUnit(term)
|
||||
|
||||
case SessionActor.Suicide() =>
|
||||
sessionFuncs.suicide(sessionFuncs.player)
|
||||
|
||||
case SessionActor.Recall() =>
|
||||
sessionFuncs.zoning.handleRecall()
|
||||
|
||||
case SessionActor.InstantAction() =>
|
||||
sessionFuncs.zoning.handleInstantAction()
|
||||
|
||||
case SessionActor.Quit() =>
|
||||
sessionFuncs.zoning.handleQuit()
|
||||
|
||||
case ICS.DroppodLaunchDenial(errorCode, _) =>
|
||||
sessionFuncs.zoning.handleDroppodLaunchDenial(errorCode)
|
||||
|
||||
case ICS.DroppodLaunchConfirmation(zone, position) =>
|
||||
sessionFuncs.zoning.LoadZoneLaunchDroppod(zone, position)
|
||||
|
||||
case SessionActor.PlayerFailedToLoad(tplayer) =>
|
||||
sessionFuncs.failWithError(s"${tplayer.Name} failed to load anywhere")
|
||||
|
||||
/* csr only */
|
||||
case SessionActor.SetSpeed(speed) =>
|
||||
sessionFuncs.handleSetSpeed(speed)
|
||||
|
||||
case SessionActor.SetFlying(isFlying) =>
|
||||
sessionFuncs.handleSetFlying(isFlying)
|
||||
|
||||
case SessionActor.SetSpectator(isSpectator) =>
|
||||
sessionFuncs.handleSetSpectator(isSpectator)
|
||||
|
||||
case SessionActor.Kick(player, time) =>
|
||||
sessionFuncs.handleKick(player, time)
|
||||
|
||||
case SessionActor.SetZone(zoneId, position) =>
|
||||
sessionFuncs.zoning.handleSetZone(zoneId, position)
|
||||
|
||||
case SessionActor.SetPosition(position) =>
|
||||
sessionFuncs.zoning.spawn.handleSetPosition(position)
|
||||
|
||||
case SessionActor.SetSilenced(silenced) =>
|
||||
sessionFuncs.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(sessionFuncs.continent.GUID, obj))
|
||||
|
||||
case Zone.Deployable.IsDismissed(obj) => //only if target deployable was never fully introduced
|
||||
TaskWorkflow.execute(GUIDTask.unregisterObject(sessionFuncs.continent.GUID, obj))
|
||||
|
||||
case msg: Containable.ItemPutInSlot =>
|
||||
log.debug(s"ItemPutInSlot: $msg")
|
||||
|
||||
case msg: Containable.CanNotPutItemInSlot =>
|
||||
log.debug(s"CanNotPutItemInSlot: $msg")
|
||||
|
||||
case default =>
|
||||
log.warning(s"Invalid packet class received: $default from ${sender()}")
|
||||
case packet =>
|
||||
logic.parse(sender())(packet)
|
||||
}
|
||||
|
||||
private def handleGamePkt: PlanetSideGamePacket => Unit = {
|
||||
case packet: ConnectToWorldRequestMessage =>
|
||||
sessionFuncs.handleConnectToWorldRequest(packet)
|
||||
|
||||
case packet: MountVehicleCargoMsg =>
|
||||
sessionFuncs.vehicles.handleMountVehicleCargo(packet)
|
||||
|
||||
case packet: DismountVehicleCargoMsg =>
|
||||
sessionFuncs.vehicles.handleDismountVehicleCargo(packet)
|
||||
|
||||
case packet: CharacterCreateRequestMessage =>
|
||||
sessionFuncs.handleCharacterCreateRequest(packet)
|
||||
|
||||
case packet: CharacterRequestMessage =>
|
||||
sessionFuncs.handleCharacterRequest(packet)
|
||||
|
||||
case _: KeepAliveMessage =>
|
||||
sessionFuncs.keepAliveFunc()
|
||||
|
||||
case packet: BeginZoningMessage =>
|
||||
sessionFuncs.zoning.handleBeginZoning(packet)
|
||||
|
||||
case packet: PlayerStateMessageUpstream =>
|
||||
sessionFuncs.handlePlayerStateUpstream(packet)
|
||||
|
||||
case packet: ChildObjectStateMessage =>
|
||||
sessionFuncs.vehicles.handleChildObjectState(packet)
|
||||
|
||||
case packet: VehicleStateMessage =>
|
||||
sessionFuncs.vehicles.handleVehicleState(packet)
|
||||
|
||||
case packet: VehicleSubStateMessage =>
|
||||
sessionFuncs.vehicles.handleVehicleSubState(packet)
|
||||
|
||||
case packet: FrameVehicleStateMessage =>
|
||||
sessionFuncs.vehicles.handleFrameVehicleState(packet)
|
||||
|
||||
case packet: ProjectileStateMessage =>
|
||||
sessionFuncs.shooting.handleProjectileState(packet)
|
||||
|
||||
case packet: LongRangeProjectileInfoMessage =>
|
||||
sessionFuncs.shooting.handleLongRangeProjectileState(packet)
|
||||
|
||||
case packet: ReleaseAvatarRequestMessage =>
|
||||
sessionFuncs.zoning.spawn.handleReleaseAvatarRequest(packet)
|
||||
|
||||
case packet: SpawnRequestMessage =>
|
||||
sessionFuncs.zoning.spawn.handleSpawnRequest(packet)
|
||||
|
||||
case packet: ChatMsg =>
|
||||
sessionFuncs.handleChat(packet)
|
||||
|
||||
case packet: SetChatFilterMessage =>
|
||||
sessionFuncs.handleChatFilter(packet)
|
||||
|
||||
case packet: VoiceHostRequest =>
|
||||
sessionFuncs.handleVoiceHostRequest(packet)
|
||||
|
||||
case packet: VoiceHostInfo =>
|
||||
sessionFuncs.handleVoiceHostInfo(packet)
|
||||
|
||||
case packet: ChangeAmmoMessage =>
|
||||
sessionFuncs.shooting.handleChangeAmmo(packet)
|
||||
|
||||
case packet: ChangeFireModeMessage =>
|
||||
sessionFuncs.shooting.handleChangeFireMode(packet)
|
||||
|
||||
case packet: ChangeFireStateMessage_Start =>
|
||||
sessionFuncs.shooting.handleChangeFireStateStart(packet)
|
||||
|
||||
case packet: ChangeFireStateMessage_Stop =>
|
||||
sessionFuncs.shooting.handleChangeFireStateStop(packet)
|
||||
|
||||
case packet: EmoteMsg =>
|
||||
sessionFuncs.handleEmote(packet)
|
||||
|
||||
case packet: DropItemMessage =>
|
||||
sessionFuncs.handleDropItem(packet)
|
||||
|
||||
case packet: PickupItemMessage =>
|
||||
sessionFuncs.handlePickupItem(packet)
|
||||
|
||||
case packet: ReloadMessage =>
|
||||
sessionFuncs.shooting.handleReload(packet)
|
||||
|
||||
case packet: ObjectHeldMessage =>
|
||||
sessionFuncs.handleObjectHeld(packet)
|
||||
|
||||
case packet: AvatarJumpMessage =>
|
||||
sessionFuncs.handleAvatarJump(packet)
|
||||
|
||||
case packet: ZipLineMessage =>
|
||||
sessionFuncs.handleZipLine(packet)
|
||||
|
||||
case packet: RequestDestroyMessage =>
|
||||
sessionFuncs.handleRequestDestroy(packet)
|
||||
|
||||
case packet: MoveItemMessage =>
|
||||
sessionFuncs.handleMoveItem(packet)
|
||||
|
||||
case packet: LootItemMessage =>
|
||||
sessionFuncs.handleLootItem(packet)
|
||||
|
||||
case packet: AvatarImplantMessage =>
|
||||
sessionFuncs.handleAvatarImplant(packet)
|
||||
|
||||
case packet: UseItemMessage =>
|
||||
sessionFuncs.handleUseItem(packet)
|
||||
|
||||
case packet: UnuseItemMessage =>
|
||||
sessionFuncs.handleUnuseItem(packet)
|
||||
|
||||
case packet: ProximityTerminalUseMessage =>
|
||||
sessionFuncs.terminals.handleProximityTerminalUse(packet)
|
||||
|
||||
case packet: DeployObjectMessage =>
|
||||
sessionFuncs.handleDeployObject(packet)
|
||||
|
||||
case packet: GenericObjectActionMessage =>
|
||||
sessionFuncs.handleGenericObjectAction(packet)
|
||||
|
||||
case packet: GenericObjectActionAtPositionMessage =>
|
||||
sessionFuncs.handleGenericObjectActionAtPosition(packet)
|
||||
|
||||
case packet: GenericObjectStateMsg =>
|
||||
sessionFuncs.handleGenericObjectState(packet)
|
||||
|
||||
case packet: GenericActionMessage =>
|
||||
sessionFuncs.handleGenericAction(packet)
|
||||
|
||||
case packet: ItemTransactionMessage =>
|
||||
sessionFuncs.terminals.handleItemTransaction(packet)
|
||||
|
||||
case packet: FavoritesRequest =>
|
||||
sessionFuncs.handleFavoritesRequest(packet)
|
||||
|
||||
case packet: WeaponDelayFireMessage =>
|
||||
sessionFuncs.shooting.handleWeaponDelayFire(packet)
|
||||
|
||||
case packet: WeaponDryFireMessage =>
|
||||
sessionFuncs.shooting.handleWeaponDryFire(packet)
|
||||
|
||||
case packet: WeaponFireMessage =>
|
||||
sessionFuncs.shooting.handleWeaponFire(packet)
|
||||
|
||||
case packet: WeaponLazeTargetPositionMessage =>
|
||||
sessionFuncs.shooting.handleWeaponLazeTargetPosition(packet)
|
||||
|
||||
case packet: HitMessage =>
|
||||
sessionFuncs.shooting.handleDirectHit(packet)
|
||||
|
||||
case packet: SplashHitMessage =>
|
||||
sessionFuncs.shooting.handleSplashHit(packet)
|
||||
|
||||
case packet: LashMessage =>
|
||||
sessionFuncs.shooting.handleLashHit(packet)
|
||||
|
||||
case packet: AIDamage =>
|
||||
sessionFuncs.shooting.handleAIDamage(packet)
|
||||
|
||||
case packet: AvatarFirstTimeEventMessage =>
|
||||
sessionFuncs.handleAvatarFirstTimeEvent(packet)
|
||||
|
||||
case packet: WarpgateRequest =>
|
||||
sessionFuncs.zoning.handleWarpgateRequest(packet)
|
||||
|
||||
case packet: MountVehicleMsg =>
|
||||
sessionFuncs.vehicles.handleMountVehicle(packet)
|
||||
|
||||
case packet: DismountVehicleMsg =>
|
||||
sessionFuncs.vehicles.handleDismountVehicle(packet)
|
||||
|
||||
case packet: DeployRequestMessage =>
|
||||
sessionFuncs.vehicles.handleDeployRequest(packet)
|
||||
|
||||
case packet: AvatarGrenadeStateMessage =>
|
||||
sessionFuncs.shooting.handleAvatarGrenadeState(packet)
|
||||
|
||||
case packet: SquadDefinitionActionMessage =>
|
||||
sessionFuncs.squad.handleSquadDefinitionAction(packet)
|
||||
|
||||
case packet: SquadMembershipRequest =>
|
||||
sessionFuncs.squad.handleSquadMemberRequest(packet)
|
||||
|
||||
case packet: SquadWaypointRequest =>
|
||||
sessionFuncs.squad.handleSquadWaypointRequest(packet)
|
||||
|
||||
case packet: GenericCollisionMsg =>
|
||||
sessionFuncs.handleGenericCollision(packet)
|
||||
|
||||
case packet: BugReportMessage =>
|
||||
sessionFuncs.handleBugReport(packet)
|
||||
|
||||
case packet: BindPlayerMessage =>
|
||||
sessionFuncs.handleBindPlayer(packet)
|
||||
|
||||
case packet: PlanetsideAttributeMessage =>
|
||||
sessionFuncs.handlePlanetsideAttribute(packet)
|
||||
|
||||
case packet: FacilityBenefitShieldChargeRequestMessage =>
|
||||
sessionFuncs.handleFacilityBenefitShieldChargeRequest(packet)
|
||||
|
||||
case packet: BattleplanMessage =>
|
||||
sessionFuncs.handleBattleplan(packet)
|
||||
|
||||
case packet: CreateShortcutMessage =>
|
||||
sessionFuncs.handleCreateShortcut(packet)
|
||||
|
||||
case packet: ChangeShortcutBankMessage =>
|
||||
sessionFuncs.handleChangeShortcutBank(packet)
|
||||
|
||||
case packet: FriendsRequest =>
|
||||
sessionFuncs.handleFriendRequest(packet)
|
||||
|
||||
case packet: DroppodLaunchRequestMessage =>
|
||||
sessionFuncs.zoning.handleDroppodLaunchRequest(packet)
|
||||
|
||||
case packet: InvalidTerrainMessage =>
|
||||
sessionFuncs.handleInvalidTerrain(packet)
|
||||
|
||||
case packet: ActionCancelMessage =>
|
||||
sessionFuncs.handleActionCancel(packet)
|
||||
|
||||
case packet: TradeMessage =>
|
||||
sessionFuncs.handleTrade(packet)
|
||||
|
||||
case packet: DisplayedAwardMessage =>
|
||||
sessionFuncs.handleDisplayedAward(packet)
|
||||
|
||||
case packet: ObjectDetectedMessage =>
|
||||
sessionFuncs.handleObjectDetected(packet)
|
||||
|
||||
case packet: TargetingImplantRequest =>
|
||||
sessionFuncs.handleTargetingImplantRequest(packet)
|
||||
|
||||
case packet: HitHint =>
|
||||
sessionFuncs.handleHitHint(packet)
|
||||
|
||||
case _: OutfitRequest => ()
|
||||
|
||||
case pkt =>
|
||||
log.warning(s"Unhandled GamePacket $pkt")
|
||||
private def startHeartbeat(): Unit = {
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
clientKeepAlive.cancel()
|
||||
clientKeepAlive = context.system.scheduler.scheduleWithFixedDelay(
|
||||
initialDelay = 0.seconds,
|
||||
delay = 500.milliseconds,
|
||||
context.self,
|
||||
SessionActor.PokeClient
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,573 @@
|
|||
// Copyright (c) 2024 PSForever
|
||||
package net.psforever.actors.session.normal
|
||||
|
||||
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,239 @@
|
|||
// Copyright (c) 2024 PSForever
|
||||
package net.psforever.actors.session.normal
|
||||
|
||||
import akka.actor.ActorContext
|
||||
import net.psforever.actors.session.support.{ChatFunctions, ChatOperations, SessionData}
|
||||
import net.psforever.objects.Session
|
||||
import net.psforever.objects.avatar.ModePermissions
|
||||
import net.psforever.packet.game.{ChatMsg, SetChatFilterMessage}
|
||||
import net.psforever.services.chat.DefaultChannel
|
||||
import net.psforever.types.ChatMessageType
|
||||
import net.psforever.util.Config
|
||||
|
||||
object ChatLogic {
|
||||
def apply(ops: ChatOperations): ChatLogic = {
|
||||
new ChatLogic(ops, ops.context)
|
||||
}
|
||||
}
|
||||
|
||||
class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) extends ChatFunctions {
|
||||
def sessionLogic: SessionData = ops.sessionLogic
|
||||
|
||||
def handleChatMsg(message: ChatMsg): Unit = {
|
||||
import net.psforever.types.ChatMessageType._
|
||||
val isAlive = if (player != null) player.isAlive else false
|
||||
val perms = if (avatar != null) avatar.permissions else ModePermissions()
|
||||
val gmCommandAllowed = (session.account.gm && perms.canGM) ||
|
||||
Config.app.development.unprivilegedGmCommands.contains(message.messageType)
|
||||
(message.messageType, message.recipient.trim, message.contents.trim) match {
|
||||
/** Messages starting with ! are custom chat commands */
|
||||
case (_, _, contents) if contents.startsWith("!") &&
|
||||
customCommandMessages(message, session) => ()
|
||||
|
||||
case (CMT_FLY, recipient, contents) if gmCommandAllowed =>
|
||||
ops.commandFly(contents, recipient)
|
||||
|
||||
case (CMT_ANONYMOUS, _, _) =>
|
||||
// ?
|
||||
|
||||
case (CMT_TOGGLE_GM, _, _) =>
|
||||
// ?
|
||||
|
||||
case (CMT_CULLWATERMARK, _, contents) =>
|
||||
ops.commandWatermark(contents)
|
||||
|
||||
case (CMT_SPEED, _, contents) if gmCommandAllowed =>
|
||||
ops.commandSpeed(message, contents)
|
||||
|
||||
case (CMT_TOGGLESPECTATORMODE, _, contents) if isAlive && (gmCommandAllowed || perms.canSpectate) =>
|
||||
ops.commandToggleSpectatorMode(session, contents)
|
||||
|
||||
case (CMT_RECALL, _, _) =>
|
||||
ops.commandRecall(session)
|
||||
|
||||
case (CMT_INSTANTACTION, _, _) =>
|
||||
ops.commandInstantAction(session)
|
||||
|
||||
case (CMT_QUIT, _, _) =>
|
||||
ops.commandQuit(session)
|
||||
|
||||
case (CMT_SUICIDE, _, _) =>
|
||||
ops.commandSuicide(session)
|
||||
|
||||
case (CMT_DESTROY, _, contents) if contents.matches("\\d+") =>
|
||||
ops.commandDestroy(session, message, contents)
|
||||
|
||||
case (CMT_SETBASERESOURCES, _, contents) if gmCommandAllowed =>
|
||||
ops.commandSetBaseResources(session, contents)
|
||||
|
||||
case (CMT_ZONELOCK, _, contents) if gmCommandAllowed =>
|
||||
ops.commandZoneLock(contents)
|
||||
|
||||
case (U_CMT_ZONEROTATE, _, _) if gmCommandAllowed =>
|
||||
ops.commandZoneRotate()
|
||||
|
||||
case (CMT_CAPTUREBASE, _, contents) if gmCommandAllowed =>
|
||||
ops.commandCaptureBase(session, message, contents)
|
||||
|
||||
case (CMT_GMBROADCAST | CMT_GMBROADCAST_NC | CMT_GMBROADCAST_VS | CMT_GMBROADCAST_TR, _, _)
|
||||
if gmCommandAllowed =>
|
||||
ops.commandSendToRecipient(session, message, DefaultChannel)
|
||||
|
||||
case (CMT_GMTELL, _, _) if gmCommandAllowed =>
|
||||
ops.commandSend(session, message, DefaultChannel)
|
||||
|
||||
case (CMT_GMBROADCASTPOPUP, _, _) if gmCommandAllowed =>
|
||||
ops.commandSendToRecipient(session, message, DefaultChannel)
|
||||
|
||||
case (CMT_OPEN, _, _) if !player.silenced =>
|
||||
ops.commandSendToRecipient(session, message, DefaultChannel)
|
||||
|
||||
case (CMT_VOICE, _, contents) =>
|
||||
ops.commandVoice(session, message, contents, DefaultChannel)
|
||||
|
||||
case (CMT_TELL, _, _) if !player.silenced =>
|
||||
ops.commandTellOrIgnore(session, message, DefaultChannel)
|
||||
|
||||
case (CMT_BROADCAST, _, _) if !player.silenced =>
|
||||
ops.commandSendToRecipient(session, message, DefaultChannel)
|
||||
|
||||
case (CMT_PLATOON, _, _) if !player.silenced =>
|
||||
ops.commandSendToRecipient(session, message, DefaultChannel)
|
||||
|
||||
case (CMT_COMMAND, _, _) if gmCommandAllowed =>
|
||||
ops.commandSendToRecipient(session, message, DefaultChannel)
|
||||
|
||||
case (CMT_NOTE, _, _) =>
|
||||
ops.commandSend(session, message, DefaultChannel)
|
||||
|
||||
case (CMT_SILENCE, _, _) if gmCommandAllowed =>
|
||||
ops.commandSend(session, message, DefaultChannel)
|
||||
|
||||
case (CMT_SQUAD, _, _) =>
|
||||
ops.commandSquad(session, message, DefaultChannel) //todo SquadChannel, but what is the guid
|
||||
|
||||
case (CMT_WHO | CMT_WHO_CSR | CMT_WHO_CR | CMT_WHO_PLATOONLEADERS | CMT_WHO_SQUADLEADERS | CMT_WHO_TEAMS, _, _) =>
|
||||
ops.commandWho(session)
|
||||
|
||||
case (CMT_ZONE, _, contents) if gmCommandAllowed =>
|
||||
ops.commandZone(message, contents)
|
||||
|
||||
case (CMT_WARP, _, contents) if gmCommandAllowed =>
|
||||
ops.commandWarp(session, message, contents)
|
||||
|
||||
case (CMT_SETBATTLERANK, _, contents) if gmCommandAllowed =>
|
||||
ops.commandSetBattleRank(session, message, contents)
|
||||
|
||||
case (CMT_SETCOMMANDRANK, _, contents) if gmCommandAllowed =>
|
||||
ops.commandSetCommandRank(session, message, contents)
|
||||
|
||||
case (CMT_ADDBATTLEEXPERIENCE, _, contents) if gmCommandAllowed =>
|
||||
ops.commandAddBattleExperience(message, contents)
|
||||
|
||||
case (CMT_ADDCOMMANDEXPERIENCE, _, contents) if gmCommandAllowed =>
|
||||
ops.commandAddCommandExperience(message, contents)
|
||||
|
||||
case (CMT_TOGGLE_HAT, _, contents) =>
|
||||
ops.commandToggleHat(session, message, contents)
|
||||
|
||||
case (CMT_HIDE_HELMET | CMT_TOGGLE_SHADES | CMT_TOGGLE_EARPIECE, _, contents) =>
|
||||
ops.commandToggleCosmetics(session, message, contents)
|
||||
|
||||
case (CMT_ADDCERTIFICATION, _, contents) if gmCommandAllowed =>
|
||||
ops.commandAddCertification(session, message, contents)
|
||||
|
||||
case (CMT_KICK, _, contents) if gmCommandAllowed =>
|
||||
ops.commandKick(session, message, contents)
|
||||
|
||||
case _ =>
|
||||
log.warn(s"Unhandled chat message $message")
|
||||
}
|
||||
}
|
||||
|
||||
def handleChatFilter(pkt: SetChatFilterMessage): Unit = {
|
||||
val SetChatFilterMessage(_, _, _) = pkt
|
||||
}
|
||||
|
||||
def handleIncomingMessage(message: ChatMsg, fromSession: Session): Unit = {
|
||||
import ChatMessageType._
|
||||
message.messageType match {
|
||||
case CMT_BROADCAST | CMT_SQUAD | CMT_PLATOON | CMT_COMMAND | CMT_NOTE =>
|
||||
ops.commandIncomingSendAllIfOnline(session, message)
|
||||
|
||||
case CMT_OPEN =>
|
||||
ops.commandIncomingSendToLocalIfOnline(session, fromSession, message)
|
||||
|
||||
case CMT_TELL | U_CMT_TELLFROM |
|
||||
CMT_GMOPEN | CMT_GMBROADCAST | CMT_GMBROADCAST_NC | CMT_GMBROADCAST_TR | CMT_GMBROADCAST_VS |
|
||||
CMT_GMBROADCASTPOPUP | CMT_GMTELL | U_CMT_GMTELLFROM | UNK_45 | UNK_71 | UNK_227 | UNK_229 =>
|
||||
ops.commandIncomingSend(message)
|
||||
|
||||
case CMT_VOICE =>
|
||||
ops.commandIncomingVoice(session, fromSession, message)
|
||||
|
||||
case CMT_SILENCE =>
|
||||
ops.commandIncomingSilence(session, message)
|
||||
|
||||
case _ =>
|
||||
log.warn(s"Unexpected messageType $message")
|
||||
}
|
||||
}
|
||||
|
||||
private def customCommandMessages(
|
||||
message: ChatMsg,
|
||||
session: Session
|
||||
): Boolean = {
|
||||
val contents = message.contents
|
||||
if (contents.startsWith("!")) {
|
||||
val (command, params) = ops.cliTokenization(contents.drop(1)) match {
|
||||
case a :: b => (a, b)
|
||||
case _ => ("", Seq(""))
|
||||
}
|
||||
val perms = if (avatar != null) avatar.permissions else ModePermissions()
|
||||
val gmBangCommandAllowed = (session.account.gm && perms.canGM) ||
|
||||
Config.app.development.unprivilegedGmBangCommands.contains(command)
|
||||
//try gm commands
|
||||
val tryGmCommandResult = if (gmBangCommandAllowed) {
|
||||
command match {
|
||||
case "whitetext" => Some(ops.customCommandWhitetext(session, params))
|
||||
case "list" => Some(ops.customCommandList(session, params, message))
|
||||
case "ntu" => Some(ops.customCommandNtu(session, params))
|
||||
case "zonerotate" => Some(ops.customCommandZonerotate(params))
|
||||
case "nearby" => Some(ops.customCommandNearby(session))
|
||||
case _ => None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
//try commands for all players if not caught as a gm command
|
||||
val result = tryGmCommandResult match {
|
||||
case None =>
|
||||
command match {
|
||||
case "loc" => ops.customCommandLoc(session, message)
|
||||
case "suicide" => ops.customCommandSuicide(session)
|
||||
case "grenade" => ops.customCommandGrenade(session, log)
|
||||
case "macro" => ops.customCommandMacro(session, params)
|
||||
case "progress" => ops.customCommandProgress(session, params)
|
||||
case _ => false
|
||||
}
|
||||
case Some(out) =>
|
||||
out
|
||||
}
|
||||
if (!result) {
|
||||
// command was not handled
|
||||
sendResponse(
|
||||
ChatMsg(
|
||||
ChatMessageType.CMT_GMOPEN, // CMT_GMTELL
|
||||
message.wideContents,
|
||||
"Server",
|
||||
s"Unknown command !$command",
|
||||
message.note
|
||||
)
|
||||
)
|
||||
}
|
||||
result
|
||||
} else {
|
||||
false // not a handled command
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
// Copyright (c) 2024 PSForever
|
||||
package net.psforever.actors.session.normal
|
||||
|
||||
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 _ => ()
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,248 @@
|
|||
// Copyright (c) 2024 PSForever
|
||||
package net.psforever.actors.session.normal
|
||||
|
||||
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,520 @@
|
|||
// Copyright (c) 2024 PSForever
|
||||
package net.psforever.actors.session.normal
|
||||
|
||||
import akka.actor.{ActorContext, typed}
|
||||
import net.psforever.actors.session.AvatarActor
|
||||
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.definition.{BasicDefinition, ObjectDefinition}
|
||||
import net.psforever.objects.serverobject.affinity.FactionAffinity
|
||||
import net.psforever.objects.serverobject.environment.interaction.ResetAllEnvironmentInteractions
|
||||
import net.psforever.objects.serverobject.hackable.GenericHackables
|
||||
import net.psforever.objects.serverobject.mount.Mountable
|
||||
import net.psforever.objects.serverobject.terminals.implant.ImplantTerminalMech
|
||||
import net.psforever.objects.serverobject.turret.{FacilityTurret, WeaponTurret}
|
||||
import net.psforever.objects.vehicles.{AccessPermissionGroup, CargoBehavior}
|
||||
import net.psforever.objects.vital.InGameHistory
|
||||
import net.psforever.packet.game.{ChatMsg, DelayedPathMountMsg, DismountVehicleCargoMsg, DismountVehicleMsg, GenericObjectActionMessage, MountVehicleCargoMsg, MountVehicleMsg, ObjectAttachMessage, ObjectDetachMessage, PlanetsideAttributeMessage, 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, ChatMessageType, PlanetSideGUID, Vector3}
|
||||
|
||||
import scala.concurrent.duration._
|
||||
|
||||
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
|
||||
|
||||
private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor
|
||||
|
||||
/* packets */
|
||||
|
||||
def handleMountVehicle(pkt: MountVehicleMsg): Unit = {
|
||||
val MountVehicleMsg(_, mountable_guid, entry_point) = pkt
|
||||
sessionLogic.validObject(mountable_guid, decorator = "MountVehicle").collect {
|
||||
case obj: Mountable =>
|
||||
obj.Actor ! Mountable.TryMount(player, entry_point)
|
||||
case _ =>
|
||||
log.error(s"MountVehicleMsg: object ${mountable_guid.guid} not a mountable thing, ${player.Name}")
|
||||
}
|
||||
}
|
||||
|
||||
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 = {
|
||||
val MountVehicleCargoMsg(_, cargo_guid, carrier_guid, _) = pkt
|
||||
(continent.GUID(cargo_guid), continent.GUID(carrier_guid)) match {
|
||||
case (Some(cargo: Vehicle), Some(carrier: Vehicle)) =>
|
||||
carrier.CargoHolds.find({ case (_, hold) => !hold.isOccupied }) match {
|
||||
case Some((mountPoint, _)) =>
|
||||
cargo.Actor ! CargoBehavior.StartCargoMounting(carrier_guid, mountPoint)
|
||||
case _ =>
|
||||
log.warn(
|
||||
s"MountVehicleCargoMsg: ${player.Name} trying to load cargo into a ${carrier.Definition.Name} which oes not have a cargo hold"
|
||||
)
|
||||
}
|
||||
case (None, _) | (Some(_), None) =>
|
||||
log.warn(
|
||||
s"MountVehicleCargoMsg: ${player.Name} lost a vehicle while working with cargo - either $carrier_guid or $cargo_guid"
|
||||
)
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
|
||||
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.CanMount(obj: ImplantTerminalMech, seatNumber, _) =>
|
||||
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
|
||||
log.info(s"${player.Name} mounts an implant terminal")
|
||||
sessionLogic.terminals.CancelAllProximityUnits()
|
||||
MountingAction(tplayer, obj, seatNumber)
|
||||
sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistence
|
||||
|
||||
case Mountable.CanMount(obj: Vehicle, seatNumber, _)
|
||||
if obj.Definition == GlobalDefinitions.orbital_shuttle =>
|
||||
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
|
||||
log.info(s"${player.Name} mounts the orbital shuttle")
|
||||
sessionLogic.terminals.CancelAllProximityUnits()
|
||||
MountingAction(tplayer, obj, seatNumber)
|
||||
sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistence
|
||||
|
||||
case Mountable.CanMount(obj: Vehicle, seatNumber, _)
|
||||
if obj.Definition == GlobalDefinitions.ant =>
|
||||
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
|
||||
log.info(s"${player.Name} mounts the driver seat of the ${obj.Definition.Name}")
|
||||
val obj_guid: PlanetSideGUID = obj.GUID
|
||||
sessionLogic.terminals.CancelAllProximityUnits()
|
||||
sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health))
|
||||
sendResponse(PlanetsideAttributeMessage(obj_guid, obj.Definition.shieldUiAttribute, obj.Shields))
|
||||
sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=45, obj.NtuCapacitorScaled))
|
||||
sendResponse(GenericObjectActionMessage(obj_guid, code=11))
|
||||
sessionLogic.general.accessContainer(obj)
|
||||
tplayer.Actor ! ResetAllEnvironmentInteractions
|
||||
MountingAction(tplayer, obj, seatNumber)
|
||||
|
||||
case Mountable.CanMount(obj: Vehicle, seatNumber, _)
|
||||
if obj.Definition == GlobalDefinitions.quadstealth =>
|
||||
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
|
||||
log.info(s"${player.Name} mounts the driver seat of the ${obj.Definition.Name}")
|
||||
val obj_guid: PlanetSideGUID = obj.GUID
|
||||
sessionLogic.terminals.CancelAllProximityUnits()
|
||||
sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health))
|
||||
sendResponse(PlanetsideAttributeMessage(obj_guid, obj.Definition.shieldUiAttribute, obj.Shields))
|
||||
//exclusive to the wraith, cloak state matches the cloak state of the driver
|
||||
//phantasm doesn't uncloak if the driver is uncloaked and no other vehicle cloaks
|
||||
obj.Cloaked = tplayer.Cloaked
|
||||
sendResponse(GenericObjectActionMessage(obj_guid, code=11))
|
||||
sessionLogic.general.accessContainer(obj)
|
||||
tplayer.Actor ! ResetAllEnvironmentInteractions
|
||||
MountingAction(tplayer, obj, seatNumber)
|
||||
|
||||
case Mountable.CanMount(obj: Vehicle, seatNumber, _)
|
||||
if seatNumber == 0 && obj.Definition.MaxCapacitor > 0 =>
|
||||
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
|
||||
log.info(s"${player.Name} mounts the driver seat of the ${obj.Definition.Name}")
|
||||
val obj_guid: PlanetSideGUID = obj.GUID
|
||||
sessionLogic.terminals.CancelAllProximityUnits()
|
||||
sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health))
|
||||
sendResponse(PlanetsideAttributeMessage(obj_guid, obj.Definition.shieldUiAttribute, obj.Shields))
|
||||
sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=113, obj.Capacitor))
|
||||
sendResponse(GenericObjectActionMessage(obj_guid, code=11))
|
||||
sessionLogic.general.accessContainer(obj)
|
||||
ops.updateWeaponAtSeatPosition(obj, seatNumber)
|
||||
tplayer.Actor ! ResetAllEnvironmentInteractions
|
||||
MountingAction(tplayer, obj, seatNumber)
|
||||
|
||||
case Mountable.CanMount(obj: Vehicle, seatNumber, _)
|
||||
if seatNumber == 0 =>
|
||||
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
|
||||
log.info(s"${player.Name} mounts the driver seat of the ${obj.Definition.Name}")
|
||||
val obj_guid: PlanetSideGUID = obj.GUID
|
||||
sessionLogic.terminals.CancelAllProximityUnits()
|
||||
sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health))
|
||||
sendResponse(PlanetsideAttributeMessage(obj_guid, obj.Definition.shieldUiAttribute, obj.Shields))
|
||||
sendResponse(GenericObjectActionMessage(obj_guid, code=11))
|
||||
sessionLogic.general.accessContainer(obj)
|
||||
ops.updateWeaponAtSeatPosition(obj, seatNumber)
|
||||
tplayer.Actor ! ResetAllEnvironmentInteractions
|
||||
MountingAction(tplayer, obj, seatNumber)
|
||||
|
||||
case Mountable.CanMount(obj: Vehicle, seatNumber, _)
|
||||
if obj.Definition.MaxCapacitor > 0 =>
|
||||
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
|
||||
log.info(s"${player.Name} mounts ${
|
||||
obj.SeatPermissionGroup(seatNumber) match {
|
||||
case Some(seatType) => s"a $seatType seat (#$seatNumber)"
|
||||
case None => "a seat"
|
||||
}
|
||||
} of the ${obj.Definition.Name}")
|
||||
val obj_guid: PlanetSideGUID = obj.GUID
|
||||
sessionLogic.terminals.CancelAllProximityUnits()
|
||||
sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health))
|
||||
sendResponse(PlanetsideAttributeMessage(obj_guid, obj.Definition.shieldUiAttribute, obj.Shields))
|
||||
sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=113, obj.Capacitor))
|
||||
sessionLogic.general.accessContainer(obj)
|
||||
ops.updateWeaponAtSeatPosition(obj, seatNumber)
|
||||
sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistence
|
||||
tplayer.Actor ! ResetAllEnvironmentInteractions
|
||||
MountingAction(tplayer, obj, seatNumber)
|
||||
|
||||
case Mountable.CanMount(obj: Vehicle, seatNumber, _) =>
|
||||
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
|
||||
log.info(s"${player.Name} mounts the ${
|
||||
obj.SeatPermissionGroup(seatNumber) match {
|
||||
case Some(seatType) => s"a $seatType seat (#$seatNumber)"
|
||||
case None => "a seat"
|
||||
}
|
||||
} of the ${obj.Definition.Name}")
|
||||
val obj_guid: PlanetSideGUID = obj.GUID
|
||||
sessionLogic.terminals.CancelAllProximityUnits()
|
||||
sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health))
|
||||
sendResponse(PlanetsideAttributeMessage(obj_guid, obj.Definition.shieldUiAttribute, obj.Shields))
|
||||
sessionLogic.general.accessContainer(obj)
|
||||
ops.updateWeaponAtSeatPosition(obj, seatNumber)
|
||||
sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistence
|
||||
tplayer.Actor ! ResetAllEnvironmentInteractions
|
||||
MountingAction(tplayer, obj, seatNumber)
|
||||
|
||||
case Mountable.CanMount(obj: FacilityTurret, seatNumber, _)
|
||||
if obj.Definition == GlobalDefinitions.vanu_sentry_turret =>
|
||||
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
|
||||
log.info(s"${player.Name} mounts the ${obj.Definition.Name}")
|
||||
obj.Zone.LocalEvents ! LocalServiceMessage(obj.Zone.id, LocalAction.SetEmpire(obj.GUID, player.Faction))
|
||||
sendResponse(PlanetsideAttributeMessage(obj.GUID, attribute_type=0, obj.Health))
|
||||
ops.updateWeaponAtSeatPosition(obj, seatNumber)
|
||||
MountingAction(tplayer, obj, seatNumber)
|
||||
|
||||
case Mountable.CanMount(obj: FacilityTurret, seatNumber, _)
|
||||
if !obj.isUpgrading || System.currentTimeMillis() - GenericHackables.getTurretUpgradeTime >= 1500L =>
|
||||
obj.setMiddleOfUpgrade(false)
|
||||
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
|
||||
log.info(s"${player.Name} mounts the ${obj.Definition.Name}")
|
||||
sendResponse(PlanetsideAttributeMessage(obj.GUID, attribute_type=0, obj.Health))
|
||||
ops.updateWeaponAtSeatPosition(obj, seatNumber)
|
||||
MountingAction(tplayer, obj, seatNumber)
|
||||
|
||||
case Mountable.CanMount(obj: FacilityTurret, _, _) =>
|
||||
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
|
||||
log.warn(
|
||||
s"MountVehicleMsg: ${tplayer.Name} wants to mount turret ${obj.GUID.guid}, but needs to wait until it finishes updating"
|
||||
)
|
||||
|
||||
case Mountable.CanMount(obj: PlanetSideGameObject with FactionAffinity with WeaponTurret with InGameHistory, seatNumber, _) =>
|
||||
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
|
||||
log.info(s"${player.Name} mounts the ${obj.Definition.asInstanceOf[BasicDefinition].Name}")
|
||||
sendResponse(PlanetsideAttributeMessage(obj.GUID, attribute_type=0, obj.Health))
|
||||
ops.updateWeaponAtSeatPosition(obj, seatNumber)
|
||||
MountingAction(tplayer, obj, seatNumber)
|
||||
|
||||
case Mountable.CanMount(obj: Mountable, _, _) =>
|
||||
log.warn(s"MountVehicleMsg: $obj is some kind of mountable object but nothing will happen for ${player.Name}")
|
||||
|
||||
case Mountable.CanDismount(obj: ImplantTerminalMech, seatNum, _) =>
|
||||
log.info(s"${tplayer.Name} dismounts the implant terminal")
|
||||
DismountAction(tplayer, obj, seatNum)
|
||||
|
||||
case Mountable.CanDismount(obj: Vehicle, _, mountPoint)
|
||||
if obj.Definition == GlobalDefinitions.orbital_shuttle && obj.MountedIn.nonEmpty =>
|
||||
//dismount to hart lobby
|
||||
val pguid = player.GUID
|
||||
log.info(s"${tplayer.Name} dismounts the orbital shuttle into the lobby")
|
||||
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))
|
||||
)
|
||||
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
|
||||
log.info(s"${player.Name} is prepped for dropping")
|
||||
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
|
||||
)
|
||||
sessionLogic.keepAliveFunc = sessionLogic.zoning.NormalKeepAlive
|
||||
|
||||
case Mountable.CanDismount(obj: Vehicle, seatNum, _)
|
||||
if obj.Definition == GlobalDefinitions.droppod =>
|
||||
log.info(s"${tplayer.Name} has landed on ${continent.id}")
|
||||
sessionLogic.general.unaccessContainer(obj)
|
||||
DismountAction(tplayer, obj, seatNum)
|
||||
obj.Actor ! Vehicle.Deconstruct()
|
||||
|
||||
case Mountable.CanDismount(obj: Vehicle, seatNum, _)
|
||||
if tplayer.GUID == player.GUID =>
|
||||
//disembarking self
|
||||
log.info(s"${player.Name} dismounts the ${obj.Definition.Name}'s ${
|
||||
obj.SeatPermissionGroup(seatNum) match {
|
||||
case Some(AccessPermissionGroup.Driver) => "driver seat"
|
||||
case Some(seatType) => s"$seatType seat (#$seatNum)"
|
||||
case None => "seat"
|
||||
}
|
||||
}")
|
||||
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, _) =>
|
||||
log.info(s"${tplayer.Name} dismounts a ${obj.Definition.asInstanceOf[ObjectDefinition].Name}")
|
||||
DismountAction(tplayer, obj, seatNum)
|
||||
|
||||
case Mountable.CanDismount(obj: Mountable, _, _) =>
|
||||
log.warn(s"DismountVehicleMsg: $obj is some dismountable object but nothing will happen for ${player.Name}")
|
||||
|
||||
case Mountable.CanNotMount(obj: Vehicle, seatNumber) =>
|
||||
log.warn(s"MountVehicleMsg: ${tplayer.Name} attempted to mount $obj's seat $seatNumber, but was not allowed")
|
||||
obj.GetSeatFromMountPoint(seatNumber).collect {
|
||||
case seatNum if obj.SeatPermissionGroup(seatNum).contains(AccessPermissionGroup.Driver) =>
|
||||
sendResponse(
|
||||
ChatMsg(ChatMessageType.CMT_OPEN, wideContents=false, recipient="", "You are not the driver of this vehicle.", note=None)
|
||||
)
|
||||
}
|
||||
|
||||
case Mountable.CanNotMount(obj: Mountable, seatNumber) =>
|
||||
log.warn(s"MountVehicleMsg: ${tplayer.Name} attempted to mount $obj's seat $seatNumber, but was not allowed")
|
||||
|
||||
case Mountable.CanNotDismount(obj, seatNum) =>
|
||||
log.warn(s"DismountVehicleMsg: ${tplayer.Name} attempted to dismount $obj's mount $seatNum, but was not allowed")
|
||||
}
|
||||
}
|
||||
|
||||
/* 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 mounts a valid object.
|
||||
* @param tplayer the player
|
||||
* @param obj the mountable object
|
||||
* @param seatNum the mount into which the player is mounting
|
||||
*/
|
||||
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()
|
||||
avatarActor ! AvatarActor.DeactivateActiveImplants()
|
||||
avatarActor ! AvatarActor.SuspendStaminaRegeneration(3.seconds)
|
||||
sendResponse(ObjectAttachMessage(objGuid, playerGuid, seatNum))
|
||||
continent.VehicleEvents ! VehicleServiceMessage(
|
||||
continent.id,
|
||||
VehicleAction.MountVehicle(playerGuid, objGuid, seatNum)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
)
|
||||
)
|
||||
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,517 @@
|
|||
// Copyright (c) 2024 PSForever
|
||||
package net.psforever.actors.session.normal
|
||||
|
||||
import akka.actor.Actor.Receive
|
||||
import akka.actor.ActorRef
|
||||
import net.psforever.actors.session.support.{ChatFunctions, GeneralFunctions, LocalHandlerFunctions, MountHandlerFunctions, SquadHandlerFunctions, TerminalHandlerFunctions, VehicleFunctions, VehicleHandlerFunctions, WeaponAndProjectileFunctions}
|
||||
import net.psforever.packet.game.UplinkRequest
|
||||
import net.psforever.services.chat.ChatService
|
||||
//
|
||||
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, 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
|
||||
import net.psforever.util.Config
|
||||
|
||||
class NormalModeLogic(data: SessionData) extends ModeLogic {
|
||||
val avatarResponse: AvatarHandlerLogic = AvatarHandlerLogic(data.avatarResponse)
|
||||
val chat: ChatFunctions = ChatLogic(data.chat)
|
||||
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) */
|
||||
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 ChatService.MessageResponse(fromSession, message, _) =>
|
||||
chat.handleIncomingMessage(message, fromSession)
|
||||
|
||||
case SessionActor.SendResponse(packet) =>
|
||||
data.sendResponse(packet)
|
||||
|
||||
case SessionActor.CharSaved =>
|
||||
general.ops.renewCharSavedTimer(
|
||||
Config.app.game.savedMsg.interruptedByAction.fixed,
|
||||
Config.app.game.savedMsg.interruptedByAction.variable
|
||||
)
|
||||
|
||||
case SessionActor.CharSavedMsg =>
|
||||
general.ops.displayCharSavedMsgThenRenewTimer(
|
||||
Config.app.game.savedMsg.renewal.fixed,
|
||||
Config.app.game.savedMsg.renewal.variable
|
||||
)
|
||||
|
||||
/* 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 default =>
|
||||
data.log.warn(s"Invalid packet class received: $default from $sender")
|
||||
}
|
||||
|
||||
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 =>
|
||||
chat.handleChatMsg(packet)
|
||||
|
||||
case packet: SetChatFilterMessage =>
|
||||
chat.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 _: UplinkRequest => ()
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
case object NormalMode extends PlayerMode {
|
||||
def setup(data: SessionData): ModeLogic = {
|
||||
new NormalModeLogic(data)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,358 @@
|
|||
// Copyright (c) 2024 PSForever
|
||||
package net.psforever.actors.session.normal
|
||||
|
||||
import akka.actor.{ActorContext, ActorRef, typed}
|
||||
import net.psforever.actors.session.support.SessionSquadHandlers.SquadUIElement
|
||||
import net.psforever.actors.session.AvatarActor
|
||||
import net.psforever.actors.session.support.{SessionData, SessionSquadHandlers, SquadHandlerFunctions}
|
||||
import net.psforever.objects.{Default, LivePlayerList}
|
||||
import net.psforever.objects.avatar.Avatar
|
||||
import net.psforever.packet.game.{CharacterKnowledgeInfo, CharacterKnowledgeMessage, ChatMsg, MemberEvent, PlanetsideAttributeMessage, ReplicationStreamMessage, SquadAction, SquadDefinitionActionMessage, SquadDetailDefinitionUpdateMessage, SquadListing, SquadMemberEvent, SquadMembershipRequest, SquadMembershipResponse, SquadState, SquadStateInfo, SquadWaypointEvent, SquadWaypointRequest, WaypointEvent, WaypointEventAction}
|
||||
import net.psforever.services.chat.SquadChannel
|
||||
import net.psforever.services.teamwork.{SquadResponse, SquadServiceMessage, SquadAction => SquadServiceAction}
|
||||
import net.psforever.types.{ChatMessageType, PlanetSideGUID, SquadListDecoration, SquadResponseType, WaypointSubtype}
|
||||
|
||||
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 squadService: ActorRef = ops.squadService
|
||||
|
||||
private var waypointCooldown: Long = 0L
|
||||
|
||||
/* packet */
|
||||
|
||||
def handleSquadDefinitionAction(pkt: SquadDefinitionActionMessage): Unit = {
|
||||
val SquadDefinitionActionMessage(u1, u2, action) = pkt
|
||||
squadService ! SquadServiceMessage(player, continent, SquadServiceAction.Definition(u1, u2, action))
|
||||
}
|
||||
|
||||
def handleSquadMemberRequest(pkt: SquadMembershipRequest): Unit = {
|
||||
val SquadMembershipRequest(request_type, char_id, unk3, player_name, unk5) = pkt
|
||||
squadService ! SquadServiceMessage(
|
||||
player,
|
||||
continent,
|
||||
SquadServiceAction.Membership(request_type, char_id, unk3, player_name, unk5)
|
||||
)
|
||||
}
|
||||
|
||||
def handleSquadWaypointRequest(pkt: SquadWaypointRequest): Unit = {
|
||||
val SquadWaypointRequest(request, _, wtype, unk, info) = pkt
|
||||
val time = System.currentTimeMillis()
|
||||
val subtype = wtype.subtype
|
||||
if(subtype == WaypointSubtype.Squad) {
|
||||
squadService ! SquadServiceMessage(player, continent, SquadServiceAction.Waypoint(request, wtype, unk, info))
|
||||
} else if (subtype == WaypointSubtype.Laze && time - waypointCooldown > 1000) {
|
||||
//guarding against duplicating laze waypoints
|
||||
waypointCooldown = time
|
||||
squadService ! SquadServiceMessage(player, continent, SquadServiceAction.Waypoint(request, wtype, unk, info))
|
||||
}
|
||||
}
|
||||
|
||||
/* response handlers */
|
||||
|
||||
def handle(response: SquadResponse.Response, excluded: Iterable[Long]): Unit = {
|
||||
if (!excluded.exists(_ == avatar.id)) {
|
||||
response match {
|
||||
case SquadResponse.ListSquadFavorite(line, task) =>
|
||||
sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), line, SquadAction.ListSquadFavorite(task)))
|
||||
|
||||
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.IdentifyAsSquadLeader(squad_guid) =>
|
||||
sendResponse(SquadDefinitionActionMessage(squad_guid, 0, SquadAction.IdentifyAsSquadLeader()))
|
||||
|
||||
case SquadResponse.SetListSquad(squad_guid) =>
|
||||
sendResponse(SquadDefinitionActionMessage(squad_guid, 0, SquadAction.SetListSquad()))
|
||||
|
||||
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.WantsSquadPosition(_, name) =>
|
||||
sendResponse(
|
||||
ChatMsg(
|
||||
ChatMessageType.CMT_SQUAD,
|
||||
wideContents=true,
|
||||
name,
|
||||
s"\\#6 would like to join your squad. (respond with \\#3/accept\\#6 or \\#3/reject\\#6)",
|
||||
None
|
||||
)
|
||||
)
|
||||
|
||||
case SquadResponse.Join(squad, positionsToUpdate, _, ref) =>
|
||||
val avatarId = avatar.id
|
||||
val membershipPositions = (positionsToUpdate map squad.Membership.zipWithIndex)
|
||||
.filter { case (mem, index) =>
|
||||
mem.CharId > 0 && positionsToUpdate.contains(index)
|
||||
}
|
||||
membershipPositions.find { case (mem, _) => mem.CharId == avatarId } match {
|
||||
case Some((ourMember, ourIndex)) =>
|
||||
//we are joining the squad
|
||||
//load each member's entry (our own too)
|
||||
ops.squad_supplement_id = squad.GUID.guid + 1
|
||||
membershipPositions.foreach {
|
||||
case (member, index) =>
|
||||
sendResponse(
|
||||
SquadMemberEvent.Add(
|
||||
ops.squad_supplement_id,
|
||||
member.CharId,
|
||||
index,
|
||||
member.Name,
|
||||
member.ZoneId,
|
||||
outfit_id = 0
|
||||
)
|
||||
)
|
||||
ops.squadUI(member.CharId) =
|
||||
SquadUIElement(member.Name, outfit=0L, index, member.ZoneId, member.Health, member.Armor, member.Position)
|
||||
}
|
||||
//repeat our entry
|
||||
sendResponse(
|
||||
SquadMemberEvent.Add(
|
||||
ops.squad_supplement_id,
|
||||
ourMember.CharId,
|
||||
ourIndex,
|
||||
ourMember.Name,
|
||||
ourMember.ZoneId,
|
||||
outfit_id = 0
|
||||
)
|
||||
)
|
||||
//turn lfs off
|
||||
if (avatar.lookingForSquad) {
|
||||
avatarActor ! AvatarActor.SetLookingForSquad(false)
|
||||
}
|
||||
val playerGuid = player.GUID
|
||||
val factionChannel = s"${player.Faction}"
|
||||
//squad colors
|
||||
ops.GiveSquadColorsToMembers()
|
||||
ops.GiveSquadColorsForOthers(playerGuid, factionChannel, ops.squad_supplement_id)
|
||||
//associate with member position in squad
|
||||
sendResponse(PlanetsideAttributeMessage(playerGuid, 32, ourIndex))
|
||||
//a finalization? what does this do?
|
||||
sendResponse(SquadDefinitionActionMessage(squad.GUID, 0, SquadAction.Unknown(18)))
|
||||
squadService ! SquadServiceMessage(player, continent, SquadServiceAction.ReloadDecoration())
|
||||
ops.updateSquadRef = ref
|
||||
ops.updateSquad = ops.PeriodicUpdatesWhenEnrolledInSquad
|
||||
sessionLogic.chat.JoinChannel(SquadChannel(squad.GUID))
|
||||
case _ =>
|
||||
//other player is joining our squad
|
||||
//load each member's entry
|
||||
ops.GiveSquadColorsToMembers(
|
||||
membershipPositions.map {
|
||||
case (member, index) =>
|
||||
val charId = member.CharId
|
||||
sendResponse(
|
||||
SquadMemberEvent.Add(ops.squad_supplement_id, charId, index, member.Name, member.ZoneId, outfit_id = 0)
|
||||
)
|
||||
ops.squadUI(charId) =
|
||||
SquadUIElement(member.Name, outfit=0L, index, member.ZoneId, member.Health, member.Armor, member.Position)
|
||||
charId
|
||||
}
|
||||
)
|
||||
}
|
||||
//send an initial dummy update for map icon(s)
|
||||
sendResponse(
|
||||
SquadState(
|
||||
PlanetSideGUID(ops.squad_supplement_id),
|
||||
membershipPositions.map { case (member, _) =>
|
||||
SquadStateInfo(member.CharId, member.Health, member.Armor, member.Position)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
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
|
||||
sessionLogic.chat.LeaveChannel(SquadChannel(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.AssignMember(squad, from_index, to_index) =>
|
||||
//we've already swapped position internally; now we swap the cards
|
||||
ops.SwapSquadUIElements(squad, from_index, to_index)
|
||||
|
||||
case SquadResponse.PromoteMember(squad, promotedPlayer, from_index) =>
|
||||
if (promotedPlayer != player.CharId) {
|
||||
//demoted from leader; no longer lfsm
|
||||
if (player.avatar.lookingForSquad) {
|
||||
avatarActor ! AvatarActor.SetLookingForSquad(false)
|
||||
}
|
||||
}
|
||||
sendResponse(SquadMemberEvent(MemberEvent.Promote, squad.GUID.guid, promotedPlayer, position = 0))
|
||||
//the players have already been swapped in the backend object
|
||||
ops.PromoteSquadUIElements(squad, from_index)
|
||||
|
||||
case SquadResponse.UpdateMembers(_, positions) =>
|
||||
val pairedEntries = positions.collect {
|
||||
case entry if ops.squadUI.contains(entry.char_id) =>
|
||||
(entry, ops.squadUI(entry.char_id))
|
||||
}
|
||||
//prune entries
|
||||
val updatedEntries = pairedEntries
|
||||
.collect({
|
||||
case (entry, element) if entry.zone_number != element.zone =>
|
||||
//zone gets updated for these entries
|
||||
sendResponse(
|
||||
SquadMemberEvent.UpdateZone(ops.squad_supplement_id, entry.char_id, element.index, entry.zone_number)
|
||||
)
|
||||
ops.squadUI(entry.char_id) =
|
||||
SquadUIElement(element.name, element.outfit, element.index, entry.zone_number, entry.health, entry.armor, entry.pos)
|
||||
entry
|
||||
case (entry, element)
|
||||
if entry.health != element.health || entry.armor != element.armor || entry.pos != element.position =>
|
||||
//other elements that need to be updated
|
||||
ops.squadUI(entry.char_id) =
|
||||
SquadUIElement(element.name, element.outfit, element.index, entry.zone_number, entry.health, entry.armor, entry.pos)
|
||||
entry
|
||||
})
|
||||
.filterNot(_.char_id == avatar.id) //we want to update our backend, but not our frontend
|
||||
if (updatedEntries.nonEmpty) {
|
||||
sendResponse(
|
||||
SquadState(
|
||||
PlanetSideGUID(ops.squad_supplement_id),
|
||||
updatedEntries.map { entry =>
|
||||
SquadStateInfo(entry.char_id, entry.health, entry.armor, entry.pos)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
case SquadResponse.CharacterKnowledge(charId, name, certs, u1, u2, zone) =>
|
||||
sendResponse(CharacterKnowledgeMessage(charId, Some(CharacterKnowledgeInfo(name, certs, u1, u2, zone))))
|
||||
|
||||
case SquadResponse.SquadSearchResults(_/*results*/) =>
|
||||
//TODO positive squad search results message?
|
||||
// if(results.nonEmpty) {
|
||||
// results.foreach { guid =>
|
||||
// sendResponse(SquadDefinitionActionMessage(
|
||||
// guid,
|
||||
// 0,
|
||||
// SquadAction.SquadListDecorator(SquadListDecoration.SearchResult))
|
||||
// )
|
||||
// }
|
||||
// } else {
|
||||
// sendResponse(SquadDefinitionActionMessage(player.GUID, 0, SquadAction.NoSquadSearchResults()))
|
||||
// }
|
||||
// sendResponse(SquadDefinitionActionMessage(player.GUID, 0, SquadAction.CancelSquadSearch()))
|
||||
|
||||
case SquadResponse.InitWaypoints(char_id, waypoints) =>
|
||||
waypoints.foreach {
|
||||
case (waypoint_type, info, unk) =>
|
||||
sendResponse(
|
||||
SquadWaypointEvent.Add(
|
||||
ops.squad_supplement_id,
|
||||
char_id,
|
||||
waypoint_type,
|
||||
WaypointEvent(info.zone_number, info.pos, unk)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
case SquadResponse.WaypointEvent(WaypointEventAction.Add, char_id, waypoint_type, _, Some(info), unk) =>
|
||||
sendResponse(
|
||||
SquadWaypointEvent.Add(
|
||||
ops.squad_supplement_id,
|
||||
char_id,
|
||||
waypoint_type,
|
||||
WaypointEvent(info.zone_number, info.pos, unk)
|
||||
)
|
||||
)
|
||||
|
||||
case SquadResponse.WaypointEvent(WaypointEventAction.Remove, char_id, waypoint_type, _, _, _) =>
|
||||
sendResponse(SquadWaypointEvent.Remove(ops.squad_supplement_id, char_id, waypoint_type))
|
||||
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,184 @@
|
|||
// Copyright (c) 2024 PSForever
|
||||
package net.psforever.actors.session.normal
|
||||
|
||||
import akka.actor.{ActorContext, typed}
|
||||
import net.psforever.actors.session.AvatarActor
|
||||
import net.psforever.actors.session.support.{SessionData, SessionTerminalHandlers, TerminalHandlerFunctions}
|
||||
import net.psforever.login.WorldSession.{BuyNewEquipmentPutInInventory, SellEquipmentFromInventory}
|
||||
import net.psforever.objects.{GlobalDefinitions, Player, Vehicle}
|
||||
import net.psforever.objects.guid.TaskWorkflow
|
||||
import net.psforever.objects.serverobject.pad.VehicleSpawnPad
|
||||
import net.psforever.objects.serverobject.terminals.{ProximityUnit, Terminal}
|
||||
import net.psforever.objects.sourcing.AmenitySource
|
||||
import net.psforever.objects.vital.TerminalUsedActivity
|
||||
import net.psforever.packet.game.{FavoritesAction, FavoritesRequest, ItemTransactionMessage, ItemTransactionResultMessage, ProximityTerminalUseMessage, UnuseItemMessage}
|
||||
import net.psforever.types.{TransactionType, Vector3}
|
||||
|
||||
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
|
||||
|
||||
private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor
|
||||
|
||||
def handleItemTransaction(pkt: ItemTransactionMessage): Unit = {
|
||||
val ItemTransactionMessage(terminalGuid, transactionType, _, itemName, _, _) = pkt
|
||||
continent.GUID(terminalGuid) match {
|
||||
case Some(term: Terminal) if ops.lastTerminalOrderFulfillment =>
|
||||
val msg: String = if (itemName.nonEmpty) s" of $itemName" else ""
|
||||
log.info(s"${player.Name} is submitting an order - a $transactionType from a ${term.Definition.Name}$msg")
|
||||
ops.lastTerminalOrderFulfillment = false
|
||||
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
|
||||
term.Actor ! Terminal.Request(player, pkt)
|
||||
case Some(_: Terminal) =>
|
||||
log.warn(s"Please Wait until your previous order has been fulfilled, ${player.Name}")
|
||||
case Some(obj) =>
|
||||
log.error(s"ItemTransaction: ${obj.Definition.Name} is not a terminal, ${player.Name}")
|
||||
case _ =>
|
||||
log.error(s"ItemTransaction: entity with guid=${terminalGuid.guid} does not exist, ${player.Name}")
|
||||
}
|
||||
}
|
||||
|
||||
def handleProximityTerminalUse(pkt: ProximityTerminalUseMessage): Unit = {
|
||||
val ProximityTerminalUseMessage(_, objectGuid, _) = pkt
|
||||
continent.GUID(objectGuid) match {
|
||||
case Some(obj: Terminal with ProximityUnit) =>
|
||||
ops.HandleProximityTerminalUse(obj)
|
||||
case Some(obj) =>
|
||||
log.warn(s"ProximityTerminalUse: ${obj.Definition.Name} guid=${objectGuid.guid} is not ready to implement proximity effects")
|
||||
case None =>
|
||||
log.error(s"ProximityTerminalUse: ${player.Name} can not find an object with guid ${objectGuid.guid}")
|
||||
}
|
||||
}
|
||||
|
||||
def handleFavoritesRequest(pkt: FavoritesRequest): Unit = {
|
||||
val FavoritesRequest(_, loadoutType, action, line, label) = pkt
|
||||
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
|
||||
action match {
|
||||
case FavoritesAction.Save =>
|
||||
avatarActor ! AvatarActor.SaveLoadout(player, loadoutType, label, line)
|
||||
case FavoritesAction.Delete =>
|
||||
avatarActor ! AvatarActor.DeleteLoadout(player, loadoutType, line)
|
||||
case FavoritesAction.Unknown =>
|
||||
log.warn(s"FavoritesRequest: ${player.Name} requested an unknown favorites action")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* na
|
||||
* @param tplayer na
|
||||
* @param msg na
|
||||
* @param order na
|
||||
*/
|
||||
def handle(tplayer: Player, msg: ItemTransactionMessage, order: Terminal.Exchange): Unit = {
|
||||
order match {
|
||||
case Terminal.BuyEquipment(item)
|
||||
if tplayer.avatar.purchaseCooldown(item.Definition).nonEmpty =>
|
||||
sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, success = false))
|
||||
ops.lastTerminalOrderFulfillment = true
|
||||
|
||||
case Terminal.BuyEquipment(item) =>
|
||||
avatarActor ! AvatarActor.UpdatePurchaseTime(item.Definition)
|
||||
TaskWorkflow.execute(BuyNewEquipmentPutInInventory(
|
||||
continent.GUID(tplayer.VehicleSeated) match {
|
||||
case Some(v: Vehicle) => v
|
||||
case _ => player
|
||||
},
|
||||
tplayer,
|
||||
msg.terminal_guid
|
||||
)(item))
|
||||
|
||||
case Terminal.SellEquipment() =>
|
||||
SellEquipmentFromInventory(tplayer, tplayer, msg.terminal_guid)(Player.FreeHandSlot)
|
||||
|
||||
case Terminal.LearnCertification(cert) =>
|
||||
avatarActor ! AvatarActor.LearnCertification(msg.terminal_guid, cert)
|
||||
ops.lastTerminalOrderFulfillment = true
|
||||
|
||||
case Terminal.SellCertification(cert) =>
|
||||
avatarActor ! AvatarActor.SellCertification(msg.terminal_guid, cert)
|
||||
ops.lastTerminalOrderFulfillment = true
|
||||
|
||||
case Terminal.LearnImplant(implant) =>
|
||||
avatarActor ! AvatarActor.LearnImplant(msg.terminal_guid, implant)
|
||||
ops.lastTerminalOrderFulfillment = true
|
||||
|
||||
case Terminal.SellImplant(implant) =>
|
||||
avatarActor ! AvatarActor.SellImplant(msg.terminal_guid, implant)
|
||||
ops.lastTerminalOrderFulfillment = true
|
||||
|
||||
case Terminal.BuyVehicle(vehicle, _, _)
|
||||
if tplayer.avatar.purchaseCooldown(vehicle.Definition).nonEmpty || tplayer.spectator =>
|
||||
sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, success = false))
|
||||
ops.lastTerminalOrderFulfillment = true
|
||||
|
||||
case Terminal.BuyVehicle(vehicle, weapons, trunk) =>
|
||||
continent.map.terminalToSpawnPad
|
||||
.find { case (termid, _) => termid == msg.terminal_guid.guid }
|
||||
.map { case (a: Int, b: Int) => (continent.GUID(a), continent.GUID(b)) }
|
||||
.collect { case (Some(term: Terminal), Some(pad: VehicleSpawnPad)) =>
|
||||
avatarActor ! AvatarActor.UpdatePurchaseTime(vehicle.Definition)
|
||||
vehicle.Faction = tplayer.Faction
|
||||
vehicle.Position = pad.Position
|
||||
vehicle.Orientation = pad.Orientation + Vector3.z(pad.Definition.VehicleCreationZOrientOffset)
|
||||
//default loadout, weapons
|
||||
val vWeapons = vehicle.Weapons
|
||||
weapons.foreach { entry =>
|
||||
vWeapons.get(entry.start) match {
|
||||
case Some(slot) =>
|
||||
entry.obj.Faction = tplayer.Faction
|
||||
slot.Equipment = None
|
||||
slot.Equipment = entry.obj
|
||||
case None =>
|
||||
log.warn(
|
||||
s"BuyVehicle: ${player.Name} tries to apply default loadout to $vehicle on spawn, but can not find a mounted weapon for ${entry.start}"
|
||||
)
|
||||
}
|
||||
}
|
||||
//default loadout, trunk
|
||||
val vTrunk = vehicle.Trunk
|
||||
vTrunk.Clear()
|
||||
trunk.foreach { entry =>
|
||||
entry.obj.Faction = tplayer.Faction
|
||||
vTrunk.InsertQuickly(entry.start, entry.obj)
|
||||
}
|
||||
TaskWorkflow.execute(ops.registerVehicleFromSpawnPad(vehicle, pad, term))
|
||||
sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, success = true))
|
||||
if (GlobalDefinitions.isBattleFrameVehicle(vehicle.Definition)) {
|
||||
sendResponse(UnuseItemMessage(player.GUID, msg.terminal_guid))
|
||||
}
|
||||
player.LogActivity(TerminalUsedActivity(AmenitySource(term), msg.transaction_type))
|
||||
}
|
||||
.orElse {
|
||||
log.error(
|
||||
s"${tplayer.Name} wanted to spawn a vehicle, but there was no spawn pad associated with terminal ${msg.terminal_guid} to accept it"
|
||||
)
|
||||
sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, success = false))
|
||||
None
|
||||
}
|
||||
ops.lastTerminalOrderFulfillment = true
|
||||
|
||||
case Terminal.NoDeal() if msg != null =>
|
||||
val transaction = msg.transaction_type
|
||||
log.warn(s"NoDeal: ${tplayer.Name} made a request but the terminal rejected the ${transaction.toString} order")
|
||||
sendResponse(ItemTransactionResultMessage(msg.terminal_guid, transaction, success = false))
|
||||
ops.lastTerminalOrderFulfillment = true
|
||||
|
||||
case _ =>
|
||||
val terminal = msg.terminal_guid.guid
|
||||
continent.GUID(terminal) match {
|
||||
case Some(term: Terminal) =>
|
||||
log.warn(s"NoDeal?: ${tplayer.Name} made a request but the ${term.Definition.Name}#$terminal rejected the missing order")
|
||||
case Some(_) =>
|
||||
log.warn(s"NoDeal?: ${tplayer.Name} made a request to a non-terminal entity#$terminal")
|
||||
case None =>
|
||||
log.warn(s"NoDeal?: ${tplayer.Name} made a request to a missing entity#$terminal")
|
||||
}
|
||||
ops.lastTerminalOrderFulfillment = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,399 @@
|
|||
// Copyright (c) 2024 PSForever
|
||||
package net.psforever.actors.session.normal
|
||||
|
||||
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,391 @@
|
|||
// Copyright (c) 2024 PSForever
|
||||
package net.psforever.actors.session.normal
|
||||
|
||||
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 =>
|
||||
//todo: old warning; this state is problematic, but can trigger in otherwise valid instances
|
||||
//log.warn(
|
||||
// s"ChildObjectState: ${player.Name} is using a different controllable agent than entity ${object_guid.guid}"
|
||||
//)
|
||||
case Some(_) =>
|
||||
//TODO set tool orientation?
|
||||
player.Orientation = Vector3(0f, pitch, yaw)
|
||||
continent.VehicleEvents ! VehicleServiceMessage(
|
||||
continent.id,
|
||||
VehicleAction.ChildObjectState(player.GUID, object_guid, pitch, yaw)
|
||||
)
|
||||
}
|
||||
//TODO status condition of "playing getting out of vehicle to allow for late packets without warning
|
||||
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) =>
|
||||
log.info(s"${player.Name} is requesting a deployment change for ${obj.Definition.Name} - $deploy_state")
|
||||
obj.Actor ! Deployment.TryDeploymentChange(deploy_state)
|
||||
|
||||
case _ =>
|
||||
log.error(s"DeployRequest: ${player.Name} can not find vehicle $vehicle_guid")
|
||||
avatarActor ! AvatarActor.SetVehicle(None)
|
||||
}
|
||||
} else {
|
||||
log.warn(s"${player.Name} must be mounted to request a deployment change")
|
||||
}
|
||||
} else {
|
||||
log.warn(s"DeployRequest: ${player.Name} does not own the deploying $vehicle_guid object")
|
||||
}
|
||||
}
|
||||
|
||||
/* messages */
|
||||
|
||||
def handleCanDeploy(obj: Deployment.DeploymentObject, state: DriveState.Value): Unit = {
|
||||
if (state == DriveState.Deploying) {
|
||||
log.trace(s"DeployRequest: $obj transitioning to deploy state")
|
||||
} else if (state == DriveState.Deployed) {
|
||||
log.trace(s"DeployRequest: $obj has been Deployed")
|
||||
} else {
|
||||
CanNotChangeDeployment(obj, state, "incorrect deploy state")
|
||||
}
|
||||
}
|
||||
|
||||
def handleCanUndeploy(obj: Deployment.DeploymentObject, state: DriveState.Value): Unit = {
|
||||
if (state == DriveState.Undeploying) {
|
||||
log.trace(s"DeployRequest: $obj transitioning to undeploy state")
|
||||
} else if (state == DriveState.Mobile) {
|
||||
log.trace(s"DeployRequest: $obj is Mobile")
|
||||
} else {
|
||||
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")
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,588 @@
|
|||
// Copyright (c) 2024 PSForever
|
||||
package net.psforever.actors.session.spectator
|
||||
|
||||
import akka.actor.{ActorContext, typed}
|
||||
import net.psforever.actors.session.support.AvatarHandlerFunctions
|
||||
import net.psforever.actors.zone.ZoneActor
|
||||
import net.psforever.objects.Players
|
||||
import net.psforever.objects.avatar.scoring.Kill
|
||||
import net.psforever.objects.sourcing.PlayerSource
|
||||
import net.psforever.packet.game.{AvatarImplantMessage, ImplantAction}
|
||||
|
||||
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: Kill) if kda.experienceEarned > 0 =>
|
||||
continent.actor ! ZoneActor.RewardOurSupporters(
|
||||
PlayerSource(player),
|
||||
Players.produceContributionTranscriptFromKill(continent, player, kda),
|
||||
kda,
|
||||
kda.experienceEarned
|
||||
)
|
||||
|
||||
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(pkt: AvatarImplantMessage)
|
||||
if pkt.player_guid == player.GUID && pkt.action == ImplantAction.Initialization =>
|
||||
//special spectator implants stay initialized and do not deinitialize
|
||||
()
|
||||
|
||||
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,142 @@
|
|||
// 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.{ChatFunctions, ChatOperations, SessionData}
|
||||
import net.psforever.objects.Session
|
||||
import net.psforever.packet.game.{ChatMsg, SetChatFilterMessage}
|
||||
import net.psforever.services.chat.SpectatorChannel
|
||||
import net.psforever.types.ChatMessageType
|
||||
|
||||
import scala.collection.Seq
|
||||
|
||||
object ChatLogic {
|
||||
def apply(ops: ChatOperations): ChatLogic = {
|
||||
new ChatLogic(ops, ops.context)
|
||||
}
|
||||
}
|
||||
|
||||
class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) extends ChatFunctions {
|
||||
def sessionLogic: SessionData = ops.sessionLogic
|
||||
|
||||
def handleChatMsg(message: ChatMsg): Unit = {
|
||||
import ChatMessageType._
|
||||
(message.messageType, message.recipient.trim, message.contents.trim) match {
|
||||
/** Messages starting with ! are custom chat commands */
|
||||
case (_, _, contents) if contents.startsWith("!") &&
|
||||
customCommandMessages(message, session) => ()
|
||||
|
||||
case (CMT_FLY, recipient, contents) =>
|
||||
ops.commandFly(contents, recipient)
|
||||
|
||||
case (CMT_ANONYMOUS, _, _) =>
|
||||
// ?
|
||||
|
||||
case (CMT_TOGGLE_GM, _, _) =>
|
||||
// ?
|
||||
|
||||
case (CMT_CULLWATERMARK, _, contents) =>
|
||||
ops.commandWatermark(contents)
|
||||
|
||||
case (CMT_SPEED, _, contents) =>
|
||||
ops.commandSpeed(message, contents)
|
||||
|
||||
case (CMT_TOGGLESPECTATORMODE, _, contents) =>
|
||||
commandToggleSpectatorMode(contents)
|
||||
|
||||
case (CMT_RECALL, _, _) =>
|
||||
commandToggleSpectatorMode(contents = "off")
|
||||
|
||||
case (CMT_QUIT, _, _) =>
|
||||
ops.commandQuit(session)
|
||||
|
||||
case (CMT_SUICIDE, _, _) =>
|
||||
commandToggleSpectatorMode(contents = "off")
|
||||
|
||||
case (CMT_OPEN, _, _) =>
|
||||
ops.commandSendToRecipient(session, message, SpectatorChannel)
|
||||
|
||||
case (CMT_VOICE, _, contents) =>
|
||||
ops.commandVoice(session, message, contents, SpectatorChannel)
|
||||
|
||||
case (CMT_TELL, _, _) =>
|
||||
ops.commandTellOrIgnore(session, message, SpectatorChannel)
|
||||
|
||||
case (CMT_BROADCAST, _, _) =>
|
||||
ops.commandSendToRecipient(session, message, SpectatorChannel)
|
||||
|
||||
case (CMT_PLATOON, _, _) =>
|
||||
ops.commandSendToRecipient(session, message, SpectatorChannel)
|
||||
|
||||
case (CMT_GMTELL, _, _) =>
|
||||
ops.commandSend(session, message, SpectatorChannel)
|
||||
|
||||
case (CMT_NOTE, _, _) =>
|
||||
ops.commandSend(session, message, SpectatorChannel)
|
||||
|
||||
case (CMT_WHO | CMT_WHO_CSR | CMT_WHO_CR | CMT_WHO_PLATOONLEADERS | CMT_WHO_SQUADLEADERS | CMT_WHO_TEAMS, _, _) =>
|
||||
ops.commandWho(session)
|
||||
|
||||
case (CMT_ZONE, _, contents) =>
|
||||
ops.commandZone(message, contents)
|
||||
|
||||
case (CMT_WARP, _, contents) =>
|
||||
ops.commandWarp(session, message, contents)
|
||||
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
|
||||
def handleChatFilter(pkt: SetChatFilterMessage): Unit = {
|
||||
val SetChatFilterMessage(_, _, _) = pkt
|
||||
}
|
||||
|
||||
def handleIncomingMessage(message: ChatMsg, fromSession: Session): Unit = {
|
||||
import ChatMessageType._
|
||||
message.messageType match {
|
||||
case CMT_BROADCAST | CMT_SQUAD | CMT_PLATOON | CMT_COMMAND | CMT_NOTE =>
|
||||
ops.commandIncomingSendAllIfOnline(session, message)
|
||||
|
||||
case CMT_OPEN =>
|
||||
ops.commandIncomingSendToLocalIfOnline(session, fromSession, message)
|
||||
|
||||
case CMT_TELL | U_CMT_TELLFROM |
|
||||
CMT_GMOPEN | CMT_GMBROADCAST | CMT_GMBROADCAST_NC | CMT_GMBROADCAST_TR | CMT_GMBROADCAST_VS |
|
||||
CMT_GMBROADCASTPOPUP | CMT_GMTELL | U_CMT_GMTELLFROM | UNK_45 | UNK_71 | UNK_227 | UNK_229 =>
|
||||
ops.commandIncomingSend(message)
|
||||
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
|
||||
private def customCommandMessages(
|
||||
message: ChatMsg,
|
||||
session: Session
|
||||
): Boolean = {
|
||||
val contents = message.contents
|
||||
if (contents.startsWith("!")) {
|
||||
val (command, params) = ops.cliTokenization(contents.drop(1)) match {
|
||||
case a :: b => (a, b)
|
||||
case _ => ("", Seq(""))
|
||||
}
|
||||
command match {
|
||||
case "list" => ops.customCommandList(session, params, message)
|
||||
case "nearby" => ops.customCommandNearby(session)
|
||||
case "loc" => ops.customCommandLoc(session, message)
|
||||
case _ => false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private def commandToggleSpectatorMode(contents: String): Unit = {
|
||||
contents.toLowerCase() match {
|
||||
case "off" | "of" =>
|
||||
context.self ! SessionActor.SetMode(NormalMode)
|
||||
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,579 @@
|
|||
// 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.{GeneralFunctions, GeneralOperations, SessionData}
|
||||
import net.psforever.objects.{Account, GlobalDefinitions, LivePlayerList, PlanetSideGameObject, Player, TelepadDeployable, Tool, Vehicle}
|
||||
import net.psforever.objects.avatar.{Avatar, Implant}
|
||||
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.serverobject.CommonMessages
|
||||
import net.psforever.objects.serverobject.doors.Door
|
||||
import net.psforever.objects.vehicles.{Utility, UtilityType}
|
||||
import net.psforever.objects.vehicles.Utility.InternalTelepad
|
||||
import net.psforever.objects.zones.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, ObjectDetectedMessage, ObjectHeldMessage, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, PlayerStateShiftMessage, RequestDestroyMessage, ShiftState, TargetInfo, TargetingImplantRequest, TargetingInfoMessage, TradeMessage, UnuseItemMessage, UseItemMessage, VoiceHostInfo, VoiceHostKill, VoiceHostRequest, ZipLineMessage}
|
||||
import net.psforever.services.account.AccountPersistenceService
|
||||
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
|
||||
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 var customImplants = SpectatorModeLogic.SpectatorImplants.map(_.get)
|
||||
|
||||
private var additionalImplants: Seq[CreateShortcutMessage] = Seq()
|
||||
|
||||
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 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 _ => ()
|
||||
}
|
||||
}
|
||||
|
||||
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 =>
|
||||
customImplantOff(slot, implant)
|
||||
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 = {
|
||||
val CreateShortcutMessage(_, slot, wouldBeImplant) = pkt
|
||||
val pguid = player.GUID
|
||||
if (slot > 1 && slot < 5) {
|
||||
//protected
|
||||
customImplants
|
||||
.zipWithIndex
|
||||
.find { case (_, index) => index + 2 == slot}
|
||||
.foreach {
|
||||
case (implant, _) if wouldBeImplant.contains(implant.definition.implantType.shortcut) => ()
|
||||
case (implant, _) if implant.active =>
|
||||
sendResponse(CreateShortcutMessage(pguid, slot, Some(implant.definition.implantType.shortcut)))
|
||||
customImplantOff(slot, implant)
|
||||
case (implant, _) =>
|
||||
sendResponse(CreateShortcutMessage(pguid, slot, Some(implant.definition.implantType.shortcut)))
|
||||
}
|
||||
} else {
|
||||
additionalImplants.indexWhere(_.slot == slot) match {
|
||||
case -1 => ()
|
||||
case index =>
|
||||
additionalImplants = additionalImplants.take(index) ++ additionalImplants.drop(index + 1)
|
||||
}
|
||||
wouldBeImplant.collect {
|
||||
case _ =>
|
||||
additionalImplants = additionalImplants :+ pkt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 => ()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
player.Position = dest.Position
|
||||
} 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)
|
||||
}
|
||||
|
||||
private def customImplantOff(slot: Int, implant: Implant): Unit = {
|
||||
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))
|
||||
}
|
||||
|
||||
override protected[session] def stop(): Unit = {
|
||||
val pguid = player.GUID
|
||||
//set only originally blank slots blank again; rest will be overwrote later
|
||||
val originalBlankSlots = ((player.avatar.shortcuts.head, 1) +:
|
||||
player.avatar.shortcuts.drop(4).zipWithIndex.map { case (scut, slot) => (scut, slot + 4) })
|
||||
.collect { case (None, slot) => slot }
|
||||
additionalImplants
|
||||
.map(_.slot)
|
||||
.filter(originalBlankSlots.contains)
|
||||
.map(slot => CreateShortcutMessage(pguid, slot, None))
|
||||
.foreach(sendResponse)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,701 @@
|
|||
// 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, ChatFunctions, 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.ce.Deployable
|
||||
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.{DeployableInfo, DeployableObjectsInfoMessage, DeploymentAction, ObjectCreateDetailedMessage, ObjectDeleteMessage}
|
||||
import net.psforever.packet.game.objectcreate.{ObjectClass, ObjectCreateMessageParent, RibbonBars}
|
||||
import net.psforever.services.Service
|
||||
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
|
||||
import net.psforever.services.chat.{ChatService, SpectatorChannel}
|
||||
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 chat: ChatFunctions = ChatLogic(data.chat)
|
||||
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
|
||||
//
|
||||
continent.actor ! ZoneActor.RemoveFromBlockMap(player)
|
||||
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.Seat(seatNum).foreach(_.unmount(player))
|
||||
player.VehicleSeated = None
|
||||
Some(ObjectCreateMessageParent(obj.GUID, seatNum))
|
||||
case (Some(obj), Some(seatNum)) =>
|
||||
obj.Seat(seatNum).foreach(_.unmount(player))
|
||||
player.VehicleSeated = None
|
||||
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)
|
||||
)
|
||||
player.avatar
|
||||
.shortcuts
|
||||
.zipWithIndex
|
||||
.collect { case (Some(_), index) => index + 1 }
|
||||
.map(CreateShortcutMessage(pguid, _, None))
|
||||
.foreach(sendResponse)
|
||||
player.avatar.implants
|
||||
.collect { case Some(implant) if implant.active =>
|
||||
data.general.avatarActor ! AvatarActor.DeactivateImplant(implant.definition.implantType)
|
||||
}
|
||||
val playerFaction = player.Faction
|
||||
continent
|
||||
.DeployableList
|
||||
.filter(_.Faction == playerFaction)
|
||||
.foreach { obj =>
|
||||
sendResponse(DeployableObjectsInfoMessage(
|
||||
DeploymentAction.Dismiss,
|
||||
DeployableInfo(obj.GUID, Deployable.Icon.apply(obj.Definition.Item), obj.Position, Service.defaultPlayerGUID)
|
||||
))
|
||||
}
|
||||
if (player.silenced) {
|
||||
data.chat.commandIncomingSilence(session, ChatMsg(ChatMessageType.CMT_SILENCE, "player 0"))
|
||||
}
|
||||
//
|
||||
player.spectator = true
|
||||
data.chat.JoinChannel(SpectatorChannel)
|
||||
val newPlayer = SpectatorModeLogic.spectatorCharacter(player)
|
||||
newPlayer.LogActivity(player.History.headOption)
|
||||
val simpleHandHeldThing = GlobalDefinitions.flail_targeting_laser
|
||||
val handheld = new SimpleItem(simpleHandHeldThing)
|
||||
handheld.GUID = player.avatar.locker.GUID
|
||||
sendResponse(ObjectCreateDetailedMessage(
|
||||
0L,
|
||||
ObjectClass.avatar,
|
||||
pguid,
|
||||
vehicleAndSeat,
|
||||
newPlayer.Definition.Packet.DetailedConstructorData(newPlayer).get
|
||||
))
|
||||
sendResponse(ObjectCreateDetailedMessage(
|
||||
0L,
|
||||
simpleHandHeldThing.ObjectId,
|
||||
handheld.GUID,
|
||||
Some(ObjectCreateMessageParent(pguid, 4)),
|
||||
handheld.Definition.Packet.DetailedConstructorData(handheld).get
|
||||
))
|
||||
data.zoning.spawn.HandleSetCurrentAvatar(newPlayer)
|
||||
sendResponse(ChatMsg(ChatMessageType.CMT_TOGGLESPECTATORMODE, "on"))
|
||||
sendResponse(ChatMsg(ChatMessageType.UNK_227, "@SpectatorEnabled"))
|
||||
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 pguid = player.GUID
|
||||
val sendResponse: PlanetSidePacket => Unit = data.sendResponse
|
||||
//
|
||||
data.general.stop()
|
||||
player.avatar.shortcuts.slice(1, 4)
|
||||
.zipWithIndex
|
||||
.collect { case (None, slot) => slot + 1 } //set only actual blank slots blank
|
||||
.map(CreateShortcutMessage(pguid, _, None))
|
||||
.foreach(sendResponse)
|
||||
data.chat.LeaveChannel(SpectatorChannel)
|
||||
player.spectator = false
|
||||
sendResponse(ObjectDeleteMessage(player.avatar.locker.GUID, 0)) //free up the slot
|
||||
sendResponse(ChatMsg(ChatMessageType.CMT_TOGGLESPECTATORMODE, "off"))
|
||||
sendResponse(ChatMsg(ChatMessageType.UNK_227, "@SpectatorDisabled"))
|
||||
zoning.zoneReload = true
|
||||
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 ChatService.MessageResponse(fromSession, message, _) =>
|
||||
chat.handleIncomingMessage(message, fromSession)
|
||||
|
||||
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 =>
|
||||
chat.handleChatMsg(packet)
|
||||
|
||||
case packet: SetChatFilterMessage =>
|
||||
chat.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,180 @@
|
|||
// 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
|
||||
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.SquadChannel
|
||||
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
|
||||
|
||||
/* 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
|
||||
sessionLogic.chat.LeaveChannel(SquadChannel(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,681 @@
|
|||
// 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
|
||||
//todo this is not correct
|
||||
code match {
|
||||
case UplinkRequestType.RevealFriendlies =>
|
||||
sendResponse(UplinkResponse(code.value, continent.LivePlayers.count(_.Faction == playerFaction)))
|
||||
case UplinkRequestType.RevealEnemies =>
|
||||
sendResponse(UplinkResponse(code.value, continent.LivePlayers.count(_.Faction != playerFaction)))
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
|
||||
def 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -23,23 +23,25 @@ trait CommonSessionInterfacingFunctionality {
|
|||
|
||||
protected def context: ActorContext
|
||||
|
||||
protected def sessionData: SessionData
|
||||
protected def sessionLogic: SessionData
|
||||
|
||||
protected def session: Session = sessionData.session
|
||||
protected def session: Session = sessionLogic.session
|
||||
|
||||
protected def session_=(newsession: Session): Unit = sessionData.session_=(newsession)
|
||||
protected def session_=(newsession: Session): Unit = sessionLogic.session_=(newsession)
|
||||
|
||||
protected def account: Account = sessionData.account
|
||||
protected def account: Account = sessionLogic.account
|
||||
|
||||
protected def continent: Zone = sessionData.continent
|
||||
protected def continent: Zone = sessionLogic.continent
|
||||
|
||||
protected def player: Player = sessionData.player
|
||||
protected def player: Player = sessionLogic.player
|
||||
|
||||
protected def avatar: Avatar = sessionData.avatar
|
||||
protected def avatar: Avatar = sessionLogic.avatar
|
||||
|
||||
protected def log: Logger = sessionData.log
|
||||
protected def log: Logger = sessionLogic.log
|
||||
|
||||
protected def sendResponse(pkt: PlanetSideGamePacket): Unit = sessionData.sendResponse(pkt)
|
||||
protected def sendResponse(pkt: PlanetSideGamePacket): Unit = sessionLogic.sendResponse(pkt)
|
||||
|
||||
protected[session] def actionsToCancel(): Unit = { /* to override */ }
|
||||
|
||||
protected[session] def stop(): Unit = { /* to override */ }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,777 @@
|
|||
// Copyright (c) 2024 PSForever
|
||||
package net.psforever.actors.session.support
|
||||
|
||||
import akka.actor.{ActorContext, ActorRef, Cancellable, typed}
|
||||
import net.psforever.objects.sourcing.PlayerSource
|
||||
|
||||
import scala.collection.mutable
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.concurrent.duration._
|
||||
//
|
||||
import net.psforever.actors.session.{AvatarActor, SessionActor}
|
||||
import net.psforever.login.WorldSession._
|
||||
import net.psforever.objects._
|
||||
import net.psforever.objects.avatar._
|
||||
import net.psforever.objects.ce._
|
||||
import net.psforever.objects.definition._
|
||||
import net.psforever.objects.equipment._
|
||||
import net.psforever.objects.guid._
|
||||
import net.psforever.objects.inventory.{Container, InventoryItem}
|
||||
import net.psforever.objects.locker.LockerContainer
|
||||
import net.psforever.objects.serverobject.llu.CaptureFlag
|
||||
import net.psforever.objects.serverobject.mount.Mountable
|
||||
import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject}
|
||||
import net.psforever.objects.vehicles._
|
||||
import net.psforever.objects.vital._
|
||||
import net.psforever.objects.zones._
|
||||
import net.psforever.packet._
|
||||
import net.psforever.packet.game.{ActionCancelMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, ChangeShortcutBankMessage, CharacterRequestMessage, ConnectToWorldRequestMessage, CreateShortcutMessage, DeployObjectMessage, DisplayedAwardMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FriendsRequest, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, InvalidTerrainMessage, LootItemMessage, MoveItemMessage, ObjectDetectedMessage, ObjectHeldMessage, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, RequestDestroyMessage, TargetingImplantRequest, TradeMessage, UnuseItemMessage, UseItemMessage, ZipLineMessage}
|
||||
import net.psforever.packet.game.PlanetsideAttributeEnum.PlanetsideAttributeEnum
|
||||
import net.psforever.packet.game.objectcreate._
|
||||
import net.psforever.packet.game._
|
||||
import net.psforever.services.account.AccountPersistenceService
|
||||
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
|
||||
import net.psforever.services.local.support.CaptureFlagManager
|
||||
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
|
||||
import net.psforever.services.Service
|
||||
import net.psforever.types._
|
||||
import net.psforever.util.Config
|
||||
|
||||
trait GeneralFunctions extends CommonSessionInterfacingFunctionality {
|
||||
def ops: GeneralOperations
|
||||
|
||||
def handleConnectToWorldRequest(pkt: ConnectToWorldRequestMessage): Unit
|
||||
|
||||
def handleCharacterCreateRequest(pkt: CharacterCreateRequestMessage): Unit
|
||||
|
||||
def handleCharacterRequest(pkt: CharacterRequestMessage): Unit
|
||||
|
||||
def handlePlayerStateUpstream(pkt: PlayerStateMessageUpstream): Unit
|
||||
|
||||
def handleVoiceHostRequest(pkt: VoiceHostRequest): Unit
|
||||
|
||||
def handleVoiceHostInfo(pkt: VoiceHostInfo): Unit
|
||||
|
||||
def handleEmote(pkt: EmoteMsg): Unit
|
||||
|
||||
def handleDropItem(pkt: DropItemMessage): Unit
|
||||
|
||||
def handlePickupItem(pkt: PickupItemMessage): Unit
|
||||
|
||||
def handleObjectHeld(pkt: ObjectHeldMessage): Unit
|
||||
|
||||
def handleAvatarJump(pkt: AvatarJumpMessage): Unit
|
||||
|
||||
def handleZipLine(pkt: ZipLineMessage): Unit
|
||||
|
||||
def handleRequestDestroy(pkt: RequestDestroyMessage): Unit
|
||||
|
||||
def handleMoveItem(pkt: MoveItemMessage): Unit
|
||||
|
||||
def handleLootItem(pkt: LootItemMessage): Unit
|
||||
|
||||
def handleAvatarImplant(pkt: AvatarImplantMessage): Unit
|
||||
|
||||
def handleUseItem(pkt: UseItemMessage): Unit
|
||||
|
||||
def handleUnuseItem(pkt: UnuseItemMessage): Unit
|
||||
|
||||
def handleDeployObject(pkt: DeployObjectMessage): Unit
|
||||
|
||||
def handlePlanetsideAttribute(pkt: PlanetsideAttributeMessage): Unit
|
||||
|
||||
def handleGenericObjectAction(pkt: GenericObjectActionMessage): Unit
|
||||
|
||||
def handleGenericObjectActionAtPosition(pkt: GenericObjectActionAtPositionMessage): Unit
|
||||
|
||||
def handleGenericObjectState(pkt: GenericObjectStateMsg): Unit
|
||||
|
||||
def handleGenericAction(pkt: GenericActionMessage): Unit
|
||||
|
||||
def handleGenericCollision(pkt: GenericCollisionMsg): Unit
|
||||
|
||||
def handleAvatarFirstTimeEvent(pkt: AvatarFirstTimeEventMessage): Unit
|
||||
|
||||
def handleBugReport(pkt: PlanetSideGamePacket): Unit
|
||||
|
||||
def handleFacilityBenefitShieldChargeRequest(pkt: FacilityBenefitShieldChargeRequestMessage): Unit
|
||||
|
||||
def handleBattleplan(pkt: BattleplanMessage): Unit
|
||||
|
||||
def handleBindPlayer(pkt: BindPlayerMessage): Unit
|
||||
|
||||
def handleCreateShortcut(pkt: CreateShortcutMessage): Unit
|
||||
|
||||
def handleChangeShortcutBank(pkt: ChangeShortcutBankMessage): Unit
|
||||
|
||||
def handleFriendRequest(pkt: FriendsRequest): Unit
|
||||
|
||||
def handleInvalidTerrain(pkt: InvalidTerrainMessage): Unit
|
||||
|
||||
def handleActionCancel(pkt: ActionCancelMessage): Unit
|
||||
|
||||
def handleTrade(pkt: TradeMessage): Unit
|
||||
|
||||
def handleDisplayedAward(pkt: DisplayedAwardMessage): Unit
|
||||
|
||||
def handleObjectDetected(pkt: ObjectDetectedMessage): Unit
|
||||
|
||||
def handleTargetingImplantRequest(pkt: TargetingImplantRequest): Unit
|
||||
|
||||
def handleHitHint(pkt: HitHint): Unit
|
||||
|
||||
/* messages */
|
||||
|
||||
def handleSetAvatar(avatar: Avatar): Unit
|
||||
|
||||
def handleReceiveAccountData(account: Account): Unit
|
||||
|
||||
def handleUseCooldownRenew: BasicDefinition => Unit
|
||||
|
||||
def handleAvatarResponse(avatar: Avatar): Unit
|
||||
|
||||
def handleSetSpeed(speed: Float): Unit
|
||||
|
||||
def handleSetFlying(flying: Boolean): Unit
|
||||
|
||||
def handleSetSpectator(spectator: Boolean): Unit
|
||||
|
||||
def handleKick(player: Player, time: Option[Long]): Unit
|
||||
|
||||
def handleSilenced(isSilenced: Boolean): Unit
|
||||
}
|
||||
|
||||
class GeneralOperations(
|
||||
val sessionLogic: SessionData,
|
||||
val avatarActor: typed.ActorRef[AvatarActor.Command],
|
||||
implicit val context: ActorContext
|
||||
) extends CommonSessionInterfacingFunctionality {
|
||||
private[session] var progressBarValue: Option[Float] = None
|
||||
private[session] var accessedContainer: Option[PlanetSideGameObject with Container] = None
|
||||
private[session] var recentTeleportAttempt: Long = 0
|
||||
private[session] var kitToBeUsed: Option[PlanetSideGUID] = None
|
||||
// If a special item (e.g. LLU) has been attached to the player the GUID should be stored here, or cleared when dropped, since the drop hotkey doesn't send the GUID of the object to be dropped.
|
||||
private[session] var specialItemSlotGuid: Option[PlanetSideGUID] = None
|
||||
private[session] val collisionHistory: mutable.HashMap[ActorRef, Long] = mutable.HashMap()
|
||||
private[session] var heightLast: Float = 0f
|
||||
private[session] var heightTrend: Boolean = false //up = true, down = false
|
||||
private[session] var heightHistory: Float = 0f
|
||||
private[session] var progressBarUpdate: Cancellable = Default.Cancellable
|
||||
private var charSavedTimer: Cancellable = Default.Cancellable
|
||||
|
||||
/**
|
||||
* 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def dropSpecialSlotItem(): Unit = {
|
||||
specialItemSlotGuid.foreach { guid =>
|
||||
specialItemSlotGuid = None
|
||||
player.Carrying = None
|
||||
(continent.GUID(guid) match {
|
||||
case Some(llu: CaptureFlag) => Some((llu, llu.Carrier))
|
||||
case _ => None
|
||||
}) match {
|
||||
case Some((llu, Some(carrier: Player)))
|
||||
if carrier.GUID == player.GUID && !player.isAlive =>
|
||||
player.LastDamage.foreach { damage =>
|
||||
damage
|
||||
.interaction
|
||||
.adversarial
|
||||
.map { _.attacker }
|
||||
.collect {
|
||||
case attacker
|
||||
if attacker.Faction != player.Faction &&
|
||||
System.currentTimeMillis() - llu.LastCollectionTime >= Config.app.game.experience.cep.lluSlayerCreditDuration.toMillis =>
|
||||
continent.AvatarEvents ! AvatarServiceMessage(
|
||||
attacker.Name,
|
||||
AvatarAction.AwardCep(attacker.CharId, Config.app.game.experience.cep.lluSlayerCredit)
|
||||
)
|
||||
}
|
||||
}
|
||||
continent.LocalEvents ! CaptureFlagManager.DropFlag(llu)
|
||||
case Some((llu, Some(carrier: Player))) if carrier.GUID == player.GUID =>
|
||||
continent.LocalEvents ! CaptureFlagManager.DropFlag(llu)
|
||||
case Some((_, Some(carrier: Player))) =>
|
||||
log.warn(s"${player.toString} tried to drop LLU, but it is currently held by ${carrier.toString}")
|
||||
case Some((_, None)) =>
|
||||
log.warn(s"${player.toString} tried to drop LLU, but nobody is holding it.")
|
||||
case None =>
|
||||
log.warn(s"${player.toString} tried to drop a special item that wasn't recognized. GUID: $guid")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def setupProgressChange(rate: Float, finishedAction: () => Unit, stepAction: Float => Boolean): Unit = {
|
||||
if (progressBarValue.isEmpty) {
|
||||
progressBarValue = Some(-rate)
|
||||
context.self ! CommonMessages.ProgressEvent(rate, finishedAction, stepAction)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the message that indicates the level of completion of a process.
|
||||
* The process is any form of user-driven activity with a certain eventual outcome
|
||||
* but indeterminate progress feedback per cycle.<br>
|
||||
* <br>
|
||||
* This task is broken down into the "progression" from its initial state to the eventual outcome
|
||||
* as is reported back to the player through some means of messaging window feedback.
|
||||
* Though common in practice, this is not a requirement
|
||||
* and the progress can accumulate without a user reportable method.
|
||||
* To ensure that completion is reported properly,
|
||||
* an exception is made that 99% completion is accounted uniquely
|
||||
* before the final 100% is achieved.
|
||||
* If the background process recording value is never set before running the initial operation
|
||||
* or gets unset by failing a `tickAction` check
|
||||
* the process is stopped.
|
||||
* @see `progressBarUpdate`
|
||||
* @see `progressBarValue`
|
||||
* @see `essionActor.Progress`
|
||||
* @param delta how much the progress changes each tick
|
||||
* @param completionAction a custom action performed once the process is completed
|
||||
* @param tickAction an optional action is is performed for each tick of progress;
|
||||
* also performs a continuity check to determine if the process has been disrupted
|
||||
*/
|
||||
def handleProgressChange(
|
||||
delta: Float,
|
||||
completionAction: () => Unit,
|
||||
tickAction: Float => Boolean,
|
||||
tick: Long
|
||||
): Unit = {
|
||||
progressBarUpdate.cancel()
|
||||
progressBarValue.foreach { value =>
|
||||
val next = value + delta
|
||||
if (value >= 100f) {
|
||||
//complete
|
||||
progressBarValue = None
|
||||
tickAction(100)
|
||||
completionAction()
|
||||
} else if (value < 100f && next >= 100f) {
|
||||
if (tickAction(99)) {
|
||||
//will complete after this turn
|
||||
progressBarValue = Some(next)
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
progressBarUpdate = context.system.scheduler.scheduleOnce(
|
||||
delay = 100 milliseconds,
|
||||
context.self,
|
||||
CommonMessages.ProgressEvent(delta, completionAction, tickAction)
|
||||
)
|
||||
} else {
|
||||
progressBarValue = None
|
||||
}
|
||||
} else {
|
||||
if (tickAction(next)) {
|
||||
//normal progress activity
|
||||
progressBarValue = Some(next)
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
progressBarUpdate = context.system.scheduler.scheduleOnce(
|
||||
tick.milliseconds,
|
||||
context.self,
|
||||
CommonMessages.ProgressEvent(delta, completionAction, tickAction, tick)
|
||||
)
|
||||
} else {
|
||||
progressBarValue = None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For whatever container the character considers itself trying to access,
|
||||
* initiate protocol to "access" it.
|
||||
*/
|
||||
def accessContainer(container: Container): Unit = {
|
||||
container match {
|
||||
case v: Vehicle =>
|
||||
accessVehicleContents(v)
|
||||
case o: LockerContainer =>
|
||||
accessGenericContainer(o)
|
||||
case p: Player if p.isBackpack =>
|
||||
accessCorpseContents(p)
|
||||
case p: PlanetSideServerObject with Container =>
|
||||
accessedContainer = Some(p)
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For the target container, initiate protocol to "access" it.
|
||||
*/
|
||||
private def accessGenericContainer(container: PlanetSideServerObject with Container): Unit = {
|
||||
accessedContainer = Some(container)
|
||||
displayContainerContents(container.GUID, container.Inventory.Items)
|
||||
}
|
||||
|
||||
/**
|
||||
* Common preparation for interfacing with a vehicle trunk.
|
||||
* Join a vehicle-specific group for shared updates.
|
||||
* Construct every object in the vehicle's inventory for shared manipulation updates.
|
||||
* @see `Container.Inventory`
|
||||
* @see `GridInventory.Items`
|
||||
* @param vehicle the vehicle
|
||||
*/
|
||||
private def accessVehicleContents(vehicle: Vehicle): Unit = {
|
||||
accessedContainer = Some(vehicle)
|
||||
accessContainerChannel(continent.VehicleEvents, vehicle.Actor.toString)
|
||||
displayContainerContents(vehicle.GUID, vehicle.Inventory.Items)
|
||||
}
|
||||
|
||||
/**
|
||||
* Common preparation for interfacing with a corpse (former player's backpack).
|
||||
* Join a corpse-specific group for shared updates.
|
||||
* Construct every object in the player's hands and inventory for shared manipulation updates.
|
||||
* @see `Container.Inventory`
|
||||
* @see `GridInventory.Items`
|
||||
* @see `Player.HolsterItems`
|
||||
* @param tplayer the corpse
|
||||
*/
|
||||
private def accessCorpseContents(tplayer: Player): Unit = {
|
||||
accessedContainer = Some(tplayer)
|
||||
accessContainerChannel(continent.AvatarEvents, tplayer.Actor.toString)
|
||||
displayContainerContents(tplayer.GUID, tplayer.HolsterItems())
|
||||
displayContainerContents(tplayer.GUID, tplayer.Inventory.Items)
|
||||
}
|
||||
|
||||
/**
|
||||
* Join an entity-specific group for shared updates.
|
||||
* @param events the event system bus to which to subscribe
|
||||
* @param channel the channel name
|
||||
*/
|
||||
private def accessContainerChannel(events: ActorRef, channel: String): Unit = {
|
||||
events ! Service.Join(channel)
|
||||
}
|
||||
|
||||
/**
|
||||
* Depict the contents of a container by building them in the local client
|
||||
* in their container as a group of detailed entities.
|
||||
* @see `ObjectCreateDetailedMessage`
|
||||
* @see `ObjectCreateMessageParent`
|
||||
* @see `PacketConverter.DetailedConstructorData`
|
||||
* @param containerId the container's unique identifier
|
||||
* @param items a list of the entities to be depicted
|
||||
*/
|
||||
private def displayContainerContents(containerId: PlanetSideGUID, items: Iterable[InventoryItem]): Unit = {
|
||||
items.foreach(entry => {
|
||||
val obj = entry.obj
|
||||
val objDef = obj.Definition
|
||||
sendResponse(
|
||||
ObjectCreateDetailedMessage(
|
||||
objDef.ObjectId,
|
||||
obj.GUID,
|
||||
ObjectCreateMessageParent(containerId, entry.start),
|
||||
objDef.Packet.DetailedConstructorData(obj).get
|
||||
)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* For whatever container the character considers itself accessing,
|
||||
* initiate protocol to release it from "access".
|
||||
*/
|
||||
def unaccessContainer(): Unit = {
|
||||
accessedContainer.foreach { container => unaccessContainer(container) }
|
||||
}
|
||||
|
||||
/**
|
||||
* For the target container, initiate protocol to release it from "access".
|
||||
*/
|
||||
def unaccessContainer(container: Container): Unit = {
|
||||
container match {
|
||||
case v: Vehicle =>
|
||||
unaccessVehicleContainer(v)
|
||||
case o: LockerContainer =>
|
||||
unaccessGenericContainer(o)
|
||||
avatarActor ! AvatarActor.SaveLocker()
|
||||
case p: Player if p.isBackpack =>
|
||||
unaccessCorpseContainer(p)
|
||||
case _: PlanetSideServerObject with Container =>
|
||||
accessedContainer = None
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
|
||||
private def unaccessGenericContainer(container: Container): Unit = {
|
||||
accessedContainer = None
|
||||
hideContainerContents(container.Inventory.Items)
|
||||
}
|
||||
|
||||
/**
|
||||
* Common preparation for disengaging from a vehicle.
|
||||
* Leave the vehicle-specific group that was used for shared updates.
|
||||
* Deconstruct every object in the vehicle's inventory.
|
||||
* @param vehicle the vehicle
|
||||
*/
|
||||
private def unaccessVehicleContainer(vehicle: Vehicle): Unit = {
|
||||
accessedContainer = None
|
||||
if (vehicle.AccessingTrunk.contains(player.GUID)) {
|
||||
vehicle.AccessingTrunk = None
|
||||
}
|
||||
unaccessContainerChannel(continent.VehicleEvents, vehicle.Actor.toString)
|
||||
hideContainerContents(vehicle.Inventory.Items)
|
||||
}
|
||||
|
||||
/**
|
||||
* Common preparation for disengaging from a corpse.
|
||||
* Leave the corpse-specific group that was used for shared updates.
|
||||
* Deconstruct every object in the backpack's inventory.
|
||||
* @param tplayer the corpse
|
||||
*/
|
||||
private def unaccessCorpseContainer(tplayer: Player): Unit = {
|
||||
accessedContainer = None
|
||||
unaccessContainerChannel(continent.AvatarEvents, tplayer.Actor.toString)
|
||||
hideContainerContents(tplayer.HolsterItems())
|
||||
hideContainerContents(tplayer.Inventory.Items)
|
||||
}
|
||||
|
||||
/**
|
||||
* Leave an entity-specific group for shared updates.
|
||||
* @param events the event system bus to which to subscribe
|
||||
* @param channel the channel name
|
||||
*/
|
||||
private def unaccessContainerChannel(events: ActorRef, channel: String): Unit = {
|
||||
events ! Service.Leave(Some(channel))
|
||||
}
|
||||
|
||||
/**
|
||||
* Forget the contents of a container by deleting that content from the local client.
|
||||
* @see `InventoryItem`
|
||||
* @see `ObjectDeleteMessage`
|
||||
* @param items a list of the entities to be depicted
|
||||
*/
|
||||
private def hideContainerContents(items: List[InventoryItem]): Unit = {
|
||||
items.foreach { entry =>
|
||||
sendResponse(ObjectDeleteMessage(entry.obj.GUID, 0))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check two locations for a controlled piece of equipment that is associated with the `player`.<br>
|
||||
* <br>
|
||||
* The first location is dependent on whether the avatar is in a vehicle.
|
||||
* Some vehicle seats may have a "controlled weapon" which counts as the first location to be checked.
|
||||
* The second location is dependent on whether the avatar has a raised hand.
|
||||
* That is only possible if the player has something in their hand at the moment, hence the second location.
|
||||
* Players do have a concept called a "last drawn slot" (hand) but that former location is not eligible.<br>
|
||||
* <br>
|
||||
* Along with any discovered item, a containing object such that the statement:<br>
|
||||
* `container.Find(object) = Some(slot)`<br>
|
||||
* ... will return a proper result.
|
||||
* For a mount controlled weapon, the vehicle is returned.
|
||||
* For the player's hand, the player is returned.
|
||||
* @return a `Tuple` of the returned values;
|
||||
* the first value is a `Container` object;
|
||||
* the second value is an `Equipment` object in the former
|
||||
*/
|
||||
def findContainedEquipment(): (Option[PlanetSideGameObject with Container], Set[Equipment]) = {
|
||||
continent.GUID(player.VehicleSeated) match {
|
||||
case Some(vehicle: Mountable with MountableWeapons with Container) =>
|
||||
vehicle.PassengerInSeat(player) match {
|
||||
case Some(seatNum) =>
|
||||
(Some(vehicle), vehicle.WeaponControlledFromSeat(seatNum))
|
||||
case None =>
|
||||
(None, Set.empty)
|
||||
}
|
||||
case _ =>
|
||||
player.Slot(player.DrawnSlot).Equipment match {
|
||||
case Some(a) =>
|
||||
(Some(player), Set(a))
|
||||
case _ =>
|
||||
(None, Set.empty)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check two locations for a controlled piece of equipment that is associated with the `player`
|
||||
* and has the specified global unique identifier number.
|
||||
*/
|
||||
def findContainedEquipment(
|
||||
guid: PlanetSideGUID
|
||||
): (Option[PlanetSideGameObject with Container], Set[Equipment]) = {
|
||||
val (o, equipment) = findContainedEquipment()
|
||||
equipment.find { _.GUID == guid } match {
|
||||
case Some(equip) => (o, Set(equip))
|
||||
case None => (None, Set.empty)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop an `Equipment` item onto the ground.
|
||||
* Specifically, instruct the item where it will appear,
|
||||
* add it to the list of items that are visible to multiple users,
|
||||
* and then inform others that the item has been dropped.
|
||||
* @param obj a `Container` object that represents where the item will be dropped;
|
||||
* curried for callback
|
||||
* @param zone the continent in which the item is being dropped;
|
||||
* curried for callback
|
||||
* @param item the item
|
||||
*/
|
||||
def normalItemDrop(obj: PlanetSideServerObject with Container, zone: Zone)(item: Equipment): Unit = {
|
||||
zone.Ground.tell(Zone.Ground.DropItem(item, obj.Position, Vector3.z(obj.Orientation.z)), obj.Actor)
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an object globally unique identifier, search in a given location for it.
|
||||
* @param objectGuid the object
|
||||
* @param parent a `Container` object wherein to search
|
||||
* @return an optional tuple that contains two values;
|
||||
* the first value is the container that matched correctly with the object's GUID;
|
||||
* the second value is the slot position of the object
|
||||
*/
|
||||
def findInLocalContainer(
|
||||
objectGuid: PlanetSideGUID
|
||||
)(parent: PlanetSideServerObject with Container): Option[(PlanetSideServerObject with Container, Option[Int])] = {
|
||||
parent.Find(objectGuid).flatMap { slot => Some((parent, Some(slot))) }
|
||||
}
|
||||
|
||||
/**
|
||||
* na
|
||||
* @param targetGuid na
|
||||
* @param unk1 na
|
||||
* @param unk2 na
|
||||
*/
|
||||
def hackObject(targetGuid: PlanetSideGUID, unk1: Long, unk2: Long): Unit = {
|
||||
sendResponse(HackMessage(unk1=0, targetGuid, player_guid=Service.defaultPlayerGUID, progress=100, unk1, HackState.Hacked, unk2))
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a PlanetsideAttributeMessage packet to the client
|
||||
* @param targetGuid The target of the attribute
|
||||
* @param attributeNumber The attribute number
|
||||
* @param attributeValue The attribute value
|
||||
*/
|
||||
def sendPlanetsideAttributeMessage(
|
||||
targetGuid: PlanetSideGUID,
|
||||
attributeNumber: PlanetsideAttributeEnum,
|
||||
attributeValue: Long
|
||||
): Unit = {
|
||||
sendResponse(PlanetsideAttributeMessage(targetGuid, attributeNumber, attributeValue))
|
||||
}
|
||||
|
||||
/**
|
||||
* The player has lost the will to live and must be killed.
|
||||
* @see `Vitality`<br>
|
||||
* `PlayerSuicide`
|
||||
* @param tplayer the player to be killed
|
||||
*/
|
||||
def suicide(tplayer: Player): Unit = {
|
||||
tplayer.LogActivity(PlayerSuicide(PlayerSource(tplayer)))
|
||||
tplayer.Actor ! Player.Die()
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the deployables user interface elements.<br>
|
||||
* <br>
|
||||
* All element initializations require both the maximum deployable amount and the current deployables active counts.
|
||||
* Until initialized, all elements will be RED 0/0 as if the corresponding certification were not `learn`ed.
|
||||
* The respective element will become a pair of numbers, the second always being non-zero, when properly initialized.
|
||||
* The numbers will appear GREEN when more deployables of that type can be placed.
|
||||
* The numbers will appear RED if the player can not place any more of that type of deployable.
|
||||
* The numbers will appear YELLOW if the current deployable count is greater than the maximum count of that type
|
||||
* such as may be the case when a player `forget`s a certification.
|
||||
* @param list a tuple of each UI element with four numbers;
|
||||
* even numbers are attribute ids;
|
||||
* odd numbers are quantities;
|
||||
* first pair is current quantity;
|
||||
* second pair is maximum quantity
|
||||
*/
|
||||
def updateDeployableUIElements(list: List[(Int, Int, Int, Int)]): Unit = {
|
||||
val guid = PlanetSideGUID(0)
|
||||
list.foreach {
|
||||
case (currElem, curr, maxElem, max) =>
|
||||
//fields must update in ordered pairs: max, curr
|
||||
sendResponse(PlanetsideAttributeMessage(guid, maxElem, max))
|
||||
sendResponse(PlanetsideAttributeMessage(guid, currElem, curr))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Animate(?) a player using a fully-linked Router teleportation system.
|
||||
* In reality, this seems to do nothing visually?
|
||||
* @param playerGUID the player being teleported
|
||||
* @param srcGUID the origin of the teleportation
|
||||
* @param destGUID the destination of the teleportation
|
||||
*/
|
||||
def useRouterTelepadEffect(playerGUID: PlanetSideGUID, srcGUID: PlanetSideGUID, destGUID: PlanetSideGUID): Unit = {
|
||||
sendResponse(PlanetsideAttributeMessage(playerGUID, 64, 1)) //what does this do?
|
||||
sendResponse(GenericObjectActionMessage(srcGUID, 31))
|
||||
sendResponse(GenericObjectActionMessage(destGUID, 32))
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to link the router teleport system using the provided terminal information.
|
||||
* Although additional states are necessary to properly use the teleportation system,
|
||||
* e.g., deployment state, active state of the endpoints, etc.,
|
||||
* this decision is not made factoring those other conditions.
|
||||
* @param router the vehicle that houses one end of the teleportation system (the `InternalTelepad` object)
|
||||
* @param systemPlan specific object identification of the two endpoints of the teleportation system;
|
||||
* if absent, the knowable endpoint is deleted from the client reflexively
|
||||
*/
|
||||
def toggleTeleportSystem(router: Vehicle, systemPlan: Option[(Utility.InternalTelepad, TelepadDeployable)]): Unit = {
|
||||
systemPlan match {
|
||||
case Some((internalTelepad, remoteTelepad)) =>
|
||||
internalTelepad.Telepad = remoteTelepad.GUID //necessary; backwards link to the (new) telepad
|
||||
TelepadLike.StartRouterInternalTelepad(continent, router.GUID, internalTelepad)
|
||||
TelepadLike.LinkTelepad(continent, remoteTelepad.GUID)
|
||||
case _ =>
|
||||
router.Utility(UtilityType.internal_router_telepad_deployable) match {
|
||||
case Some(util: Utility.InternalTelepad) =>
|
||||
sendResponse(ObjectDeleteMessage(util.GUID, 0))
|
||||
case _ => ()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def toggleMaxSpecialState(enable: Boolean): Unit = {
|
||||
if (player.ExoSuit == ExoSuitType.MAX) {
|
||||
if (enable && player.UsingSpecial == SpecialExoSuitDefinition.Mode.Normal) {
|
||||
player.Faction match {
|
||||
case PlanetSideEmpire.TR if player.Capacitor == player.ExoSuitDef.MaxCapacitor =>
|
||||
player.UsingSpecial = SpecialExoSuitDefinition.Mode.Overdrive
|
||||
activateMaxSpecialStateMessage()
|
||||
case PlanetSideEmpire.NC if player.Capacitor > 0 =>
|
||||
player.UsingSpecial = SpecialExoSuitDefinition.Mode.Shielded
|
||||
activateMaxSpecialStateMessage()
|
||||
case PlanetSideEmpire.VS =>
|
||||
log.warn(s"${player.Name} tried to use a MAX special ability but their faction doesn't have one")
|
||||
case _ => ()
|
||||
}
|
||||
} else {
|
||||
player.UsingSpecial = SpecialExoSuitDefinition.Mode.Normal
|
||||
continent.AvatarEvents ! AvatarServiceMessage(
|
||||
continent.id,
|
||||
AvatarAction.PlanetsideAttributeToAll(player.GUID, 8, 0)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def activateMaxSpecialStateMessage(): Unit = {
|
||||
continent.AvatarEvents ! AvatarServiceMessage(
|
||||
continent.id,
|
||||
AvatarAction.PlanetsideAttributeToAll(player.GUID, 8, 1)
|
||||
)
|
||||
}
|
||||
|
||||
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 _ => ()
|
||||
}
|
||||
}
|
||||
|
||||
def fallHeightTracker(zHeight: Float): Unit = {
|
||||
if ((heightTrend && heightLast - zHeight >= 0.5f) ||
|
||||
(!heightTrend && zHeight - heightLast >= 0.5f)) {
|
||||
heightTrend = !heightTrend
|
||||
heightHistory = zHeight
|
||||
}
|
||||
heightLast = zHeight
|
||||
}
|
||||
|
||||
def canSeeReallyFar: Boolean = {
|
||||
sessionLogic.shooting.FindContainedWeapon match {
|
||||
case (Some(_: Vehicle), weapons) if weapons.nonEmpty =>
|
||||
player.avatar
|
||||
.implants
|
||||
.exists { p =>
|
||||
p.collect { implant => implant.definition.implantType == ImplantType.RangeMagnifier && implant.active }.nonEmpty
|
||||
}
|
||||
case (Some(_: Player), weapons) if weapons.nonEmpty =>
|
||||
val wep = weapons.head
|
||||
wep.Definition == GlobalDefinitions.bolt_driver ||
|
||||
wep.Definition == GlobalDefinitions.heavy_sniper ||
|
||||
(
|
||||
(wep.Projectile ne GlobalDefinitions.no_projectile) &&
|
||||
player.Crouching &&
|
||||
player.avatar
|
||||
.implants
|
||||
.exists { p =>
|
||||
p.collect { implant => implant.definition.implantType == ImplantType.RangeMagnifier && implant.active }.nonEmpty
|
||||
}
|
||||
)
|
||||
case _ =>
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
def displayCharSavedMsgThenRenewTimer(fixedLen: Long, varLen: Long): Unit = {
|
||||
charSaved()
|
||||
renewCharSavedTimer(fixedLen, varLen)
|
||||
}
|
||||
|
||||
def renewCharSavedTimer(fixedLen: Long, varLen: Long): Unit = {
|
||||
charSavedTimer.cancel()
|
||||
val delay = (fixedLen + (varLen * scala.math.random()).toInt).seconds
|
||||
charSavedTimer = context.system.scheduler.scheduleOnce(delay, context.self, SessionActor.CharSavedMsg)
|
||||
}
|
||||
|
||||
def charSaved(): Unit = {
|
||||
sendResponse(ChatMsg(ChatMessageType.UNK_227, wideContents=false, "", "@charsaved", None))
|
||||
}
|
||||
|
||||
override protected[session] def actionsToCancel(): Unit = {
|
||||
progressBarValue = None
|
||||
kitToBeUsed = None
|
||||
collisionHistory.clear()
|
||||
accessedContainer match {
|
||||
case Some(v: Vehicle) =>
|
||||
val vguid = v.GUID
|
||||
sessionLogic.vehicles.ConditionalDriverVehicleControl(v)
|
||||
if (v.AccessingTrunk.contains(player.GUID)) {
|
||||
if (player.VehicleSeated.contains(vguid)) {
|
||||
v.AccessingTrunk = None //player is seated; just stop accessing trunk
|
||||
if (player.isAlive) {
|
||||
sendResponse(UnuseItemMessage(player.GUID, vguid))
|
||||
}
|
||||
} else {
|
||||
unaccessContainer(v)
|
||||
}
|
||||
}
|
||||
|
||||
case Some(o) =>
|
||||
unaccessContainer(o)
|
||||
if (player.isAlive) {
|
||||
sendResponse(UnuseItemMessage(player.GUID, o.GUID))
|
||||
}
|
||||
|
||||
case None => ()
|
||||
}
|
||||
}
|
||||
|
||||
override protected[session] def stop(): Unit = {
|
||||
progressBarUpdate.cancel()
|
||||
charSavedTimer.cancel()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
// Copyright (c) 2024 PSForever
|
||||
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
|
||||
def chat: ChatFunctions
|
||||
def galaxy: GalaxyHandlerFunctions
|
||||
def general: GeneralFunctions
|
||||
def local: LocalHandlerFunctions
|
||||
def mountResponse: MountHandlerFunctions
|
||||
def squad: SquadHandlerFunctions
|
||||
def shooting: WeaponAndProjectileFunctions
|
||||
def terminals: TerminalHandlerFunctions
|
||||
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
|
||||
}
|
||||
|
||||
trait PlayerMode {
|
||||
def setup(data: SessionData): ModeLogic
|
||||
}
|
||||
|
|
@ -1,594 +1,37 @@
|
|||
// Copyright (c) 2023 PSForever
|
||||
package net.psforever.actors.session.support
|
||||
|
||||
import akka.actor.typed.scaladsl.adapter._
|
||||
import akka.actor.{ActorContext, typed}
|
||||
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry}
|
||||
import net.psforever.packet.game.objectcreate.ConstructorData
|
||||
import net.psforever.services.Service
|
||||
import net.psforever.objects.zones.exp
|
||||
|
||||
import scala.collection.mutable
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.concurrent.duration._
|
||||
//
|
||||
import net.psforever.actors.session.{AvatarActor, ChatActor}
|
||||
import net.psforever.login.WorldSession.{DropEquipmentFromInventory, DropLeftovers, HoldNewEquipmentUp}
|
||||
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.objects.{GlobalDefinitions, Player, Tool, Vehicle}
|
||||
import net.psforever.actors.session.AvatarActor
|
||||
import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent
|
||||
import net.psforever.packet.game._
|
||||
import net.psforever.services.avatar.{AvatarAction, AvatarResponse, AvatarServiceMessage}
|
||||
import net.psforever.services.{InterstellarClusterService => ICS}
|
||||
import net.psforever.services.avatar.AvatarResponse
|
||||
import net.psforever.types._
|
||||
import net.psforever.util.Config
|
||||
import net.psforever.zones.Zones
|
||||
|
||||
trait AvatarHandlerFunctions extends CommonSessionInterfacingFunctionality {
|
||||
val ops: SessionAvatarHandlers
|
||||
|
||||
def handle(toChannel: String, guid: PlanetSideGUID, reply: AvatarResponse.Response): Unit
|
||||
}
|
||||
|
||||
class SessionAvatarHandlers(
|
||||
val sessionData: SessionData,
|
||||
avatarActor: typed.ActorRef[AvatarActor.Command],
|
||||
chatActor: typed.ActorRef[ChatActor.Command],
|
||||
val sessionLogic: SessionData,
|
||||
val avatarActor: typed.ActorRef[AvatarActor.Command],
|
||||
implicit val context: ActorContext
|
||||
) extends CommonSessionInterfacingFunctionality {
|
||||
//TODO player characters only exist within a certain range of GUIDs for a given zone; this is overkill
|
||||
private[support] var lastSeenStreamMessage: mutable.LongMap[SessionAvatarHandlers.LastUpstream] =
|
||||
private[session] var lastSeenStreamMessage: mutable.LongMap[SessionAvatarHandlers.LastUpstream] =
|
||||
mutable.LongMap[SessionAvatarHandlers.LastUpstream]()
|
||||
private[this] val hidingPlayerRandomizer = new scala.util.Random
|
||||
private[session] val hidingPlayerRandomizer = new scala.util.Random
|
||||
|
||||
/**
|
||||
* 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) = 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 (
|
||||
sessionData.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,
|
||||
sessionData.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 ||
|
||||
sessionData.canSeeReallyFar ||
|
||||
durationSince > targetDelay
|
||||
)
|
||||
)
|
||||
) {
|
||||
//must draw
|
||||
sendResponse(
|
||||
PlayerStateMessage(
|
||||
guid,
|
||||
pos,
|
||||
vel,
|
||||
yaw,
|
||||
pitch,
|
||||
yawUpper,
|
||||
timestamp = 0, //is this okay?
|
||||
isCrouching,
|
||||
isJumping,
|
||||
jumpThrust,
|
||||
isCloaking
|
||||
)
|
||||
)
|
||||
lastSeenStreamMessage.put(guid.guid, SessionAvatarHandlers.LastUpstream(Some(pstateToSave), visible=true, wasShooting, now))
|
||||
} else {
|
||||
//is visible, but skip reinforcement
|
||||
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 + 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
|
||||
)
|
||||
)
|
||||
lastSeenStreamMessage.put(guid.guid, SessionAvatarHandlers.LastUpstream(Some(pstateToSave), visible=false, wasShooting, now))
|
||||
} else {
|
||||
//skip drawing altogether
|
||||
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(sessionData.terminals.usingMedicalTerminal).collect {
|
||||
case term: Terminal with ProximityUnit => sessionData.terminals.StopUsingProximityUnit(term)
|
||||
}
|
||||
if (sessionData.zoning.zoningStatus == Zoning.Status.Deconstructing) {
|
||||
sessionData.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 && lastSeenStreamMessage.get(guid.guid).exists { _.visible } =>
|
||||
sendResponse(ChangeFireStateMessage_Start(weaponGuid))
|
||||
val entry = lastSeenStreamMessage(guid.guid)
|
||||
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 && lastSeenStreamMessage.get(guid.guid).exists { msg => msg.visible || msg.shooting.nonEmpty } =>
|
||||
sendResponse(ChangeFireStateMessage_Stop(weaponGuid))
|
||||
val entry = lastSeenStreamMessage(guid.guid)
|
||||
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))
|
||||
sessionData.zoning.CancelZoningProcessWithDescriptiveReason("cancel_dmg")
|
||||
|
||||
case AvatarResponse.DestroyDisplay(killer, victim, method, unk)
|
||||
if killer.CharId == avatar.id && killer.Faction != victim.Faction =>
|
||||
sendResponse(sessionData.destroyDisplayMessage(killer, victim, method, unk))
|
||||
|
||||
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(sessionData.destroyDisplayMessage(killer, victim, method, unk))
|
||||
|
||||
case AvatarResponse.TerminalOrderResult(terminalGuid, action, result)
|
||||
if result && (action == TransactionType.Buy || action == TransactionType.Loadout) =>
|
||||
sendResponse(ItemTransactionResultMessage(terminalGuid, action, result))
|
||||
sessionData.terminals.lastTerminalOrderFulfillment = true
|
||||
AvatarActor.savePlayerData(player)
|
||||
sessionData.renewCharSavedTimer(
|
||||
Config.app.game.savedMsg.interruptedByAction.fixed,
|
||||
Config.app.game.savedMsg.interruptedByAction.variable
|
||||
)
|
||||
|
||||
case AvatarResponse.TerminalOrderResult(terminalGuid, action, result) =>
|
||||
sendResponse(ItemTransactionResultMessage(terminalGuid, action, result))
|
||||
sessionData.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
|
||||
))
|
||||
}
|
||||
sessionData.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(_, "") =>
|
||||
sessionData.kitToBeUsed = None
|
||||
|
||||
case AvatarResponse.KitNotUsed(_, msg) =>
|
||||
sessionData.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) =>
|
||||
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 && 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 (sessionData.shooting.shotsWhileDead > 0) {
|
||||
log.warn(
|
||||
s"SHOTS_WHILE_DEAD: client of ${avatar.name} fired ${sessionData.shooting.shotsWhileDead} rounds while character was dead on server"
|
||||
)
|
||||
sessionData.shooting.shotsWhileDead = 0
|
||||
}
|
||||
sessionData.zoning.CancelZoningProcessWithDescriptiveReason(msg = "cancel")
|
||||
sessionData.renewCharSavedTimer(fixedLen = 1800L, varLen = 0L)
|
||||
|
||||
//player state changes
|
||||
AvatarActor.updateToolDischargeFor(avatar)
|
||||
player.FreeHand.Equipment.foreach { item =>
|
||||
DropEquipmentFromInventory(player)(item)
|
||||
}
|
||||
sessionData.dropSpecialSlotItem()
|
||||
sessionData.toggleMaxSpecialState(enable = false)
|
||||
sessionData.keepAliveFunc = sessionData.zoning.NormalKeepAlive
|
||||
sessionData.zoning.zoningStatus = Zoning.Status.None
|
||||
sessionData.zoning.spawn.deadState = DeadState.Dead
|
||||
continent.GUID(mount).collect { case obj: Vehicle =>
|
||||
sessionData.vehicles.ConditionalDriverVehicleControl(obj)
|
||||
sessionData.unaccessContainer(obj)
|
||||
}
|
||||
sessionData.playerActionsToCancel()
|
||||
sessionData.terminals.CancelAllProximityUnits()
|
||||
AvatarActor.savePlayerLocation(player)
|
||||
sessionData.zoning.spawn.shiftPosition = Some(player.Position)
|
||||
|
||||
//respawn
|
||||
val respawnTimer = 300.seconds
|
||||
sessionData.zoning.spawn.reviveTimer.cancel()
|
||||
if (player.death_by == 0) {
|
||||
sessionData.zoning.spawn.reviveTimer = context.system.scheduler.scheduleOnce(respawnTimer) {
|
||||
sessionData.cluster ! ICS.GetRandomSpawnPoint(
|
||||
Zones.sanctuaryZoneNumber(player.Faction),
|
||||
player.Faction,
|
||||
Seq(SpawnGroup.Sanctuary),
|
||||
context.self
|
||||
)
|
||||
}
|
||||
} else {
|
||||
sessionData.zoning.spawn.HandleReleaseAvatar(player, continent)
|
||||
}
|
||||
|
||||
case AvatarResponse.Release(tplayer) if isNotSameTarget =>
|
||||
sessionData.zoning.spawn.DepictPlayerAsCorpse(tplayer)
|
||||
|
||||
case AvatarResponse.Revive(revivalTargetGuid) if resolvedPlayerGuid == revivalTargetGuid =>
|
||||
log.info(s"No time for rest, ${player.Name}. Back on your feet!")
|
||||
sessionData.zoning.spawn.reviveTimer.cancel()
|
||||
sessionData.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 && lastSeenStreamMessage.get(guid.guid).exists { _.visible } =>
|
||||
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 =>
|
||||
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?
|
||||
sessionData.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() =>
|
||||
sessionData.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 && 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 _ => ()
|
||||
}
|
||||
}
|
||||
|
||||
private def changeAmmoProcedures(
|
||||
def changeAmmoProcedures(
|
||||
weaponGuid: PlanetSideGUID,
|
||||
previousAmmoGuid: PlanetSideGUID,
|
||||
ammoTypeId: Int,
|
||||
|
|
@ -608,11 +51,11 @@ class SessionAvatarHandlers(
|
|||
)
|
||||
}
|
||||
|
||||
private def facilityCaptureRewards(buildingId: Int, zoneNumber: Int, cep: Long): Unit = {
|
||||
def facilityCaptureRewards(buildingId: Int, zoneNumber: Int, cep: Long): Unit = {
|
||||
//TODO squad services deactivated, participation trophy rewards for now - 11-20-2023
|
||||
//must be in a squad to earn experience
|
||||
val charId = player.CharId
|
||||
val squadUI = sessionData.squad.squadUI
|
||||
val squadUI = sessionLogic.squad.squadUI
|
||||
val participation = continent
|
||||
.Building(buildingId)
|
||||
.map { building =>
|
||||
|
|
@ -672,10 +115,49 @@ class SessionAvatarHandlers(
|
|||
Some(modifiedExp)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Properly format a `DestroyDisplayMessage` packet
|
||||
* given sufficient information about a target (victim) and an actor (killer).
|
||||
* For the packet, the `charId` field is important for determining distinction between players.
|
||||
* @param killer the killer's entry
|
||||
* @param victim the victim's entry
|
||||
* @param method the manner of death
|
||||
* @param unk na;
|
||||
* defaults to 121, the object id of `avatar`
|
||||
* @return a `DestroyDisplayMessage` packet that is properly formatted
|
||||
*/
|
||||
def destroyDisplayMessage(
|
||||
killer: SourceEntry,
|
||||
victim: SourceEntry,
|
||||
method: Int,
|
||||
unk: Int = 121
|
||||
): DestroyDisplayMessage = {
|
||||
val killerSeated = killer match {
|
||||
case obj: PlayerSource => obj.Seated
|
||||
case _ => false
|
||||
}
|
||||
val victimSeated = victim match {
|
||||
case obj: PlayerSource => obj.Seated
|
||||
case _ => false
|
||||
}
|
||||
new DestroyDisplayMessage(
|
||||
killer.Name,
|
||||
killer.CharId,
|
||||
killer.Faction,
|
||||
killerSeated,
|
||||
unk,
|
||||
method,
|
||||
victim.Name,
|
||||
victim.CharId,
|
||||
victim.Faction,
|
||||
victimSeated
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
object SessionAvatarHandlers {
|
||||
private[support] case class LastUpstream(
|
||||
private[session] case class LastUpstream(
|
||||
msg: Option[AvatarResponse.PlayerState],
|
||||
visible: Boolean,
|
||||
shooting: Option[PlanetSideGUID],
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -2,177 +2,22 @@
|
|||
package net.psforever.actors.session.support
|
||||
|
||||
import akka.actor.{ActorContext, ActorRef, typed}
|
||||
import scala.concurrent.duration._
|
||||
import net.psforever.packet.game.FriendsResponse
|
||||
//
|
||||
import net.psforever.actors.session.AvatarActor
|
||||
import net.psforever.objects.Vehicle
|
||||
import net.psforever.packet.game.{AvatarDeadStateMessage, BroadcastWarpgateUpdateMessage, DeadState, HotSpotInfo => PacketHotSpotInfo, HotSpotUpdateMessage, ZoneInfoMessage, ZonePopulationUpdateMessage}
|
||||
import net.psforever.services.Service
|
||||
import net.psforever.services.galaxy.GalaxyResponse
|
||||
import net.psforever.types.{MemberAction, PlanetSideEmpire}
|
||||
|
||||
class SessionGalaxyHandlers(
|
||||
val sessionData: SessionData,
|
||||
avatarActor: typed.ActorRef[AvatarActor.Command],
|
||||
galaxyService: ActorRef,
|
||||
implicit val context: ActorContext
|
||||
) extends CommonSessionInterfacingFunctionality {
|
||||
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) }
|
||||
)
|
||||
)
|
||||
trait GalaxyHandlerFunctions extends CommonSessionInterfacingFunctionality {
|
||||
def ops: SessionGalaxyHandlers
|
||||
|
||||
case GalaxyResponse.MapUpdate(msg) =>
|
||||
sendResponse(msg)
|
||||
def handleUpdateIgnoredPlayers(pkt: FriendsResponse): Unit
|
||||
|
||||
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) =>
|
||||
sessionData.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 _ => ()
|
||||
}
|
||||
}
|
||||
def handle(reply: GalaxyResponse.Response): Unit
|
||||
}
|
||||
|
||||
/*package net.psforever.actors.session.support
|
||||
|
||||
import akka.actor.{ActorContext, ActorRef, typed}
|
||||
import scala.concurrent.duration._
|
||||
//
|
||||
import net.psforever.actors.session.AvatarActor
|
||||
import net.psforever.objects.Vehicle
|
||||
import net.psforever.packet.game.{AvatarDeadStateMessage, BroadcastWarpgateUpdateMessage, DeadState, HotSpotInfo => PacketHotSpotInfo, HotSpotUpdateMessage, ZoneInfoMessage, ZonePopulationUpdateMessage}
|
||||
import net.psforever.services.Service
|
||||
import net.psforever.services.galaxy.GalaxyResponse
|
||||
import net.psforever.types.{MemberAction, PlanetSideEmpire}
|
||||
|
||||
class SessionGalaxyHandlers(
|
||||
val sessionData: SessionData,
|
||||
avatarActor: typed.ActorRef[AvatarActor.Command],
|
||||
galaxyService: ActorRef,
|
||||
val sessionLogic: SessionData,
|
||||
val avatarActor: typed.ActorRef[AvatarActor.Command],
|
||||
val galaxyService: ActorRef,
|
||||
implicit val context: ActorContext
|
||||
) extends CommonSessionInterfacingFunctionality {
|
||||
def handle(reply: GalaxyResponse.Response): Unit = {
|
||||
reply match {
|
||||
case GalaxyResponse.HotSpotUpdate(zoneIndex, priority, hotSpotInfo) =>
|
||||
sendResponse(
|
||||
HotSpotUpdateMessage(
|
||||
zoneIndex,
|
||||
priority,
|
||||
hotSpotInfo.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(tempChannel, vehicle, _, manifest) =>
|
||||
val playerName = player.Name
|
||||
log.debug(s"TransferPassenger: $playerName received the summons to transfer to ${vehicle.Zone.id} ...")
|
||||
manifest.passengers
|
||||
.find { _.name.equals(playerName) }
|
||||
.collect {
|
||||
case entry if vehicle.Seats(entry.mount).occupant.isEmpty =>
|
||||
player.VehicleSeated = None
|
||||
vehicle.Seats(entry.mount).mount(player)
|
||||
player.VehicleSeated = vehicle.GUID
|
||||
Some(vehicle)
|
||||
case entry if vehicle.Seats(entry.mount).occupant.contains(player) =>
|
||||
Some(vehicle)
|
||||
case entry =>
|
||||
log.warn(
|
||||
s"TransferPassenger: $playerName tried to mount seat ${entry.mount} during summoning, but it was already occupied, and ${player.Sex.pronounSubject} was rebuked"
|
||||
)
|
||||
None
|
||||
}.orElse {
|
||||
manifest.cargo.find { _.name.equals(playerName) }.flatMap { entry =>
|
||||
vehicle.CargoHolds(entry.mount).occupant.collect {
|
||||
case cargo if cargo.Seats(0).occupants.exists(_.Name.equals(playerName)) => cargo
|
||||
}
|
||||
}
|
||||
} match {
|
||||
case Some(v: Vehicle) =>
|
||||
galaxyService ! Service.Leave(Some(tempChannel)) //temporary vehicle-specific channel (see above)
|
||||
sessionData.zoning.spawn.deadState = DeadState.Release
|
||||
sendResponse(AvatarDeadStateMessage(DeadState.Release, 0, 0, player.Position, player.Faction, unk5=true))
|
||||
sessionData.zoning.interstellarFerry = Some(v) //on the other continent and registered to that continent's GUID system
|
||||
sessionData.zoning.spawn.LoadZonePhysicalSpawnPoint(v.Continent, v.Position, v.Orientation, 1 seconds, None)
|
||||
case _ =>
|
||||
sessionData.zoning.interstellarFerry match {
|
||||
case None =>
|
||||
galaxyService ! Service.Leave(Some(tempChannel)) //no longer being transferred between zones
|
||||
sessionData.zoning.interstellarFerryTopLevelGUID = None
|
||||
case Some(_) => ;
|
||||
//wait patiently
|
||||
}
|
||||
}
|
||||
|
||||
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 _ => ()
|
||||
}
|
||||
}
|
||||
}*/
|
||||
) extends CommonSessionInterfacingFunctionality
|
||||
|
|
|
|||
|
|
@ -2,262 +2,16 @@
|
|||
package net.psforever.actors.session.support
|
||||
|
||||
import akka.actor.ActorContext
|
||||
import net.psforever.objects.ce.Deployable
|
||||
import net.psforever.objects.vehicles.MountableWeapons
|
||||
import net.psforever.objects._
|
||||
import net.psforever.packet.game.PlanetsideAttributeEnum.PlanetsideAttributeEnum
|
||||
import net.psforever.packet.game._
|
||||
import net.psforever.services.Service
|
||||
import net.psforever.services.local.LocalResponse
|
||||
import net.psforever.types.{ChatMessageType, PlanetSideGUID, Vector3}
|
||||
import net.psforever.types.PlanetSideGUID
|
||||
|
||||
trait LocalHandlerFunctions extends CommonSessionInterfacingFunctionality {
|
||||
def ops: SessionLocalHandlers
|
||||
|
||||
def handle(toChannel: String, guid: PlanetSideGUID, reply: LocalResponse.Response): Unit
|
||||
}
|
||||
|
||||
class SessionLocalHandlers(
|
||||
val sessionData: SessionData,
|
||||
val sessionLogic: SessionData,
|
||||
implicit val context: ActorContext
|
||||
) extends CommonSessionInterfacingFunctionality {
|
||||
/**
|
||||
* 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) =>
|
||||
sessionData.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) =>
|
||||
HackObject(targetGuid, unk1, unk2)
|
||||
|
||||
case LocalResponse.PlanetsideAttribute(targetGuid, attributeType, attributeValue) =>
|
||||
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
|
||||
sessionData.specialItemSlotGuid.collect { case guid if guid == lluGuid =>
|
||||
sessionData.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))
|
||||
sessionData.terminals.ForgetAllProximityTerminals(objectGuid)
|
||||
|
||||
case LocalResponse.RouterTelepadMessage(msg) =>
|
||||
sendResponse(ChatMsg(ChatMessageType.UNK_229, wideContents=false, recipient="", msg, note=None))
|
||||
|
||||
case LocalResponse.RouterTelepadTransport(passengerGuid, srcGuid, destGuid) =>
|
||||
sessionData.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) =>
|
||||
sessionData.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 _ => ()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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))
|
||||
}
|
||||
|
||||
/**
|
||||
* na
|
||||
* @param targetGuid na
|
||||
* @param unk1 na
|
||||
* @param unk2 na
|
||||
*/
|
||||
def HackObject(targetGuid: PlanetSideGUID, unk1: Long, unk2: Long): Unit = {
|
||||
sendResponse(HackMessage(unk1=0, targetGuid, player_guid=Service.defaultPlayerGUID, progress=100, unk1, HackState.Hacked, unk2))
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a PlanetsideAttributeMessage packet to the client
|
||||
* @param targetGuid The target of the attribute
|
||||
* @param attributeType The attribute number
|
||||
* @param attributeValue The attribute value
|
||||
*/
|
||||
def SendPlanetsideAttributeMessage(
|
||||
targetGuid: PlanetSideGUID,
|
||||
attributeType: PlanetsideAttributeEnum,
|
||||
attributeValue: Long
|
||||
): Unit = {
|
||||
sendResponse(PlanetsideAttributeMessage(targetGuid, attributeType, attributeValue))
|
||||
}
|
||||
}
|
||||
) extends CommonSessionInterfacingFunctionality
|
||||
|
|
|
|||
|
|
@ -2,361 +2,49 @@
|
|||
package net.psforever.actors.session.support
|
||||
|
||||
import akka.actor.{ActorContext, typed}
|
||||
import net.psforever.objects.serverobject.affinity.FactionAffinity
|
||||
import net.psforever.objects.serverobject.environment.interaction.ResetAllEnvironmentInteractions
|
||||
import net.psforever.objects.vital.InGameHistory
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import net.psforever.objects.Tool
|
||||
import net.psforever.objects.vehicles.MountableWeapons
|
||||
import net.psforever.packet.game.{DismountVehicleCargoMsg, InventoryStateMessage, MountVehicleCargoMsg, MountVehicleMsg}
|
||||
//
|
||||
import net.psforever.actors.session.AvatarActor
|
||||
import net.psforever.actors.zone.ZoneActor
|
||||
import net.psforever.objects.{GlobalDefinitions, PlanetSideGameObject, Player, Vehicle, Vehicles}
|
||||
import net.psforever.objects.definition.{BasicDefinition, ObjectDefinition}
|
||||
import net.psforever.objects.serverobject.hackable.GenericHackables.getTurretUpgradeTime
|
||||
import net.psforever.objects.Player
|
||||
import net.psforever.objects.serverobject.mount.Mountable
|
||||
import net.psforever.objects.serverobject.terminals.implant.ImplantTerminalMech
|
||||
import net.psforever.objects.serverobject.turret.{FacilityTurret, WeaponTurret}
|
||||
import net.psforever.objects.vehicles.AccessPermissionGroup
|
||||
import net.psforever.packet.game.{ChatMsg, DelayedPathMountMsg, DismountVehicleMsg, GenericObjectActionMessage, ObjectAttachMessage, ObjectDetachMessage, PlanetsideAttributeMessage, 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, ChatMessageType, PlanetSideGUID, Vector3}
|
||||
import net.psforever.packet.game.DismountVehicleMsg
|
||||
|
||||
trait MountHandlerFunctions extends CommonSessionInterfacingFunctionality {
|
||||
val ops: SessionMountHandlers
|
||||
|
||||
def handleMountVehicle(pkt: MountVehicleMsg): Unit
|
||||
|
||||
def handleDismountVehicle(pkt: DismountVehicleMsg): Unit
|
||||
|
||||
def handleMountVehicleCargo(pkt: MountVehicleCargoMsg): Unit
|
||||
|
||||
def handleDismountVehicleCargo(pkt: DismountVehicleCargoMsg): Unit
|
||||
|
||||
def handle(tplayer: Player, reply: Mountable.Exchange): Unit
|
||||
}
|
||||
|
||||
class SessionMountHandlers(
|
||||
val sessionData: SessionData,
|
||||
avatarActor: typed.ActorRef[AvatarActor.Command],
|
||||
val sessionLogic: SessionData,
|
||||
val avatarActor: typed.ActorRef[AvatarActor.Command],
|
||||
implicit val context: ActorContext
|
||||
) extends CommonSessionInterfacingFunctionality {
|
||||
/**
|
||||
* na
|
||||
*
|
||||
* @param tplayer na
|
||||
* @param reply na
|
||||
* From a mount, find the weapon controlled from it, and update the ammunition counts for that weapon's magazines.
|
||||
* @param objWithSeat the object that owns seats (and weaponry)
|
||||
* @param seatNum the mount
|
||||
*/
|
||||
def handle(tplayer: Player, reply: Mountable.Exchange): Unit = {
|
||||
reply match {
|
||||
case Mountable.CanMount(obj: ImplantTerminalMech, seatNumber, _) =>
|
||||
sessionData.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
|
||||
log.info(s"${player.Name} mounts an implant terminal")
|
||||
sessionData.terminals.CancelAllProximityUnits()
|
||||
MountingAction(tplayer, obj, seatNumber)
|
||||
sessionData.keepAliveFunc = sessionData.keepAlivePersistence
|
||||
|
||||
case Mountable.CanMount(obj: Vehicle, seatNumber, _)
|
||||
if obj.Definition == GlobalDefinitions.orbital_shuttle =>
|
||||
sessionData.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
|
||||
log.info(s"${player.Name} mounts the orbital shuttle")
|
||||
sessionData.terminals.CancelAllProximityUnits()
|
||||
MountingAction(tplayer, obj, seatNumber)
|
||||
sessionData.keepAliveFunc = sessionData.keepAlivePersistence
|
||||
|
||||
case Mountable.CanMount(obj: Vehicle, seatNumber, _)
|
||||
if obj.Definition == GlobalDefinitions.ant =>
|
||||
sessionData.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
|
||||
log.info(s"${player.Name} mounts the driver seat of the ${obj.Definition.Name}")
|
||||
val obj_guid: PlanetSideGUID = obj.GUID
|
||||
sessionData.terminals.CancelAllProximityUnits()
|
||||
sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health))
|
||||
sendResponse(PlanetsideAttributeMessage(obj_guid, obj.Definition.shieldUiAttribute, obj.Shields))
|
||||
sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=45, obj.NtuCapacitorScaled))
|
||||
sendResponse(GenericObjectActionMessage(obj_guid, code=11))
|
||||
sessionData.accessContainer(obj)
|
||||
tplayer.Actor ! ResetAllEnvironmentInteractions
|
||||
MountingAction(tplayer, obj, seatNumber)
|
||||
|
||||
case Mountable.CanMount(obj: Vehicle, seatNumber, _)
|
||||
if obj.Definition == GlobalDefinitions.quadstealth =>
|
||||
sessionData.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
|
||||
log.info(s"${player.Name} mounts the driver seat of the ${obj.Definition.Name}")
|
||||
val obj_guid: PlanetSideGUID = obj.GUID
|
||||
sessionData.terminals.CancelAllProximityUnits()
|
||||
sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health))
|
||||
sendResponse(PlanetsideAttributeMessage(obj_guid, obj.Definition.shieldUiAttribute, obj.Shields))
|
||||
//exclusive to the wraith, cloak state matches the cloak state of the driver
|
||||
//phantasm doesn't uncloak if the driver is uncloaked and no other vehicle cloaks
|
||||
obj.Cloaked = tplayer.Cloaked
|
||||
sendResponse(GenericObjectActionMessage(obj_guid, code=11))
|
||||
sessionData.accessContainer(obj)
|
||||
tplayer.Actor ! ResetAllEnvironmentInteractions
|
||||
MountingAction(tplayer, obj, seatNumber)
|
||||
|
||||
case Mountable.CanMount(obj: Vehicle, seatNumber, _)
|
||||
if seatNumber == 0 && obj.Definition.MaxCapacitor > 0 =>
|
||||
sessionData.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
|
||||
log.info(s"${player.Name} mounts the driver seat of the ${obj.Definition.Name}")
|
||||
val obj_guid: PlanetSideGUID = obj.GUID
|
||||
sessionData.terminals.CancelAllProximityUnits()
|
||||
sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health))
|
||||
sendResponse(PlanetsideAttributeMessage(obj_guid, obj.Definition.shieldUiAttribute, obj.Shields))
|
||||
sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=113, obj.Capacitor))
|
||||
sendResponse(GenericObjectActionMessage(obj_guid, code=11))
|
||||
sessionData.accessContainer(obj)
|
||||
sessionData.updateWeaponAtSeatPosition(obj, seatNumber)
|
||||
tplayer.Actor ! ResetAllEnvironmentInteractions
|
||||
MountingAction(tplayer, obj, seatNumber)
|
||||
|
||||
case Mountable.CanMount(obj: Vehicle, seatNumber, _)
|
||||
if seatNumber == 0 =>
|
||||
sessionData.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
|
||||
log.info(s"${player.Name} mounts the driver seat of the ${obj.Definition.Name}")
|
||||
val obj_guid: PlanetSideGUID = obj.GUID
|
||||
sessionData.terminals.CancelAllProximityUnits()
|
||||
sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health))
|
||||
sendResponse(PlanetsideAttributeMessage(obj_guid, obj.Definition.shieldUiAttribute, obj.Shields))
|
||||
sendResponse(GenericObjectActionMessage(obj_guid, code=11))
|
||||
sessionData.accessContainer(obj)
|
||||
sessionData.updateWeaponAtSeatPosition(obj, seatNumber)
|
||||
tplayer.Actor ! ResetAllEnvironmentInteractions
|
||||
MountingAction(tplayer, obj, seatNumber)
|
||||
|
||||
case Mountable.CanMount(obj: Vehicle, seatNumber, _)
|
||||
if obj.Definition.MaxCapacitor > 0 =>
|
||||
sessionData.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
|
||||
log.info(s"${player.Name} mounts ${
|
||||
obj.SeatPermissionGroup(seatNumber) match {
|
||||
case Some(seatType) => s"a $seatType seat (#$seatNumber)"
|
||||
case None => "a seat"
|
||||
}
|
||||
} of the ${obj.Definition.Name}")
|
||||
val obj_guid: PlanetSideGUID = obj.GUID
|
||||
sessionData.terminals.CancelAllProximityUnits()
|
||||
sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health))
|
||||
sendResponse(PlanetsideAttributeMessage(obj_guid, obj.Definition.shieldUiAttribute, obj.Shields))
|
||||
sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=113, obj.Capacitor))
|
||||
sessionData.accessContainer(obj)
|
||||
sessionData.updateWeaponAtSeatPosition(obj, seatNumber)
|
||||
sessionData.keepAliveFunc = sessionData.keepAlivePersistence
|
||||
tplayer.Actor ! ResetAllEnvironmentInteractions
|
||||
MountingAction(tplayer, obj, seatNumber)
|
||||
|
||||
case Mountable.CanMount(obj: Vehicle, seatNumber, _) =>
|
||||
sessionData.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
|
||||
log.info(s"${player.Name} mounts the ${
|
||||
obj.SeatPermissionGroup(seatNumber) match {
|
||||
case Some(seatType) => s"a $seatType seat (#$seatNumber)"
|
||||
case None => "a seat"
|
||||
}
|
||||
} of the ${obj.Definition.Name}")
|
||||
val obj_guid: PlanetSideGUID = obj.GUID
|
||||
sessionData.terminals.CancelAllProximityUnits()
|
||||
sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health))
|
||||
sendResponse(PlanetsideAttributeMessage(obj_guid, obj.Definition.shieldUiAttribute, obj.Shields))
|
||||
sessionData.accessContainer(obj)
|
||||
sessionData.updateWeaponAtSeatPosition(obj, seatNumber)
|
||||
sessionData.keepAliveFunc = sessionData.keepAlivePersistence
|
||||
tplayer.Actor ! ResetAllEnvironmentInteractions
|
||||
MountingAction(tplayer, obj, seatNumber)
|
||||
|
||||
case Mountable.CanMount(obj: FacilityTurret, seatNumber, _)
|
||||
if obj.Definition == GlobalDefinitions.vanu_sentry_turret =>
|
||||
sessionData.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
|
||||
log.info(s"${player.Name} mounts the ${obj.Definition.Name}")
|
||||
obj.Zone.LocalEvents ! LocalServiceMessage(obj.Zone.id, LocalAction.SetEmpire(obj.GUID, player.Faction))
|
||||
sendResponse(PlanetsideAttributeMessage(obj.GUID, attribute_type=0, obj.Health))
|
||||
sessionData.updateWeaponAtSeatPosition(obj, seatNumber)
|
||||
MountingAction(tplayer, obj, seatNumber)
|
||||
|
||||
case Mountable.CanMount(obj: FacilityTurret, seatNumber, _)
|
||||
if !obj.isUpgrading || System.currentTimeMillis() - getTurretUpgradeTime >= 1500L =>
|
||||
obj.setMiddleOfUpgrade(false)
|
||||
sessionData.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
|
||||
log.info(s"${player.Name} mounts the ${obj.Definition.Name}")
|
||||
sendResponse(PlanetsideAttributeMessage(obj.GUID, attribute_type=0, obj.Health))
|
||||
sessionData.updateWeaponAtSeatPosition(obj, seatNumber)
|
||||
MountingAction(tplayer, obj, seatNumber)
|
||||
|
||||
case Mountable.CanMount(obj: FacilityTurret, _, _) =>
|
||||
sessionData.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
|
||||
log.warn(
|
||||
s"MountVehicleMsg: ${tplayer.Name} wants to mount turret ${obj.GUID.guid}, but needs to wait until it finishes updating"
|
||||
)
|
||||
|
||||
case Mountable.CanMount(obj: PlanetSideGameObject with FactionAffinity with WeaponTurret with InGameHistory, seatNumber, _) =>
|
||||
sessionData.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
|
||||
log.info(s"${player.Name} mounts the ${obj.Definition.asInstanceOf[BasicDefinition].Name}")
|
||||
sendResponse(PlanetsideAttributeMessage(obj.GUID, attribute_type=0, obj.Health))
|
||||
sessionData.updateWeaponAtSeatPosition(obj, seatNumber)
|
||||
MountingAction(tplayer, obj, seatNumber)
|
||||
|
||||
case Mountable.CanMount(obj: Mountable, _, _) =>
|
||||
log.warn(s"MountVehicleMsg: $obj is some kind of mountable object but nothing will happen for ${player.Name}")
|
||||
|
||||
case Mountable.CanDismount(obj: ImplantTerminalMech, seatNum, _) =>
|
||||
log.info(s"${tplayer.Name} dismounts the implant terminal")
|
||||
DismountAction(tplayer, obj, seatNum)
|
||||
|
||||
case Mountable.CanDismount(obj: Vehicle, _, mountPoint)
|
||||
if obj.Definition == GlobalDefinitions.orbital_shuttle && obj.MountedIn.nonEmpty =>
|
||||
//dismount to hart lobby
|
||||
val pguid = player.GUID
|
||||
log.info(s"${tplayer.Name} dismounts the orbital shuttle into the lobby")
|
||||
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))
|
||||
)
|
||||
sessionData.keepAliveFunc = sessionData.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
|
||||
log.info(s"${player.Name} is prepped for dropping")
|
||||
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
|
||||
)
|
||||
sessionData.keepAliveFunc = sessionData.zoning.NormalKeepAlive
|
||||
|
||||
case Mountable.CanDismount(obj: Vehicle, seatNum, _)
|
||||
if obj.Definition == GlobalDefinitions.droppod =>
|
||||
log.info(s"${tplayer.Name} has landed on ${continent.id}")
|
||||
sessionData.unaccessContainer(obj)
|
||||
DismountAction(tplayer, obj, seatNum)
|
||||
obj.Actor ! Vehicle.Deconstruct()
|
||||
|
||||
case Mountable.CanDismount(obj: Vehicle, seatNum, _)
|
||||
if tplayer.GUID == player.GUID =>
|
||||
//disembarking self
|
||||
log.info(s"${player.Name} dismounts the ${obj.Definition.Name}'s ${
|
||||
obj.SeatPermissionGroup(seatNum) match {
|
||||
case Some(AccessPermissionGroup.Driver) => "driver seat"
|
||||
case Some(seatType) => s"$seatType seat (#$seatNum)"
|
||||
case None => "seat"
|
||||
}
|
||||
}")
|
||||
sessionData.vehicles.ConditionalDriverVehicleControl(obj)
|
||||
sessionData.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, _) =>
|
||||
log.info(s"${tplayer.Name} dismounts a ${obj.Definition.asInstanceOf[ObjectDefinition].Name}")
|
||||
DismountAction(tplayer, obj, seatNum)
|
||||
|
||||
case Mountable.CanDismount(obj: Mountable, _, _) =>
|
||||
log.warn(s"DismountVehicleMsg: $obj is some dismountable object but nothing will happen for ${player.Name}")
|
||||
|
||||
case Mountable.CanNotMount(obj: Vehicle, seatNumber) =>
|
||||
log.warn(s"MountVehicleMsg: ${tplayer.Name} attempted to mount $obj's seat $seatNumber, but was not allowed")
|
||||
obj.GetSeatFromMountPoint(seatNumber).collect {
|
||||
case seatNum if obj.SeatPermissionGroup(seatNum).contains(AccessPermissionGroup.Driver) =>
|
||||
sendResponse(
|
||||
ChatMsg(ChatMessageType.CMT_OPEN, wideContents=false, recipient="", "You are not the driver of this vehicle.", note=None)
|
||||
)
|
||||
}
|
||||
|
||||
case Mountable.CanNotMount(obj: Mountable, seatNumber) =>
|
||||
log.warn(s"MountVehicleMsg: ${tplayer.Name} attempted to mount $obj's seat $seatNumber, but was not allowed")
|
||||
|
||||
case Mountable.CanNotDismount(obj, seatNum) =>
|
||||
log.warn(s"DismountVehicleMsg: ${tplayer.Name} attempted to dismount $obj's mount $seatNum, but was not allowed")
|
||||
def updateWeaponAtSeatPosition(objWithSeat: MountableWeapons, seatNum: Int): Unit = {
|
||||
objWithSeat.WeaponControlledFromSeat(seatNum) foreach {
|
||||
case weapon: Tool =>
|
||||
//update mounted weapon belonging to mount
|
||||
weapon.AmmoSlots.foreach(slot => {
|
||||
//update the magazine(s) in the weapon, specifically
|
||||
val magazine = slot.Box
|
||||
sendResponse(InventoryStateMessage(magazine.GUID, weapon.GUID, magazine.Capacity.toLong))
|
||||
})
|
||||
case _ => () //no weapons to update
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Common activities/procedure when a player mounts a valid object.
|
||||
* @param tplayer the player
|
||||
* @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 = {
|
||||
val playerGuid: PlanetSideGUID = tplayer.GUID
|
||||
val objGuid: PlanetSideGUID = obj.GUID
|
||||
sessionData.playerActionsToCancel()
|
||||
avatarActor ! AvatarActor.DeactivateActiveImplants()
|
||||
avatarActor ! AvatarActor.SuspendStaminaRegeneration(3.seconds)
|
||||
sendResponse(ObjectAttachMessage(objGuid, playerGuid, seatNum))
|
||||
continent.VehicleEvents ! VehicleServiceMessage(
|
||||
continent.id,
|
||||
VehicleAction.MountVehicle(playerGuid, objGuid, seatNum)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
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 =>
|
||||
sessionData.vehicles.serverVehicleControlVelocity.collect { _ =>
|
||||
sessionData.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
|
||||
)
|
||||
)
|
||||
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
|
||||
*/
|
||||
def DismountAction(tplayer: Player, obj: PlanetSideGameObject with FactionAffinity with InGameHistory, seatNum: Int): Unit = {
|
||||
val playerGuid: PlanetSideGUID = tplayer.GUID
|
||||
tplayer.ContributionFrom(obj)
|
||||
sessionData.keepAliveFunc = sessionData.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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,40 +4,48 @@ package net.psforever.actors.session.support
|
|||
import akka.actor.{ActorContext, ActorRef, typed}
|
||||
import scala.collection.mutable
|
||||
//
|
||||
import net.psforever.actors.session.{AvatarActor, ChatActor}
|
||||
import net.psforever.objects.avatar.Avatar
|
||||
import net.psforever.actors.session.AvatarActor
|
||||
import net.psforever.objects.teamwork.Squad
|
||||
import net.psforever.objects.{Default, LivePlayerList, Player}
|
||||
import net.psforever.objects.{Default, Player}
|
||||
import net.psforever.packet.game._
|
||||
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
|
||||
import net.psforever.services.chat.ChatService
|
||||
import net.psforever.services.teamwork.{SquadResponse, SquadServiceMessage, SquadAction => SquadServiceAction}
|
||||
import net.psforever.types.{ChatMessageType, PlanetSideEmpire, PlanetSideGUID, SquadListDecoration, SquadResponseType, Vector3, WaypointSubtype}
|
||||
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, Vector3}
|
||||
|
||||
trait SquadHandlerFunctions extends CommonSessionInterfacingFunctionality {
|
||||
val ops: SessionSquadHandlers
|
||||
|
||||
def handleSquadDefinitionAction(pkt: SquadDefinitionActionMessage): Unit
|
||||
|
||||
def handleSquadMemberRequest(pkt: SquadMembershipRequest): Unit
|
||||
|
||||
def handleSquadWaypointRequest(pkt: SquadWaypointRequest): Unit
|
||||
|
||||
def handle(response: SquadResponse.Response, excluded: Iterable[Long]): Unit
|
||||
}
|
||||
|
||||
object SessionSquadHandlers {
|
||||
protected final case class SquadUIElement(
|
||||
name: String,
|
||||
outfit: Long,
|
||||
index: Int,
|
||||
zone: Int,
|
||||
health: Int,
|
||||
armor: Int,
|
||||
position: Vector3
|
||||
)
|
||||
final case class SquadUIElement(
|
||||
name: String,
|
||||
outfit: Long,
|
||||
index: Int,
|
||||
zone: Int,
|
||||
health: Int,
|
||||
armor: Int,
|
||||
position: Vector3
|
||||
)
|
||||
}
|
||||
|
||||
class SessionSquadHandlers(
|
||||
val sessionData: SessionData,
|
||||
avatarActor: typed.ActorRef[AvatarActor.Command],
|
||||
chatActor: typed.ActorRef[ChatActor.Command],
|
||||
squadService: ActorRef,
|
||||
val sessionLogic: SessionData,
|
||||
val avatarActor: typed.ActorRef[AvatarActor.Command],
|
||||
val squadService: ActorRef,
|
||||
implicit val context: ActorContext
|
||||
) extends CommonSessionInterfacingFunctionality {
|
||||
import SessionSquadHandlers._
|
||||
|
||||
private var waypointCooldown: Long = 0L
|
||||
val squadUI: mutable.LongMap[SquadUIElement] = new mutable.LongMap[SquadUIElement]()
|
||||
var squad_supplement_id: Int = 0
|
||||
private[session] val squadUI: mutable.LongMap[SquadUIElement] = new mutable.LongMap[SquadUIElement]()
|
||||
private[session] var squad_supplement_id: Int = 0
|
||||
/**
|
||||
* When joining or creating a squad, the original state of the avatar's internal LFS variable is blanked.
|
||||
* This `WorldSessionActor`-local variable is then used to indicate the ongoing state of the LFS UI component,
|
||||
|
|
@ -46,340 +54,11 @@ class SessionSquadHandlers(
|
|||
* Upon leaving or disbanding a squad, this value is made false.
|
||||
* Control switching between the `Avatar`-local and the `WorldSessionActor`-local variable is contingent on `squadUI` being populated.
|
||||
*/
|
||||
private[support] var squadSetup: () => Unit = FirstTimeSquadSetup
|
||||
private var squadUpdateCounter: Int = 0
|
||||
private[session] var squadSetup: () => Unit = FirstTimeSquadSetup
|
||||
private[session] var squadUpdateCounter: Int = 0
|
||||
private[session] var updateSquad: () => Unit = NoSquadUpdates
|
||||
private[session] var updateSquadRef: ActorRef = Default.Actor
|
||||
private val queuedSquadActions: Seq[() => Unit] = Seq(SquadUpdates, NoSquadUpdates, NoSquadUpdates, NoSquadUpdates)
|
||||
private[support] var updateSquad: () => Unit = NoSquadUpdates
|
||||
private var updateSquadRef: ActorRef = Default.Actor
|
||||
|
||||
/* packet */
|
||||
|
||||
def handleSquadDefinitionAction(pkt: SquadDefinitionActionMessage): Unit = {
|
||||
val SquadDefinitionActionMessage(u1, u2, action) = pkt
|
||||
squadService ! SquadServiceMessage(player, continent, SquadServiceAction.Definition(u1, u2, action))
|
||||
}
|
||||
|
||||
def handleSquadMemberRequest(pkt: SquadMembershipRequest): Unit = {
|
||||
val SquadMembershipRequest(request_type, char_id, unk3, player_name, unk5) = pkt
|
||||
squadService ! SquadServiceMessage(
|
||||
player,
|
||||
continent,
|
||||
SquadServiceAction.Membership(request_type, char_id, unk3, player_name, unk5)
|
||||
)
|
||||
}
|
||||
|
||||
def handleSquadWaypointRequest(pkt: SquadWaypointRequest): Unit = {
|
||||
val SquadWaypointRequest(request, _, wtype, unk, info) = pkt
|
||||
val time = System.currentTimeMillis()
|
||||
val subtype = wtype.subtype
|
||||
if(subtype == WaypointSubtype.Squad) {
|
||||
squadService ! SquadServiceMessage(player, continent, SquadServiceAction.Waypoint(request, wtype, unk, info))
|
||||
} else if (subtype == WaypointSubtype.Laze && time - waypointCooldown > 1000) {
|
||||
//guarding against duplicating laze waypoints
|
||||
waypointCooldown = time
|
||||
squadService ! SquadServiceMessage(player, continent, SquadServiceAction.Waypoint(request, wtype, unk, info))
|
||||
}
|
||||
}
|
||||
|
||||
/* response handlers */
|
||||
|
||||
def handle(response: SquadResponse.Response, excluded: Iterable[Long]): Unit = {
|
||||
if (!excluded.exists(_ == avatar.id)) {
|
||||
response match {
|
||||
case SquadResponse.ListSquadFavorite(line, task) =>
|
||||
sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), line, SquadAction.ListSquadFavorite(task)))
|
||||
|
||||
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 (
|
||||
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.IdentifyAsSquadLeader(squad_guid) =>
|
||||
sendResponse(SquadDefinitionActionMessage(squad_guid, 0, SquadAction.IdentifyAsSquadLeader()))
|
||||
|
||||
case SquadResponse.SetListSquad(squad_guid) =>
|
||||
sendResponse(SquadDefinitionActionMessage(squad_guid, 0, SquadAction.SetListSquad()))
|
||||
|
||||
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.WantsSquadPosition(_, name) =>
|
||||
sendResponse(
|
||||
ChatMsg(
|
||||
ChatMessageType.CMT_SQUAD,
|
||||
wideContents=true,
|
||||
name,
|
||||
s"\\#6 would like to join your squad. (respond with \\#3/accept\\#6 or \\#3/reject\\#6)",
|
||||
None
|
||||
)
|
||||
)
|
||||
|
||||
case SquadResponse.Join(squad, positionsToUpdate, _, ref) =>
|
||||
val avatarId = avatar.id
|
||||
val membershipPositions = (positionsToUpdate map squad.Membership.zipWithIndex)
|
||||
.filter { case (mem, index) =>
|
||||
mem.CharId > 0 && positionsToUpdate.contains(index)
|
||||
}
|
||||
membershipPositions.find { case (mem, _) => mem.CharId == avatarId } match {
|
||||
case Some((ourMember, ourIndex)) =>
|
||||
//we are joining the squad
|
||||
//load each member's entry (our own too)
|
||||
squad_supplement_id = squad.GUID.guid + 1
|
||||
membershipPositions.foreach {
|
||||
case (member, index) =>
|
||||
sendResponse(
|
||||
SquadMemberEvent.Add(
|
||||
squad_supplement_id,
|
||||
member.CharId,
|
||||
index,
|
||||
member.Name,
|
||||
member.ZoneId,
|
||||
outfit_id = 0
|
||||
)
|
||||
)
|
||||
squadUI(member.CharId) =
|
||||
SquadUIElement(member.Name, outfit=0L, index, member.ZoneId, member.Health, member.Armor, member.Position)
|
||||
}
|
||||
//repeat our entry
|
||||
sendResponse(
|
||||
SquadMemberEvent.Add(
|
||||
squad_supplement_id,
|
||||
ourMember.CharId,
|
||||
ourIndex,
|
||||
ourMember.Name,
|
||||
ourMember.ZoneId,
|
||||
outfit_id = 0
|
||||
)
|
||||
)
|
||||
//turn lfs off
|
||||
if (avatar.lookingForSquad) {
|
||||
avatarActor ! AvatarActor.SetLookingForSquad(false)
|
||||
}
|
||||
val playerGuid = player.GUID
|
||||
val factionChannel = s"${player.Faction}"
|
||||
//squad colors
|
||||
GiveSquadColorsToMembers()
|
||||
GiveSquadColorsForOthers(playerGuid, factionChannel, squad_supplement_id)
|
||||
//associate with member position in squad
|
||||
sendResponse(PlanetsideAttributeMessage(playerGuid, 32, ourIndex))
|
||||
//a finalization? what does this do?
|
||||
sendResponse(SquadDefinitionActionMessage(squad.GUID, 0, SquadAction.Unknown(18)))
|
||||
squadService ! SquadServiceMessage(player, continent, SquadServiceAction.ReloadDecoration())
|
||||
updateSquadRef = ref
|
||||
updateSquad = PeriodicUpdatesWhenEnrolledInSquad
|
||||
chatActor ! ChatActor.JoinChannel(ChatService.ChatChannel.Squad(squad.GUID))
|
||||
case _ =>
|
||||
//other player is joining our squad
|
||||
//load each member's entry
|
||||
GiveSquadColorsToMembers(
|
||||
membershipPositions.map {
|
||||
case (member, index) =>
|
||||
val charId = member.CharId
|
||||
sendResponse(
|
||||
SquadMemberEvent.Add(squad_supplement_id, charId, index, member.Name, member.ZoneId, outfit_id = 0)
|
||||
)
|
||||
squadUI(charId) =
|
||||
SquadUIElement(member.Name, outfit=0L, index, member.ZoneId, member.Health, member.Armor, member.Position)
|
||||
charId
|
||||
}
|
||||
)
|
||||
}
|
||||
//send an initial dummy update for map icon(s)
|
||||
sendResponse(
|
||||
SquadState(
|
||||
PlanetSideGUID(squad_supplement_id),
|
||||
membershipPositions.map { case (member, _) =>
|
||||
SquadStateInfo(member.CharId, member.Health, member.Armor, member.Position)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
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)
|
||||
updateSquadRef = Default.Actor
|
||||
positionsToUpdate.foreach {
|
||||
case (member, index) =>
|
||||
sendResponse(SquadMemberEvent.Remove(squad_supplement_id, member, index))
|
||||
squadUI.remove(member)
|
||||
}
|
||||
//uninitialize
|
||||
val playerGuid = player.GUID
|
||||
sendResponse(SquadMemberEvent.Remove(squad_supplement_id, ourMember, ourIndex)) //repeat of our entry
|
||||
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)))
|
||||
squad_supplement_id = 0
|
||||
squadUpdateCounter = 0
|
||||
updateSquad = NoSquadUpdates
|
||||
chatActor ! ChatActor.LeaveChannel(ChatService.ChatChannel.Squad(squad.GUID))
|
||||
case _ =>
|
||||
//remove each member's entry
|
||||
GiveSquadColorsToMembers(
|
||||
positionsToUpdate.map {
|
||||
case (member, index) =>
|
||||
sendResponse(SquadMemberEvent.Remove(squad_supplement_id, member, index))
|
||||
squadUI.remove(member)
|
||||
member
|
||||
},
|
||||
value = 0
|
||||
)
|
||||
}
|
||||
|
||||
case SquadResponse.AssignMember(squad, from_index, to_index) =>
|
||||
//we've already swapped position internally; now we swap the cards
|
||||
SwapSquadUIElements(squad, from_index, to_index)
|
||||
|
||||
case SquadResponse.PromoteMember(squad, promotedPlayer, from_index) =>
|
||||
if (promotedPlayer != player.CharId) {
|
||||
//demoted from leader; no longer lfsm
|
||||
if (player.avatar.lookingForSquad) {
|
||||
avatarActor ! AvatarActor.SetLookingForSquad(false)
|
||||
}
|
||||
}
|
||||
sendResponse(SquadMemberEvent(MemberEvent.Promote, squad.GUID.guid, promotedPlayer, position = 0))
|
||||
//the players have already been swapped in the backend object
|
||||
PromoteSquadUIElements(squad, from_index)
|
||||
|
||||
case SquadResponse.UpdateMembers(_, positions) =>
|
||||
val pairedEntries = positions.collect {
|
||||
case entry if squadUI.contains(entry.char_id) =>
|
||||
(entry, 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(squad_supplement_id, entry.char_id, element.index, entry.zone_number)
|
||||
)
|
||||
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
|
||||
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(squad_supplement_id),
|
||||
updatedEntries.map { entry =>
|
||||
SquadStateInfo(entry.char_id, entry.health, entry.armor, entry.pos)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
case SquadResponse.CharacterKnowledge(charId, name, certs, u1, u2, zone) =>
|
||||
sendResponse(CharacterKnowledgeMessage(charId, Some(CharacterKnowledgeInfo(name, certs, u1, u2, zone))))
|
||||
|
||||
case SquadResponse.SquadSearchResults(results) =>
|
||||
//TODO positive squad search results message?
|
||||
// if(results.nonEmpty) {
|
||||
// results.foreach { guid =>
|
||||
// sendResponse(SquadDefinitionActionMessage(
|
||||
// guid,
|
||||
// 0,
|
||||
// SquadAction.SquadListDecorator(SquadListDecoration.SearchResult))
|
||||
// )
|
||||
// }
|
||||
// } else {
|
||||
// sendResponse(SquadDefinitionActionMessage(player.GUID, 0, SquadAction.NoSquadSearchResults()))
|
||||
// }
|
||||
// sendResponse(SquadDefinitionActionMessage(player.GUID, 0, SquadAction.CancelSquadSearch()))
|
||||
|
||||
case SquadResponse.InitWaypoints(char_id, waypoints) =>
|
||||
waypoints.foreach {
|
||||
case (waypoint_type, info, unk) =>
|
||||
sendResponse(
|
||||
SquadWaypointEvent.Add(
|
||||
squad_supplement_id,
|
||||
char_id,
|
||||
waypoint_type,
|
||||
WaypointEvent(info.zone_number, info.pos, unk)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
case SquadResponse.WaypointEvent(WaypointEventAction.Add, char_id, waypoint_type, _, Some(info), unk) =>
|
||||
sendResponse(
|
||||
SquadWaypointEvent.Add(
|
||||
squad_supplement_id,
|
||||
char_id,
|
||||
waypoint_type,
|
||||
WaypointEvent(info.zone_number, info.pos, unk)
|
||||
)
|
||||
)
|
||||
|
||||
case SquadResponse.WaypointEvent(WaypointEventAction.Remove, char_id, waypoint_type, _, _, _) =>
|
||||
sendResponse(SquadWaypointEvent.Remove(squad_supplement_id, char_id, waypoint_type))
|
||||
|
||||
case _ => ;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* These messages are dispatched when first starting up the client and connecting to the server for the first time.
|
||||
|
|
|
|||
|
|
@ -2,181 +2,42 @@
|
|||
package net.psforever.actors.session.support
|
||||
|
||||
import akka.actor.{ActorContext, typed}
|
||||
import net.psforever.objects.sourcing.AmenitySource
|
||||
import net.psforever.objects.vital.TerminalUsedActivity
|
||||
import net.psforever.objects.guid.GUIDTask
|
||||
import net.psforever.packet.game.FavoritesRequest
|
||||
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.concurrent.Future
|
||||
//
|
||||
import net.psforever.actors.session.AvatarActor
|
||||
import net.psforever.login.WorldSession.{BuyNewEquipmentPutInInventory, SellEquipmentFromInventory}
|
||||
import net.psforever.objects.{GlobalDefinitions, Player, Vehicle}
|
||||
import net.psforever.objects.guid.{StraightforwardTask, TaskBundle, TaskWorkflow}
|
||||
import net.psforever.objects.guid.{StraightforwardTask, TaskBundle}
|
||||
import net.psforever.objects.PlanetSideGameObject
|
||||
import net.psforever.objects.equipment.EffectTarget
|
||||
import net.psforever.objects.serverobject.CommonMessages
|
||||
import net.psforever.objects.serverobject.pad.VehicleSpawnPad
|
||||
import net.psforever.objects.serverobject.terminals.{ProximityDefinition, ProximityUnit, Terminal}
|
||||
import net.psforever.packet.game.{ItemTransactionMessage, ItemTransactionResultMessage,ProximityTerminalUseMessage, UnuseItemMessage}
|
||||
import net.psforever.types.{PlanetSideGUID, TransactionType, Vector3}
|
||||
import net.psforever.packet.game.{ItemTransactionMessage,ProximityTerminalUseMessage}
|
||||
import net.psforever.types.PlanetSideGUID
|
||||
|
||||
trait TerminalHandlerFunctions extends CommonSessionInterfacingFunctionality {
|
||||
def ops: SessionTerminalHandlers
|
||||
|
||||
def handleItemTransaction(pkt: ItemTransactionMessage): Unit
|
||||
|
||||
def handleProximityTerminalUse(pkt: ProximityTerminalUseMessage): Unit
|
||||
|
||||
def handleFavoritesRequest(pkt: FavoritesRequest): Unit
|
||||
|
||||
def handle(tplayer: Player, msg: ItemTransactionMessage, order: Terminal.Exchange): Unit
|
||||
}
|
||||
|
||||
class SessionTerminalHandlers(
|
||||
val sessionData: SessionData,
|
||||
avatarActor: typed.ActorRef[AvatarActor.Command],
|
||||
val sessionLogic: SessionData,
|
||||
val avatarActor: typed.ActorRef[AvatarActor.Command],
|
||||
implicit val context: ActorContext
|
||||
) extends CommonSessionInterfacingFunctionality {
|
||||
private[support] var lastTerminalOrderFulfillment: Boolean = true
|
||||
private[support] var usingMedicalTerminal: Option[PlanetSideGUID] = None
|
||||
|
||||
/* packets */
|
||||
|
||||
def handleItemTransaction(pkt: ItemTransactionMessage): Unit = {
|
||||
val ItemTransactionMessage(terminalGuid, transactionType, _, itemName, _, _) = pkt
|
||||
continent.GUID(terminalGuid) match {
|
||||
case Some(term: Terminal) if lastTerminalOrderFulfillment =>
|
||||
val msg: String = if (itemName.nonEmpty) s" of $itemName" else ""
|
||||
log.info(s"${player.Name} is submitting an order - a $transactionType from a ${term.Definition.Name}$msg")
|
||||
lastTerminalOrderFulfillment = false
|
||||
sessionData.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use")
|
||||
term.Actor ! Terminal.Request(player, pkt)
|
||||
case Some(_: Terminal) =>
|
||||
log.warn(s"Please Wait until your previous order has been fulfilled, ${player.Name}")
|
||||
case Some(obj) =>
|
||||
log.error(s"ItemTransaction: ${obj.Definition.Name} is not a terminal, ${player.Name}")
|
||||
case _ =>
|
||||
log.error(s"ItemTransaction: entity with guid=${terminalGuid.guid} does not exist, ${player.Name}")
|
||||
}
|
||||
}
|
||||
|
||||
def handleProximityTerminalUse(pkt: ProximityTerminalUseMessage): Unit = {
|
||||
val ProximityTerminalUseMessage(_, objectGuid, _) = pkt
|
||||
continent.GUID(objectGuid) match {
|
||||
case Some(obj: Terminal with ProximityUnit) =>
|
||||
HandleProximityTerminalUse(obj)
|
||||
case Some(obj) =>
|
||||
log.warn(s"ProximityTerminalUse: ${obj.Definition.Name} guid=${objectGuid.guid} is not ready to implement proximity effects")
|
||||
case None =>
|
||||
log.error(s"ProximityTerminalUse: ${player.Name} can not find an object with guid ${objectGuid.guid}")
|
||||
}
|
||||
}
|
||||
|
||||
/* response handler */
|
||||
|
||||
/**
|
||||
* na
|
||||
* @param tplayer na
|
||||
* @param msg na
|
||||
* @param order na
|
||||
*/
|
||||
def handle(tplayer: Player, msg: ItemTransactionMessage, order: Terminal.Exchange): Unit = {
|
||||
order match {
|
||||
case Terminal.BuyEquipment(item)
|
||||
if tplayer.avatar.purchaseCooldown(item.Definition).nonEmpty =>
|
||||
sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, success = false))
|
||||
lastTerminalOrderFulfillment = true
|
||||
|
||||
case Terminal.BuyEquipment(item) =>
|
||||
avatarActor ! AvatarActor.UpdatePurchaseTime(item.Definition)
|
||||
TaskWorkflow.execute(BuyNewEquipmentPutInInventory(
|
||||
continent.GUID(tplayer.VehicleSeated) match {
|
||||
case Some(v: Vehicle) => v
|
||||
case _ => player
|
||||
},
|
||||
tplayer,
|
||||
msg.terminal_guid
|
||||
)(item))
|
||||
|
||||
case Terminal.SellEquipment() =>
|
||||
SellEquipmentFromInventory(tplayer, tplayer, msg.terminal_guid)(Player.FreeHandSlot)
|
||||
|
||||
case Terminal.LearnCertification(cert) =>
|
||||
avatarActor ! AvatarActor.LearnCertification(msg.terminal_guid, cert)
|
||||
lastTerminalOrderFulfillment = true
|
||||
|
||||
case Terminal.SellCertification(cert) =>
|
||||
avatarActor ! AvatarActor.SellCertification(msg.terminal_guid, cert)
|
||||
lastTerminalOrderFulfillment = true
|
||||
|
||||
case Terminal.LearnImplant(implant) =>
|
||||
avatarActor ! AvatarActor.LearnImplant(msg.terminal_guid, implant)
|
||||
lastTerminalOrderFulfillment = true
|
||||
|
||||
case Terminal.SellImplant(implant) =>
|
||||
avatarActor ! AvatarActor.SellImplant(msg.terminal_guid, implant)
|
||||
lastTerminalOrderFulfillment = true
|
||||
|
||||
case Terminal.BuyVehicle(vehicle, _, _)
|
||||
if tplayer.avatar.purchaseCooldown(vehicle.Definition).nonEmpty || tplayer.spectator =>
|
||||
sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, success = false))
|
||||
lastTerminalOrderFulfillment = true
|
||||
|
||||
case Terminal.BuyVehicle(vehicle, weapons, trunk) =>
|
||||
continent.map.terminalToSpawnPad
|
||||
.find { case (termid, _) => termid == msg.terminal_guid.guid }
|
||||
.map { case (a: Int, b: Int) => (continent.GUID(a), continent.GUID(b)) }
|
||||
.collect { case (Some(term: Terminal), Some(pad: VehicleSpawnPad)) =>
|
||||
avatarActor ! AvatarActor.UpdatePurchaseTime(vehicle.Definition)
|
||||
vehicle.Faction = tplayer.Faction
|
||||
vehicle.Position = pad.Position
|
||||
vehicle.Orientation = pad.Orientation + Vector3.z(pad.Definition.VehicleCreationZOrientOffset)
|
||||
//default loadout, weapons
|
||||
val vWeapons = vehicle.Weapons
|
||||
weapons.foreach { entry =>
|
||||
vWeapons.get(entry.start) match {
|
||||
case Some(slot) =>
|
||||
entry.obj.Faction = tplayer.Faction
|
||||
slot.Equipment = None
|
||||
slot.Equipment = entry.obj
|
||||
case None =>
|
||||
log.warn(
|
||||
s"BuyVehicle: ${player.Name} tries to apply default loadout to $vehicle on spawn, but can not find a mounted weapon for ${entry.start}"
|
||||
)
|
||||
}
|
||||
}
|
||||
//default loadout, trunk
|
||||
val vTrunk = vehicle.Trunk
|
||||
vTrunk.Clear()
|
||||
trunk.foreach { entry =>
|
||||
entry.obj.Faction = tplayer.Faction
|
||||
vTrunk.InsertQuickly(entry.start, entry.obj)
|
||||
}
|
||||
TaskWorkflow.execute(registerVehicleFromSpawnPad(vehicle, pad, term))
|
||||
sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, success = true))
|
||||
if (GlobalDefinitions.isBattleFrameVehicle(vehicle.Definition)) {
|
||||
sendResponse(UnuseItemMessage(player.GUID, msg.terminal_guid))
|
||||
}
|
||||
player.LogActivity(TerminalUsedActivity(AmenitySource(term), msg.transaction_type))
|
||||
}
|
||||
.orElse {
|
||||
log.error(
|
||||
s"${tplayer.Name} wanted to spawn a vehicle, but there was no spawn pad associated with terminal ${msg.terminal_guid} to accept it"
|
||||
)
|
||||
sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, success = false))
|
||||
None
|
||||
}
|
||||
lastTerminalOrderFulfillment = true
|
||||
|
||||
case Terminal.NoDeal() if msg != null =>
|
||||
val transaction = msg.transaction_type
|
||||
log.warn(s"NoDeal: ${tplayer.Name} made a request but the terminal rejected the ${transaction.toString} order")
|
||||
sendResponse(ItemTransactionResultMessage(msg.terminal_guid, transaction, success = false))
|
||||
lastTerminalOrderFulfillment = true
|
||||
|
||||
case _ =>
|
||||
val terminal = msg.terminal_guid.guid
|
||||
continent.GUID(terminal) match {
|
||||
case Some(term: Terminal) =>
|
||||
log.warn(s"NoDeal?: ${tplayer.Name} made a request but the ${term.Definition.Name}#$terminal rejected the missing order")
|
||||
case Some(_) =>
|
||||
log.warn(s"NoDeal?: ${tplayer.Name} made a request to a non-terminal entity#$terminal")
|
||||
case None =>
|
||||
log.warn(s"NoDeal?: ${tplayer.Name} made a request to a missing entity#$terminal")
|
||||
}
|
||||
lastTerminalOrderFulfillment = true
|
||||
}
|
||||
}
|
||||
|
||||
/* support */
|
||||
private[session] var lastTerminalOrderFulfillment: Boolean = true
|
||||
private[session] var usingMedicalTerminal: Option[PlanetSideGUID] = None
|
||||
|
||||
/**
|
||||
* Construct tasking that adds a completed and registered vehicle into the scene.
|
||||
|
|
@ -188,7 +49,7 @@ class SessionTerminalHandlers(
|
|||
* @see `RegisterVehicle`
|
||||
* @return a `TaskBundle` message
|
||||
*/
|
||||
private[session] def registerVehicleFromSpawnPad(vehicle: Vehicle, pad: VehicleSpawnPad, terminal: Terminal): TaskBundle = {
|
||||
def registerVehicleFromSpawnPad(vehicle: Vehicle, pad: VehicleSpawnPad, terminal: Terminal): TaskBundle = {
|
||||
TaskBundle(
|
||||
new StraightforwardTask() {
|
||||
private val localVehicle = vehicle
|
||||
|
|
@ -203,7 +64,7 @@ class SessionTerminalHandlers(
|
|||
Future(true)
|
||||
}
|
||||
},
|
||||
List(sessionData.registerVehicle(vehicle))
|
||||
List(registerVehicle(vehicle))
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -293,7 +154,7 @@ class SessionTerminalHandlers(
|
|||
|
||||
/**
|
||||
* Cease all current interactions with proximity-based units.
|
||||
* Pair with `PlayerActionsToCancel`, except when logging out (stopping).
|
||||
* Pair with `actionsToCancel`, except when logging out (stopping).
|
||||
* This operations may invoke callback messages.
|
||||
* @see `postStop`
|
||||
*/
|
||||
|
|
@ -303,7 +164,7 @@ class SessionTerminalHandlers(
|
|||
|
||||
/**
|
||||
* Cease all current interactions with proximity-based units.
|
||||
* Pair with `PlayerActionsToCancel`, except when logging out (stopping).
|
||||
* Pair with `actionsToCancel`, except when logging out (stopping).
|
||||
* This operations may invoke callback messages.
|
||||
* @param guid globally unique identifier for a proximity terminal
|
||||
* @see `postStop`
|
||||
|
|
@ -326,4 +187,29 @@ class SessionTerminalHandlers(
|
|||
usingMedicalTerminal = None
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct tasking that adds a completed and registered vehicle into the scene.
|
||||
* Use this function to renew the globally unique identifiers on a vehicle that has already been added to the scene once.
|
||||
* @param vehicle the `Vehicle` object
|
||||
* @see `RegisterVehicleFromSpawnPad`
|
||||
* @return a `TaskBundle` message
|
||||
*/
|
||||
def registerVehicle(vehicle: Vehicle): TaskBundle = {
|
||||
TaskBundle(
|
||||
new StraightforwardTask() {
|
||||
private val localVehicle = vehicle
|
||||
|
||||
override def description(): String = s"register a ${localVehicle.Definition.Name}"
|
||||
|
||||
def action(): Future[Any] = Future(true)
|
||||
},
|
||||
List(GUIDTask.registerVehicle(continent.GUID, vehicle))
|
||||
)
|
||||
}
|
||||
|
||||
override protected[session] def actionsToCancel(): Unit = {
|
||||
lastTerminalOrderFulfillment = true
|
||||
usingMedicalTerminal = None
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,390 +3,18 @@ package net.psforever.actors.session.support
|
|||
|
||||
import akka.actor.{ActorContext, ActorRef, typed}
|
||||
import net.psforever.actors.session.AvatarActor
|
||||
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.objects.{GlobalDefinitions, Player, Tool, Vehicle, Vehicles}
|
||||
import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent
|
||||
import net.psforever.packet.game._
|
||||
import net.psforever.services.Service
|
||||
import net.psforever.services.vehicle.{VehicleResponse, VehicleServiceResponse}
|
||||
import net.psforever.types.{BailType, ChatMessageType, PlanetSideGUID, Vector3}
|
||||
import net.psforever.services.vehicle.VehicleResponse
|
||||
import net.psforever.types.PlanetSideGUID
|
||||
|
||||
import scala.concurrent.duration._
|
||||
trait VehicleHandlerFunctions extends CommonSessionInterfacingFunctionality {
|
||||
def ops: SessionVehicleHandlers
|
||||
|
||||
def handle(toChannel: String, guid: PlanetSideGUID, reply: VehicleResponse.Response): Unit
|
||||
}
|
||||
|
||||
class SessionVehicleHandlers(
|
||||
val sessionData: SessionData,
|
||||
avatarActor: typed.ActorRef[AvatarActor.Command],
|
||||
galaxyService: ActorRef,
|
||||
val sessionLogic: SessionData,
|
||||
val avatarActor: typed.ActorRef[AvatarActor.Command],
|
||||
val galaxyService: ActorRef,
|
||||
implicit val context: ActorContext
|
||||
) extends CommonSessionInterfacingFunctionality {
|
||||
/**
|
||||
* 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
|
||||
sessionData.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) =>
|
||||
sessionData.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) =>
|
||||
sessionData.zoning.spawn.amsSpawnPoints = list.filter(tube => tube.Faction == player.Faction)
|
||||
sessionData.zoning.spawn.DrawCurrentAmsSpawnPoint()
|
||||
|
||||
case VehicleResponse.TransferPassengerChannel(oldChannel, tempChannel, vehicle, vehicleToDelete) if isNotSameTarget =>
|
||||
sessionData.zoning.interstellarFerry = Some(vehicle)
|
||||
sessionData.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 && sessionData.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
|
||||
sessionData.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
|
||||
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 && sessionData.zoning.spawn.deadState == DeadState.Alive =>
|
||||
sessionData.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)
|
||||
sessionData.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)
|
||||
)
|
||||
)
|
||||
sessionData.vehicles.serverVehicleControlVelocity = Some(0)
|
||||
|
||||
case VehicleResponse.ServerVehicleOverrideStart(vehicle, _) =>
|
||||
val vdef = vehicle.Definition
|
||||
sessionData.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, _) =>
|
||||
sessionData.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))
|
||||
}
|
||||
sessionData.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 sessionData.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))
|
||||
}
|
||||
sessionData.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
|
||||
sessionData.playerActionsToCancel()
|
||||
sessionData.terminals.CancelAllProximityUnits()
|
||||
sessionData.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
) extends CommonSessionInterfacingFunctionality
|
||||
|
|
|
|||
|
|
@ -5,466 +5,39 @@ import akka.actor.{ActorContext, typed}
|
|||
import net.psforever.actors.session.AvatarActor
|
||||
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.vehicles.{AccessPermissionGroup, CargoBehavior}
|
||||
import net.psforever.objects.zones.Zone
|
||||
import net.psforever.objects._
|
||||
import net.psforever.objects.serverobject.PlanetSideServerObject
|
||||
import net.psforever.packet.game.{ChildObjectStateMessage, DeployRequestMessage, DismountVehicleCargoMsg, DismountVehicleMsg, MountVehicleCargoMsg, MountVehicleMsg, VehicleSubStateMessage, _}
|
||||
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
|
||||
import net.psforever.types.{BailType, DriveState, Vector3}
|
||||
import net.psforever.packet.game.{ChildObjectStateMessage, DeployRequestMessage, VehicleSubStateMessage, _}
|
||||
import net.psforever.types.DriveState
|
||||
|
||||
class VehicleOperations(
|
||||
val sessionData: SessionData,
|
||||
avatarActor: typed.ActorRef[AvatarActor.Command],
|
||||
implicit val context: ActorContext
|
||||
) extends CommonSessionInterfacingFunctionality {
|
||||
private[support] var serverVehicleControlVelocity: Option[Int] = None
|
||||
trait VehicleFunctions extends CommonSessionInterfacingFunctionality {
|
||||
def ops: VehicleOperations
|
||||
|
||||
/* packets */
|
||||
def handleVehicleState(pkt: VehicleStateMessage): Unit
|
||||
|
||||
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
|
||||
sessionData.persist()
|
||||
sessionData.turnCounterFunc(player.GUID)
|
||||
sessionData.fallHeightTracker(pos.z)
|
||||
if (obj.MountedIn.isEmpty) {
|
||||
sessionData.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
|
||||
)
|
||||
)
|
||||
sessionData.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) {
|
||||
sessionData.kickedByAdministration()
|
||||
}
|
||||
}
|
||||
def handleFrameVehicleState(pkt: FrameVehicleStateMessage): Unit
|
||||
|
||||
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
|
||||
sessionData.persist()
|
||||
sessionData.turnCounterFunc(player.GUID)
|
||||
val (position, angle, velocity, notMountedState) = continent.GUID(obj.MountedIn) match {
|
||||
case Some(v: Vehicle) =>
|
||||
sessionData.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
|
||||
)
|
||||
)
|
||||
sessionData.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) {
|
||||
sessionData.kickedByAdministration()
|
||||
}
|
||||
}
|
||||
def handleChildObjectState(pkt: ChildObjectStateMessage): Unit
|
||||
|
||||
def handleChildObjectState(pkt: ChildObjectStateMessage): Unit = {
|
||||
val ChildObjectStateMessage(object_guid, pitch, yaw) = pkt
|
||||
val (o, tools) = sessionData.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 _ =>
|
||||
sessionData.persist()
|
||||
sessionData.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 =>
|
||||
//todo: old warning; this state is problematic, but can trigger in otherwise valid instances
|
||||
//log.warn(
|
||||
// s"ChildObjectState: ${player.Name} is using a different controllable agent than entity ${object_guid.guid}"
|
||||
//)
|
||||
case Some(_) =>
|
||||
//TODO set tool orientation?
|
||||
player.Orientation = Vector3(0f, pitch, yaw)
|
||||
continent.VehicleEvents ! VehicleServiceMessage(
|
||||
continent.id,
|
||||
VehicleAction.ChildObjectState(player.GUID, object_guid, pitch, yaw)
|
||||
)
|
||||
}
|
||||
//TODO status condition of "playing getting out of vehicle to allow for late packets without warning
|
||||
if (player.death_by == -1) {
|
||||
sessionData.kickedByAdministration()
|
||||
}
|
||||
}
|
||||
def handleVehicleSubState(pkt: VehicleSubStateMessage): Unit
|
||||
|
||||
def handleVehicleSubState(pkt: VehicleSubStateMessage): Unit = {
|
||||
val VehicleSubStateMessage(vehicle_guid, _, pos, ang, vel, unk1, _) = pkt
|
||||
sessionData.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
|
||||
sessionData.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 handleMountVehicle(pkt: MountVehicleMsg): Unit = {
|
||||
val MountVehicleMsg(_, mountable_guid, entry_point) = pkt
|
||||
sessionData.validObject(mountable_guid, decorator = "MountVehicle").collect {
|
||||
case obj: Mountable =>
|
||||
obj.Actor ! Mountable.TryMount(player, entry_point)
|
||||
case _ =>
|
||||
log.error(s"MountVehicleMsg: object ${mountable_guid.guid} not a mountable thing, ${player.Name}")
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
(sessionData.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
|
||||
sessionData.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) =>
|
||||
(
|
||||
(
|
||||
sessionData.validObject(obj_guid, decorator = "DismountVehicle/Vehicle"),
|
||||
sessionData.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
def handleMountVehicleCargo(pkt: MountVehicleCargoMsg): Unit = {
|
||||
val MountVehicleCargoMsg(_, cargo_guid, carrier_guid, _) = pkt
|
||||
(continent.GUID(cargo_guid), continent.GUID(carrier_guid)) match {
|
||||
case (Some(cargo: Vehicle), Some(carrier: Vehicle)) =>
|
||||
carrier.CargoHolds.find({ case (_, hold) => !hold.isOccupied }) match {
|
||||
case Some((mountPoint, _)) =>
|
||||
cargo.Actor ! CargoBehavior.StartCargoMounting(carrier_guid, mountPoint)
|
||||
case _ =>
|
||||
log.warn(
|
||||
s"MountVehicleCargoMsg: ${player.Name} trying to load cargo into a ${carrier.Definition.Name} which oes not have a cargo hold"
|
||||
)
|
||||
}
|
||||
case (None, _) | (Some(_), None) =>
|
||||
log.warn(
|
||||
s"MountVehicleCargoMsg: ${player.Name} lost a vehicle while working with cargo - either $carrier_guid or $cargo_guid"
|
||||
)
|
||||
case _ => ;
|
||||
}
|
||||
}
|
||||
|
||||
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 _ => ;
|
||||
}
|
||||
}
|
||||
|
||||
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) =>
|
||||
log.info(s"${player.Name} is requesting a deployment change for ${obj.Definition.Name} - $deploy_state")
|
||||
obj.Actor ! Deployment.TryDeploymentChange(deploy_state)
|
||||
|
||||
case _ =>
|
||||
log.error(s"DeployRequest: ${player.Name} can not find vehicle $vehicle_guid")
|
||||
avatarActor ! AvatarActor.SetVehicle(None)
|
||||
}
|
||||
} else {
|
||||
log.warn(s"${player.Name} must be mounted to request a deployment change")
|
||||
}
|
||||
} else {
|
||||
log.warn(s"DeployRequest: ${player.Name} does not own the deploying $vehicle_guid object")
|
||||
}
|
||||
}
|
||||
def handleDeployRequest(pkt: DeployRequestMessage): Unit
|
||||
|
||||
/* messages */
|
||||
|
||||
def handleCanDeploy(obj: Deployment.DeploymentObject, state: DriveState.Value): Unit = {
|
||||
if (state == DriveState.Deploying) {
|
||||
log.trace(s"DeployRequest: $obj transitioning to deploy state")
|
||||
} else if (state == DriveState.Deployed) {
|
||||
log.trace(s"DeployRequest: $obj has been Deployed")
|
||||
} else {
|
||||
CanNotChangeDeployment(obj, state, "incorrect deploy state")
|
||||
}
|
||||
}
|
||||
def handleCanDeploy(obj: Deployment.DeploymentObject, state: DriveState.Value): Unit
|
||||
|
||||
def handleCanUndeploy(obj: Deployment.DeploymentObject, state: DriveState.Value): Unit = {
|
||||
if (state == DriveState.Undeploying) {
|
||||
log.trace(s"DeployRequest: $obj transitioning to undeploy state")
|
||||
} else if (state == DriveState.Mobile) {
|
||||
log.trace(s"DeployRequest: $obj is Mobile")
|
||||
} else {
|
||||
CanNotChangeDeployment(obj, state, "incorrect undeploy state")
|
||||
}
|
||||
}
|
||||
def handleCanUndeploy(obj: Deployment.DeploymentObject, state: DriveState.Value): Unit
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
def handleCanNotChangeDeployment(obj: Deployment.DeploymentObject, state: DriveState.Value, reason: String): Unit
|
||||
}
|
||||
|
||||
/* support functions */
|
||||
class VehicleOperations(
|
||||
val sessionLogic: SessionData,
|
||||
val avatarActor: typed.ActorRef[AvatarActor.Command],
|
||||
implicit val context: ActorContext
|
||||
) extends CommonSessionInterfacingFunctionality {
|
||||
private[session] var serverVehicleControlVelocity: Option[Int] = None
|
||||
|
||||
/**
|
||||
* If the player is mounted in some entity, find that entity and get the mount index number at which the player is sat.
|
||||
|
|
@ -508,7 +81,7 @@ class VehicleOperations(
|
|||
* `(None, None)`, otherwise (even if the vehicle can be determined)
|
||||
*/
|
||||
def GetKnownVehicleAndSeat(): (Option[Vehicle], Option[Int]) =
|
||||
GetMountableAndSeat(sessionData.zoning.interstellarFerry, player, continent) match {
|
||||
GetMountableAndSeat(sessionLogic.zoning.interstellarFerry, player, continent) match {
|
||||
case (Some(v: Vehicle), Some(seat)) => (Some(v), Some(seat))
|
||||
case _ => (None, None)
|
||||
}
|
||||
|
|
@ -604,34 +177,9 @@ class VehicleOperations(
|
|||
* the client's player who is receiving this packet should be mounted as its driver, but this is not explicitly tested
|
||||
* @param pkt packet to instigate cancellable control
|
||||
*/
|
||||
def TotalDriverVehicleControlWithPacket(vehicle: Vehicle, pkt: ServerVehicleOverrideMsg): Unit = {
|
||||
private def TotalDriverVehicleControlWithPacket(vehicle: Vehicle, pkt: ServerVehicleOverrideMsg): Unit = {
|
||||
serverVehicleControlVelocity = None
|
||||
vehicle.DeploymentState = DriveState.Mobile
|
||||
sendResponse(pkt)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -6,6 +6,7 @@ import akka.actor.typed.scaladsl.adapter._
|
|||
import akka.actor.{ActorContext, ActorRef, Cancellable, typed}
|
||||
import akka.pattern.ask
|
||||
import akka.util.Timeout
|
||||
import net.psforever.actors.session.spectator.SpectatorMode
|
||||
import net.psforever.login.WorldSession
|
||||
import net.psforever.objects.avatar.BattleRank
|
||||
import net.psforever.objects.avatar.scoring.{CampaignStatistics, ScoreCard, SessionStatistics}
|
||||
|
|
@ -17,6 +18,7 @@ import net.psforever.objects.serverobject.turret.auto.AutomatedTurret
|
|||
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource}
|
||||
import net.psforever.objects.vital.{InGameHistory, IncarnationActivity, ReconstructionActivity, SpawningActivity}
|
||||
import net.psforever.packet.game.{CampaignStatistic, ChangeFireStateMessage_Start, MailMessage, ObjectDetectedMessage, SessionStatistic}
|
||||
import net.psforever.services.chat.DefaultChannel
|
||||
|
||||
import scala.collection.mutable
|
||||
import scala.concurrent.duration._
|
||||
|
|
@ -73,6 +75,11 @@ import net.psforever.util.{Config, DefinitionUtil}
|
|||
import net.psforever.zones.Zones
|
||||
|
||||
object ZoningOperations {
|
||||
private[session] final case class AvatarAwardMessageBundle(
|
||||
bundle: Iterable[Iterable[PlanetSideGamePacket]],
|
||||
delay: Long
|
||||
)
|
||||
|
||||
private final val zoningCountdownMessages: Seq[Int] = Seq(5, 10, 20)
|
||||
|
||||
def reportProgressionSystem(sessionActor: ActorRef): Unit = {
|
||||
|
|
@ -101,24 +108,26 @@ object ZoningOperations {
|
|||
}
|
||||
|
||||
class ZoningOperations(
|
||||
val sessionData: SessionData,
|
||||
val sessionLogic: SessionData,
|
||||
avatarActor: typed.ActorRef[AvatarActor.Command],
|
||||
galaxyService: ActorRef,
|
||||
cluster: typed.ActorRef[ICS.Command],
|
||||
implicit val context: ActorContext
|
||||
) extends CommonSessionInterfacingFunctionality {
|
||||
private var zoningType: Zoning.Method = Zoning.Method.None
|
||||
private var zoningChatMessageType: ChatMessageType = ChatMessageType.CMT_QUIT
|
||||
private[support] var zoningStatus: Zoning.Status = Zoning.Status.None
|
||||
private var zoningCounter: Int = 0
|
||||
private var instantActionFallbackDestination: Option[Zoning.InstantAction.Located] = None
|
||||
private[session] var zoningStatus: Zoning.Status = Zoning.Status.None
|
||||
/** a flag for the zone having finished loading during zoning
|
||||
* `None` when no zone is loaded
|
||||
* `Some(true)` when a zone has successfully loaded
|
||||
* `Some(false)` when the loading process has failed or was executed but did not complete for some reason
|
||||
*/
|
||||
private[session] var zoneLoaded: Option[Boolean] = None
|
||||
/**
|
||||
* used during zone transfers to maintain reference to seated vehicle (which does not yet exist in the new zone)
|
||||
* used during intrazone gate transfers, but not in a way distinct from prior zone transfer procedures
|
||||
* should only be set during the transient period when moving between one spawn point and the next
|
||||
* leaving set prior to a subsequent transfers may cause unstable vehicle associations, with memory leak potential
|
||||
*/
|
||||
private[support] var interstellarFerry: Option[Vehicle] = None
|
||||
private[session] var interstellarFerry: Option[Vehicle] = None
|
||||
/**
|
||||
* used during zone transfers for cleanup to refer to the vehicle that instigated a transfer
|
||||
* "top level" is the carrier in a carrier/ferried association or a projected carrier/(ferried carrier)/ferried association
|
||||
|
|
@ -126,20 +135,18 @@ class ZoningOperations(
|
|||
* the old-zone unique identifier for the carrier
|
||||
* no harm should come from leaving the field set to an old unique identifier value after the transfer period
|
||||
*/
|
||||
private[support] var interstellarFerryTopLevelGUID: Option[PlanetSideGUID] = None
|
||||
private var loadConfZone: Boolean = false
|
||||
/** a flag for the zone having finished loading during zoning
|
||||
* `None` when no zone is loaded
|
||||
* `Some(true)` when a zone has successfully loaded
|
||||
* `Some(false)` when the loading process has failed or was executed but did not complete for some reason
|
||||
*/
|
||||
private[support] var zoneLoaded: Option[Boolean] = None
|
||||
private[session] var interstellarFerryTopLevelGUID: Option[PlanetSideGUID] = None
|
||||
/** a flag that forces the current zone to reload itself during a zoning operation */
|
||||
private[support] var zoneReload: Boolean = false
|
||||
private var zoningTimer: Cancellable = Default.Cancellable
|
||||
|
||||
private[session] var zoneReload: Boolean = false
|
||||
private[session] val spawn: SpawnOperations = new SpawnOperations()
|
||||
|
||||
private var loadConfZone: Boolean = false
|
||||
private var instantActionFallbackDestination: Option[Zoning.InstantAction.Located] = None
|
||||
private var zoningType: Zoning.Method = Zoning.Method.None
|
||||
private var zoningChatMessageType: ChatMessageType = ChatMessageType.CMT_QUIT
|
||||
private var zoningCounter: Int = 0
|
||||
private var zoningTimer: Cancellable = Default.Cancellable
|
||||
|
||||
/* packets */
|
||||
|
||||
def handleWarpgateRequest(pkt: WarpgateRequest): Unit = {
|
||||
|
|
@ -147,7 +154,7 @@ class ZoningOperations(
|
|||
CancelZoningProcessWithDescriptiveReason("cancel_use")
|
||||
if (spawn.deadState != DeadState.RespawnTime) {
|
||||
continent.Buildings.values.find(_.GUID == building_guid) match {
|
||||
case Some(wg: WarpGate) if wg.Active && (sessionData.vehicles.GetKnownVehicleAndSeat() match {
|
||||
case Some(wg: WarpGate) if wg.Active && (sessionLogic.vehicles.GetKnownVehicleAndSeat() match {
|
||||
case (Some(vehicle), _) =>
|
||||
wg.Definition.VehicleAllowance && !wg.Definition.NoWarp.contains(vehicle.Definition)
|
||||
case _ =>
|
||||
|
|
@ -176,7 +183,7 @@ class ZoningOperations(
|
|||
}
|
||||
}
|
||||
|
||||
def handleDroppodLaunchRequest(pkt: DroppodLaunchRequestMessage)(implicit context: ActorContext): Unit = {
|
||||
def handleDroppodLaunchRequest(pkt: DroppodLaunchRequestMessage): Unit = {
|
||||
val DroppodLaunchRequestMessage(info, _) = pkt
|
||||
cluster ! ICS.DroppodLaunchRequest(
|
||||
info.zone_number,
|
||||
|
|
@ -202,7 +209,7 @@ class ZoningOperations(
|
|||
continent.VehicleEvents ! Service.Join(name)
|
||||
continent.VehicleEvents ! Service.Join(continentId)
|
||||
continent.VehicleEvents ! Service.Join(factionChannel)
|
||||
if (sessionData.connectionState != 100) configZone(continent)
|
||||
if (sessionLogic.connectionState != 100) configZone(continent)
|
||||
sendResponse(TimeOfDayMessage(1191182336))
|
||||
//custom
|
||||
sendResponse(ReplicationStreamMessage(5, Some(6), Vector.empty)) //clear squad list
|
||||
|
|
@ -487,7 +494,7 @@ class ZoningOperations(
|
|||
//the router won't work if it doesn't completely deploy
|
||||
sendResponse(DeployRequestMessage(player.GUID, obj.GUID, DriveState.Deploying, 0, unk3=false, Vector3.Zero))
|
||||
sendResponse(DeployRequestMessage(player.GUID, obj.GUID, DriveState.Deployed, 0, unk3=false, Vector3.Zero))
|
||||
sessionData.toggleTeleportSystem(obj, TelepadLike.AppraiseTeleportationSystem(obj, continent))
|
||||
sessionLogic.general.toggleTeleportSystem(obj, TelepadLike.AppraiseTeleportationSystem(obj, continent))
|
||||
}
|
||||
ServiceManager.serviceManager
|
||||
.ask(Lookup("hart"))(Timeout(2 seconds))
|
||||
|
|
@ -656,8 +663,8 @@ class ZoningOperations(
|
|||
//the following subscriptions last until character switch/logout
|
||||
galaxyService ! Service.Join("galaxy") //for galaxy-wide messages
|
||||
galaxyService ! Service.Join(s"${avatar.faction}") //for hotspots, etc.
|
||||
sessionData.squadService ! Service.Join(s"${avatar.faction}") //channel will be player.Faction
|
||||
sessionData.squadService ! Service.Join(s"${avatar.id}") //channel will be player.CharId (in order to work with packets)
|
||||
sessionLogic.squadService ! Service.Join(s"${avatar.faction}") //channel will be player.Faction
|
||||
sessionLogic.squadService ! Service.Join(s"${avatar.id}") //channel will be player.CharId (in order to work with packets)
|
||||
player.Zone match {
|
||||
case Zone.Nowhere =>
|
||||
RandomSanctuarySpawnPosition(player)
|
||||
|
|
@ -669,7 +676,7 @@ class ZoningOperations(
|
|||
session = session.copy(zone = zone)
|
||||
//the only zone-level event system subscription necessary before BeginZoningMessage (for persistence purposes)
|
||||
zone.AvatarEvents ! Service.Join(player.Name)
|
||||
sessionData.persist()
|
||||
sessionLogic.persist()
|
||||
oldZone.AvatarEvents ! Service.Leave()
|
||||
oldZone.LocalEvents ! Service.Leave()
|
||||
oldZone.VehicleEvents ! Service.Leave()
|
||||
|
|
@ -699,7 +706,7 @@ class ZoningOperations(
|
|||
loadConfZone = true
|
||||
val oldZone = session.zone
|
||||
session = session.copy(zone = foundZone)
|
||||
sessionData.persist()
|
||||
sessionLogic.persist()
|
||||
oldZone.AvatarEvents ! Service.Leave()
|
||||
oldZone.LocalEvents ! Service.Leave()
|
||||
oldZone.VehicleEvents ! Service.Leave()
|
||||
|
|
@ -709,9 +716,9 @@ class ZoningOperations(
|
|||
player.avatar = avatar
|
||||
interstellarFerry match {
|
||||
case Some(vehicle) if vehicle.PassengerInSeat(player).contains(0) =>
|
||||
TaskWorkflow.execute(sessionData.registerDrivenVehicle(vehicle, player))
|
||||
TaskWorkflow.execute(registerDrivenVehicle(vehicle, player))
|
||||
case _ =>
|
||||
TaskWorkflow.execute(sessionData.registerNewAvatar(player))
|
||||
TaskWorkflow.execute(registerNewAvatar(player))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -749,9 +756,9 @@ class ZoningOperations(
|
|||
}
|
||||
val previousZoningType = ztype
|
||||
CancelZoningProcess()
|
||||
sessionData.playerActionsToCancel()
|
||||
sessionData.terminals.CancelAllProximityUnits()
|
||||
sessionData.dropSpecialSlotItem()
|
||||
sessionLogic.actionsToCancel()
|
||||
sessionLogic.terminals.CancelAllProximityUnits()
|
||||
sessionLogic.general.dropSpecialSlotItem()
|
||||
continent.Population ! Zone.Population.Release(avatar)
|
||||
spawn.resolveZoningSpawnPointLoad(response, previousZoningType)
|
||||
}
|
||||
|
|
@ -859,13 +866,13 @@ class ZoningOperations(
|
|||
zoningStatus = Zoning.Status.Request
|
||||
beginZoningCountdown(() => {
|
||||
log.info(s"Good-bye, ${player.Name}")
|
||||
sessionData.immediateDisconnect()
|
||||
sessionLogic.immediateDisconnect()
|
||||
})
|
||||
}
|
||||
|
||||
def handleSetZone(zoneId: String, position: Vector3): Unit = {
|
||||
if (sessionData.vehicles.serverVehicleControlVelocity.isEmpty) {
|
||||
sessionData.playerActionsToCancel()
|
||||
if (sessionLogic.vehicles.serverVehicleControlVelocity.isEmpty) {
|
||||
sessionLogic.actionsToCancel()
|
||||
continent.GUID(player.VehicleSeated) match {
|
||||
case Some(vehicle: Vehicle) if vehicle.MountedIn.isEmpty =>
|
||||
vehicle.PassengerInSeat(player) match {
|
||||
|
|
@ -1143,13 +1150,13 @@ class ZoningOperations(
|
|||
//sync hack state
|
||||
amenity.Definition match {
|
||||
case GlobalDefinitions.capture_terminal =>
|
||||
sessionData.sendPlanetsideAttributeMessage(
|
||||
sessionLogic.general.sendPlanetsideAttributeMessage(
|
||||
amenity.GUID,
|
||||
PlanetsideAttributeEnum.ControlConsoleHackUpdate,
|
||||
HackCaptureActor.GetHackUpdateAttributeValue(amenity.asInstanceOf[CaptureTerminal], isResecured = false)
|
||||
)
|
||||
case _ =>
|
||||
sessionData.hackObject(amenity.GUID, 1114636288L, 8L) //generic hackable object
|
||||
sessionLogic.general.hackObject(amenity.GUID, 1114636288L, 8L) //generic hackable object
|
||||
}
|
||||
|
||||
// sync capture flags
|
||||
|
|
@ -1216,7 +1223,7 @@ class ZoningOperations(
|
|||
if (!zoneReload && zoneId.equals(continent.id)) {
|
||||
if (player.isBackpack) { // important! test the actor-wide player ref, not the parameter
|
||||
// respawning from unregistered player
|
||||
TaskWorkflow.execute(sessionData.registerAvatar(targetPlayer))
|
||||
TaskWorkflow.execute(registerAvatar(targetPlayer))
|
||||
} else {
|
||||
// move existing player; this is the one case where the original GUID is retained by the player
|
||||
context.self ! SessionActor.PlayerLoaded(targetPlayer)
|
||||
|
|
@ -1336,7 +1343,7 @@ class ZoningOperations(
|
|||
ICS.FindZone(_.id == zoneId, context.self)
|
||||
))
|
||||
} else {
|
||||
sessionData.unaccessContainer(vehicle)
|
||||
sessionLogic.general.unaccessContainer(vehicle)
|
||||
LoadZoneCommonTransferActivity()
|
||||
player.VehicleSeated = vehicle.GUID
|
||||
player.Continent = zoneId //forward-set the continent id to perform a test
|
||||
|
|
@ -1354,7 +1361,7 @@ class ZoningOperations(
|
|||
//unregister vehicle and driver whole + GiveWorld
|
||||
continent.Transport ! Zone.Vehicle.Despawn(vehicle)
|
||||
TaskWorkflow.execute(taskThenZoneChange(
|
||||
sessionData.unregisterDrivenVehicle(vehicle, player),
|
||||
unregisterDrivenVehicle(vehicle, player),
|
||||
ICS.FindZone(_.id == zoneId, context.self)
|
||||
))
|
||||
}
|
||||
|
|
@ -1480,9 +1487,9 @@ class ZoningOperations(
|
|||
// allow AMS, ANT and Router to remain deployed when owner leaves the zone
|
||||
vehicle.Definition match {
|
||||
case GlobalDefinitions.ams | GlobalDefinitions.ant | GlobalDefinitions.router
|
||||
=> sessionData.vehicles.ConditionalDriverVehicleControl(vehicle)
|
||||
=> sessionLogic.vehicles.ConditionalDriverVehicleControl(vehicle)
|
||||
|
||||
case _ => sessionData.vehicles.TotalDriverVehicleControl(vehicle)
|
||||
case _ => sessionLogic.vehicles.TotalDriverVehicleControl(vehicle)
|
||||
}
|
||||
|
||||
// remove owner
|
||||
|
|
@ -1492,12 +1499,12 @@ class ZoningOperations(
|
|||
}
|
||||
avatarActor ! AvatarActor.SetVehicle(None)
|
||||
}
|
||||
sessionData.removeBoomerTriggersFromInventory().foreach(obj => {
|
||||
spawn.removeBoomerTriggersFromInventory().foreach(obj => {
|
||||
TaskWorkflow.execute(GUIDTask.unregisterObject(continent.GUID, obj))
|
||||
})
|
||||
Deployables.Disown(continent, avatar, context.self)
|
||||
spawn.drawDeloyableIcon = spawn.RedrawDeployableIcons //important for when SetCurrentAvatar initializes the UI next zone
|
||||
sessionData.squad.squadSetup = sessionData.squad.ZoneChangeSquadSetup
|
||||
sessionLogic.squad.squadSetup = sessionLogic.squad.ZoneChangeSquadSetup
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1514,7 +1521,7 @@ class ZoningOperations(
|
|||
if (currentZone == Zones.sanctuaryZoneNumber(tplayer.Faction)) {
|
||||
log.error(s"RequestSanctuaryZoneSpawn: ${player.Name} is already in faction sanctuary zone.")
|
||||
sendResponse(DisconnectMessage("RequestSanctuaryZoneSpawn: player is already in sanctuary."))
|
||||
sessionData.immediateDisconnect()
|
||||
sessionLogic.immediateDisconnect()
|
||||
} else {
|
||||
continent.GUID(player.VehicleSeated) match {
|
||||
case Some(obj: Vehicle) if !obj.Destroyed =>
|
||||
|
|
@ -1547,8 +1554,8 @@ class ZoningOperations(
|
|||
def LoadZoneLaunchDroppod(zone: Zone, spawnPosition: Vector3): Unit = {
|
||||
log.info(s"${player.Name} is launching to ${zone.id} in ${player.Sex.possessive} droppod")
|
||||
CancelZoningProcess()
|
||||
sessionData.playerActionsToCancel()
|
||||
sessionData.terminals.CancelAllProximityUnits()
|
||||
sessionLogic.actionsToCancel()
|
||||
sessionLogic.terminals.CancelAllProximityUnits()
|
||||
//droppod action
|
||||
val droppod = Vehicle(GlobalDefinitions.droppod)
|
||||
droppod.GUID = PlanetSideGUID(0) //droppod is not registered, we must jury-rig this
|
||||
|
|
@ -1596,6 +1603,88 @@ class ZoningOperations(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct tasking that registers all aspects of a `Player` avatar
|
||||
* as if that player was already introduced and is just being renewed.
|
||||
* `Players` are complex objects that contain a variety of other register-able objects and each of these objects much be handled.
|
||||
* @param tplayer the avatar `Player`
|
||||
* @return a `TaskBundle` message
|
||||
*/
|
||||
private[session] def registerAvatar(tplayer: Player): TaskBundle = {
|
||||
TaskBundle(
|
||||
new StraightforwardTask() {
|
||||
private val localPlayer = tplayer
|
||||
private val localAnnounce = context.self
|
||||
|
||||
override def description(): String = s"register player avatar ${localPlayer.Name}"
|
||||
|
||||
def action(): Future[Any] = {
|
||||
localAnnounce ! SessionActor.PlayerLoaded(localPlayer)
|
||||
Future(true)
|
||||
}
|
||||
},
|
||||
List(GUIDTask.registerPlayer(continent.GUID, tplayer))
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct tasking that registers all aspects of a `Player` avatar
|
||||
* as if that player is only just being introduced.
|
||||
* `Players` are complex objects that contain a variety of other register-able objects and each of these objects much be handled.
|
||||
* @param tplayer the avatar `Player`
|
||||
* @return a `TaskBundle` message
|
||||
*/
|
||||
private[session] def registerNewAvatar(tplayer: Player): TaskBundle = {
|
||||
TaskBundle(
|
||||
new StraightforwardTask() {
|
||||
private val localPlayer = tplayer
|
||||
private val localAnnounce = context.self
|
||||
|
||||
override def description(): String = s"register new player avatar ${localPlayer.Name}"
|
||||
|
||||
def action(): Future[Any] = {
|
||||
localAnnounce ! SessionActor.NewPlayerLoaded(localPlayer)
|
||||
Future(true)
|
||||
}
|
||||
},
|
||||
List(GUIDTask.registerAvatar(continent.GUID, tplayer))
|
||||
)
|
||||
}
|
||||
|
||||
private[session] def registerDrivenVehicle(vehicle: Vehicle, driver: Player): TaskBundle = {
|
||||
TaskBundle(
|
||||
new StraightforwardTask() {
|
||||
private val localVehicle = vehicle
|
||||
private val localDriver = driver
|
||||
private val localAnnounce = context.self
|
||||
|
||||
override def description(): String = s"register a ${localVehicle.Definition.Name} driven by ${localDriver.Name}"
|
||||
|
||||
def action(): Future[Any] = {
|
||||
localDriver.VehicleSeated = localVehicle.GUID
|
||||
Vehicles.Own(localVehicle, localDriver)
|
||||
localAnnounce ! SessionActor.NewPlayerLoaded(localDriver)
|
||||
Future(true)
|
||||
}
|
||||
},
|
||||
List(GUIDTask.registerAvatar(continent.GUID, driver), GUIDTask.registerVehicle(continent.GUID, vehicle))
|
||||
)
|
||||
}
|
||||
|
||||
private[session] def unregisterDrivenVehicle(vehicle: Vehicle, driver: Player): TaskBundle = {
|
||||
TaskBundle(
|
||||
new StraightforwardTask() {
|
||||
private val localVehicle = vehicle
|
||||
private val localDriver = driver
|
||||
|
||||
override def description(): String = s"unregister a ${localVehicle.Definition.Name} driven by ${localDriver.Name}"
|
||||
|
||||
def action(): Future[Any] = Future(true)
|
||||
},
|
||||
List(GUIDTask.unregisterAvatar(continent.GUID, driver), GUIDTask.unregisterVehicle(continent.GUID, vehicle))
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this function to facilitate registering a droppod for a globally unique identifier
|
||||
* in the event that the user has instigated an instant action event to a destination within the current zone.<br>
|
||||
|
|
@ -1665,15 +1754,15 @@ class ZoningOperations(
|
|||
/* nested class - spawn operations */
|
||||
|
||||
class SpawnOperations() {
|
||||
private[support] var deadState: DeadState.Value = DeadState.Dead
|
||||
private[support] var loginChatMessage: mutable.ListBuffer[String] = new mutable.ListBuffer[String]()
|
||||
private[support] var amsSpawnPoints: List[SpawnPoint] = Nil
|
||||
private[support] var noSpawnPointHere: Boolean = false
|
||||
private[support] var setupAvatarFunc: () => Unit = AvatarCreate
|
||||
private[support] var setCurrentAvatarFunc: Player => Unit = SetCurrentAvatarNormally
|
||||
private[support] var nextSpawnPoint: Option[SpawnPoint] = None
|
||||
private[support] var interimUngunnedVehicle: Option[PlanetSideGUID] = None
|
||||
private[support] var interimUngunnedVehicleSeat: Option[Int] = None
|
||||
private[session] var deadState: DeadState.Value = DeadState.Dead
|
||||
private[session] var loginChatMessage: mutable.ListBuffer[String] = new mutable.ListBuffer[String]()
|
||||
private[session] var amsSpawnPoints: List[SpawnPoint] = Nil
|
||||
private[session] var noSpawnPointHere: Boolean = false
|
||||
private[session] var setupAvatarFunc: () => Unit = AvatarCreate
|
||||
private[session] var setCurrentAvatarFunc: Player => Unit = SetCurrentAvatarNormally
|
||||
private[session] var nextSpawnPoint: Option[SpawnPoint] = None
|
||||
private[session] var interimUngunnedVehicle: Option[PlanetSideGUID] = None
|
||||
private[session] var interimUngunnedVehicleSeat: Option[Int] = None
|
||||
/** Upstream message counter<br>
|
||||
* Checks for server acknowledgement of the following messages in the following conditions:<br>
|
||||
* `PlayerStateMessageUpstream` (infantry)<br>
|
||||
|
|
@ -1682,14 +1771,14 @@ class ZoningOperations(
|
|||
* `KeepAliveMessage` (any passenger mount that is not the driver)<br>
|
||||
* As they should arrive roughly every 250 milliseconds this allows for a very crude method of scheduling tasks up to four times per second
|
||||
*/
|
||||
private[support] var upstreamMessageCount: Int = 0
|
||||
private[support] var shiftPosition: Option[Vector3] = None
|
||||
private[support] var shiftOrientation: Option[Vector3] = None
|
||||
private[support] var drawDeloyableIcon: PlanetSideGameObject with Deployable => Unit = RedrawDeployableIcons
|
||||
private[support] var populateAvatarAwardRibbonsFunc: (Int, Long) => Unit = setupAvatarAwardMessageDelivery
|
||||
private[support] var setAvatar: Boolean = false
|
||||
private[support] var reviveTimer: Cancellable = Default.Cancellable
|
||||
private[support] var respawnTimer: Cancellable = Default.Cancellable
|
||||
private[session] var upstreamMessageCount: Int = 0
|
||||
private[session] var shiftPosition: Option[Vector3] = None
|
||||
private[session] var shiftOrientation: Option[Vector3] = None
|
||||
private[session] var drawDeloyableIcon: PlanetSideGameObject with Deployable => Unit = RedrawDeployableIcons
|
||||
private[session] var populateAvatarAwardRibbonsFunc: (Int, Long) => Unit = setupAvatarAwardMessageDelivery
|
||||
private[session] var setAvatar: Boolean = false
|
||||
private[session] var reviveTimer: Cancellable = Default.Cancellable
|
||||
private[session] var respawnTimer: Cancellable = Default.Cancellable
|
||||
|
||||
private var statisticsPacketFunc: () => Unit = loginAvatarStatisticsFields
|
||||
|
||||
|
|
@ -1703,7 +1792,7 @@ class ZoningOperations(
|
|||
HandleReleaseAvatar(player, continent)
|
||||
}
|
||||
|
||||
def handleSpawnRequest(pkt: SpawnRequestMessage)(implicit context: ActorContext): Unit = {
|
||||
def handleSpawnRequest(pkt: SpawnRequestMessage): Unit = {
|
||||
val SpawnRequestMessage(_, spawnGroup, _, _, zoneNumber) = pkt
|
||||
log.info(s"${player.Name} on ${continent.id} wants to respawn in zone #$zoneNumber")
|
||||
if (deadState != DeadState.RespawnTime) {
|
||||
|
|
@ -1730,7 +1819,7 @@ class ZoningOperations(
|
|||
|
||||
def handleLoginInfoNowhere(name: String, from: ActorRef): Unit = {
|
||||
log.info(s"LoginInfo: player $name is considered a fresh character")
|
||||
sessionData.persistFunc = UpdatePersistence(from)
|
||||
sessionLogic.persistFunc = UpdatePersistence(from)
|
||||
deadState = DeadState.RespawnTime
|
||||
val tplayer = new Player(avatar)
|
||||
session = session.copy(player = tplayer)
|
||||
|
|
@ -1742,7 +1831,7 @@ class ZoningOperations(
|
|||
|
||||
def handleLoginInfoSomewhere(name: String, inZone: Zone, optionalSavedData: Option[Savedplayer], from: ActorRef): Unit = {
|
||||
log.info(s"LoginInfo: player $name is considered a fresh character")
|
||||
sessionData.persistFunc = UpdatePersistence(from)
|
||||
sessionLogic.persistFunc = UpdatePersistence(from)
|
||||
deadState = DeadState.RespawnTime
|
||||
session = session.copy(player = new Player(avatar))
|
||||
player.Zone = inZone
|
||||
|
|
@ -1836,25 +1925,25 @@ class ZoningOperations(
|
|||
|
||||
def handleLoginInfoRestore(name: String, inZone: Zone, pos: Vector3, from: ActorRef): Unit = {
|
||||
log.info(s"RestoreInfo: player $name is already logged in zone ${inZone.id}; rejoining that character")
|
||||
sessionData.persistFunc = UpdatePersistence(from)
|
||||
sessionLogic.persistFunc = UpdatePersistence(from)
|
||||
//tell the old WorldSessionActor to kill itself by using its own subscriptions against itself
|
||||
inZone.AvatarEvents ! AvatarServiceMessage(name, AvatarAction.TeardownConnection())
|
||||
spawn.switchAvatarStatisticsFieldToRefreshAfterRespawn()
|
||||
//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
|
||||
sessionData.kickedByAdministration()
|
||||
sessionLogic.kickedByAdministration()
|
||||
|
||||
case (Some(a), Some(p)) if p.isAlive =>
|
||||
//rejoin current avatar/player
|
||||
log.info(s"RestoreInfo: player $name is alive")
|
||||
deadState = DeadState.Alive
|
||||
session = session.copy(player = p, avatar = a)
|
||||
sessionData.persist()
|
||||
sessionLogic.persist()
|
||||
setupAvatarFunc = AvatarRejoin
|
||||
dropMedicalApplicators(p)
|
||||
avatarActor ! AvatarActor.ReplaceAvatar(a)
|
||||
|
|
@ -1865,7 +1954,7 @@ class ZoningOperations(
|
|||
log.info(s"RestoreInfo: player $name is dead")
|
||||
deadState = DeadState.Dead
|
||||
session = session.copy(player = p, avatar = a)
|
||||
sessionData.persist()
|
||||
sessionLogic.persist()
|
||||
dropMedicalApplicators(p)
|
||||
HandleReleaseAvatar(p, inZone)
|
||||
avatarActor ! AvatarActor.ReplaceAvatar(a)
|
||||
|
|
@ -1900,7 +1989,7 @@ class ZoningOperations(
|
|||
def handleLoginCanNot(name: String, reason: PlayerToken.DeniedLoginReason.Value): Unit = {
|
||||
log.warn(s"LoginInfo: $name is denied login for reason - $reason")
|
||||
reason match {
|
||||
case PlayerToken.DeniedLoginReason.Kicked => sessionData.kickedByAdministration()
|
||||
case PlayerToken.DeniedLoginReason.Kicked => sessionLogic.kickedByAdministration()
|
||||
case _ => sendResponse(DisconnectMessage("You will be logged out."))
|
||||
}
|
||||
}
|
||||
|
|
@ -1939,9 +2028,9 @@ class ZoningOperations(
|
|||
}
|
||||
val previousZoningType = ztype
|
||||
CancelZoningProcess()
|
||||
sessionData.playerActionsToCancel()
|
||||
sessionData.terminals.CancelAllProximityUnits()
|
||||
sessionData.dropSpecialSlotItem()
|
||||
sessionLogic.actionsToCancel()
|
||||
sessionLogic.terminals.CancelAllProximityUnits()
|
||||
sessionLogic.general.dropSpecialSlotItem()
|
||||
continent.Population ! Zone.Population.Release(avatar)
|
||||
resolveZoningSpawnPointLoad(response, previousZoningType)
|
||||
}
|
||||
|
|
@ -1954,8 +2043,8 @@ class ZoningOperations(
|
|||
val map = zone.map
|
||||
val mapName = map.name
|
||||
log.info(s"${tplayer.Name} has spawned into $id")
|
||||
sessionData.oldRefsMap.clear()
|
||||
sessionData.persist = UpdatePersistenceAndRefs
|
||||
sessionLogic.oldRefsMap.clear()
|
||||
sessionLogic.persist = UpdatePersistenceAndRefs
|
||||
tplayer.avatar = avatar
|
||||
session = session.copy(player = tplayer)
|
||||
avatarActor ! AvatarActor.CreateImplants()
|
||||
|
|
@ -1967,7 +2056,7 @@ class ZoningOperations(
|
|||
//important! the LoadMapMessage must be processed by the client before the avatar is created
|
||||
setupAvatarFunc()
|
||||
//interimUngunnedVehicle should have been setup by setupAvatarFunc, if it is applicable
|
||||
sessionData.turnCounterFunc = interimUngunnedVehicle match {
|
||||
sessionLogic.turnCounterFunc = interimUngunnedVehicle match {
|
||||
case Some(_) =>
|
||||
TurnCounterDuringInterimWhileInPassengerSeat
|
||||
case None if zoningType == Zoning.Method.Login || zoningType == Zoning.Method.Reset =>
|
||||
|
|
@ -1975,14 +2064,14 @@ class ZoningOperations(
|
|||
case None =>
|
||||
TurnCounterDuringInterim
|
||||
}
|
||||
sessionData.keepAliveFunc = NormalKeepAlive
|
||||
sessionLogic.keepAliveFunc = NormalKeepAlive
|
||||
if (zoningStatus == Zoning.Status.Deconstructing) {
|
||||
sessionData.stopDeconstructing()
|
||||
stopDeconstructing()
|
||||
}
|
||||
sessionData.avatarResponse.lastSeenStreamMessage.clear()
|
||||
sessionLogic.avatarResponse.lastSeenStreamMessage.clear()
|
||||
upstreamMessageCount = 0
|
||||
setAvatar = false
|
||||
sessionData.persist()
|
||||
sessionLogic.persist()
|
||||
} else {
|
||||
//look for different spawn point in same zone
|
||||
cluster ! ICS.GetNearbySpawnPoint(
|
||||
|
|
@ -2004,20 +2093,20 @@ class ZoningOperations(
|
|||
//try this spawn point
|
||||
setupAvatarFunc()
|
||||
//interimUngunnedVehicle should have been setup by setupAvatarFunc, if it is applicable
|
||||
sessionData.turnCounterFunc = interimUngunnedVehicle match {
|
||||
sessionLogic.turnCounterFunc = interimUngunnedVehicle match {
|
||||
case Some(_) =>
|
||||
TurnCounterDuringInterimWhileInPassengerSeat
|
||||
case None =>
|
||||
TurnCounterDuringInterim
|
||||
}
|
||||
sessionData.keepAliveFunc = NormalKeepAlive
|
||||
sessionLogic.keepAliveFunc = NormalKeepAlive
|
||||
if (zoningStatus == Zoning.Status.Deconstructing) {
|
||||
sessionData.stopDeconstructing()
|
||||
stopDeconstructing()
|
||||
}
|
||||
sessionData.avatarResponse.lastSeenStreamMessage.clear()
|
||||
sessionLogic.avatarResponse.lastSeenStreamMessage.clear()
|
||||
upstreamMessageCount = 0
|
||||
setAvatar = false
|
||||
sessionData.persist()
|
||||
sessionLogic.persist()
|
||||
} else {
|
||||
//look for different spawn point in same zone
|
||||
cluster ! ICS.GetNearbySpawnPoint(
|
||||
|
|
@ -2077,6 +2166,7 @@ class ZoningOperations(
|
|||
*/
|
||||
def avatarLoginResponse(avatar: Avatar): Unit = {
|
||||
session = session.copy(avatar = avatar)
|
||||
sessionLogic.chat.JoinChannel(DefaultChannel)
|
||||
Deployables.InitializeDeployableQuantities(avatar)
|
||||
cluster ! ICS.FilterZones(_ => true, context.self)
|
||||
}
|
||||
|
|
@ -2087,7 +2177,7 @@ class ZoningOperations(
|
|||
* @param zone na
|
||||
*/
|
||||
def HandleReleaseAvatar(tplayer: Player, zone: Zone): Unit = {
|
||||
sessionData.keepAliveFunc = sessionData.keepAlivePersistence
|
||||
sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistence
|
||||
tplayer.Release
|
||||
tplayer.VehicleSeated match {
|
||||
case None =>
|
||||
|
|
@ -2101,8 +2191,8 @@ class ZoningOperations(
|
|||
}
|
||||
|
||||
def handleSetPosition(position: Vector3): Unit = {
|
||||
if (sessionData.vehicles.serverVehicleControlVelocity.isEmpty) {
|
||||
sessionData.playerActionsToCancel()
|
||||
if (sessionLogic.vehicles.serverVehicleControlVelocity.isEmpty) {
|
||||
sessionLogic.actionsToCancel()
|
||||
continent.GUID(player.VehicleSeated) match {
|
||||
case Some(vehicle: Vehicle) if vehicle.MountedIn.isEmpty =>
|
||||
vehicle.PassengerInSeat(player) match {
|
||||
|
|
@ -2147,7 +2237,7 @@ class ZoningOperations(
|
|||
player.Armor = armor
|
||||
}
|
||||
player.death_by = math.min(player.death_by, 0)
|
||||
sessionData.vehicles.GetKnownVehicleAndSeat() match {
|
||||
sessionLogic.vehicles.GetKnownVehicleAndSeat() match {
|
||||
case (Some(vehicle: Vehicle), Some(seat: Int)) =>
|
||||
//if the vehicle is the cargo of another vehicle in this zone
|
||||
val carrierInfo = continent.GUID(vehicle.MountedIn) match {
|
||||
|
|
@ -2254,7 +2344,7 @@ class ZoningOperations(
|
|||
* Neither the player avatar nor the vehicle should be reconstructed before the next zone load operation
|
||||
* to avoid damaging the critical setup of this function.
|
||||
* @see `AccessContainer`
|
||||
* @see `UpdateWeaponAtSeatPosition`
|
||||
* @see `SessionMountHandlers.updateWeaponAtSeatPosition`
|
||||
* @param tplayer the player avatar seated in the vehicle's mount
|
||||
* @param vehicle the vehicle the player is riding
|
||||
* @param seat the mount index
|
||||
|
|
@ -2271,8 +2361,8 @@ class ZoningOperations(
|
|||
sendResponse(ObjectCreateDetailedMessage(pdef.ObjectId, pguid, pdata))
|
||||
if (seat == 0 || vehicle.WeaponControlledFromSeat(seat).nonEmpty) {
|
||||
sendResponse(ObjectAttachMessage(vguid, pguid, seat))
|
||||
sessionData.accessContainer(vehicle)
|
||||
sessionData.updateWeaponAtSeatPosition(vehicle, seat)
|
||||
sessionLogic.general.accessContainer(vehicle)
|
||||
sessionLogic.mountResponse.updateWeaponAtSeatPosition(vehicle, seat)
|
||||
} else {
|
||||
interimUngunnedVehicle = Some(vguid)
|
||||
interimUngunnedVehicleSeat = Some(seat)
|
||||
|
|
@ -2317,7 +2407,7 @@ class ZoningOperations(
|
|||
* to avoid damaging the critical setup of this function.
|
||||
*/
|
||||
def AvatarRejoin(): Unit = {
|
||||
sessionData.vehicles.GetKnownVehicleAndSeat() match {
|
||||
sessionLogic.vehicles.GetKnownVehicleAndSeat() match {
|
||||
case (Some(vehicle: Vehicle), Some(seat: Int)) =>
|
||||
//vehicle and driver/passenger
|
||||
val vdef = vehicle.Definition
|
||||
|
|
@ -2336,8 +2426,8 @@ class ZoningOperations(
|
|||
log.debug(s"AvatarRejoin: ${player.Name} - $pguid -> $pdata")
|
||||
if (seat == 0 || vehicle.WeaponControlledFromSeat(seat).nonEmpty) {
|
||||
sendResponse(ObjectAttachMessage(vguid, pguid, seat))
|
||||
sessionData.accessContainer(vehicle)
|
||||
sessionData.updateWeaponAtSeatPosition(vehicle, seat)
|
||||
sessionLogic.general.accessContainer(vehicle)
|
||||
sessionLogic.mountResponse.updateWeaponAtSeatPosition(vehicle, seat)
|
||||
} else {
|
||||
interimUngunnedVehicle = Some(vguid)
|
||||
interimUngunnedVehicleSeat = Some(seat)
|
||||
|
|
@ -2406,10 +2496,37 @@ class ZoningOperations(
|
|||
case Some(_) | None => ;
|
||||
}
|
||||
})
|
||||
sessionData.removeBoomerTriggersFromInventory().foreach(trigger => { sessionData.normalItemDrop(obj, continent)(trigger) })
|
||||
removeBoomerTriggersFromInventory().foreach(trigger => { sessionLogic.general.normalItemDrop(obj, continent)(trigger) })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search through the player's holsters and their inventory space
|
||||
* and remove all `BoomerTrigger` objects, both functionally and visually.
|
||||
* @return all discovered `BoomTrigger` objects
|
||||
*/
|
||||
def removeBoomerTriggersFromInventory(): List[BoomerTrigger] = {
|
||||
val events = continent.AvatarEvents
|
||||
val zoneId = continent.id
|
||||
(player.Inventory.Items ++ player.HolsterItems())
|
||||
.collect { case InventoryItem(obj: BoomerTrigger, index) =>
|
||||
player.Slot(index).Equipment = None
|
||||
continent.GUID(obj.Companion) match {
|
||||
case Some(mine: BoomerDeployable) => mine.Actor ! Deployable.Ownership(None)
|
||||
case _ => ()
|
||||
}
|
||||
if (player.VisibleSlots.contains(index)) {
|
||||
events ! AvatarServiceMessage(
|
||||
zoneId,
|
||||
AvatarAction.ObjectDelete(Service.defaultPlayerGUID, obj.GUID)
|
||||
)
|
||||
} else {
|
||||
sendResponse(ObjectDeleteMessage(obj.GUID, 0))
|
||||
}
|
||||
obj
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a player that has the characteristics of a corpse
|
||||
* so long as the player has items in their knapsack or their holsters.
|
||||
|
|
@ -2762,10 +2879,10 @@ class ZoningOperations(
|
|||
SessionActor.SetCurrentAvatar(player, max_attempts, attempt + max_attempts / 3)
|
||||
)
|
||||
} else {
|
||||
sessionData.keepAliveFunc = sessionData.vehicles.GetMountableAndSeat(None, player, continent) match {
|
||||
sessionLogic.keepAliveFunc = sessionLogic.vehicles.GetMountableAndSeat(None, player, continent) match {
|
||||
case (Some(v: Vehicle), Some(seatNumber))
|
||||
if seatNumber > 0 && v.WeaponControlledFromSeat(seatNumber).isEmpty =>
|
||||
sessionData.keepAlivePersistence
|
||||
sessionLogic.keepAlivePersistence
|
||||
case _ =>
|
||||
NormalKeepAlive
|
||||
}
|
||||
|
|
@ -2791,36 +2908,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
|
||||
sessionData.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))
|
||||
|
|
@ -2851,7 +2966,7 @@ class ZoningOperations(
|
|||
|
||||
sendResponse(PlanetsideStringAttributeMessage(guid, 0, "Outfit Name"))
|
||||
//squad stuff (loadouts, assignment)
|
||||
sessionData.squad.squadSetup()
|
||||
sessionLogic.squad.squadSetup()
|
||||
//MapObjectStateBlockMessage and ObjectCreateMessage?
|
||||
//TacticsMessage?
|
||||
//change the owner on our deployables (re-draw the icons for our deployables too)
|
||||
|
|
@ -2865,7 +2980,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(
|
||||
|
|
@ -2875,7 +2990,7 @@ class ZoningOperations(
|
|||
case _ =>
|
||||
avatarActor ! AvatarActor.SetVehicle(None)
|
||||
}
|
||||
sessionData.vehicles.GetVehicleAndSeat() match {
|
||||
sessionLogic.vehicles.GetVehicleAndSeat() match {
|
||||
case (Some(vehicle), _) if vehicle.Definition == GlobalDefinitions.droppod =>
|
||||
//we're falling
|
||||
sendResponse(
|
||||
|
|
@ -2919,22 +3034,22 @@ class ZoningOperations(
|
|||
)
|
||||
case (Some(vehicle), _) =>
|
||||
//passenger
|
||||
vehicle.Actor ! Vehicle.UpdateZoneInteractionProgressUI(player)
|
||||
vehicle.Actor ! Vehicle.UpdateZoneInteractionProgressUI(tplayer)
|
||||
case _ => ;
|
||||
}
|
||||
interstellarFerryTopLevelGUID = None
|
||||
if (loadConfZone && sessionData.connectionState == 100) {
|
||||
if (loadConfZone && sessionLogic.connectionState == 100) {
|
||||
configZone(continent)
|
||||
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)
|
||||
sessionData.displayCharSavedMsgThenRenewTimer(
|
||||
AvatarActor.savePlayerData(tplayer)
|
||||
sessionLogic.general.displayCharSavedMsgThenRenewTimer(
|
||||
Config.app.game.savedMsg.short.fixed,
|
||||
Config.app.game.savedMsg.short.variable
|
||||
)
|
||||
|
|
@ -2947,17 +3062,20 @@ 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
if (!setAvatar && tplayer.spectator) {
|
||||
context.self ! SessionActor.SetMode(SpectatorMode) //should reload spectator status
|
||||
}
|
||||
}
|
||||
upstreamMessageCount = 0
|
||||
setAvatar = true
|
||||
|
|
@ -2965,9 +3083,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)
|
||||
}
|
||||
|
|
@ -3024,7 +3142,13 @@ class ZoningOperations(
|
|||
* Set up and dispatch a list of `CreateShortcutMessage` packets and a single `ChangeShortcutBankMessage` packet.
|
||||
*/
|
||||
def initializeShortcutsAndBank(guid: PlanetSideGUID): Unit = {
|
||||
avatar.shortcuts
|
||||
initializeShortcutsAndBank(guid, avatar.shortcuts)
|
||||
}
|
||||
/**
|
||||
* Set up and dispatch a list of `CreateShortcutMessage` packets and a single `ChangeShortcutBankMessage` packet.
|
||||
*/
|
||||
def initializeShortcutsAndBank(guid: PlanetSideGUID, shortcuts: Array[Option[AvatarShortcut]]): Unit = {
|
||||
shortcuts
|
||||
.zipWithIndex
|
||||
.collect { case (Some(shortcut), index) =>
|
||||
sendResponse(CreateShortcutMessage(
|
||||
|
|
@ -3167,7 +3291,7 @@ class ZoningOperations(
|
|||
def TurnCounterDuringInterim(guid: PlanetSideGUID): Unit = {
|
||||
upstreamMessageCount = 0
|
||||
if (player != null && player.HasGUID && player.GUID == guid && player.Zone == continent) {
|
||||
sessionData.turnCounterFunc = NormalTurnCounter
|
||||
sessionLogic.turnCounterFunc = NormalTurnCounter
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3201,15 +3325,15 @@ class ZoningOperations(
|
|||
case (Some(vehicle: Vehicle), Some(vguid), Some(seat)) =>
|
||||
//sit down
|
||||
sendResponse(ObjectAttachMessage(vguid, pguid, seat))
|
||||
sessionData.accessContainer(vehicle)
|
||||
sessionData.keepAliveFunc = sessionData.keepAlivePersistence
|
||||
sessionLogic.general.accessContainer(vehicle)
|
||||
sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistence
|
||||
case _ => ;
|
||||
//we can't find a vehicle? and we're still here? that's bad
|
||||
player.VehicleSeated = None
|
||||
}
|
||||
interimUngunnedVehicle = None
|
||||
interimUngunnedVehicleSeat = None
|
||||
sessionData.turnCounterFunc = NormalTurnCounter
|
||||
sessionLogic.turnCounterFunc = NormalTurnCounter
|
||||
}
|
||||
}
|
||||
/**
|
||||
|
|
@ -3226,7 +3350,7 @@ class ZoningOperations(
|
|||
loginChatMessage.foreach { msg => sendResponse(ChatMsg(zoningChatMessageType, wideContents=false, "", msg, None)) }
|
||||
loginChatMessage.clear()
|
||||
CancelZoningProcess()
|
||||
sessionData.turnCounterFunc = NormalTurnCounter
|
||||
sessionLogic.turnCounterFunc = NormalTurnCounter
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -3403,7 +3527,7 @@ class ZoningOperations(
|
|||
context.system.scheduler.scheduleOnce(
|
||||
delay.milliseconds,
|
||||
context.self,
|
||||
SessionActor.AvatarAwardMessageBundle(xs, delay)
|
||||
ZoningOperations.AvatarAwardMessageBundle(xs, delay)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -3413,8 +3537,8 @@ class ZoningOperations(
|
|||
* Set to `persist` when (new) player is loaded.
|
||||
*/
|
||||
def UpdatePersistenceAndRefs(): Unit = {
|
||||
sessionData.persistFunc()
|
||||
sessionData.updateOldRefsMap()
|
||||
sessionLogic.persistFunc()
|
||||
sessionLogic.updateOldRefsMap()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -3425,6 +3549,45 @@ class ZoningOperations(
|
|||
def UpdatePersistence(persistRef: ActorRef)(): Unit = {
|
||||
persistRef ! AccountPersistenceService.Update(player.Name, continent, player.Position)
|
||||
}
|
||||
|
||||
def startDeconstructing(obj: SpawnTube): Unit = {
|
||||
log.info(s"${player.Name} is deconstructing at the ${obj.Owner.Definition.Name}'s spawns")
|
||||
avatar.implants.collect {
|
||||
case Some(implant) if implant.active && !implant.definition.Passive =>
|
||||
avatarActor ! AvatarActor.DeactivateImplant(implant.definition.implantType)
|
||||
}
|
||||
if (player.ExoSuit != ExoSuitType.MAX) {
|
||||
player.Actor ! PlayerControl.ObjectHeld(Player.HandsDownSlot, updateMyHolsterArm = true)
|
||||
}
|
||||
nextSpawnPoint = Some(obj) //set fallback
|
||||
zoningStatus = Zoning.Status.Deconstructing
|
||||
player.allowInteraction = false
|
||||
if (player.death_by == 0) {
|
||||
player.death_by = 1
|
||||
}
|
||||
GoToDeploymentMap()
|
||||
}
|
||||
|
||||
def stopDeconstructing(): Unit = {
|
||||
zoningStatus = Zoning.Status.None
|
||||
player.death_by = math.min(player.death_by, 0)
|
||||
player.allowInteraction = true
|
||||
nextSpawnPoint.foreach { tube =>
|
||||
sendResponse(PlayerStateShiftMessage(ShiftState(0, tube.Position, tube.Orientation.z)))
|
||||
nextSpawnPoint = None
|
||||
}
|
||||
}
|
||||
|
||||
def randomRespawn(time: FiniteDuration = 300.seconds): Unit = {
|
||||
reviveTimer = context.system.scheduler.scheduleOnce(time) {
|
||||
cluster ! ICS.GetRandomSpawnPoint(
|
||||
Zones.sanctuaryZoneNumber(player.Faction),
|
||||
player.Faction,
|
||||
Seq(SpawnGroup.Sanctuary),
|
||||
context.self
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override protected[session] def stop(): Unit = {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ import net.psforever.services.Service
|
|||
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
|
||||
import net.psforever.types.PlanetSideEmpire
|
||||
|
||||
import scala.annotation.unused
|
||||
|
||||
class BoomerDeployable(cdef: ExplosiveDeployableDefinition)
|
||||
extends ExplosiveDeployable(cdef) {
|
||||
private var trigger: Option[BoomerTrigger] = None
|
||||
|
|
@ -70,8 +72,8 @@ class BoomerDeployableControl(mine: BoomerDeployable)
|
|||
case _ => ;
|
||||
}
|
||||
|
||||
override def loseOwnership(faction: PlanetSideEmpire.Value): Unit = {
|
||||
super.loseOwnership(PlanetSideEmpire.NEUTRAL)
|
||||
def loseOwnership(@unused faction: PlanetSideEmpire.Value): Unit = {
|
||||
super.loseOwnership(mine, PlanetSideEmpire.NEUTRAL)
|
||||
val guid = mine.OwnerGuid
|
||||
mine.AssignOwnership(None)
|
||||
mine.OwnerGuid = guid
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import net.psforever.objects.vital.{HealFromEquipment, InGameActivity, RepairFro
|
|||
import net.psforever.objects.vital.damage.DamageProfile
|
||||
import net.psforever.objects.vital.interaction.DamageInteraction
|
||||
import net.psforever.objects.vital.resolution.DamageResistanceModel
|
||||
import net.psforever.objects.zones.blockmap.{BlockMapEntity, SectorPopulation}
|
||||
import net.psforever.objects.zones.blockmap.BlockMapEntity
|
||||
import net.psforever.objects.zones.{InteractsWithZone, ZoneAware, Zoning}
|
||||
import net.psforever.types._
|
||||
|
||||
|
|
@ -47,7 +47,7 @@ class Player(var avatar: Avatar)
|
|||
new WithGantry(avatar.name),
|
||||
new WithMovementTrigger()
|
||||
)))
|
||||
interaction(new InteractWithMinesUnlessSpectating(obj = this, range = 10))
|
||||
interaction(new InteractWithMines(range = 10))
|
||||
interaction(new InteractWithTurrets())
|
||||
interaction(new InteractWithRadiationClouds(range = 10f, Some(this)))
|
||||
|
||||
|
|
@ -653,14 +653,3 @@ object Player {
|
|||
false
|
||||
}
|
||||
}
|
||||
|
||||
private class InteractWithMinesUnlessSpectating(
|
||||
private val obj: Player,
|
||||
override val range: Float
|
||||
) extends InteractWithMines(range) {
|
||||
override def interaction(sector: SectorPopulation, target: InteractsWithZone): Unit = {
|
||||
if (!obj.spectator) {
|
||||
super.interaction(sector, target)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,14 +4,18 @@ package net.psforever.objects
|
|||
import net.psforever.objects.avatar.Certification
|
||||
import net.psforever.login.WorldSession.FindEquipmentStock
|
||||
import net.psforever.objects.avatar.PlayerControl
|
||||
import net.psforever.objects.avatar.scoring.Kill
|
||||
import net.psforever.objects.ce.Deployable
|
||||
import net.psforever.objects.definition.ExoSuitDefinition
|
||||
import net.psforever.objects.equipment.EquipmentSlot
|
||||
import net.psforever.objects.guid.{GUIDTask, TaskWorkflow}
|
||||
import net.psforever.objects.inventory.InventoryItem
|
||||
import net.psforever.objects.loadouts.InfantryLoadout
|
||||
import net.psforever.objects.serverobject.affinity.FactionAffinity
|
||||
import net.psforever.objects.sourcing.PlayerSource
|
||||
import net.psforever.objects.vital.RevivingActivity
|
||||
import net.psforever.objects.vehicles.MountedWeapons
|
||||
import net.psforever.objects.vital.projectile.ProjectileReason
|
||||
import net.psforever.objects.vital.{InGameActivity, InGameHistory, RevivingActivity}
|
||||
import net.psforever.objects.zones.Zone
|
||||
import net.psforever.packet.game._
|
||||
import net.psforever.types.{ChatMessageType, ExoSuitType, Vector3}
|
||||
|
|
@ -256,7 +260,7 @@ object Players {
|
|||
PlayerControl.sendResponse(
|
||||
zone,
|
||||
channel,
|
||||
ChatMsg(ChatMessageType.UNK_229, false, "", s"@${definition.Descriptor}OldestDestroyed", None)
|
||||
ChatMsg(ChatMessageType.UNK_229, s"@${definition.Descriptor}OldestDestroyed")
|
||||
)
|
||||
}
|
||||
true
|
||||
|
|
@ -278,7 +282,7 @@ object Players {
|
|||
PlayerControl.sendResponse(
|
||||
zone,
|
||||
channel,
|
||||
ChatMsg(ChatMessageType.UNK_229, false, "", s"@${definition.Descriptor}LimitReached", None)
|
||||
ChatMsg(ChatMessageType.UNK_229, s"@${definition.Descriptor}LimitReached")
|
||||
)
|
||||
}
|
||||
true
|
||||
|
|
@ -400,7 +404,7 @@ object Players {
|
|||
val zone = player.Zone
|
||||
zone.AvatarEvents ! AvatarServiceMessage(
|
||||
zone.id,
|
||||
AvatarAction.ObjectDelete(Service.defaultPlayerGUID, tool.GUID, 0)
|
||||
AvatarAction.ObjectDelete(Service.defaultPlayerGUID, tool.GUID)
|
||||
)
|
||||
true
|
||||
} else {
|
||||
|
|
@ -466,4 +470,22 @@ object Players {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* na
|
||||
* @param zone where the event occurred
|
||||
* @param player person attributed to the event
|
||||
* @param killStat information about the event
|
||||
* @return player-specific historical information and then related information that is inherited from other entities
|
||||
*/
|
||||
def produceContributionTranscriptFromKill(zone: Zone, player: Player, killStat: Kill): List[InGameActivity] = {
|
||||
(killStat.info.interaction.cause match {
|
||||
case pr: ProjectileReason => pr.projectile.mounted_in.flatMap { a => zone.GUID(a._1) } //what fired the projectile
|
||||
case _ => None
|
||||
}).collect {
|
||||
case mount: PlanetSideGameObject with FactionAffinity with InGameHistory with MountedWeapons =>
|
||||
player.ContributionFrom(mount)
|
||||
}
|
||||
player.HistoryAndContributions()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,3 +15,7 @@ case class Session(
|
|||
speed: Float = 1.0f,
|
||||
flying: Boolean = false
|
||||
)
|
||||
|
||||
trait SessionSource {
|
||||
def session: Session
|
||||
}
|
||||
|
|
|
|||
|
|
@ -115,6 +115,11 @@ case class MemberLists(
|
|||
ignored: List[Ignored] = List[Ignored]()
|
||||
)
|
||||
|
||||
case class ModePermissions(
|
||||
canSpectate: Boolean = false,
|
||||
canGM: Boolean = false
|
||||
)
|
||||
|
||||
case class Avatar(
|
||||
/** unique identifier corresponding to a database table row index */
|
||||
id: Int,
|
||||
|
|
@ -134,7 +139,8 @@ case class Avatar(
|
|||
loadouts: Loadouts = Loadouts(),
|
||||
cooldowns: Cooldowns = Cooldowns(),
|
||||
people: MemberLists = MemberLists(),
|
||||
scorecard: ScoreCard = new ScoreCard()
|
||||
scorecard: ScoreCard = new ScoreCard(),
|
||||
permissions: ModePermissions = ModePermissions()
|
||||
) {
|
||||
assert(bep >= 0)
|
||||
assert(cep >= 0)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ object Shortcut {
|
|||
*/
|
||||
def convert(shortcut: Shortcut): GameShortcut = {
|
||||
shortcut.tile match {
|
||||
case "medkit" => GameShortcut.Medkit()
|
||||
case "medkit" => GameShortcut.Medkit
|
||||
case "shortcut_macro" => GameShortcut.Macro(shortcut.effect1, shortcut.effect2)
|
||||
case _ => GameShortcut.Implant(shortcut.tile)
|
||||
}
|
||||
|
|
@ -67,7 +67,7 @@ object Shortcut {
|
|||
*/
|
||||
private def typeEquals(a: Shortcut, b: GameShortcut): Boolean = {
|
||||
b match {
|
||||
case GameShortcut.Medkit() => true
|
||||
case GameShortcut.Medkit => true
|
||||
case GameShortcut.Macro(x, y) => x.equals(a.effect1) && y.equals(a.effect2)
|
||||
case GameShortcut.Implant(tile) => tile.equals(a.tile)
|
||||
case _ => true
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ trait DeployableBehavior {
|
|||
if DeployableObject.OwnerGuid.nonEmpty =>
|
||||
val obj = DeployableObject
|
||||
if (constructed.contains(true)) {
|
||||
loseOwnership(obj.Faction)
|
||||
loseOwnership(obj, obj.Faction)
|
||||
} else {
|
||||
obj.OwnerGuid = None
|
||||
}
|
||||
|
|
@ -98,35 +98,16 @@ trait DeployableBehavior {
|
|||
* Losing ownership involves updating map screen UI, to remove management rights from the now-previous owner,
|
||||
* and may involve concealing the deployable from the map screen for the entirety of the previous owner's faction.
|
||||
* Displaying the deployable on the map screen of another faction may be required.
|
||||
* @param obj na
|
||||
* @param toFaction the faction to which to set the deployable to be visualized on the map and in the game world;
|
||||
* may also affect deployable operation
|
||||
*/
|
||||
def loseOwnership(toFaction: PlanetSideEmpire.Value): Unit = {
|
||||
val obj = DeployableObject
|
||||
val guid = obj.GUID
|
||||
val zone = obj.Zone
|
||||
val localEvents = zone.LocalEvents
|
||||
val originalFaction = obj.Faction
|
||||
val changeFaction = originalFaction != toFaction
|
||||
val info = DeployableInfo(guid, DeployableIcon.Boomer, obj.Position, Service.defaultPlayerGUID)
|
||||
if (changeFaction) {
|
||||
obj.Faction = toFaction
|
||||
//visual tells in regards to ownership by faction
|
||||
zone.AvatarEvents ! AvatarServiceMessage(
|
||||
zone.id,
|
||||
AvatarAction.SetEmpire(Service.defaultPlayerGUID, guid, toFaction)
|
||||
)
|
||||
//remove knowledge by the previous owner's faction
|
||||
localEvents ! LocalServiceMessage(
|
||||
originalFaction.toString,
|
||||
LocalAction.DeployableMapIcon(Service.defaultPlayerGUID, DeploymentAction.Dismiss, info)
|
||||
)
|
||||
//display to the given faction
|
||||
localEvents ! LocalServiceMessage(
|
||||
toFaction.toString,
|
||||
LocalAction.DeployableMapIcon(Service.defaultPlayerGUID, DeploymentAction.Build, info)
|
||||
)
|
||||
}
|
||||
def loseOwnership(obj: Deployable, toFaction: PlanetSideEmpire.Value): Unit = {
|
||||
DeployableBehavior.changeOwership(
|
||||
obj,
|
||||
obj.Faction,
|
||||
DeployableInfo(obj.GUID, Deployable.Icon.apply(obj.Definition.Item), obj.Position, Service.defaultPlayerGUID)
|
||||
)
|
||||
startOwnerlessDecay()
|
||||
}
|
||||
|
||||
|
|
@ -159,27 +140,11 @@ trait DeployableBehavior {
|
|||
val obj = DeployableObject
|
||||
obj.AssignOwnership(player)
|
||||
decay.cancel()
|
||||
|
||||
val guid = obj.GUID
|
||||
val zone = obj.Zone
|
||||
val originalFaction = obj.Faction
|
||||
val info = DeployableInfo(guid, DeployableIcon.Boomer, obj.Position, obj.OwnerGuid.get)
|
||||
if (originalFaction != toFaction) {
|
||||
obj.Faction = toFaction
|
||||
val localEvents = zone.LocalEvents
|
||||
zone.AvatarEvents ! AvatarServiceMessage(
|
||||
zone.id,
|
||||
AvatarAction.SetEmpire(Service.defaultPlayerGUID, guid, toFaction)
|
||||
)
|
||||
localEvents ! LocalServiceMessage(
|
||||
originalFaction.toString,
|
||||
LocalAction.DeployableMapIcon(Service.defaultPlayerGUID, DeploymentAction.Dismiss, info)
|
||||
)
|
||||
localEvents ! LocalServiceMessage(
|
||||
toFaction.toString,
|
||||
LocalAction.DeployableMapIcon(Service.defaultPlayerGUID, DeploymentAction.Build, info)
|
||||
)
|
||||
}
|
||||
DeployableBehavior.changeOwership(
|
||||
obj,
|
||||
toFaction,
|
||||
DeployableInfo(obj.GUID, Deployable.Icon.apply(obj.Definition.Item), obj.Position, obj.OwnerGuid.get)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -318,4 +283,35 @@ object DeployableBehavior {
|
|||
|
||||
/** internal message for progresisng the deconstruction process */
|
||||
private case class FinalizeElimination()
|
||||
|
||||
/**
|
||||
* na
|
||||
* @param obj na
|
||||
* @param toFaction na
|
||||
* @param info na
|
||||
*/
|
||||
def changeOwership(obj: Deployable, toFaction: PlanetSideEmpire.Value, info: DeployableInfo): Unit = {
|
||||
val guid = obj.GUID
|
||||
val zone = obj.Zone
|
||||
val localEvents = zone.LocalEvents
|
||||
val originalFaction = obj.Faction
|
||||
if (originalFaction != toFaction) {
|
||||
obj.Faction = toFaction
|
||||
//visual tells in regards to ownership by faction
|
||||
zone.AvatarEvents ! AvatarServiceMessage(
|
||||
zone.id,
|
||||
AvatarAction.SetEmpire(Service.defaultPlayerGUID, guid, toFaction)
|
||||
)
|
||||
//remove knowledge by the previous owner's faction
|
||||
localEvents ! LocalServiceMessage(
|
||||
originalFaction.toString,
|
||||
LocalAction.DeployableMapIcon(Service.defaultPlayerGUID, DeploymentAction.Dismiss, info)
|
||||
)
|
||||
//display to the given faction
|
||||
localEvents ! LocalServiceMessage(
|
||||
toFaction.toString,
|
||||
LocalAction.DeployableMapIcon(Service.defaultPlayerGUID, DeploymentAction.Build, info)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ object AvatarConverter {
|
|||
obj.avatar.basic,
|
||||
CommonFieldData(
|
||||
obj.Faction,
|
||||
bops = false,
|
||||
bops = obj.spectator,
|
||||
alt_model_flag,
|
||||
v1 = false,
|
||||
None,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,23 @@ object CommonMessages {
|
|||
final case class Hack(player: Player, obj: PlanetSideServerObject with Hackable, data: Option[Any] = None)
|
||||
final case class ClearHack()
|
||||
|
||||
/**
|
||||
* The message that progresses some form of user-driven activity with a certain eventual outcome
|
||||
* and potential feedback per cycle.
|
||||
* @param delta how much the progress value changes each tick, which will be treated as a percentage;
|
||||
* must be a positive value
|
||||
* @param completionAction a finalizing action performed once the progress reaches 100(%)
|
||||
* @param tickAction an action that is performed for each increase of progress
|
||||
* @param tickTime how long between each `tickAction` (ms);
|
||||
* defaults to 250 milliseconds
|
||||
*/
|
||||
final case class ProgressEvent(
|
||||
delta: Float,
|
||||
completionAction: () => Unit,
|
||||
tickAction: Float => Boolean,
|
||||
tickTime: Long = 250L
|
||||
)
|
||||
|
||||
/**
|
||||
* The message that initializes a process -
|
||||
* some form of user-driven activity with a certain eventual outcome and potential feedback per cycle.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ final case class CreateShortcutMessage(
|
|||
|
||||
object Shortcut extends Marshallable[Shortcut] {
|
||||
/** Preset for the medkit quick-use option. */
|
||||
final case class Medkit() extends Shortcut(code=0) {
|
||||
case object Medkit extends Shortcut(code=0) {
|
||||
def tile = "medkit"
|
||||
}
|
||||
|
||||
|
|
@ -98,14 +98,14 @@ object Shortcut extends Marshallable[Shortcut] {
|
|||
/**
|
||||
* Main transcoder for medkit shortcuts.
|
||||
*/
|
||||
val medkitCodec: Codec[Medkit] = (
|
||||
val medkitCodec: Codec[Shortcut] = (
|
||||
("tile" | PacketHelpers.encodedStringAligned(adjustment=5)) ::
|
||||
("effect1" | PacketHelpers.encodedWideString) ::
|
||||
("effect2" | PacketHelpers.encodedWideString)
|
||||
).xmap[Medkit](
|
||||
_ => Medkit(),
|
||||
).xmap[Shortcut](
|
||||
_ => Medkit,
|
||||
{
|
||||
case Medkit() => "medkit" :: "" :: "" :: HNil
|
||||
case Medkit => "medkit" :: "" :: "" :: HNil
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
}
|
||||
81
src/main/scala/net/psforever/packet/game/UplinkRequest.scala
Normal file
81
src/main/scala/net/psforever/packet/game/UplinkRequest.scala
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
// 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)
|
||||
|
||||
case object Unknown5 extends UplinkRequestType(value = 5)
|
||||
|
||||
case object Function6 extends UplinkRequestType(value = 6)
|
||||
|
||||
case object Function7 extends UplinkRequestType(value = 7)
|
||||
|
||||
case object Function8 extends UplinkRequestType(value = 8)
|
||||
|
||||
case object Unknown9 extends UplinkRequestType(value = 9)
|
||||
|
||||
case object UnknownA extends UplinkRequestType(value = 10)
|
||||
|
||||
case object FunctionB extends UplinkRequestType(value = 11)
|
||||
|
||||
case object FunctionC extends UplinkRequestType(value = 12)
|
||||
|
||||
case object UnknownD extends UplinkRequestType(value = 13)
|
||||
|
||||
case object UnknownE extends UplinkRequestType(value = 14)
|
||||
|
||||
case object FunctionF extends UplinkRequestType(value = 15)
|
||||
|
||||
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]
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
// Copyright (c) 2024 PSForever
|
||||
package net.psforever.persistence
|
||||
|
||||
case class Avatarmodepermission(
|
||||
avatarId: Int,
|
||||
canSpectate: Boolean = false,
|
||||
canGm: Boolean = false
|
||||
)
|
||||
|
|
@ -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())
|
||||
|
|
|
|||
12
src/main/scala/net/psforever/services/chat/ChatChannel.scala
Normal file
12
src/main/scala/net/psforever/services/chat/ChatChannel.scala
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
// Copyright (c) 2024 PSForever
|
||||
package net.psforever.services.chat
|
||||
|
||||
import net.psforever.types.PlanetSideGUID
|
||||
|
||||
trait ChatChannel
|
||||
|
||||
case object DefaultChannel extends ChatChannel
|
||||
|
||||
final case class SquadChannel(guid: PlanetSideGUID) extends ChatChannel
|
||||
|
||||
case object SpectatorChannel extends ChatChannel
|
||||
|
|
@ -4,9 +4,9 @@ package net.psforever.services.chat
|
|||
import akka.actor.typed.receptionist.{Receptionist, ServiceKey}
|
||||
import akka.actor.typed.{ActorRef, Behavior}
|
||||
import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext, Behaviors}
|
||||
import net.psforever.objects.Session
|
||||
import net.psforever.objects.{Session, SessionSource}
|
||||
import net.psforever.packet.game.ChatMsg
|
||||
import net.psforever.types.{ChatMessageType, PlanetSideEmpire, PlanetSideGUID}
|
||||
import net.psforever.types.{ChatMessageType, PlanetSideEmpire}
|
||||
|
||||
object ChatService {
|
||||
val ChatServiceKey: ServiceKey[Command] = ServiceKey[ChatService.Command]("chatService")
|
||||
|
|
@ -19,20 +19,12 @@ object ChatService {
|
|||
|
||||
sealed trait Command
|
||||
|
||||
final case class JoinChannel(actor: ActorRef[MessageResponse], session: Session, channel: ChatChannel) extends Command
|
||||
final case class JoinChannel(actor: ActorRef[MessageResponse], sessionSource: SessionSource, channel: ChatChannel) extends Command
|
||||
final case class LeaveChannel(actor: ActorRef[MessageResponse], channel: ChatChannel) extends Command
|
||||
final case class LeaveAllChannels(actor: ActorRef[MessageResponse]) extends Command
|
||||
|
||||
final case class Message(session: Session, message: ChatMsg, channel: ChatChannel) extends Command
|
||||
final case class MessageResponse(session: Session, message: ChatMsg, channel: ChatChannel)
|
||||
|
||||
trait ChatChannel
|
||||
object ChatChannel {
|
||||
// one of the default channels that the player is always subscribed to (local, broadcast, command...)
|
||||
final case class Default() extends ChatChannel
|
||||
final case class Squad(guid: PlanetSideGUID) extends ChatChannel
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class ChatService(context: ActorContext[ChatService.Command]) extends AbstractBehavior[ChatService.Command](context) {
|
||||
|
|
@ -63,9 +55,10 @@ class ChatService(context: ActorContext[ChatService.Command]) extends AbstractBe
|
|||
|
||||
case Message(session, message, channel) =>
|
||||
(channel, message.messageType) match {
|
||||
case (ChatChannel.Squad(_), CMT_SQUAD) => ()
|
||||
case (ChatChannel.Squad(_), CMT_VOICE) if message.contents.startsWith("SH") => ()
|
||||
case (ChatChannel.Default(), messageType) if messageType != CMT_SQUAD => ()
|
||||
case (SquadChannel(_), CMT_SQUAD) => ()
|
||||
case (SquadChannel(_), CMT_VOICE) if message.contents.startsWith("SH") => ()
|
||||
case (DefaultChannel, messageType) if messageType != CMT_SQUAD => ()
|
||||
case (SpectatorChannel, messageType) if messageType != CMT_SQUAD => ()
|
||||
case _ =>
|
||||
log.error(s"invalid chat channel $channel for messageType ${message.messageType}")
|
||||
return this
|
||||
|
|
@ -78,8 +71,8 @@ class ChatService(context: ActorContext[ChatService.Command]) extends AbstractBe
|
|||
val recipientName = message.recipient
|
||||
val recipientNameLower = recipientName.toLowerCase()
|
||||
(
|
||||
subs.find(_.session.player.Name.toLowerCase().equals(playerNameLower)),
|
||||
subs.find(_.session.player.Name.toLowerCase().equals(recipientNameLower))
|
||||
subs.find(_.sessionSource.session.player.Name.toLowerCase().equals(playerNameLower)),
|
||||
subs.find(_.sessionSource.session.player.Name.toLowerCase().equals(recipientNameLower))
|
||||
) match {
|
||||
case (Some(JoinChannel(sender, _, _)), Some(JoinChannel(receiver, _, _))) =>
|
||||
val replyType = if (mtype == CMT_TELL) { U_CMT_TELLFROM } else { U_CMT_GMTELLFROM }
|
||||
|
|
@ -122,14 +115,14 @@ class ChatService(context: ActorContext[ChatService.Command]) extends AbstractBe
|
|||
case _ => (None, None, None)
|
||||
}
|
||||
|
||||
val sender = subs.find(_.session.player.Name == session.player.Name)
|
||||
val sender = subs.find(_.sessionSource.session.player.Name == session.player.Name)
|
||||
|
||||
(sender, name, time, error) match {
|
||||
case (Some(sender), Some(name), Some(_), None) =>
|
||||
val recipient = subs.find(_.session.player.Name == name)
|
||||
val recipient = subs.find(_.sessionSource.session.player.Name == name)
|
||||
recipient match {
|
||||
case Some(recipient) =>
|
||||
if (recipient.session.player.silenced) {
|
||||
if (recipient.sessionSource.session.player.silenced) {
|
||||
sender.actor ! MessageResponse(
|
||||
session,
|
||||
ChatMsg(UNK_229, wideContents = true, "", "@silence_disabled_ack", None),
|
||||
|
|
@ -167,7 +160,7 @@ class ChatService(context: ActorContext[ChatService.Command]) extends AbstractBe
|
|||
|
||||
case CMT_NOTE =>
|
||||
subs
|
||||
.filter(_.session.player.Name == message.recipient)
|
||||
.filter(_.sessionSource.session.player.Name == message.recipient)
|
||||
.foreach(
|
||||
_.actor ! MessageResponse(session, message.copy(recipient = session.player.Name), channel)
|
||||
)
|
||||
|
|
@ -175,23 +168,23 @@ class ChatService(context: ActorContext[ChatService.Command]) extends AbstractBe
|
|||
// faction commands
|
||||
case CMT_OPEN | CMT_PLATOON | CMT_COMMAND =>
|
||||
subs
|
||||
.filter(_.session.player.Faction == session.player.Faction)
|
||||
.filter(_.sessionSource.session.player.Faction == session.player.Faction)
|
||||
.foreach(
|
||||
_.actor ! MessageResponse(session, message, channel)
|
||||
)
|
||||
|
||||
case CMT_GMBROADCAST_NC =>
|
||||
subs.filter(_.session.player.Faction == PlanetSideEmpire.NC).foreach {
|
||||
subs.filter(_.sessionSource.session.player.Faction == PlanetSideEmpire.NC).foreach {
|
||||
case JoinChannel(actor, _, _) => actor ! MessageResponse(session, message, channel)
|
||||
}
|
||||
|
||||
case CMT_GMBROADCAST_TR =>
|
||||
subs.filter(_.session.player.Faction == PlanetSideEmpire.TR).foreach {
|
||||
subs.filter(_.sessionSource.session.player.Faction == PlanetSideEmpire.TR).foreach {
|
||||
case JoinChannel(actor, _, _) => actor ! MessageResponse(session, message, channel)
|
||||
}
|
||||
|
||||
case CMT_GMBROADCAST_VS =>
|
||||
subs.filter(_.session.player.Faction == PlanetSideEmpire.VS).foreach {
|
||||
subs.filter(_.sessionSource.session.player.Faction == PlanetSideEmpire.VS).foreach {
|
||||
case JoinChannel(actor, _, _) => actor ! MessageResponse(session, message, channel)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ class CreateShortcutMessageTest extends Specification {
|
|||
player_guid mustEqual PlanetSideGUID(4210)
|
||||
slot mustEqual 1
|
||||
shortcut match {
|
||||
case Some(Shortcut.Medkit()) => ok
|
||||
case Some(Shortcut.Medkit) => ok
|
||||
case _ => ko
|
||||
}
|
||||
case _ =>
|
||||
|
|
@ -53,7 +53,7 @@ class CreateShortcutMessageTest extends Specification {
|
|||
}
|
||||
|
||||
"encode (medkit)" in {
|
||||
val msg = CreateShortcutMessage(PlanetSideGUID(4210), 1, Some(Shortcut.Medkit()))
|
||||
val msg = CreateShortcutMessage(PlanetSideGUID(4210), 1, Some(Shortcut.Medkit))
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual stringMedkit
|
||||
|
|
@ -90,8 +90,8 @@ class CreateShortcutMessageTest extends Specification {
|
|||
ImplantType.DarklightVision.shortcut.tile mustEqual "darklight_vision"
|
||||
ImplantType.Targeting.shortcut.code mustEqual 2
|
||||
ImplantType.Targeting.shortcut.tile mustEqual "targeting"
|
||||
Shortcut.Medkit().code mustEqual 0
|
||||
Shortcut.Medkit().tile mustEqual "medkit"
|
||||
Shortcut.Medkit.code mustEqual 0
|
||||
Shortcut.Medkit.tile mustEqual "medkit"
|
||||
ImplantType.MeleeBooster.shortcut.code mustEqual 2
|
||||
ImplantType.MeleeBooster.shortcut.tile mustEqual "melee_booster"
|
||||
ImplantType.PersonalShield.shortcut.code mustEqual 2
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue