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:
Fate-JH 2024-05-10 22:30:20 -04:00 committed by GitHub
parent 21637108c2
commit 426ab84f0a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
64 changed files with 14058 additions and 8917 deletions

View file

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

View file

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

View file

@ -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 _ => ()
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 _ => ()
}
}
}
}

View file

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

View file

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

View file

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

View file

@ -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 _ => ()
}
}
}

View file

@ -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 _ => ()
}
}
}

View file

@ -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 _ => ()
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -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 _ => ()
}
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -15,3 +15,7 @@ case class Session(
speed: Float = 1.0f,
flying: Boolean = false
)
trait SessionSource {
def session: Session
}

View file

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

View file

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

View file

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

View file

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

View file

@ -69,7 +69,7 @@ object AvatarConverter {
obj.avatar.basic,
CommonFieldData(
obj.Faction,
bops = false,
bops = obj.spectator,
alt_model_flag,
v1 = false,
None,

View file

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

View file

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

View file

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

View file

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

View file

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

View 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]
}

View file

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

View file

@ -0,0 +1,8 @@
// Copyright (c) 2024 PSForever
package net.psforever.persistence
case class Avatarmodepermission(
avatarId: Int,
canSpectate: Boolean = false,
canGm: Boolean = false
)

View file

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

View 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

View file

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

View file

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