diff --git a/src/main/scala/net/psforever/actors/session/SessionActor.scala b/src/main/scala/net/psforever/actors/session/SessionActor.scala index a3ee36e54..2caeb24db 100644 --- a/src/main/scala/net/psforever/actors/session/SessionActor.scala +++ b/src/main/scala/net/psforever/actors/session/SessionActor.scala @@ -1,38 +1,20 @@ -// 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.support.NormalUser 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.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 + +import scala.collection.mutable object SessionActor { sealed trait Command @@ -86,28 +68,6 @@ object SessionActor { final case object CharSaved extends Command 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 - ) - - private[session] final case class AvatarAwardMessageBundle( - bundle: Iterable[Iterable[PlanetSideGamePacket]], - delay: Long - ) } class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], connectionId: String, sessionId: Long) @@ -115,493 +75,46 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con 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 logic = new NormalUser(middlewareActor, context) 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() + logic.stop() } def receive: Receive = startup - def startup: Receive = { - case msg if !sessionFuncs.assignEventBus(msg) => + private def startup: Receive = { + case msg if !logic.assignEventBus(msg) => buffer.addOne(msg) - case _ if sessionFuncs.whenAllEventBusesLoaded() => + case _ if logic.whenAllEventBusesLoaded() => context.become(inTheGame) + startHeartbeat() 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) - - case AvatarServiceResponse(toChannel, guid, reply) => - sessionFuncs.avatarResponse.handle(toChannel, guid, reply) - - 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 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()}") + 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() + ) } - private def handleGamePkt: PlanetSideGamePacket => Unit = { - case packet: ConnectToWorldRequestMessage => - sessionFuncs.handleConnectToWorldRequest(packet) + private def inTheGame: Receive = { + /* used for the game's heartbeat*/ + case SessionActor.PokeClient() => + middlewareActor ! MiddlewareActor.Send(KeepAliveMessage()) - 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") + case packet => + logic.parse(sender())(packet) } } diff --git a/src/main/scala/net/psforever/actors/session/support/CommonSessionInterfacingFunctionality.scala b/src/main/scala/net/psforever/actors/session/support/CommonSessionInterfacingFunctionality.scala index 0856a2e98..187f8e8ab 100644 --- a/src/main/scala/net/psforever/actors/session/support/CommonSessionInterfacingFunctionality.scala +++ b/src/main/scala/net/psforever/actors/session/support/CommonSessionInterfacingFunctionality.scala @@ -23,23 +23,25 @@ trait CommonSessionInterfacingFunctionality { protected def context: ActorContext - protected def sessionData: SessionData + protected def sessionLogic: SessionLogic - 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 stop(): Unit = { /* to override */ } + protected[support] def actionsToCancel(): Unit = { /* to override */ } + + protected[support] def stop(): Unit = { /* to override */ } } diff --git a/src/main/scala/net/psforever/actors/session/support/SessionData.scala b/src/main/scala/net/psforever/actors/session/support/GeneralOperations.scala similarity index 70% rename from src/main/scala/net/psforever/actors/session/support/SessionData.scala rename to src/main/scala/net/psforever/actors/session/support/GeneralOperations.scala index 0143400f8..4130003c1 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionData.scala +++ b/src/main/scala/net/psforever/actors/session/support/GeneralOperations.scala @@ -1,20 +1,16 @@ -// Copyright (c) 2023 PSForever +// Copyright (c) 2024 PSForever package net.psforever.actors.session.support import akka.actor.typed.scaladsl.adapter._ import akka.actor.{ActorContext, ActorRef, Cancellable, typed} import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource} import net.psforever.objects.vital.etc.SuicideReason -import net.psforever.objects.zones.blockmap.{SectorGroup, SectorPopulation} import scala.collection.mutable import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.Future import scala.concurrent.duration._ // -import net.psforever.actors.net.MiddlewareActor import net.psforever.actors.session.{AvatarActor, ChatActor, SessionActor} -import net.psforever.actors.zone.ZoneActor import net.psforever.login.WorldSession._ import net.psforever.objects._ import net.psforever.objects.avatar._ @@ -27,7 +23,6 @@ import net.psforever.objects.guid._ import net.psforever.objects.inventory.{Container, InventoryItem} import net.psforever.objects.locker.LockerContainer import net.psforever.objects.serverobject.affinity.FactionAffinity -import net.psforever.objects.serverobject.deploy.Deployment import net.psforever.objects.serverobject.doors.Door import net.psforever.objects.serverobject.generator.Generator import net.psforever.objects.serverobject.llu.CaptureFlag @@ -35,7 +30,7 @@ import net.psforever.objects.serverobject.locks.IFFLock import net.psforever.objects.serverobject.mblocker.Locker import net.psforever.objects.serverobject.mount.Mountable import net.psforever.objects.serverobject.resourcesilo.ResourceSilo -import net.psforever.objects.serverobject.structures.{Amenity, Building, WarpGate} +import net.psforever.objects.serverobject.structures.{Building, WarpGate} import net.psforever.objects.serverobject.terminals._ import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal import net.psforever.objects.serverobject.terminals.implant.ImplantTerminalMech @@ -48,7 +43,6 @@ import net.psforever.objects.vital._ import net.psforever.objects.vital.collision.{CollisionReason, CollisionWithReason} import net.psforever.objects.vital.interaction.DamageInteraction import net.psforever.objects.zones._ -import net.psforever.objects.zones.blockmap.{BlockMap, BlockMapEntity} import net.psforever.packet._ import net.psforever.packet.game.{ActionCancelMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, BugReportMessage, ChangeShortcutBankMessage, CharacterRequestMessage, ConnectToWorldRequestMessage, CreateShortcutMessage, DeployObjectMessage, DisplayedAwardMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FavoritesRequest, FriendsRequest, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, InvalidTerrainMessage, LootItemMessage, MoveItemMessage, ObjectDetectedMessage, ObjectHeldMessage, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, RequestDestroyMessage, SetChatFilterMessage, TargetingImplantRequest, TradeMessage, UnuseItemMessage, UseItemMessage, ZipLineMessage} import net.psforever.packet.game.PlanetsideAttributeEnum.PlanetsideAttributeEnum @@ -56,111 +50,32 @@ import net.psforever.packet.game.objectcreate._ import net.psforever.packet.game._ import net.psforever.services.account.{AccountPersistenceService, RetrieveAccountData} import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} -import net.psforever.services.galaxy.{GalaxyAction, GalaxyServiceMessage} import net.psforever.services.local.support.CaptureFlagManager import net.psforever.services.local.{LocalAction, LocalServiceMessage} -import net.psforever.services.ServiceManager.LookupResult import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} -import net.psforever.services.{RemoverActor, Service, InterstellarClusterService => ICS} +import net.psforever.services.{RemoverActor, Service} import net.psforever.types._ import net.psforever.util.Config -object SessionData { - //noinspection ScalaUnusedSymbol - private def NoTurnCounterYet(guid: PlanetSideGUID): Unit = { } -} - -class SessionData( - middlewareActor: typed.ActorRef[MiddlewareActor.Command], - implicit val context: ActorContext - ) { - /** - * Hardwire an implicit `sender` to be the same as `context.self` of the `SessionActor` actor class - * for which this support class was initialized. - * Allows for proper use for `ActorRef.tell` or an actor's `!` in the support class, - * one where the result is always directed back to the same `SessionActor` instance. - * If there is a different packet "sender" that has to be respected by a given method, - * pass that `ActorRef` into the method as a parameter. - * @see `ActorRef.!(Any)(ActorRef)` - * @see `ActorRef.tell(Any)(ActorRef)` - */ - private[this] implicit val sender: ActorRef = context.self - - private val avatarActor: typed.ActorRef[AvatarActor.Command] = context.spawnAnonymous(AvatarActor(context.self)) - private val chatActor: typed.ActorRef[ChatActor.Command] = context.spawnAnonymous(ChatActor(context.self, avatarActor)) - - private[support] val log = org.log4s.getLogger - private[support] var theSession: Session = Session() - private[support] var accountIntermediary: ActorRef = Default.Actor - private[support] var accountPersistence: ActorRef = Default.Actor - private[support] var galaxyService: ActorRef = Default.Actor - private[support] var squadService: ActorRef = Default.Actor - private[support] var cluster: typed.ActorRef[ICS.Command] = Default.typed.Actor +class GeneralOperations( + val sessionLogic: SessionLogic, + avatarActor: typed.ActorRef[AvatarActor.Command], + chatActor: typed.ActorRef[ChatActor.Command], + implicit val context: ActorContext + ) extends CommonSessionInterfacingFunctionality { private[support] var progressBarValue: Option[Float] = None private[support] var accessedContainer: Option[PlanetSideGameObject with Container] = None - private[session] var connectionState: Int = 25 - private var recentTeleportAttempt: Long = 0 + private[support] var recentTeleportAttempt: Long = 0 private[support] var kitToBeUsed: Option[PlanetSideGUID] = None - private[support] var persistFunc: () => Unit = noPersistence - private[support] var persist: () => Unit = updatePersistenceOnly - private[support] var specialItemSlotGuid: 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[support] var serverTime: Long = 0 //unused? - private[session] var keepAliveFunc: () => Unit = keepAlivePersistenceInitial - private[support] var turnCounterFunc: PlanetSideGUID => Unit = SessionData.NoTurnCounterYet + // 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[support] var specialItemSlotGuid: Option[PlanetSideGUID] = None + private[support] val collisionHistory: mutable.HashMap[ActorRef, Long] = mutable.HashMap() private var heightLast: Float = 0f private var heightTrend: Boolean = false //up = true, down = false private var heightHistory: Float = 0f - private var contextSafeEntity: PlanetSideGUID = PlanetSideGUID(0) - private val collisionHistory: mutable.HashMap[ActorRef, Long] = mutable.HashMap() - - private var clientKeepAlive: Cancellable = Default.Cancellable private[support] var progressBarUpdate: Cancellable = Default.Cancellable private var charSavedTimer: Cancellable = Default.Cancellable - val shooting: WeaponAndProjectileOperations = - new WeaponAndProjectileOperations(sessionData=this, avatarActor, chatActor, context) - val vehicles: VehicleOperations = - new VehicleOperations(sessionData=this, avatarActor, context) - val avatarResponse: SessionAvatarHandlers = - new SessionAvatarHandlers(sessionData=this, avatarActor, chatActor, context) - val localResponse: SessionLocalHandlers = - new SessionLocalHandlers(sessionData=this, context) - val mountResponse: SessionMountHandlers = - new SessionMountHandlers(sessionData=this, avatarActor, context) - val terminals: SessionTerminalHandlers = - new SessionTerminalHandlers(sessionData=this, avatarActor, context) - private var vehicleResponseOpt: Option[SessionVehicleHandlers] = None - private var galaxyResponseOpt: Option[SessionGalaxyHandlers] = None - private var squadResponseOpt: Option[SessionSquadHandlers] = None - private var zoningOpt: Option[ZoningOperations] = None - def vehicleResponseOperations: SessionVehicleHandlers = vehicleResponseOpt.orNull - def galaxyResponseHanders: SessionGalaxyHandlers = galaxyResponseOpt.orNull - def squad: SessionSquadHandlers = squadResponseOpt.orNull - def zoning: ZoningOperations = zoningOpt.orNull - - /** - * updated when an upstream packet arrives; - * allow to be a little stale for a short while - */ - private[support] var localSector: SectorPopulation = SectorGroup(Nil) - - def session: Session = theSession - - def session_=(session: Session): Unit = { - chatActor ! ChatActor.SetSession(session) - avatarActor ! AvatarActor.SetSession(session) - theSession = session - } - - def account: Account = theSession.account - - def continent: Zone = theSession.zone - - def player: Player = theSession.player - - def avatar: Avatar = theSession.avatar - /* packets */ def handleConnectToWorldRequest(pkt: ConnectToWorldRequestMessage)(implicit context: ActorContext): Unit = { @@ -169,15 +84,7 @@ class SessionData( s"ConnectToWorldRequestMessage: client with versioning $majorVersion.$minorVersion.$revision, $buildDate has sent a token to the server" ) sendResponse(ChatMsg(ChatMessageType.CMT_CULLWATERMARK, wideContents=false, "", "", None)) - import scala.concurrent.ExecutionContext.Implicits.global - clientKeepAlive.cancel() - clientKeepAlive = context.system.scheduler.scheduleWithFixedDelay( - initialDelay = 0.seconds, - delay = 500.milliseconds, - context.self, - SessionActor.PokeClient() - ) - accountIntermediary ! RetrieveAccountData(token) + sessionLogic.accountIntermediary ! RetrieveAccountData(token) } def handleCharacterCreateRequest(pkt: CharacterCreateRequestMessage): Unit = { @@ -212,22 +119,22 @@ class SessionData( _, _ )= pkt - persist() - turnCounterFunc(avatarGuid) - updateBlockMap(player, pos) + sessionLogic.persist() + sessionLogic.turnCounterFunc(avatarGuid) + sessionLogic.updateBlockMap(player, pos) val isMoving = WorldEntity.isMoving(vel) val isMovingPlus = isMoving || isJumping || jumpThrust if (isMovingPlus) { - if (zoning.zoningStatus == Zoning.Status.Deconstructing) { - stopDeconstructing() - } else if (zoning.zoningStatus != Zoning.Status.None) { - zoning.CancelZoningProcessWithDescriptiveReason("cancel_motion") + if (sessionLogic.zoning.zoningStatus == Zoning.Status.Deconstructing) { + sessionLogic.zoning.spawn.stopDeconstructing() + } else if (sessionLogic.zoning.zoningStatus != Zoning.Status.None) { + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_motion") } } fallHeightTracker(pos.z) -// if (isCrouching && !player.Crouching) { -// //dev stuff goes here -// } + // if (isCrouching && !player.Crouching) { + // //dev stuff goes here + // } player.Position = pos player.Velocity = vel player.Orientation = Vector3(player.Orientation.x, pitch, yaw) @@ -235,14 +142,14 @@ class SessionData( player.Crouching = isCrouching player.Jumping = isJumping if (isCloaking && !player.Cloaked) { - zoning.CancelZoningProcessWithDescriptiveReason("cancel_cloak") + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_cloak") } player.Cloaked = player.ExoSuit == ExoSuitType.Infiltration && isCloaking maxCapacitorTick(jumpThrust) - if (isMovingPlus && terminals.usingMedicalTerminal.isDefined) { - continent.GUID(terminals.usingMedicalTerminal) match { + if (isMovingPlus && sessionLogic.terminals.usingMedicalTerminal.isDefined) { + continent.GUID(sessionLogic.terminals.usingMedicalTerminal) match { case Some(term: Terminal with ProximityUnit) => - terminals.StopUsingProximityUnit(term) + sessionLogic.terminals.StopUsingProximityUnit(term) case _ => () } } @@ -271,8 +178,8 @@ class SessionData( } val eagleEye: Boolean = canSeeReallyFar val isNotVisible: Boolean = player.spectator || - zoning.zoningStatus == Zoning.Status.Deconstructing || - (player.isAlive && zoning.spawn.deadState == DeadState.RespawnTime) + sessionLogic.zoning.zoningStatus == Zoning.Status.Deconstructing || + (player.isAlive && sessionLogic.zoning.spawn.deadState == DeadState.RespawnTime) continent.AvatarEvents ! AvatarServiceMessage( continent.id, AvatarAction.PlayerState( @@ -291,9 +198,9 @@ class SessionData( eagleEye ) ) - squad.updateSquad() + sessionLogic.squad.updateSquad() if (player.death_by == -1) { - kickedByAdministration() + sessionLogic.kickedByAdministration() } player.zoneInteractions() } @@ -329,14 +236,14 @@ class SessionData( def handleDropItem(pkt: DropItemMessage): Unit = { val DropItemMessage(itemGuid) = pkt - (validObject(itemGuid, decorator = "DropItem"), player.FreeHand.Equipment) match { + (sessionLogic.validObject(itemGuid, decorator = "DropItem"), player.FreeHand.Equipment) match { case (Some(anItem: Equipment), Some(heldItem)) if (anItem eq heldItem) && continent.GUID(player.VehicleSeated).nonEmpty => - zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") RemoveOldEquipmentFromInventory(player)(heldItem) case (Some(anItem: Equipment), Some(heldItem)) if anItem eq heldItem => - zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") DropEquipmentFromInventory(player)(heldItem) case (Some(anItem: Equipment), _) if continent.GUID(player.VehicleSeated).isEmpty => @@ -350,9 +257,9 @@ class SessionData( def handlePickupItem(pkt: PickupItemMessage): Unit = { val PickupItemMessage(itemGuid, _, _, _) = pkt - validObject(itemGuid, decorator = "PickupItem").collect { + sessionLogic.validObject(itemGuid, decorator = "PickupItem").collect { case item: Equipment if player.Fit(item).nonEmpty => - zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") PickUpEquipmentFromGround(player)(item) case _: Equipment => sendResponse(ActionResultMessage.Fail(16)) //error code? @@ -374,13 +281,13 @@ class SessionData( val ZipLineMessage(playerGuid, forwards, action, pathId, pos) = pkt continent.zipLinePaths.find(x => x.PathId == pathId) match { case Some(path) if path.IsTeleporter => - zoning.CancelZoningProcessWithDescriptiveReason("cancel") + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel") 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(_) => - zoning.CancelZoningProcessWithDescriptiveReason("cancel_motion") + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_motion") action match { case 0 => //travel along the zipline in the direction specified @@ -404,7 +311,7 @@ class SessionData( def handleRequestDestroy(pkt: RequestDestroyMessage): Unit = { val RequestDestroyMessage(objectGuid) = pkt //make sure this is the correct response for all cases - validObject(objectGuid, decorator = "RequestDestroy") match { + sessionLogic.validObject(objectGuid, decorator = "RequestDestroy") match { case Some(vehicle: Vehicle) => /* line 1a: player is admin (and overrules other access requirements) */ /* line 1b: vehicle and player (as the owner) acknowledge each other */ @@ -464,7 +371,7 @@ class SessionData( ( continent.GUID(sourceGuid), continent.GUID(destinationGuid), - validObject(itemGuid, decorator = "MoveItem") + sessionLogic.validObject(itemGuid, decorator = "MoveItem") ) match { case ( Some(source: PlanetSideServerObject with Container), @@ -490,7 +397,7 @@ class SessionData( def handleLootItem(pkt: LootItemMessage): Unit = { val LootItemMessage(itemGuid, targetGuid) = pkt - (validObject(itemGuid, decorator = "LootItem"), continent.GUID(targetGuid)) match { + (sessionLogic.validObject(itemGuid, decorator = "LootItem"), continent.GUID(targetGuid)) match { case (Some(item: Equipment), Some(destination: PlanetSideServerObject with Container)) => //figure out the source ( @@ -531,9 +438,9 @@ class SessionData( def handleAvatarImplant(pkt: AvatarImplantMessage): Unit = { val AvatarImplantMessage(_, action, slot, status) = pkt if (action == ImplantAction.Activation) { - if (zoning.zoningStatus == Zoning.Status.Deconstructing) { + if (sessionLogic.zoning.zoningStatus == Zoning.Status.Deconstructing) { //do not activate; play deactivation sound instead - stopDeconstructing() + sessionLogic.zoning.spawn.stopDeconstructing() avatar.implants(slot).collect { case implant if implant.active => avatarActor ! AvatarActor.DeactivateImplant(implant.definition.implantType) @@ -541,7 +448,7 @@ class SessionData( sendResponse(PlanetsideAttributeMessage(player.GUID, 28, implant.definition.implantType.value * 2)) } } else { - zoning.CancelZoningProcessWithDescriptiveReason("cancel_implant") + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_implant") avatar.implants(slot) match { case Some(implant) => if (status == 1) { @@ -559,13 +466,13 @@ class SessionData( def handleUseItem(pkt: UseItemMessage): Unit = { val equipment = findContainedEquipment(pkt.item_used_guid) match { case (o @ Some(_), a) if a.exists(_.isInstanceOf[Tool]) => - shooting.FindEnabledWeaponsToHandleWeaponFireAccountability(o, a.collect { case w: Tool => w })._2.headOption + sessionLogic.shooting.FindEnabledWeaponsToHandleWeaponFireAccountability(o, a.collect { case w: Tool => w })._2.headOption case (Some(_), a) => a.headOption case _ => None } - validObject(pkt.object_guid, decorator = "UseItem") match { + sessionLogic.validObject(pkt.object_guid, decorator = "UseItem") match { case Some(door: Door) => handleUseDoor(door, equipment) case Some(resourceSilo: ResourceSilo) => @@ -614,10 +521,10 @@ class SessionData( def handleUnuseItem(pkt: UnuseItemMessage): Unit = { val UnuseItemMessage(_, objectGuid) = pkt - validObject(objectGuid, decorator = "UnuseItem") match { + sessionLogic.validObject(objectGuid, decorator = "UnuseItem") match { case Some(obj: Player) => unaccessContainer(obj) - zoning.spawn.TryDisposeOfLootedCorpse(obj) + sessionLogic.zoning.spawn.TryDisposeOfLootedCorpse(obj) case Some(obj: Container) => // Make sure we don't unload the contents of the vehicle the player is seated in // An example scenario of this would be closing the trunk contents when rearming at a landing pad @@ -637,7 +544,7 @@ class SessionData( case dtype => dtype } log.info(s"${player.Name} is constructing a $ammoType deployable") - zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") val dObj: Deployable = Deployables.Make(ammoType)() dObj.Position = pos dObj.Orientation = orient @@ -660,7 +567,7 @@ class SessionData( def handlePlanetsideAttribute(pkt: PlanetsideAttributeMessage): Unit = { val PlanetsideAttributeMessage(objectGuid, attributeType, attributeValue) = pkt - validObject(objectGuid, decorator = "PlanetsideAttribute") match { + sessionLogic.validObject(objectGuid, decorator = "PlanetsideAttribute") match { case Some(vehicle: Vehicle) if player.avatar.vehicle.contains(vehicle.GUID) => vehicle.Actor ! ServerObject.AttributeMsg(attributeType, attributeValue) case Some(vehicle: Vehicle) => @@ -676,7 +583,7 @@ class SessionData( def handleGenericObjectAction(pkt: GenericObjectActionMessage): Unit = { val GenericObjectActionMessage(objectGuid, code) = pkt - validObject(objectGuid, decorator = "GenericObjectAction") match { + sessionLogic.validObject(objectGuid, decorator = "GenericObjectAction") match { case Some(vehicle: Vehicle) if vehicle.OwnerName.contains(player.Name) => vehicle.Actor ! ServerObject.GenericObjectAction(objectGuid, code, Some(player.GUID)) @@ -687,9 +594,9 @@ class SessionData( ) { //maelstrom primary fire mode discharge (no target) //aphelion_laser discharge (no target) - shooting.HandleWeaponFireAccountability(objectGuid, PlanetSideGUID(Projectile.baseUID)) + sessionLogic.shooting.HandleWeaponFireAccountability(objectGuid, PlanetSideGUID(Projectile.baseUID)) } else { - validObject(player.VehicleSeated, decorator = "GenericObjectAction/Vehicle") match { + sessionLogic.validObject(player.VehicleSeated, decorator = "GenericObjectAction/Vehicle") match { case Some(vehicle: Vehicle) if vehicle.OwnerName.contains(player.Name) => vehicle.Actor ! ServerObject.GenericObjectAction(objectGuid, code, Some(tool)) @@ -703,9 +610,9 @@ class SessionData( def handleGenericObjectActionAtPosition(pkt: GenericObjectActionAtPositionMessage): Unit = { val GenericObjectActionAtPositionMessage(objectGuid, _, _) = pkt - validObject(objectGuid, decorator = "GenericObjectActionAtPosition") match { + sessionLogic.validObject(objectGuid, decorator = "GenericObjectActionAtPosition") match { case Some(tool: Tool) if GlobalDefinitions.isBattleFrameNTUSiphon(tool.Definition) => - shooting.FindContainedWeapon match { + sessionLogic.shooting.FindContainedWeapon match { case (Some(vehicle: Vehicle), weps) if weps.exists(_.GUID == objectGuid) => vehicle.Actor ! SpecialEmp.Burst() case _ => () @@ -807,11 +714,11 @@ class SessionData( Config.app.game.savedMsg.renewal.variable ) case GenericAction.LookingForSquad_RCV => //Looking For Squad ON - if (!avatar.lookingForSquad && (squad.squadUI.isEmpty || squad.squadUI(player.CharId).index == 0)) { + 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 && (squad.squadUI.isEmpty || squad.squadUI(player.CharId).index == 0)) { + if (avatar.lookingForSquad && (sessionLogic.squad.squadUI.isEmpty || sessionLogic.squad.squadUI(player.CharId).index == 0)) { avatarActor ! AvatarActor.SetLookingForSquad(false) } case _ => @@ -822,7 +729,7 @@ class SessionData( def handleFavoritesRequest(pkt: FavoritesRequest): Unit = { val FavoritesRequest(_, loadoutType, action, line, label) = pkt - zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") action match { case FavoritesAction.Save => avatarActor ! AvatarActor.SaveLoadout(player, loadoutType, label, line) @@ -851,7 +758,7 @@ class SessionData( 0f } } - val (target1, target2, bailProtectStatus, velocity) = (ctype, validObject(p, decorator = "GenericCollision/Primary")) match { + val (target1, target2, bailProtectStatus, velocity) = (ctype, sessionLogic.validObject(p, decorator = "GenericCollision/Primary")) match { case (CollisionIs.OfInfantry, out @ Some(user: Player)) if user == player => val bailStatus = session.flying || player.spectator || session.speed > 1f || player.BailProtection @@ -869,10 +776,10 @@ class SessionData( if v.Seats(0).occupant.contains(player) => val bailStatus = v.BailProtection v.BailProtection = false - (out, validObject(t, decorator = "GenericCollision/GroundVehicle"), bailStatus, pv) + (out, sessionLogic.validObject(t, decorator = "GenericCollision/GroundVehicle"), bailStatus, pv) case (CollisionIs.OfAircraft, out @ Some(v: Vehicle)) if v.Definition.CanFly && v.Seats(0).occupant.contains(player) => - (out, validObject(t, decorator = "GenericCollision/Aircraft"), false, pv) + (out, sessionLogic.validObject(t, decorator = "GenericCollision/Aircraft"), false, pv) case (CollisionIs.BetweenThings, _) => log.warn("GenericCollision: CollisionIs.BetweenThings detected - no handling case") (None, None, false, Vector3.Zero) @@ -886,7 +793,7 @@ class SessionData( case (Some(us: PlanetSideServerObject with Vitality with FactionAffinity), PlanetSideGUID(0), _) => if (updateCollisionHistoryForTarget(us, curr)) { if (!bailProtectStatus) { - handleDealingDamage( + sessionLogic.handleDealingDamage( us, DamageInteraction( SourceEntry(us), @@ -1036,10 +943,10 @@ class SessionData( def handleObjectDetected(pkt: ObjectDetectedMessage): Unit = { val ObjectDetectedMessage(_, _, _, targets) = pkt - shooting.FindWeapon.foreach { + sessionLogic.shooting.FindWeapon.foreach { case weapon if weapon.Projectile.AutoLock => //projectile with auto-lock instigates a warning on the target - val detectedTargets = shooting.FindDetectedProjectileTargets(targets) + val detectedTargets = sessionLogic.shooting.FindDetectedProjectileTargets(targets) val mode = 7 + (weapon.Projectile == GlobalDefinitions.wasp_rocket_projectile) detectedTargets.foreach { target => continent.AvatarEvents ! AvatarServiceMessage(target, AvatarAction.ProjectileAutoLockAwareness(mode)) @@ -1090,15 +997,6 @@ class SessionData( avatarActor ! AvatarActor.SetAccount(account) } - def handleUpdateIgnoredPlayers: PlanetSideGamePacket => Unit = { - case msg: FriendsResponse => - sendResponse(msg) - msg.friends.foreach { f => - galaxyService ! GalaxyServiceMessage(GalaxyAction.LogStatusChange(f.name)) - } - case _ => () - } - def handleUseCooldownRenew: BasicDefinition => Unit = { case _: KitDefinition => kitToBeUsed = None case _ => () @@ -1106,7 +1004,7 @@ class SessionData( def handleAvatarResponse(avatar: Avatar): Unit = { session = session.copy(avatar = avatar) - accountPersistence ! AccountPersistenceService.Login(avatar.name, avatar.id) + sessionLogic.accountPersistence ! AccountPersistenceService.Login(avatar.name, avatar.id) } def handleSetSpeed(speed: Float): Unit = { @@ -1123,7 +1021,7 @@ class SessionData( def handleKick(player: Player, time: Option[Long]): Unit = { administrativeKick(player) - accountPersistence ! AccountPersistenceService.Kick(player.Name, time) + sessionLogic.accountPersistence ! AccountPersistenceService.Kick(player.Name, time) } def handleSilenced(isSilenced: Boolean): Unit = { @@ -1132,126 +1030,28 @@ class SessionData( /* supporting functions */ - def buildDependentOperationsForGalaxy(galaxyActor: ActorRef): Unit = { - if (vehicleResponseOpt.isEmpty && galaxyActor != Default.Actor) { - galaxyResponseOpt = Some(new SessionGalaxyHandlers(sessionData=this, avatarActor, galaxyActor, context)) - vehicleResponseOpt = Some(new SessionVehicleHandlers(sessionData=this, avatarActor, galaxyActor, context)) - } - } - - def buildDependentOperations(galaxyActor: ActorRef, clusterActor: typed.ActorRef[ICS.Command]): Unit = { - if (zoningOpt.isEmpty && galaxyActor != Default.Actor && clusterActor != Default.typed.Actor) { - zoningOpt = Some(new ZoningOperations(sessionData=this, avatarActor, galaxyActor, clusterActor, context)) - } - } - - def buildDependentOperationsForSquad(squadActor: ActorRef): Unit = { - if (squadResponseOpt.isEmpty && squadActor != Default.Actor) { - squadResponseOpt = Some(new SessionSquadHandlers(sessionData=this, avatarActor, chatActor, squadActor, context)) - } - } - - def assignEventBus(msg: Any): Boolean = { - msg match { - case LookupResult("accountIntermediary", endpoint) => - accountIntermediary = endpoint - true - case LookupResult("accountPersistence", endpoint) => - accountPersistence = endpoint - true - case LookupResult("galaxy", endpoint) => - galaxyService = endpoint - buildDependentOperationsForGalaxy(endpoint) - buildDependentOperations(endpoint, cluster) - true - case LookupResult("squad", endpoint) => - squadService = endpoint - buildDependentOperationsForSquad(endpoint) - true - case ICS.InterstellarClusterServiceKey.Listing(listings) => - cluster = listings.head - buildDependentOperations(galaxyService, cluster) - true - - case _ => - false - } - } - - def whenAllEventBusesLoaded(): Boolean = { - accountIntermediary != Default.Actor && - accountPersistence != Default.Actor && - vehicleResponseOpt.nonEmpty && - galaxyResponseOpt.nonEmpty && - squadResponseOpt.nonEmpty && - zoningOpt.nonEmpty - } - - def validObject(id: Int): Option[PlanetSideGameObject] = validObject(Some(PlanetSideGUID(id)), decorator = "") - - def validObject(id: Int, decorator: String): Option[PlanetSideGameObject] = validObject(Some(PlanetSideGUID(id)), decorator) - - def validObject(id: PlanetSideGUID): Option[PlanetSideGameObject] = validObject(Some(id), decorator = "") - - def validObject(id: PlanetSideGUID, decorator: String): Option[PlanetSideGameObject] = validObject(Some(id), decorator) - - def validObject(id: Option[PlanetSideGUID]): Option[PlanetSideGameObject] = validObject(id, decorator = "") - - def validObject(id: Option[PlanetSideGUID], decorator: String): Option[PlanetSideGameObject] = { - val elevatedDecorator = if (decorator.nonEmpty) decorator else "ValidObject" - id match { - case Some(guid) => - val hint = oldRefsMap.getOrElse(guid, "thing") - continent.GUID(guid) match { - case Some(_: LocalProjectile) => - shooting.FindProjectileEntry(guid) - - case Some(_: LocalLockerItem) => - player.avatar.locker.Inventory.hasItem(guid) match { - case out @ Some(_) => - contextSafeEntity = guid - out - case None if contextSafeEntity == guid => - //safeguard - None - case None => - //delete stale entity reference from client - log.warn( - s"$elevatedDecorator: ${player.Name} is looking for an invalid GUID $guid, believing it a $hint in ${player.Sex.possessive} locker" - ) - sendResponse(ObjectDeleteMessage(guid, 0)) - None - } - - case Some(obj) if obj.HasGUID && obj.GUID != guid => - log.error( - s"$elevatedDecorator: ${player.Name} found a ${obj.Definition.Name} that isn't the $hint ${player.Sex.pronounSubject} thought it was in zone ${continent.id}" - ) - log.debug( - s"$elevatedDecorator: potentially fatal error in ${continent.id} - requested $hint with $guid, got ${obj.Definition.Name} with ${obj.GUID}; mismatch" - ) - None - - case out @ Some(obj) if obj.HasGUID => - out - - case None if !id.contains(PlanetSideGUID(0)) => - //delete stale entity reference from client - //deleting guid=0 will cause BAD things to happen - log.error(s"$elevatedDecorator: ${player.Name} has an invalid reference to $hint with GUID $guid in zone ${continent.id}") - sendResponse(ObjectDeleteMessage(guid, 0)) - None - - case None if contextSafeEntity == guid => - //safeguard - None - - case _ => - None - } - - case None => - None + /** + * 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)) + } } } @@ -1269,7 +1069,7 @@ class SessionData( } private def handleUseResourceSilo(resourceSilo: ResourceSilo, equipment: Option[Equipment]): Unit = { - zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") (continent.GUID(player.VehicleSeated), equipment) match { case (Some(vehicle: Vehicle), Some(item)) if GlobalDefinitions.isBattleFrameVehicle(vehicle.Definition) && @@ -1281,7 +1081,7 @@ class SessionData( } private def handleUsePlayer(obj: Player, equipment: Option[Equipment], msg: UseItemMessage): Unit = { - zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") if (obj.isBackpack) { if (equipment.isEmpty) { log.info(s"${player.Name} is looting the corpse of ${obj.Name}") @@ -1318,7 +1118,7 @@ class SessionData( sendUseGeneralEntityMessage(locker, item) case None if locker.Faction == player.Faction || locker.HackedBy.nonEmpty => log.info(s"${player.Name} is accessing a locker") - zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") val playerLocker = player.avatar.locker sendResponse(msg.copy(object_guid = playerLocker.GUID, object_id = 456)) accessContainer(playerLocker) @@ -1365,7 +1165,7 @@ class SessionData( .contains(player.GUID)) ) { log.info(s"${player.Name} is looking in the ${obj.Definition.Name}'s trunk") - zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") obj.AccessingTrunk = player.GUID accessContainer(obj) sendResponse(msg) @@ -1384,7 +1184,7 @@ class SessionData( val tdef = terminal.Definition if (tdef.isInstanceOf[MatrixTerminalDefinition]) { //TODO matrix spawn point; for now, just blindly bind to show work (and hope nothing breaks) - zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") sendResponse( BindPlayerMessage(BindStatus.Bind, "", display_icon=true, logging=true, SpawnGroup.Sanctuary, 0, 0, terminal.Position) ) @@ -1405,7 +1205,7 @@ class SessionData( } else if (tdef == GlobalDefinitions.teleportpad_terminal) { //explicit request log.info(s"${player.Name} is purchasing a router telepad") - zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") terminal.Actor ! Terminal.Request( player, ItemTransactionMessage(msg.object_guid, TransactionType.Buy, 0, "router_telepad", 0, PlanetSideGUID(0)) @@ -1413,14 +1213,14 @@ class SessionData( } else if (tdef == GlobalDefinitions.targeting_laser_dispenser) { //explicit request log.info(s"${player.Name} is purchasing a targeting laser") - zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") terminal.Actor ! Terminal.Request( player, ItemTransactionMessage(msg.object_guid, TransactionType.Buy, 0, "flail_targeting_laser", 0, PlanetSideGUID(0)) ) } else { log.info(s"${player.Name} is accessing a ${terminal.Definition.Name}") - zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") sendResponse(msg) } case _ => () @@ -1433,10 +1233,10 @@ class SessionData( sendUseGeneralEntityMessage(obj, item) case None if player.Faction == obj.Faction => //deconstruction - zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") - playerActionsToCancel() - terminals.CancelAllProximityUnits() - startDeconstructing(obj) + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + sessionLogic.actionsToCancel() + sessionLogic.terminals.CancelAllProximityUnits() + sessionLogic.zoning.spawn.startDeconstructing(obj) case _ => () } } @@ -1449,7 +1249,7 @@ class SessionData( case None => None }) match { case Some((vehicle: Vehicle, Some(util: Utility.InternalTelepad))) => - zoning.CancelZoningProcessWithDescriptiveReason("cancel") + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel") player.WhichSide = vehicle.WhichSide useRouterTelepadSystem( router = vehicle, @@ -1475,7 +1275,7 @@ class SessionData( private def handleUseInternalTelepad(obj: InternalTelepad, msg: UseItemMessage): Unit = { continent.GUID(obj.Telepad) match { case Some(pad: TelepadDeployable) => - zoning.CancelZoningProcessWithDescriptiveReason("cancel") + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel") player.WhichSide = pad.WhichSide useRouterTelepadSystem( router = obj.Owner.asInstanceOf[Vehicle], @@ -1511,7 +1311,7 @@ class SessionData( } private def handleUseWarpGate(equipment: Option[Equipment]): Unit = { - zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") (continent.GUID(player.VehicleSeated), equipment) match { case (Some(vehicle: Vehicle), Some(item)) if GlobalDefinitions.isBattleFrameVehicle(vehicle.Definition) && @@ -1523,18 +1323,18 @@ class SessionData( private def handleUseGeneralEntity(obj: PlanetSideServerObject, equipment: Option[Equipment]): Unit = { equipment.foreach { item => - zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") obj.Actor ! CommonMessages.Use(player, Some(item)) } } private def sendUseGeneralEntityMessage(obj: PlanetSideServerObject, equipment: Equipment): Unit = { - zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") obj.Actor ! CommonMessages.Use(player, Some(equipment)) } private def handleUseDefaultEntity(obj: PlanetSideGameObject, equipment: Option[Equipment]): Unit = { - zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") equipment match { case Some(item) if GlobalDefinitions.isBattleFrameArmorSiphon(item.Definition) || @@ -1544,20 +1344,6 @@ class SessionData( } } - /** - * Update this player avatar for persistence. - * Set to `persist` initially. - */ - def updatePersistenceOnly(): Unit = { - persistFunc() - } - - /** - * Do not update this player avatar for persistence. - * Set to `persistFunc` initially. - */ - def noPersistence(): Unit = { } - def dropSpecialSlotItem(): Unit = { specialItemSlotGuid.foreach { guid => specialItemSlotGuid = None @@ -1596,35 +1382,10 @@ class SessionData( } } - /** - * 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 setupProgressChange(rate: Float, finishedAction: () => Unit, stepAction: Float => Boolean): Unit = { if (progressBarValue.isEmpty) { progressBarValue = Some(-rate) - context.self ! SessionActor.ProgressEvent(rate, finishedAction, stepAction) + context.self ! CommonMessages.ProgressEvent(rate, finishedAction, stepAction) } } @@ -1673,7 +1434,7 @@ class SessionData( progressBarUpdate = context.system.scheduler.scheduleOnce( delay = 100 milliseconds, context.self, - SessionActor.ProgressEvent(delta, completionAction, tickAction) + CommonMessages.ProgressEvent(delta, completionAction, tickAction) ) } else { progressBarValue = None @@ -1686,7 +1447,7 @@ class SessionData( progressBarUpdate = context.system.scheduler.scheduleOnce( tick.milliseconds, context.self, - SessionActor.ProgressEvent(delta, completionAction, tickAction, tick) + CommonMessages.ProgressEvent(delta, completionAction, tickAction, tick) ) } else { progressBarValue = None @@ -1695,108 +1456,6 @@ class SessionData( } } - /** - * 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)) - ) - } - - /** - * 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 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 - */ - private[session] 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)) - ) - } - - 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)) - ) - } - def accessContainer(container: Container): Unit = { container match { case v: Vehicle => @@ -1811,7 +1470,7 @@ class SessionData( } } - def accessGenericContainer(container: PlanetSideServerObject with Container): Unit = { + private def accessGenericContainer(container: PlanetSideServerObject with Container): Unit = { accessedContainer = Some(container) displayContainerContents(container.GUID, container.Inventory.Items) } @@ -1824,7 +1483,7 @@ class SessionData( * @see `GridInventory.Items` * @param vehicle the vehicle */ - def accessVehicleContents(vehicle: Vehicle): Unit = { + private def accessVehicleContents(vehicle: Vehicle): Unit = { accessedContainer = Some(vehicle) accessContainerChannel(continent.VehicleEvents, vehicle.Actor.toString) displayContainerContents(vehicle.GUID, vehicle.Inventory.Items) @@ -1839,7 +1498,7 @@ class SessionData( * @see `Player.HolsterItems` * @param tplayer the corpse */ - def accessCorpseContents(tplayer: Player): Unit = { + private def accessCorpseContents(tplayer: Player): Unit = { accessedContainer = Some(tplayer) accessContainerChannel(continent.AvatarEvents, tplayer.Actor.toString) displayContainerContents(tplayer.GUID, tplayer.HolsterItems()) @@ -1851,7 +1510,7 @@ class SessionData( * @param events the event system bus to which to subscribe * @param channel the channel name */ - def accessContainerChannel(events: ActorRef, channel: String): Unit = { + private def accessContainerChannel(events: ActorRef, channel: String): Unit = { events ! Service.Join(channel) } @@ -1864,7 +1523,7 @@ class SessionData( * @param containerId the container's unique identifier * @param items a list of the entities to be depicted */ - def displayContainerContents(containerId: PlanetSideGUID, items: Iterable[InventoryItem]): Unit = { + private def displayContainerContents(containerId: PlanetSideGUID, items: Iterable[InventoryItem]): Unit = { items.foreach(entry => { val obj = entry.obj val objDef = obj.Definition @@ -1905,7 +1564,7 @@ class SessionData( } } - def unaccessGenericContainer(container: Container): Unit = { + private def unaccessGenericContainer(container: Container): Unit = { accessedContainer = None hideContainerContents(container.Inventory.Items) } @@ -1916,7 +1575,7 @@ class SessionData( * Deconstruct every object in the vehicle's inventory. * @param vehicle the vehicle */ - def unaccessVehicleContainer(vehicle: Vehicle): Unit = { + private def unaccessVehicleContainer(vehicle: Vehicle): Unit = { accessedContainer = None if (vehicle.AccessingTrunk.contains(player.GUID)) { vehicle.AccessingTrunk = None @@ -1931,7 +1590,7 @@ class SessionData( * Deconstruct every object in the backpack's inventory. * @param tplayer the corpse */ - def unaccessCorpseContainer(tplayer: Player): Unit = { + private def unaccessCorpseContainer(tplayer: Player): Unit = { accessedContainer = None unaccessContainerChannel(continent.AvatarEvents, tplayer.Actor.toString) hideContainerContents(tplayer.HolsterItems()) @@ -1943,7 +1602,7 @@ class SessionData( * @param events the event system bus to which to subscribe * @param channel the channel name */ - def unaccessContainerChannel(events: ActorRef, channel: String): Unit = { + private def unaccessContainerChannel(events: ActorRef, channel: String): Unit = { events ! Service.Leave(Some(channel)) } @@ -1953,7 +1612,7 @@ class SessionData( * @see `ObjectDeleteMessage` * @param items a list of the entities to be depicted */ - def hideContainerContents(items: List[InventoryItem]): Unit = { + private def hideContainerContents(items: List[InventoryItem]): Unit = { items.foreach { entry => sendResponse(ObjectDeleteMessage(entry.obj.GUID, 0)) } @@ -2064,31 +1723,6 @@ class SessionData( parent.Find(objectGuid).flatMap { slot => Some((parent, Some(slot))) } } - /** - * 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 youCanNotChangeDeployment( - 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") - } - /** * na * @param targetGuid na @@ -2096,7 +1730,7 @@ class SessionData( * @param unk2 na */ def hackObject(targetGuid: PlanetSideGUID, unk1: Long, unk2: Long): Unit = { - sendResponse(HackMessage(0, targetGuid, PlanetSideGUID(0), 100, unk1, HackState.Hacked, unk2)) + sendResponse(HackMessage(unk1=0, targetGuid, player_guid=Service.defaultPlayerGUID, progress=100, unk1, HackState.Hacked, unk2)) } /** @@ -2124,160 +1758,6 @@ class SessionData( tplayer.Actor ! Player.Die() } - /** - * An event has occurred that would cause the player character to stop certain stateful activities. - * These activities include shooting, the weapon being drawn, hacking, accessing (a container), flying, and running. - * Other players in the same zone must be made aware that the player has stopped as well.
- *
- * Things whose configuration should not be changed:
- * - if the player is seated
- * - if the player is anchored
- * This is not a complete list but, for the purpose of enforcement, some pointers will be documented here. - */ - def playerActionsToCancel(): Unit = { - shooting.shootingStart.clear() - shooting.shootingStop.clear() - progressBarUpdate.cancel() - progressBarValue = None - terminals.lastTerminalOrderFulfillment = true - kitToBeUsed = None - collisionHistory.clear() - accessedContainer match { - case Some(v: Vehicle) => - val vguid = v.GUID - 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 => () - } - (shooting.prefire ++ shooting.shooting).foreach { guid => - sendResponse(ChangeFireStateMessage_Stop(guid)) - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.ChangeFireState_Stop(player.GUID, guid) - ) - } - shooting.prefire.clear() - shooting.shooting.clear() - if (session.flying) { - session = session.copy(flying = false) - chatActor ! ChatActor.Message(ChatMsg(ChatMessageType.CMT_FLY, wideContents=false, "", "off", None)) - } - if (session.speed > 1) { - session = session.copy(speed = 1) - chatActor ! ChatActor.Message(ChatMsg(ChatMessageType.CMT_SPEED, wideContents=false, "", "1.000", None)) - } - } - - /** - * Calculate the amount of damage to be dealt to an active `target` - * using the information reconstructed from a `ResolvedProjectile` - * and affect the `target` in a synchronized manner. - * The active `target` and the target of the `DamageResult` do not have be the same. - * While the "tell" for being able to sustain damage is an entity of type `Vitality`, - * only specific `Vitality` entity types are being screened for sustaining damage. - * @see `DamageResistanceModel` - * @see `Vitality` - * @param target a valid game object that is known to the server - * @param data a projectile that will affect the target - */ - def handleDealingDamage(target: PlanetSideGameObject with Vitality, data: DamageInteraction): Unit = { - val func = data.calculate() - target match { - case obj: Player if obj.CanDamage && obj.Actor != Default.Actor => - if (obj.CharId != player.CharId) { - log.info(s"${player.Name} is attacking ${obj.Name}") - } else { - log.info(s"${player.Name} hurt ${player.Sex.pronounObject}self") - } - // auto kick players damaging spectators - if (obj.spectator && obj != player) { - administrativeKick(player) - } else { - obj.Actor ! Vitality.Damage(func) - } - - case obj: Vehicle if obj.CanDamage => - val name = player.Name - val ownerName = obj.OwnerName.getOrElse("someone") - if (ownerName.equals(name)) { - log.info(s"$name is damaging ${player.Sex.possessive} own ${obj.Definition.Name}") - } else { - log.info(s"$name is attacking $ownerName's ${obj.Definition.Name}") - } - obj.Actor ! Vitality.Damage(func) - - case obj: Amenity if obj.CanDamage => - obj.Actor ! Vitality.Damage(func) - - case obj: Deployable if obj.CanDamage => - val name = player.Name - val ownerName = obj.OwnerName.getOrElse("someone") - if (ownerName.equals(name)) { - log.info(s"$name is damaging ${player.Sex.possessive} own ${obj.Definition.Name}") - } else { - log.info(s"$name is attacking $ownerName's ${obj.Definition.Name}") - } - obj.Actor ! Vitality.Damage(func) - - case _ => () - } - } - - /** - * 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 - ) - } - /** * Initialize the deployables user interface elements.
*
@@ -2304,19 +1784,6 @@ class SessionData( } } - /** - * Common actions related to constructing a new `Deployable` object in the game environment.
- *
- * The map icon for the deployable just introduced is also created on the clients of all faction-affiliated players. - * This icon is important as, short of destroying it, - * the owner has no other means of controlling the created object that it is associated with. - * @param obj the `Deployable` object to be built - */ - def deployableBuildActivity(obj: Deployable): Unit = { - sendResponse(GenericObjectActionMessage(obj.GUID, 21)) //reset build cooldown - updateDeployableUIElements(avatar.deployables.UpdateUIElement(obj.Definition.Item)) - } - /** * A simple object searching algorithm that is limited to containers currently known and accessible by the player. * If all relatively local containers are checked and the object is not found, @@ -2377,53 +1844,6 @@ class SessionData( } } - /** - * 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)) - } - - /** - * 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 - } - } - /** * Attempt to link the router teleport system using the provided terminal information. * Although additional states are necessary to properly use the teleportation system, @@ -2502,24 +1922,6 @@ class SessionData( sendResponse(GenericObjectActionMessage(destGUID, 32)) } - /** - * 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 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 - } - } - def maxCapacitorTick(jumpThrust: Boolean): Unit = { if (player.ExoSuit == ExoSuitType.MAX) { val activate = (jumpThrust || player.isOverdrived || player.isShielded) && player.Capacitor > 0 @@ -2618,43 +2020,12 @@ class SessionData( ) } - /** - * The atypical response to receiving a `KeepAliveMessage` packet from the client.
- *
- * `KeepAliveMessage` packets are the primary vehicle for persistence due to client reporting - * in the case where the player's avatar is riding in a vehicle in a mount with no weapon to control. - * @see `KeepAliveMessage` - * @see `keepAliveFunc` - * @see `turnCounterFunc` - * @see `persist` - */ - def keepAlivePersistence(): Unit = { - zoning.spawn.interimUngunnedVehicle = None - persist() - if (player.HasGUID) { - turnCounterFunc(player.GUID) - } else { - turnCounterFunc(PlanetSideGUID(0)) - } - } - - /** - * A really atypical response to receiving a `KeepAliveMessage` packet from the client - * that applies only during the character select portion and part of the first zone load activity. - */ - def keepAlivePersistenceInitial(): Unit = { - persist() - if (player != null && player.HasGUID) { - keepAliveFunc = keepAlivePersistence - } - } - def administrativeKick(tplayer: Player): Unit = { log.warn(s"${tplayer.Name} has been kicked by ${player.Name}") tplayer.death_by = -1 - accountPersistence ! AccountPersistenceService.Kick(tplayer.Name) + sessionLogic.accountPersistence ! AccountPersistenceService.Kick(tplayer.Name) //get out of that vehicle - vehicles.GetMountableAndSeat(None, tplayer, continent) match { + sessionLogic.vehicles.GetMountableAndSeat(None, tplayer, continent) match { case (Some(obj), Some(seatNum)) => tplayer.VehicleSeated = None obj.Seats(seatNum).unmount(tplayer) @@ -2666,82 +2037,6 @@ class SessionData( } } - def kickedByAdministration(): Unit = { - sendResponse(DisconnectMessage("@kick_w")) - context.system.scheduler.scheduleOnce( - delay = 300 milliseconds, - middlewareActor.toClassic, - MiddlewareActor.Teardown() - ) - } - - def immediateDisconnect(): Unit = { - if (avatar != null) { - accountPersistence ! AccountPersistenceService.Logout(avatar.name) - } - middlewareActor ! MiddlewareActor.Teardown() - } - - def updateBlockMap(target: BlockMapEntity, newCoords: Vector3): Unit = { - target.blockMapEntry.foreach { entry => - val sectorIndices = BlockMap.findSectorIndices(continent.blockMap, newCoords, entry.rangeX, entry.rangeY).toSet - if (sectorIndices.equals(entry.sectors)) { - target.updateBlockMapEntry(newCoords) //soft update - localSector = continent.blockMap.sector(sectorIndices, Config.app.game.playerDraw.rangeMax.toFloat) - } else { - continent.actor ! ZoneActor.UpdateBlockMap(target, newCoords) //hard update - } - } - } - - def updateLocalBlockMap(pos: Vector3): Unit = { - localSector = continent.blockMap.sector(pos, Config.app.game.playerDraw.rangeMax.toFloat) - } - - private[support] var oldRefsMap: mutable.HashMap[PlanetSideGUID, String] = new mutable.HashMap[PlanetSideGUID, String]() - def updateOldRefsMap(): Unit = { - if(player.HasGUID) { - oldRefsMap.addAll( - (continent.GUID(player.VehicleSeated) match { - case Some(v : Vehicle) => - v.Weapons.toList.collect { - case (_, slot : EquipmentSlot) if slot.Equipment.nonEmpty => updateOldRefsMap(slot.Equipment.get) - }.flatten ++ - updateOldRefsMap(v.Inventory) - case _ => - Map.empty[PlanetSideGUID, String] - }) ++ - (accessedContainer match { - case Some(cont) => updateOldRefsMap(cont.Inventory) - case None => Map.empty[PlanetSideGUID, String] - }) ++ - player.Holsters().toList.collect { - case slot if slot.Equipment.nonEmpty => updateOldRefsMap(slot.Equipment.get) - }.flatten ++ - updateOldRefsMap(player.Inventory) ++ - updateOldRefsMap(player.avatar.locker.Inventory) - ) - } - } - - def updateOldRefsMap(inventory: net.psforever.objects.inventory.GridInventory): IterableOnce[(PlanetSideGUID, String)] = { - inventory.Items.flatMap { - case InventoryItem(o, _) => updateOldRefsMap(o) - } - } - - def updateOldRefsMap(item: PlanetSideGameObject): IterableOnce[(PlanetSideGUID, String)] = { - item match { - case t: Tool => - t.AmmoSlots.map { slot => - val box = slot.Box - box.GUID -> box.Definition.Name - } :+ (t.GUID -> t.Definition.Name) - case _ => - Seq(item.GUID -> item.Definition.Name) - } - } - def fallHeightTracker(zHeight: Float): Unit = { if ((heightTrend && heightLast - zHeight >= 0.5f) || (!heightTrend && zHeight - heightLast >= 0.5f)) { @@ -2752,7 +2047,7 @@ class SessionData( } def canSeeReallyFar: Boolean = { - shooting.FindContainedWeapon match { + sessionLogic.shooting.FindContainedWeapon match { case (Some(_: Vehicle), weapons) if weapons.nonEmpty => player.avatar .implants @@ -2792,34 +2087,6 @@ class SessionData( sendResponse(ChatMsg(ChatMessageType.UNK_227, wideContents=false, "", "@charsaved", None)) } - 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) - } - zoning.spawn.nextSpawnPoint = Some(obj) //set fallback - zoning.zoningStatus = Zoning.Status.Deconstructing - player.allowInteraction = false - if (player.death_by == 0) { - player.death_by = 1 - } - zoning.spawn.GoToDeploymentMap() - } - - def stopDeconstructing(): Unit = { - zoning.zoningStatus = Zoning.Status.None - player.death_by = math.min(player.death_by, 0) - player.allowInteraction = true - zoning.spawn.nextSpawnPoint.foreach { tube => - sendResponse(PlayerStateShiftMessage(ShiftState(0, tube.Position, tube.Orientation.z))) - zoning.spawn.nextSpawnPoint = None - } - } - private def updateCollisionHistoryForTarget( target: PlanetSideServerObject with Vitality with FactionAffinity, curr: Long @@ -2834,21 +2101,21 @@ class SessionData( } private def collisionBetweenVehicleAndFragileDeployable( - vehicle: Vehicle, - vehiclePosition: Vector3, - smallDeployable: Deployable, - smallDeployablePosition: Vector3, - velocity: Vector3, - fallHeight: Float, - collisionTime: Long - ): Unit = { + vehicle: Vehicle, + vehiclePosition: Vector3, + smallDeployable: Deployable, + smallDeployablePosition: Vector3, + velocity: Vector3, + fallHeight: Float, + collisionTime: Long + ): Unit = { if (updateCollisionHistoryForTarget(smallDeployable, collisionTime)) { val smallDeployableSource = SourceEntry(smallDeployable) //vehicle takes damage from the collision (ignore bail protection in this case) performCollisionWithSomethingDamage(vehicle, SourceEntry(vehicle), vehiclePosition, smallDeployableSource, fallHeight, velocity) //deployable gets absolutely destroyed collisionHistory.put(vehicle.Actor, collisionTime) - handleDealingDamage( + sessionLogic.handleDealingDamage( smallDeployable, DamageInteraction(smallDeployableSource, SuicideReason(), smallDeployablePosition) ) @@ -2863,7 +2130,7 @@ class SessionData( fallHeight: Float, velocity: Vector3 ): Unit = { - handleDealingDamage( + sessionLogic.handleDealingDamage( target, DamageInteraction( targetSource, @@ -2873,37 +2140,37 @@ class SessionData( ) } - def failWithError(error: String): Unit = { - log.error(error) - middlewareActor ! MiddlewareActor.Teardown() - } + override protected[support] 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) + } + } - def sendResponse(packet: PlanetSidePacket): Unit = { - middlewareActor ! MiddlewareActor.Send(packet) - } + case Some(o) => + unaccessContainer(o) + if (player.isAlive) { + sendResponse(UnuseItemMessage(player.GUID, o.GUID)) + } - def stop(): Unit = { - continent.AvatarEvents ! Service.Leave() - continent.LocalEvents ! Service.Leave() - continent.VehicleEvents ! Service.Leave() - context.stop(avatarActor) - context.stop(chatActor) - galaxyService ! Service.Leave() - if (avatar != null && squadService != Default.Actor) { - squadService ! Service.Leave(Some(s"${avatar.faction}")) + case None => () } - clientKeepAlive.cancel() + } + + override protected[support] def stop(): Unit = { progressBarUpdate.cancel() charSavedTimer.cancel() - shooting.stop() - vehicles.stop() - avatarResponse.stop() - localResponse.stop() - mountResponse.stop() - terminals.stop() - vehicleResponseOpt.foreach { _.stop() } - galaxyResponseOpt.foreach { _.stop() } - squadResponseOpt.foreach { _.stop() } - zoningOpt.foreach { _.stop() } } } diff --git a/src/main/scala/net/psforever/actors/session/support/NormalUser.scala b/src/main/scala/net/psforever/actors/session/support/NormalUser.scala new file mode 100644 index 000000000..fdbb89f83 --- /dev/null +++ b/src/main/scala/net/psforever/actors/session/support/NormalUser.scala @@ -0,0 +1,496 @@ +// Copyright (c) 2024 PSForever +package net.psforever.actors.session.support + +import akka.actor.Actor.Receive +import akka.actor.{ActorContext, ActorRef, typed} +import net.psforever.actors.net.MiddlewareActor +import net.psforever.actors.session.{AvatarActor, SessionActor} +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 NormalUser( + val middlewareActor: typed.ActorRef[MiddlewareActor.Command], + implicit val context: ActorContext + ) extends SessionLogic { + 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) => + galaxyResponseHandlers.handle(reply) + + case LocalServiceResponse(toChannel, guid, reply) => + localResponse.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) => + vehicleResponseOperations.handle(toChannel, guid, reply) + + case SessionActor.PokeClient() => + sendResponse(KeepAliveMessage()) + + case SessionActor.SendResponse(packet) => + sendResponse(packet) + + case SessionActor.CharSaved => + general.renewCharSavedTimer( + Config.app.game.savedMsg.interruptedByAction.fixed, + Config.app.game.savedMsg.interruptedByAction.variable + ) + + case SessionActor.CharSavedMsg => + general.displayCharSavedMsgThenRenewTimer( + Config.app.game.savedMsg.renewal.fixed, + Config.app.game.savedMsg.renewal.variable + ) + + /* common messages (maybe once every respawn) */ + case ICS.SpawnPointResponse(response) => + zoning.handleSpawnPointResponse(response) + + case SessionActor.NewPlayerLoaded(tplayer) => + zoning.spawn.handleNewPlayerLoaded(tplayer) + + case SessionActor.PlayerLoaded(tplayer) => + zoning.spawn.handlePlayerLoaded(tplayer) + + case Zone.Population.PlayerHasLeft(zone, None) => + log.debug(s"PlayerHasLeft: ${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 Zone.Population.PlayerCanNotSpawn(zone, tplayer) => + log.warn(s"${tplayer.Name} can not spawn in zone ${zone.id}; why?") + + case Zone.Population.PlayerAlreadySpawned(zone, tplayer) => + log.warn(s"${tplayer.Name} is already spawned on zone ${zone.id}; is this a clerical error?") + + case Zone.Vehicle.CanNotSpawn(zone, vehicle, reason) => + log.warn( + s"${player.Name}'s ${vehicle.Definition.Name} can not spawn in ${zone.id} because $reason" + ) + + case Zone.Vehicle.CanNotDespawn(zone, vehicle, reason) => + log.warn( + s"${player.Name}'s ${vehicle.Definition.Name} can not deconstruct in ${zone.id} because $reason" + ) + + case ICS.ZoneResponse(Some(zone)) => + zoning.handleZoneResponse(zone) + + /* uncommon messages (once a session) */ + case ICS.ZonesResponse(zones) => + zoning.handleZonesResponse(zones) + + case SessionActor.SetAvatar(avatar) => + general.handleSetAvatar(avatar) + + case PlayerToken.LoginInfo(name, Zone.Nowhere, _) => + zoning.spawn.handleLoginInfoNowhere(name, sender) + + case PlayerToken.LoginInfo(name, inZone, optionalSavedData) => + zoning.spawn.handleLoginInfoSomewhere(name, inZone, optionalSavedData, sender) + + case PlayerToken.RestoreInfo(playerName, inZone, pos) => + zoning.spawn.handleLoginInfoRestore(playerName, inZone, pos, sender) + + case PlayerToken.CanNotLogin(playerName, reason) => + zoning.spawn.handleLoginCanNot(playerName, reason) + + case ReceiveAccountData(account) => + general.handleReceiveAccountData(account) + + case AvatarActor.AvatarResponse(avatar) => + general.handleAvatarResponse(avatar) + + case AvatarActor.AvatarLoginResponse(avatar) => + zoning.spawn.avatarLoginResponse(avatar) + + case SessionActor.SetCurrentAvatar(tplayer, max_attempts, attempt) => + zoning.spawn.ReadyToSetCurrentAvatar(tplayer, max_attempts, attempt) + + case SessionActor.SetConnectionState(state) => + connectionState = state + + case SessionActor.AvatarLoadingSync(state) => + zoning.spawn.handleAvatarLoadingSync(state) + + /* uncommon messages (utility, or once in a while) */ + case ZoningOperations.AvatarAwardMessageBundle(pkts, delay) => + zoning.spawn.performAvatarAwardMessageDelivery(pkts, delay) + + case CommonMessages.ProgressEvent(delta, finishedAction, stepAction, tick) => + general.handleProgressChange(delta, finishedAction, stepAction, tick) + + case CommonMessages.Progress(rate, finishedAction, stepAction) => + general.setupProgressChange(rate, finishedAction, stepAction) + + case CavernRotationService.CavernRotationServiceKey.Listing(listings) => + listings.head ! SendCavernRotationUpdates(context.self) + + case LookupResult("propertyOverrideManager", endpoint) => + zoning.propertyOverrideManagerLoadOverrides(endpoint) + + case SessionActor.UpdateIgnoredPlayers(msg) => + galaxyResponseHandlers.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.LocalStopUsingProximityUnit(term) + + case SessionActor.Suicide() => + general.suicide(player) + + case SessionActor.Recall() => + zoning.handleRecall() + + case SessionActor.InstantAction() => + zoning.handleInstantAction() + + case SessionActor.Quit() => + zoning.handleQuit() + + case ICS.DroppodLaunchDenial(errorCode, _) => + zoning.handleDroppodLaunchDenial(errorCode) + + case ICS.DroppodLaunchConfirmation(zone, position) => + zoning.LoadZoneLaunchDroppod(zone, position) + + case SessionActor.PlayerFailedToLoad(tplayer) => + 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) => + zoning.handleSetZone(zoneId, position) + + case SessionActor.SetPosition(position) => + 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(continent.GUID, obj)) + + case Zone.Deployable.IsDismissed(obj) => //only if target deployable was never fully introduced + TaskWorkflow.execute(GUIDTask.unregisterObject(continent.GUID, obj)) + + case msg: Containable.ItemPutInSlot => + log.debug(s"ItemPutInSlot: $msg") + + case msg: Containable.CanNotPutItemInSlot => + log.debug(s"CanNotPutItemInSlot: $msg") + + case default => + log.warn(s"Invalid packet class received: $default from $sender") + } + + private def handleGamePkt: PlanetSideGamePacket => Unit = { + case packet: ConnectToWorldRequestMessage => + general.handleConnectToWorldRequest(packet) + + case packet: MountVehicleCargoMsg => + vehicles.handleMountVehicleCargo(packet) + + case packet: DismountVehicleCargoMsg => + vehicles.handleDismountVehicleCargo(packet) + + case packet: CharacterCreateRequestMessage => + general.handleCharacterCreateRequest(packet) + + case packet: CharacterRequestMessage => + general.handleCharacterRequest(packet) + + case _: KeepAliveMessage => + keepAliveFunc() + + case packet: BeginZoningMessage => + 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 => + zoning.spawn.handleReleaseAvatarRequest(packet) + + case packet: SpawnRequestMessage => + zoning.spawn.handleSpawnRequest(packet) + + case packet: ChatMsg => + general.handleChat(packet) + + case packet: SetChatFilterMessage => + general.handleChatFilter(packet) + + case packet: VoiceHostRequest => + general.handleVoiceHostRequest(packet) + + case packet: VoiceHostInfo => + general.handleVoiceHostInfo(packet) + + case packet: ChangeAmmoMessage => + shooting.handleChangeAmmo(packet) + + case packet: ChangeFireModeMessage => + shooting.handleChangeFireMode(packet) + + case packet: ChangeFireStateMessage_Start => + shooting.handleChangeFireStateStart(packet) + + case packet: ChangeFireStateMessage_Stop => + shooting.handleChangeFireStateStop(packet) + + case packet: EmoteMsg => + general.handleEmote(packet) + + case packet: DropItemMessage => + general.handleDropItem(packet) + + case packet: PickupItemMessage => + general.handlePickupItem(packet) + + case packet: ReloadMessage => + shooting.handleReload(packet) + + case packet: ObjectHeldMessage => + general.handleObjectHeld(packet) + + case packet: AvatarJumpMessage => + general.handleAvatarJump(packet) + + case packet: ZipLineMessage => + general.handleZipLine(packet) + + case packet: RequestDestroyMessage => + general.handleRequestDestroy(packet) + + case packet: MoveItemMessage => + general.handleMoveItem(packet) + + case packet: LootItemMessage => + general.handleLootItem(packet) + + case packet: AvatarImplantMessage => + general.handleAvatarImplant(packet) + + case packet: UseItemMessage => + general.handleUseItem(packet) + + case packet: UnuseItemMessage => + general.handleUnuseItem(packet) + + case packet: ProximityTerminalUseMessage => + terminals.handleProximityTerminalUse(packet) + + case packet: DeployObjectMessage => + general.handleDeployObject(packet) + + case packet: GenericObjectActionMessage => + general.handleGenericObjectAction(packet) + + case packet: GenericObjectActionAtPositionMessage => + general.handleGenericObjectActionAtPosition(packet) + + case packet: GenericObjectStateMsg => + general.handleGenericObjectState(packet) + + case packet: GenericActionMessage => + general.handleGenericAction(packet) + + case packet: ItemTransactionMessage => + terminals.handleItemTransaction(packet) + + case packet: FavoritesRequest => + general.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: 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 => + zoning.handleWarpgateRequest(packet) + + case packet: MountVehicleMsg => + vehicles.handleMountVehicle(packet) + + case packet: DismountVehicleMsg => + vehicles.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 => + 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 => + log.warn(s"Unhandled GamePacket $pkt") + } +} diff --git a/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala b/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala index b3aefad4d..f92ea3425 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala @@ -3,6 +3,7 @@ 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 @@ -29,7 +30,7 @@ import net.psforever.util.Config import net.psforever.zones.Zones class SessionAvatarHandlers( - val sessionData: SessionData, + val sessionLogic: SessionLogic, avatarActor: typed.ActorRef[AvatarActor.Command], chatActor: typed.ActorRef[ChatActor.Command], implicit val context: ActorContext @@ -86,7 +87,7 @@ class SessionAvatarHandlers( val inDrawableRange = currentDistance <= maxRange val now = System.currentTimeMillis() //ms if ( - sessionData.zoning.zoningStatus != Zoning.Status.Deconstructing && + sessionLogic.zoning.zoningStatus != Zoning.Status.Deconstructing && !isNotRendered && inDrawableRange ) { //conditions where visibility is assured @@ -95,7 +96,7 @@ class SessionAvatarHandlers( lazy val targetDelay = { val populationOver = math.max( 0, - sessionData.localSector.livePlayerList.size - drawConfig.populationThreshold + sessionLogic.localSector.livePlayerList.size - drawConfig.populationThreshold ) val distanceAdjustment = math.pow(populationOver / drawConfig.populationStep * drawConfig.rangeStep, 2) //sq.m val adjustedDistance = currentDistance + distanceAdjustment //sq.m @@ -110,7 +111,7 @@ class SessionAvatarHandlers( (!lastMsg.contains(pstateToSave) && (canSeeReallyFar || currentDistance < drawConfig.rangeMin * drawConfig.rangeMin || - sessionData.canSeeReallyFar || + sessionLogic.general.canSeeReallyFar || durationSince > targetDelay ) ) @@ -164,11 +165,11 @@ class SessionAvatarHandlers( 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) + continent.GUID(sessionLogic.terminals.usingMedicalTerminal).collect { + case term: Terminal with ProximityUnit => sessionLogic.terminals.StopUsingProximityUnit(term) } - if (sessionData.zoning.zoningStatus == Zoning.Status.Deconstructing) { - sessionData.stopDeconstructing() + if (sessionLogic.zoning.zoningStatus == Zoning.Status.Deconstructing) { + sessionLogic.zoning.spawn.stopDeconstructing() } case AvatarResponse.ObjectHeld(slot, _) @@ -221,32 +222,28 @@ class SessionAvatarHandlers( 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)) + 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(sessionData.destroyDisplayMessage(killer, victim, method, unk)) + sendResponse(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 + sessionLogic.terminals.lastTerminalOrderFulfillment = true AvatarActor.savePlayerData(player) - sessionData.renewCharSavedTimer( + 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)) - sessionData.terminals.lastTerminalOrderFulfillment = true + sessionLogic.terminals.lastTerminalOrderFulfillment = true case AvatarResponse.ChangeExosuit( target, @@ -358,7 +355,7 @@ class SessionAvatarHandlers( slot = 0 )) } - sessionData.applyPurchaseTimersBeforePackingLoadout(player, player, holsters ++ inventory) + sessionLogic.general.applyPurchaseTimersBeforePackingLoadout(player, player, holsters ++ inventory) DropLeftovers(player)(drops) case AvatarResponse.ChangeLoadout(target, armor, exosuit, subtype, slot, _, oldHolsters, _, _, _, _) => @@ -389,10 +386,10 @@ class SessionAvatarHandlers( sendResponse(ObjectDeleteMessage(kguid, unk1=0)) case AvatarResponse.KitNotUsed(_, "") => - sessionData.kitToBeUsed = None + sessionLogic.general.kitToBeUsed = None case AvatarResponse.KitNotUsed(_, msg) => - sessionData.kitToBeUsed = None + sessionLogic.general.kitToBeUsed = None sendResponse(ChatMsg(ChatMessageType.UNK_225, msg)) case AvatarResponse.UpdateKillsDeathsAssists(_, kda) => @@ -439,40 +436,40 @@ class SessionAvatarHandlers( 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) { + if (sessionLogic.shooting.shotsWhileDead > 0) { log.warn( - s"SHOTS_WHILE_DEAD: client of ${avatar.name} fired ${sessionData.shooting.shotsWhileDead} rounds while character was dead on server" + s"SHOTS_WHILE_DEAD: client of ${avatar.name} fired ${sessionLogic.shooting.shotsWhileDead} rounds while character was dead on server" ) - sessionData.shooting.shotsWhileDead = 0 + sessionLogic.shooting.shotsWhileDead = 0 } - sessionData.zoning.CancelZoningProcessWithDescriptiveReason(msg = "cancel") - sessionData.renewCharSavedTimer(fixedLen = 1800L, varLen = 0L) + 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) } - sessionData.dropSpecialSlotItem() - sessionData.toggleMaxSpecialState(enable = false) - sessionData.keepAliveFunc = sessionData.zoning.NormalKeepAlive - sessionData.zoning.zoningStatus = Zoning.Status.None - sessionData.zoning.spawn.deadState = DeadState.Dead + 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 => - sessionData.vehicles.ConditionalDriverVehicleControl(obj) - sessionData.unaccessContainer(obj) + sessionLogic.vehicles.ConditionalDriverVehicleControl(obj) + sessionLogic.general.unaccessContainer(obj) } - sessionData.playerActionsToCancel() - sessionData.terminals.CancelAllProximityUnits() + sessionLogic.actionsToCancel() + sessionLogic.terminals.CancelAllProximityUnits() AvatarActor.savePlayerLocation(player) - sessionData.zoning.spawn.shiftPosition = Some(player.Position) + sessionLogic.zoning.spawn.shiftPosition = Some(player.Position) //respawn val respawnTimer = 300.seconds - sessionData.zoning.spawn.reviveTimer.cancel() + sessionLogic.zoning.spawn.reviveTimer.cancel() if (player.death_by == 0) { - sessionData.zoning.spawn.reviveTimer = context.system.scheduler.scheduleOnce(respawnTimer) { - sessionData.cluster ! ICS.GetRandomSpawnPoint( + sessionLogic.zoning.spawn.reviveTimer = context.system.scheduler.scheduleOnce(respawnTimer) { + sessionLogic.cluster ! ICS.GetRandomSpawnPoint( Zones.sanctuaryZoneNumber(player.Faction), player.Faction, Seq(SpawnGroup.Sanctuary), @@ -480,16 +477,16 @@ class SessionAvatarHandlers( ) } } else { - sessionData.zoning.spawn.HandleReleaseAvatar(player, continent) + sessionLogic.zoning.spawn.HandleReleaseAvatar(player, continent) } case AvatarResponse.Release(tplayer) if isNotSameTarget => - sessionData.zoning.spawn.DepictPlayerAsCorpse(tplayer) + 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!") - sessionData.zoning.spawn.reviveTimer.cancel() - sessionData.zoning.spawn.deadState = DeadState.Alive + sessionLogic.zoning.spawn.reviveTimer.cancel() + sessionLogic.zoning.spawn.deadState = DeadState.Alive player.Revive val health = player.Health sendResponse(PlanetsideAttributeMessage(revivalTargetGuid, attribute_type=0, health)) @@ -517,7 +514,7 @@ class SessionAvatarHandlers( case AvatarResponse.EnvironmentalDamage(_, _, _) => //TODO damage marker? - sessionData.zoning.CancelZoningProcessWithDescriptiveReason("cancel_dmg") + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_dmg") case AvatarResponse.DropItem(pkt) if isNotSameTarget => sendResponse(pkt) @@ -530,7 +527,7 @@ class SessionAvatarHandlers( sendResponse(SetEmpireMessage(objectGuid, faction)) case AvatarResponse.DropSpecialItem() => - sessionData.dropSpecialSlotItem() + sessionLogic.general.dropSpecialSlotItem() case AvatarResponse.OxygenState(player, vehicle) => sendResponse(OxygenStateMessage( @@ -612,7 +609,7 @@ class SessionAvatarHandlers( //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,6 +669,45 @@ 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 { diff --git a/src/main/scala/net/psforever/actors/session/support/SessionGalaxyHandlers.scala b/src/main/scala/net/psforever/actors/session/support/SessionGalaxyHandlers.scala index 976ee10fa..c2cabd167 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionGalaxyHandlers.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionGalaxyHandlers.scala @@ -2,21 +2,34 @@ package net.psforever.actors.session.support import akka.actor.{ActorContext, ActorRef, typed} -import scala.concurrent.duration._ +import net.psforever.packet.PlanetSideGamePacket +import net.psforever.packet.game.FriendsResponse +import net.psforever.services.galaxy.{GalaxyAction, GalaxyServiceMessage} // 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.packet.game.{BroadcastWarpgateUpdateMessage, HotSpotInfo => PacketHotSpotInfo, HotSpotUpdateMessage, ZoneInfoMessage, ZonePopulationUpdateMessage} import net.psforever.services.galaxy.GalaxyResponse import net.psforever.types.{MemberAction, PlanetSideEmpire} class SessionGalaxyHandlers( - val sessionData: SessionData, + val sessionLogic: SessionLogic, avatarActor: typed.ActorRef[AvatarActor.Command], galaxyService: ActorRef, implicit val context: ActorContext ) extends CommonSessionInterfacingFunctionality { + /* packets */ + + def handleUpdateIgnoredPlayers: PlanetSideGamePacket => Unit = { + case msg: FriendsResponse => + sendResponse(msg) + msg.friends.foreach { f => + galaxyService ! GalaxyServiceMessage(GalaxyAction.LogStatusChange(f.name)) + } + case _ => () + } + + /* response handlers */ + def handle(reply: GalaxyResponse.Response): Unit = { reply match { case GalaxyResponse.HotSpotUpdate(zone_index, priority, hot_spot_info) => @@ -45,7 +58,7 @@ class SessionGalaxyHandlers( sendResponse(msg) case GalaxyResponse.TransferPassenger(temp_channel, vehicle, _, manifest) => - sessionData.zoning.handleTransferPassenger(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)) @@ -58,7 +71,7 @@ class SessionGalaxyHandlers( 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) }) => + case GalaxyResponse.LogStatusChange(name) if avatar.people.friend.exists(_.name.equals(name)) => avatarActor ! AvatarActor.MemberListRequest(MemberAction.UpdateFriend, name) case GalaxyResponse.SendResponse(msg) => diff --git a/src/main/scala/net/psforever/actors/session/support/SessionLocalHandlers.scala b/src/main/scala/net/psforever/actors/session/support/SessionLocalHandlers.scala index f0dc4f03f..6d80ff682 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionLocalHandlers.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionLocalHandlers.scala @@ -5,14 +5,13 @@ 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} class SessionLocalHandlers( - val sessionData: SessionData, + val sessionLogic: SessionLogic, implicit val context: ActorContext ) extends CommonSessionInterfacingFunctionality { /** @@ -33,7 +32,7 @@ class SessionLocalHandlers( sendResponse(DeployableObjectsInfoMessage(behavior, deployInfo)) case LocalResponse.DeployableUIFor(item) => - sessionData.updateDeployableUIElements(avatar.deployables.UpdateUIElement(item)) + sessionLogic.general.updateDeployableUIElements(avatar.deployables.UpdateUIElement(item)) case LocalResponse.Detonate(dguid, _: BoomerDeployable) => sendResponse(TriggerEffectMessage(dguid, "detonate_boomer")) @@ -112,10 +111,10 @@ class SessionLocalHandlers( sendResponse(HackMessage(unk1=0, targetGuid, guid, progress=0, unk1, HackState.HackCleared, unk2)) case LocalResponse.HackObject(targetGuid, unk1, unk2) => - HackObject(targetGuid, unk1, unk2) + sessionLogic.general.hackObject(targetGuid, unk1, unk2) case LocalResponse.PlanetsideAttribute(targetGuid, attributeType, attributeValue) => - SendPlanetsideAttributeMessage(targetGuid, attributeType, attributeValue) + sessionLogic.general.sendPlanetsideAttributeMessage(targetGuid, attributeType, attributeValue) case LocalResponse.GenericObjectAction(targetGuid, actionNumber) => sendResponse(GenericObjectActionMessage(targetGuid, actionNumber)) @@ -142,8 +141,8 @@ class SessionLocalHandlers( 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 + sessionLogic.general.specialItemSlotGuid.collect { case guid if guid == lluGuid => + sessionLogic.general.specialItemSlotGuid = None player.Carrying = None } @@ -155,13 +154,13 @@ class SessionLocalHandlers( case LocalResponse.ProximityTerminalEffect(objectGuid, false) => sendResponse(ProximityTerminalUseMessage(Service.defaultPlayerGUID, objectGuid, unk=false)) - sessionData.terminals.ForgetAllProximityTerminals(objectGuid) + 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) => - sessionData.useRouterTelepadEffect(passengerGuid, srcGuid, destGuid) + sessionLogic.general.useRouterTelepadEffect(passengerGuid, srcGuid, destGuid) case LocalResponse.SendResponse(msg) => sendResponse(msg) @@ -190,7 +189,7 @@ class SessionLocalHandlers( 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) + sessionLogic.general.toggleTeleportSystem(router, systemPlan) case LocalResponse.TriggerEffect(targetGuid, effect, effectInfo, triggerLocation) => sendResponse(TriggerEffectMessage(targetGuid, effect, effectInfo, triggerLocation)) @@ -236,28 +235,4 @@ class SessionLocalHandlers( 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)) - } } diff --git a/src/main/scala/net/psforever/actors/session/support/SessionLogic.scala b/src/main/scala/net/psforever/actors/session/support/SessionLogic.scala new file mode 100644 index 000000000..b07ea88e5 --- /dev/null +++ b/src/main/scala/net/psforever/actors/session/support/SessionLogic.scala @@ -0,0 +1,573 @@ +// Copyright (c) 2023 PSForever +package net.psforever.actors.session.support + +import akka.actor.Actor.Receive +import akka.actor.typed.receptionist.Receptionist +import akka.actor.typed.scaladsl.adapter._ +import akka.actor.{ActorContext, ActorRef, typed} +import net.psforever.objects.zones.blockmap.{SectorGroup, SectorPopulation} +import net.psforever.services.ServiceManager +import net.psforever.services.ServiceManager.Lookup + +import scala.collection.mutable +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.duration._ +// +import net.psforever.actors.net.MiddlewareActor +import net.psforever.actors.session.{AvatarActor, ChatActor} +import net.psforever.actors.zone.ZoneActor +import net.psforever.objects._ +import net.psforever.objects.avatar._ +import net.psforever.objects.ce._ +import net.psforever.objects.equipment._ +import net.psforever.objects.inventory.{Container, InventoryItem} +import net.psforever.objects.serverobject.mount.Mountable +import net.psforever.objects.serverobject.structures.Amenity +import net.psforever.objects.vehicles._ +import net.psforever.objects.vital._ +import net.psforever.objects.vital.interaction.DamageInteraction +import net.psforever.objects.zones._ +import net.psforever.objects.zones.blockmap.{BlockMap, BlockMapEntity} +import net.psforever.packet._ +import net.psforever.packet.game._ +import net.psforever.services.account.AccountPersistenceService +import net.psforever.services.ServiceManager.LookupResult +import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} +import net.psforever.services.{Service, InterstellarClusterService => ICS} +import net.psforever.types._ +import net.psforever.util.Config + +object SessionLogic { + //noinspection ScalaUnusedSymbol + private def NoTurnCounterYet(guid: PlanetSideGUID): Unit = { } + + private def updateOldRefsMap(inventory: net.psforever.objects.inventory.GridInventory): IterableOnce[(PlanetSideGUID, String)] = { + inventory.Items.flatMap { + case InventoryItem(o, _) => updateOldRefsMap(o) + } + } + + private def updateOldRefsMap(item: PlanetSideGameObject): IterableOnce[(PlanetSideGUID, String)] = { + item match { + case t: Tool => + t.AmmoSlots.map { slot => + val box = slot.Box + box.GUID -> box.Definition.Name + } :+ (t.GUID -> t.Definition.Name) + case _ => + Seq(item.GUID -> item.Definition.Name) + } + } +} + +trait SessionLogic { + def middlewareActor: typed.ActorRef[MiddlewareActor.Command] + + implicit def context: ActorContext + + /** + * Hardwire an implicit `sender` to be the same as `context.self` of the `SessionActor` actor class + * for which this support class was initialized. + * Allows for proper use for `ActorRef.tell` or an actor's `!` in the support class, + * one where the result is always directed back to the same `SessionActor` instance. + * If there is a different packet "sender" that has to be respected by a given method, + * pass that `ActorRef` into the method as a parameter. + * @see `ActorRef.!(Any)(ActorRef)` + * @see `ActorRef.tell(Any)(ActorRef)` + */ + private[this] implicit val sender: ActorRef = context.self + + private val avatarActor: typed.ActorRef[AvatarActor.Command] = context.spawnAnonymous(AvatarActor(context.self)) + private val chatActor: typed.ActorRef[ChatActor.Command] = context.spawnAnonymous(ChatActor(context.self, avatarActor)) + + private[support] val log = org.log4s.getLogger + private[support] var theSession: Session = Session() + private[support] var accountIntermediary: ActorRef = Default.Actor + private[support] var accountPersistence: ActorRef = Default.Actor + private[support] var galaxyService: ActorRef = Default.Actor + private[support] var squadService: ActorRef = Default.Actor + private[support] var cluster: typed.ActorRef[ICS.Command] = Default.typed.Actor + private[session] var connectionState: Int = 25 + private[support] var persistFunc: () => Unit = noPersistence + private[support] var persist: () => Unit = updatePersistenceOnly + private[session] var keepAliveFunc: () => Unit = keepAlivePersistenceInitial + private[support] var turnCounterFunc: PlanetSideGUID => Unit = SessionLogic.NoTurnCounterYet + private[support] val oldRefsMap: mutable.HashMap[PlanetSideGUID, String] = new mutable.HashMap[PlanetSideGUID, String]() + private var contextSafeEntity: PlanetSideGUID = PlanetSideGUID(0) + + val general: GeneralOperations = + new GeneralOperations(sessionLogic=this, avatarActor, chatActor, context) + val shooting: WeaponAndProjectileOperations = + new WeaponAndProjectileOperations(sessionLogic=this, avatarActor, chatActor, context) + val vehicles: VehicleOperations = + new VehicleOperations(sessionLogic=this, avatarActor, context) + val avatarResponse: SessionAvatarHandlers = + new SessionAvatarHandlers(sessionLogic=this, avatarActor, chatActor, context) + val localResponse: SessionLocalHandlers = + new SessionLocalHandlers(sessionLogic=this, context) + val mountResponse: SessionMountHandlers = + new SessionMountHandlers(sessionLogic=this, avatarActor, context) + val terminals: SessionTerminalHandlers = + new SessionTerminalHandlers(sessionLogic=this, avatarActor, context) + private var vehicleResponseOpt: Option[SessionVehicleHandlers] = None + private var galaxyResponseOpt: Option[SessionGalaxyHandlers] = None + private var squadResponseOpt: Option[SessionSquadHandlers] = None + private var zoningOpt: Option[ZoningOperations] = None + def vehicleResponseOperations: SessionVehicleHandlers = vehicleResponseOpt.orNull + def galaxyResponseHandlers: SessionGalaxyHandlers = galaxyResponseOpt.orNull + def squad: SessionSquadHandlers = squadResponseOpt.orNull + def zoning: ZoningOperations = zoningOpt.orNull + + ServiceManager.serviceManager ! Lookup("accountIntermediary") + ServiceManager.serviceManager ! Lookup("accountPersistence") + ServiceManager.serviceManager ! Lookup("galaxy") + ServiceManager.serviceManager ! Lookup("squad") + ServiceManager.receptionist ! Receptionist.Find(ICS.InterstellarClusterServiceKey, context.self) + + /** + * updated when an upstream packet arrives; + * allow to be a little stale for a short while + */ + private[support] var localSector: SectorPopulation = SectorGroup(Nil) + + def session: Session = theSession + + def session_=(session: Session): Unit = { + chatActor ! ChatActor.SetSession(session) + avatarActor ! AvatarActor.SetSession(session) + theSession = session + } + + def account: Account = theSession.account + + def continent: Zone = theSession.zone + + def player: Player = theSession.player + + def avatar: Avatar = theSession.avatar + + /* setup functions */ + + def assignEventBus(msg: Any): Boolean = { + msg match { + case LookupResult("accountIntermediary", endpoint) => + accountIntermediary = endpoint + true + case LookupResult("accountPersistence", endpoint) => + accountPersistence = endpoint + true + case LookupResult("galaxy", endpoint) => + galaxyService = endpoint + buildDependentOperationsForGalaxy(endpoint) + buildDependentOperations(endpoint, cluster) + true + case LookupResult("squad", endpoint) => + squadService = endpoint + buildDependentOperationsForSquad(endpoint) + true + case ICS.InterstellarClusterServiceKey.Listing(listings) => + cluster = listings.head + buildDependentOperations(galaxyService, cluster) + true + + case _ => + false + } + } + + def buildDependentOperationsForGalaxy(galaxyActor: ActorRef): Unit = { + if (vehicleResponseOpt.isEmpty && galaxyActor != Default.Actor) { + galaxyResponseOpt = Some(new SessionGalaxyHandlers(sessionLogic=this, avatarActor, galaxyActor, context)) + vehicleResponseOpt = Some(new SessionVehicleHandlers(sessionLogic=this, avatarActor, galaxyActor, context)) + } + } + + def buildDependentOperations(galaxyActor: ActorRef, clusterActor: typed.ActorRef[ICS.Command]): Unit = { + if (zoningOpt.isEmpty && galaxyActor != Default.Actor && clusterActor != Default.typed.Actor) { + zoningOpt = Some(new ZoningOperations(sessionLogic=this, avatarActor, galaxyActor, clusterActor, context)) + } + } + + def buildDependentOperationsForSquad(squadActor: ActorRef): Unit = { + if (squadResponseOpt.isEmpty && squadActor != Default.Actor) { + squadResponseOpt = Some(new SessionSquadHandlers(sessionLogic=this, avatarActor, chatActor, squadActor, context)) + } + } + + def whenAllEventBusesLoaded(): Boolean = { + accountIntermediary != Default.Actor && + accountPersistence != Default.Actor && + vehicleResponseOpt.nonEmpty && + galaxyResponseOpt.nonEmpty && + squadResponseOpt.nonEmpty && + zoningOpt.nonEmpty + } + + /* message processing */ + + def parse(sender: ActorRef): Receive + + /* support functions */ + + def validObject(id: Int): Option[PlanetSideGameObject] = validObject(Some(PlanetSideGUID(id)), decorator = "") + + def validObject(id: Int, decorator: String): Option[PlanetSideGameObject] = validObject(Some(PlanetSideGUID(id)), decorator) + + def validObject(id: PlanetSideGUID): Option[PlanetSideGameObject] = validObject(Some(id), decorator = "") + + def validObject(id: PlanetSideGUID, decorator: String): Option[PlanetSideGameObject] = validObject(Some(id), decorator) + + def validObject(id: Option[PlanetSideGUID]): Option[PlanetSideGameObject] = validObject(id, decorator = "") + + def validObject(id: Option[PlanetSideGUID], decorator: String): Option[PlanetSideGameObject] = { + val elevatedDecorator = if (decorator.nonEmpty) decorator else "ValidObject" + id match { + case Some(guid) => + val hint = oldRefsMap.getOrElse(guid, "thing") + continent.GUID(guid) match { + case Some(_: LocalProjectile) => + shooting.FindProjectileEntry(guid) + + case Some(_: LocalLockerItem) => + player.avatar.locker.Inventory.hasItem(guid) match { + case out @ Some(_) => + contextSafeEntity = guid + out + case None if contextSafeEntity == guid => + //safeguard + None + case None => + //delete stale entity reference from client + log.warn( + s"$elevatedDecorator: ${player.Name} is looking for an invalid GUID $guid, believing it a $hint in ${player.Sex.possessive} locker" + ) + sendResponse(ObjectDeleteMessage(guid, 0)) + None + } + + case Some(obj) if obj.HasGUID && obj.GUID != guid => + log.error( + s"$elevatedDecorator: ${player.Name} found a ${obj.Definition.Name} that isn't the $hint ${player.Sex.pronounSubject} thought it was in zone ${continent.id}" + ) + log.debug( + s"$elevatedDecorator: potentially fatal error in ${continent.id} - requested $hint with $guid, got ${obj.Definition.Name} with ${obj.GUID}; mismatch" + ) + None + + case out @ Some(obj) if obj.HasGUID => + out + + case None if !id.contains(PlanetSideGUID(0)) => + //delete stale entity reference from client + //deleting guid=0 will cause BAD things to happen + log.error(s"$elevatedDecorator: ${player.Name} has an invalid reference to $hint with GUID $guid in zone ${continent.id}") + sendResponse(ObjectDeleteMessage(guid, 0)) + None + + case None if contextSafeEntity == guid => + //safeguard + None + + case _ => + None + } + + case None => + None + } + } + + /** + * Update this player avatar for persistence. + * Set to `persist` initially. + */ + def updatePersistenceOnly(): Unit = { + persistFunc() + } + + /** + * Do not update this player avatar for persistence. + * Set to `persistFunc` initially. + */ + def noPersistence(): Unit = { } + + /** + * Check two locations for a controlled piece of equipment that is associated with the `player`.
+ *
+ * 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.
+ *
+ * Along with any discovered item, a containing object such that the statement:
+ * `container.Find(object) = Some(slot)`
+ * ... 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) + } + } + + /** + * Runs `FindContainedEquipment` but ignores the `Container` object output. + * @return an `Equipment` object + */ + def findEquipment(): Set[Equipment] = findContainedEquipment()._2 + + /** + * Runs `FindContainedEquipment` but ignores the `Container` object output + * and only discovers `Equipment` with the specified global unique identifier number. + * @return an `Equipment` object + */ + def findEquipment(guid: PlanetSideGUID): Option[Equipment] = findEquipment().find { _.GUID == guid } + + /** + * An event has occurred that would cause the player character to stop certain stateful activities. + * These activities include shooting, the weapon being drawn, hacking, accessing (a container), flying, and running. + * Other players in the same zone must be made aware that the player has stopped as well.
+ *
+ * Things whose configuration should not be changed:
+ * - if the player is seated
+ * - if the player is anchored
+ * This is not a complete list but, for the purpose of enforcement, some pointers will be documented here. + */ + def actionsToCancel(): Unit = { + general.actionsToCancel() + shooting.actionsToCancel() + terminals.actionsToCancel() + if (session.flying) { + session = session.copy(flying = false) + chatActor ! ChatActor.Message(ChatMsg(ChatMessageType.CMT_FLY, wideContents=false, "", "off", None)) + } + if (session.speed > 1) { + session = session.copy(speed = 1) + chatActor ! ChatActor.Message(ChatMsg(ChatMessageType.CMT_SPEED, wideContents=false, "", "1.000", None)) + } + } + + /** + * Calculate the amount of damage to be dealt to an active `target` + * using the information reconstructed from a `ResolvedProjectile` + * and affect the `target` in a synchronized manner. + * The active `target` and the target of the `DamageResult` do not have be the same. + * While the "tell" for being able to sustain damage is an entity of type `Vitality`, + * only specific `Vitality` entity types are being screened for sustaining damage. + * @see `DamageResistanceModel` + * @see `Vitality` + * @param target a valid game object that is known to the server + * @param data a projectile that will affect the target + */ + def handleDealingDamage(target: PlanetSideGameObject with Vitality, data: DamageInteraction): Unit = { + val func = data.calculate() + target match { + case obj: Player if obj.CanDamage && obj.Actor != Default.Actor => + if (obj.CharId != player.CharId) { + log.info(s"${player.Name} is attacking ${obj.Name}") + } else { + log.info(s"${player.Name} hurt ${player.Sex.pronounObject}self") + } + // auto kick players damaging spectators + if (obj.spectator && obj != player) { + administrativeKick(player) + } else { + obj.Actor ! Vitality.Damage(func) + } + + case obj: Vehicle if obj.CanDamage => + val name = player.Name + val ownerName = obj.OwnerName.getOrElse("someone") + if (ownerName.equals(name)) { + log.info(s"$name is damaging ${player.Sex.possessive} own ${obj.Definition.Name}") + } else { + log.info(s"$name is attacking $ownerName's ${obj.Definition.Name}") + } + obj.Actor ! Vitality.Damage(func) + + case obj: Amenity if obj.CanDamage => + obj.Actor ! Vitality.Damage(func) + + case obj: Deployable if obj.CanDamage => + val name = player.Name + val ownerName = obj.OwnerName.getOrElse("someone") + if (ownerName.equals(name)) { + log.info(s"$name is damaging ${player.Sex.possessive} own ${obj.Definition.Name}") + } else { + log.info(s"$name is attacking $ownerName's ${obj.Definition.Name}") + } + obj.Actor ! Vitality.Damage(func) + + case _ => () + } + } + + /** + * The atypical response to receiving a `KeepAliveMessage` packet from the client.
+ *
+ * `KeepAliveMessage` packets are the primary vehicle for persistence due to client reporting + * in the case where the player's avatar is riding in a vehicle in a mount with no weapon to control. + * @see `KeepAliveMessage` + * @see `keepAliveFunc` + * @see `turnCounterFunc` + * @see `persist` + */ + def keepAlivePersistence(): Unit = { + zoning.spawn.interimUngunnedVehicle = None + persist() + if (player.HasGUID) { + turnCounterFunc(player.GUID) + } else { + turnCounterFunc(PlanetSideGUID(0)) + } + } + + /** + * A really atypical response to receiving a `KeepAliveMessage` packet from the client + * that applies only during the character select portion and part of the first zone load activity. + */ + def keepAlivePersistenceInitial(): Unit = { + persist() + if (player != null && player.HasGUID) { + keepAliveFunc = keepAlivePersistence + } + } + + def updateBlockMap(target: BlockMapEntity, newCoords: Vector3): Unit = { + target.blockMapEntry.foreach { entry => + val sectorIndices = BlockMap.findSectorIndices(continent.blockMap, newCoords, entry.rangeX, entry.rangeY).toSet + if (sectorIndices.equals(entry.sectors)) { + target.updateBlockMapEntry(newCoords) //soft update + localSector = continent.blockMap.sector(sectorIndices, Config.app.game.playerDraw.rangeMax.toFloat) + } else { + continent.actor ! ZoneActor.UpdateBlockMap(target, newCoords) //hard update + } + } + } + + def updateLocalBlockMap(pos: Vector3): Unit = { + localSector = continent.blockMap.sector(pos, Config.app.game.playerDraw.rangeMax.toFloat) + } + + def updateOldRefsMap(): Unit = { + if(player.HasGUID) { + oldRefsMap.addAll( + (continent.GUID(player.VehicleSeated) match { + case Some(v : Vehicle) => + v.Weapons.toList.collect { + case (_, slot : EquipmentSlot) if slot.Equipment.nonEmpty => SessionLogic.updateOldRefsMap(slot.Equipment.get) + }.flatten ++ + SessionLogic.updateOldRefsMap(v.Inventory) + case _ => + Map.empty[PlanetSideGUID, String] + }) ++ + (general.accessedContainer match { + case Some(cont) => SessionLogic.updateOldRefsMap(cont.Inventory) + case None => Map.empty[PlanetSideGUID, String] + }) ++ + player.Holsters().toList.collect { + case slot if slot.Equipment.nonEmpty => SessionLogic.updateOldRefsMap(slot.Equipment.get) + }.flatten ++ + SessionLogic.updateOldRefsMap(player.Inventory) ++ + SessionLogic.updateOldRefsMap(player.avatar.locker.Inventory) + ) + } + } + + def administrativeKick(tplayer: Player): Unit = { + log.warn(s"${tplayer.Name} has been kicked by ${player.Name}") + tplayer.death_by = -1 + accountPersistence ! AccountPersistenceService.Kick(tplayer.Name) + //get out of that vehicle + 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 kickedByAdministration(): Unit = { + sendResponse(DisconnectMessage("@kick_w")) + context.system.scheduler.scheduleOnce( + delay = 300.milliseconds, + middlewareActor.toClassic, + MiddlewareActor.Teardown() + ) + } + + def immediateDisconnect(): Unit = { + if (avatar != null) { + accountPersistence ! AccountPersistenceService.Logout(avatar.name) + } + middlewareActor ! MiddlewareActor.Teardown() + } + + def failWithError(error: String): Unit = { + log.error(error) + middlewareActor ! MiddlewareActor.Teardown() + } + + def sendResponse(packet: PlanetSidePacket): Unit = { + middlewareActor ! MiddlewareActor.Send(packet) + } + + def stop(): Unit = { + context.stop(avatarActor) + context.stop(chatActor) + general.stop() + shooting.stop() + vehicles.stop() + avatarResponse.stop() + localResponse.stop() + mountResponse.stop() + terminals.stop() + vehicleResponseOpt.foreach(_.stop()) + galaxyResponseOpt.foreach(_.stop()) + squadResponseOpt.foreach(_.stop()) + zoningOpt.foreach(_.stop()) + continent.AvatarEvents ! Service.Leave() + continent.LocalEvents ! Service.Leave() + continent.VehicleEvents ! Service.Leave() + galaxyService ! Service.Leave() + if (avatar != null && squadService != Default.Actor) { + squadService ! Service.Leave() + } + } +} diff --git a/src/main/scala/net/psforever/actors/session/support/SessionMountHandlers.scala b/src/main/scala/net/psforever/actors/session/support/SessionMountHandlers.scala index 7fcda14b3..8d04ae652 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionMountHandlers.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionMountHandlers.scala @@ -2,9 +2,12 @@ package net.psforever.actors.session.support import akka.actor.{ActorContext, typed} +import net.psforever.objects.Tool import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.serverobject.environment.interaction.ResetAllEnvironmentInteractions +import net.psforever.objects.vehicles.MountableWeapons import net.psforever.objects.vital.InGameHistory +import net.psforever.packet.game.InventoryStateMessage import scala.concurrent.duration._ // @@ -24,7 +27,7 @@ import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} import net.psforever.types.{BailType, ChatMessageType, PlanetSideGUID, Vector3} class SessionMountHandlers( - val sessionData: SessionData, + val sessionLogic: SessionLogic, avatarActor: typed.ActorRef[AvatarActor.Command], implicit val context: ActorContext ) extends CommonSessionInterfacingFunctionality { @@ -37,82 +40,82 @@ class SessionMountHandlers( def handle(tplayer: Player, reply: Mountable.Exchange): Unit = { reply match { case Mountable.CanMount(obj: ImplantTerminalMech, seatNumber, _) => - sessionData.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") log.info(s"${player.Name} mounts an implant terminal") - sessionData.terminals.CancelAllProximityUnits() + sessionLogic.terminals.CancelAllProximityUnits() MountingAction(tplayer, obj, seatNumber) - sessionData.keepAliveFunc = sessionData.keepAlivePersistence + sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistence case Mountable.CanMount(obj: Vehicle, seatNumber, _) if obj.Definition == GlobalDefinitions.orbital_shuttle => - sessionData.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount") + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount") log.info(s"${player.Name} mounts the orbital shuttle") - sessionData.terminals.CancelAllProximityUnits() + sessionLogic.terminals.CancelAllProximityUnits() MountingAction(tplayer, obj, seatNumber) - sessionData.keepAliveFunc = sessionData.keepAlivePersistence + sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistence case Mountable.CanMount(obj: Vehicle, seatNumber, _) if obj.Definition == GlobalDefinitions.ant => - sessionData.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount") + 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 - sessionData.terminals.CancelAllProximityUnits() + 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)) - sessionData.accessContainer(obj) + sessionLogic.general.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") + 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 - sessionData.terminals.CancelAllProximityUnits() + 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)) - sessionData.accessContainer(obj) + sessionLogic.general.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") + 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 - sessionData.terminals.CancelAllProximityUnits() + 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)) - sessionData.accessContainer(obj) - sessionData.updateWeaponAtSeatPosition(obj, seatNumber) + sessionLogic.general.accessContainer(obj) + updateWeaponAtSeatPosition(obj, seatNumber) tplayer.Actor ! ResetAllEnvironmentInteractions MountingAction(tplayer, obj, seatNumber) case Mountable.CanMount(obj: Vehicle, seatNumber, _) if seatNumber == 0 => - sessionData.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount") + 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 - sessionData.terminals.CancelAllProximityUnits() + 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)) - sessionData.accessContainer(obj) - sessionData.updateWeaponAtSeatPosition(obj, seatNumber) + sessionLogic.general.accessContainer(obj) + 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") + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount") log.info(s"${player.Name} mounts ${ obj.SeatPermissionGroup(seatNumber) match { case Some(seatType) => s"a $seatType seat (#$seatNumber)" @@ -120,18 +123,18 @@ class SessionMountHandlers( } } of the ${obj.Definition.Name}") val obj_guid: PlanetSideGUID = obj.GUID - sessionData.terminals.CancelAllProximityUnits() + 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)) - sessionData.accessContainer(obj) - sessionData.updateWeaponAtSeatPosition(obj, seatNumber) - sessionData.keepAliveFunc = sessionData.keepAlivePersistence + sessionLogic.general.accessContainer(obj) + updateWeaponAtSeatPosition(obj, seatNumber) + sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistence tplayer.Actor ! ResetAllEnvironmentInteractions MountingAction(tplayer, obj, seatNumber) case Mountable.CanMount(obj: Vehicle, seatNumber, _) => - sessionData.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount") + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount") log.info(s"${player.Name} mounts the ${ obj.SeatPermissionGroup(seatNumber) match { case Some(seatType) => s"a $seatType seat (#$seatNumber)" @@ -139,44 +142,44 @@ class SessionMountHandlers( } } of the ${obj.Definition.Name}") val obj_guid: PlanetSideGUID = obj.GUID - sessionData.terminals.CancelAllProximityUnits() + sessionLogic.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 + sessionLogic.general.accessContainer(obj) + 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 => - sessionData.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount") + 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)) - sessionData.updateWeaponAtSeatPosition(obj, seatNumber) + 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") + sessionLogic.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) + updateWeaponAtSeatPosition(obj, seatNumber) MountingAction(tplayer, obj, seatNumber) case Mountable.CanMount(obj: FacilityTurret, _, _) => - sessionData.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount") + 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, _) => - sessionData.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount") + 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)) - sessionData.updateWeaponAtSeatPosition(obj, seatNumber) + updateWeaponAtSeatPosition(obj, seatNumber) MountingAction(tplayer, obj, seatNumber) case Mountable.CanMount(obj: Mountable, _, _) => @@ -199,7 +202,7 @@ class SessionMountHandlers( continent.id, LocalAction.SendResponse(ObjectDetachMessage(sguid, pguid, pos, roll=0, pitch=0, zang)) ) - sessionData.keepAliveFunc = sessionData.zoning.NormalKeepAlive + sessionLogic.keepAliveFunc = sessionLogic.zoning.NormalKeepAlive case Mountable.CanDismount(obj: Vehicle, seatNum, _) if obj.Definition == GlobalDefinitions.orbital_shuttle => @@ -229,12 +232,12 @@ class SessionMountHandlers( continent.id, VehicleAction.SendResponse(pguid, GenericObjectActionMessage(pguid, code=9)) //conceal the player ) - sessionData.keepAliveFunc = sessionData.zoning.NormalKeepAlive + 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}") - sessionData.unaccessContainer(obj) + sessionLogic.general.unaccessContainer(obj) DismountAction(tplayer, obj, seatNum) obj.Actor ! Vehicle.Deconstruct() @@ -248,8 +251,8 @@ class SessionMountHandlers( case None => "seat" } }") - sessionData.vehicles.ConditionalDriverVehicleControl(obj) - sessionData.unaccessContainer(obj) + sessionLogic.vehicles.ConditionalDriverVehicleControl(obj) + sessionLogic.general.unaccessContainer(obj) DismountVehicleAction(tplayer, obj, seatNum) case Mountable.CanDismount(obj: Vehicle, seat_num, _) => @@ -291,7 +294,7 @@ class SessionMountHandlers( 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() + sessionLogic.actionsToCancel() avatarActor ! AvatarActor.DeactivateActiveImplants() avatarActor ! AvatarActor.SuspendStaminaRegeneration(3.seconds) sendResponse(ObjectAttachMessage(objGuid, playerGuid, seatNum)) @@ -313,8 +316,8 @@ class SessionMountHandlers( obj match { case v: Vehicle if seatNum == 0 && Vector3.MagnitudeSquared(v.Velocity.getOrElse(Vector3.Zero)) > 0f => - sessionData.vehicles.serverVehicleControlVelocity.collect { _ => - sessionData.vehicles.ServerVehicleOverrideStop(v) + sessionLogic.vehicles.serverVehicleControlVelocity.collect { _ => + sessionLogic.vehicles.ServerVehicleOverrideStop(v) } v.Velocity = Vector3.Zero continent.VehicleEvents ! VehicleServiceMessage( @@ -347,7 +350,7 @@ class SessionMountHandlers( 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 + sessionLogic.keepAliveFunc = sessionLogic.zoning.NormalKeepAlive val bailType = if (tplayer.BailProtection) { BailType.Bailed } else { @@ -359,4 +362,22 @@ class SessionMountHandlers( VehicleAction.DismountVehicle(playerGuid, bailType, unk2 = false) ) } + + /** + * 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 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 + } + } } diff --git a/src/main/scala/net/psforever/actors/session/support/SessionSquadHandlers.scala b/src/main/scala/net/psforever/actors/session/support/SessionSquadHandlers.scala index e3ff2bb0e..14cf91710 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionSquadHandlers.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionSquadHandlers.scala @@ -27,7 +27,7 @@ object SessionSquadHandlers { } class SessionSquadHandlers( - val sessionData: SessionData, + val sessionLogic: SessionLogic, avatarActor: typed.ActorRef[AvatarActor.Command], chatActor: typed.ActorRef[ChatActor.Command], squadService: ActorRef, diff --git a/src/main/scala/net/psforever/actors/session/support/SessionTerminalHandlers.scala b/src/main/scala/net/psforever/actors/session/support/SessionTerminalHandlers.scala index 30333abe3..b74c5f60d 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionTerminalHandlers.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionTerminalHandlers.scala @@ -2,6 +2,7 @@ package net.psforever.actors.session.support import akka.actor.{ActorContext, typed} +import net.psforever.objects.guid.GUIDTask import net.psforever.objects.sourcing.AmenitySource import net.psforever.objects.vital.TerminalUsedActivity @@ -21,7 +22,7 @@ import net.psforever.packet.game.{ItemTransactionMessage, ItemTransactionResultM import net.psforever.types.{PlanetSideGUID, TransactionType, Vector3} class SessionTerminalHandlers( - val sessionData: SessionData, + val sessionLogic: SessionLogic, avatarActor: typed.ActorRef[AvatarActor.Command], implicit val context: ActorContext ) extends CommonSessionInterfacingFunctionality { @@ -37,7 +38,7 @@ class SessionTerminalHandlers( 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") + 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}") @@ -203,7 +204,7 @@ class SessionTerminalHandlers( Future(true) } }, - List(sessionData.registerVehicle(vehicle)) + List(registerVehicle(vehicle)) ) } @@ -293,7 +294,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 +304,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 +327,28 @@ 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 + */ + private[session] 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[support] def actionsToCancel(): Unit = { + lastTerminalOrderFulfillment = true + } } diff --git a/src/main/scala/net/psforever/actors/session/support/SessionVehicleHandlers.scala b/src/main/scala/net/psforever/actors/session/support/SessionVehicleHandlers.scala index 915390ee5..f1736f233 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionVehicleHandlers.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionVehicleHandlers.scala @@ -17,7 +17,7 @@ import net.psforever.types.{BailType, ChatMessageType, PlanetSideGUID, Vector3} import scala.concurrent.duration._ class SessionVehicleHandlers( - val sessionData: SessionData, + val sessionLogic: SessionLogic, avatarActor: typed.ActorRef[AvatarActor.Command], galaxyService: ActorRef, implicit val context: ActorContext @@ -55,7 +55,7 @@ class SessionVehicleHandlers( player.Position = pos player.Orientation = orient player.Velocity = vel - sessionData.updateLocalBlockMap(pos) + sessionLogic.updateLocalBlockMap(pos) case VehicleResponse.VehicleState( vehicleGuid, @@ -165,7 +165,7 @@ class SessionVehicleHandlers( sendResponse(DismountVehicleMsg(guid, BailType.Kicked, wasKickedByDriver)) val typeOfRide = continent.GUID(vehicleGuid) match { case Some(obj: Vehicle) => - sessionData.unaccessContainer(obj) + sessionLogic.general.unaccessContainer(obj) s"the ${obj.Definition.Name}'s seat by ${obj.OwnerName.getOrElse("the pilot")}" case _ => s"${player.Sex.possessive} ride" @@ -217,22 +217,22 @@ class SessionVehicleHandlers( sendResponse(ObjectDeleteMessage(itemGuid, unk1=0)) case VehicleResponse.UpdateAmsSpawnPoint(list) => - sessionData.zoning.spawn.amsSpawnPoints = list.filter(tube => tube.Faction == player.Faction) - sessionData.zoning.spawn.DrawCurrentAmsSpawnPoint() + sessionLogic.zoning.spawn.amsSpawnPoints = list.filter(tube => tube.Faction == player.Faction) + sessionLogic.zoning.spawn.DrawCurrentAmsSpawnPoint() case VehicleResponse.TransferPassengerChannel(oldChannel, tempChannel, vehicle, vehicleToDelete) if isNotSameTarget => - sessionData.zoning.interstellarFerry = Some(vehicle) - sessionData.zoning.interstellarFerryTopLevelGUID = Some(vehicleToDelete) + 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 && sessionData.zoning.spawn.deadState == DeadState.Alive && speed > 0 => + 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 - sessionData.vehicles.ServerVehicleOverrideWithPacket( + sessionLogic.vehicles.ServerVehicleOverrideWithPacket( vehicle, ServerVehicleOverrideMsg( lock_accelerator=true, @@ -253,8 +253,8 @@ class SessionVehicleHandlers( ) case VehicleResponse.KickCargo(cargo, _, _) - if player.VehicleSeated.nonEmpty && sessionData.zoning.spawn.deadState == DeadState.Alive => - sessionData.vehicles.TotalDriverVehicleControl(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) => @@ -266,7 +266,7 @@ class SessionVehicleHandlers( case VehicleResponse.PlayerSeatedInVehicle(vehicle, _) => Vehicles.ReloadAccessPermissions(vehicle, player.Name) - sessionData.vehicles.ServerVehicleOverrideWithPacket( + sessionLogic.vehicles.ServerVehicleOverrideWithPacket( vehicle, ServerVehicleOverrideMsg( lock_accelerator=true, @@ -279,11 +279,11 @@ class SessionVehicleHandlers( unk8=Some(0) ) ) - sessionData.vehicles.serverVehicleControlVelocity = Some(0) + sessionLogic.vehicles.serverVehicleControlVelocity = Some(0) case VehicleResponse.ServerVehicleOverrideStart(vehicle, _) => val vdef = vehicle.Definition - sessionData.vehicles.ServerVehicleOverrideWithPacket( + sessionLogic.vehicles.ServerVehicleOverrideWithPacket( vehicle, ServerVehicleOverrideMsg( lock_accelerator=true, @@ -298,7 +298,7 @@ class SessionVehicleHandlers( ) case VehicleResponse.ServerVehicleOverrideEnd(vehicle, _) => - sessionData.vehicles.ServerVehicleOverrideStop(vehicle) + sessionLogic.vehicles.ServerVehicleOverrideStop(vehicle) case VehicleResponse.PeriodicReminder(VehicleSpawnPad.Reminders.Blocked, data) => sendResponse(ChatMsg( @@ -328,7 +328,7 @@ class SessionVehicleHandlers( sendResponse(ObjectDeleteMessage(eguid, unk1=0)) TaskWorkflow.execute(GUIDTask.unregisterEquipment(continent.GUID, obj)) } - sessionData.applyPurchaseTimersBeforePackingLoadout(player, vehicle, addedWeapons ++ newInventory) + sessionLogic.general.applyPurchaseTimersBeforePackingLoadout(player, vehicle, addedWeapons ++ newInventory) //jammer or unjamm new weapons based on vehicle status val vehicleJammered = vehicle.Jammed addedWeapons @@ -342,7 +342,7 @@ class SessionVehicleHandlers( } case VehicleResponse.ChangeLoadout(target, oldWeapons, _, oldInventory, _) - if sessionData.accessedContainer.map { _.GUID }.contains(target) => + 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 @@ -371,7 +371,7 @@ class SessionVehicleHandlers( (oldWeapons ++ oldInventory).foreach { case (_, eguid) => sendResponse(ObjectDeleteMessage(eguid, unk1=0)) } - sessionData.updateWeaponAtSeatPosition(vehicle, seatNum) + sessionLogic.mountResponse.updateWeaponAtSeatPosition(vehicle, seatNum) case None => //observer: observe changes to external equipment oldWeapons.foreach { case (_, eguid) => sendResponse(ObjectDeleteMessage(eguid, unk1=0)) } @@ -380,9 +380,9 @@ class SessionVehicleHandlers( private def startPlayerSeatedInVehicle(vehicle: Vehicle): Unit = { val vehicle_guid = vehicle.GUID - sessionData.playerActionsToCancel() - sessionData.terminals.CancelAllProximityUnits() - sessionData.vehicles.serverVehicleControlVelocity = Some(0) + 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 { diff --git a/src/main/scala/net/psforever/actors/session/support/VehicleOperations.scala b/src/main/scala/net/psforever/actors/session/support/VehicleOperations.scala index a3a91f61f..7a753f8b5 100644 --- a/src/main/scala/net/psforever/actors/session/support/VehicleOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/VehicleOperations.scala @@ -15,7 +15,7 @@ import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} import net.psforever.types.{BailType, DriveState, Vector3} class VehicleOperations( - val sessionData: SessionData, + val sessionLogic: SessionLogic, avatarActor: typed.ActorRef[AvatarActor.Command], implicit val context: ActorContext ) extends CommonSessionInterfacingFunctionality { @@ -40,11 +40,11 @@ class VehicleOperations( GetVehicleAndSeat() match { case (Some(obj), Some(0)) => //we're driving the vehicle - sessionData.persist() - sessionData.turnCounterFunc(player.GUID) - sessionData.fallHeightTracker(pos.z) + sessionLogic.persist() + sessionLogic.turnCounterFunc(player.GUID) + sessionLogic.general.fallHeightTracker(pos.z) if (obj.MountedIn.isEmpty) { - sessionData.updateBlockMap(obj, pos) + sessionLogic.updateBlockMap(obj, pos) } player.Position = pos //convenient if (obj.WeaponControlledFromSeat(0).isEmpty) { @@ -87,7 +87,7 @@ class VehicleOperations( obj.Cloaked ) ) - sessionData.squad.updateSquad() + sessionLogic.squad.updateSquad() obj.zoneInteractions() case (None, _) => //log.error(s"VehicleState: no vehicle $vehicle_guid found in zone") @@ -99,7 +99,7 @@ class VehicleOperations( case _ => ; } if (player.death_by == -1) { - sessionData.kickedByAdministration() + sessionLogic.kickedByAdministration() } } @@ -123,11 +123,11 @@ class VehicleOperations( GetVehicleAndSeat() match { case (Some(obj), Some(0)) => //we're driving the vehicle - sessionData.persist() - sessionData.turnCounterFunc(player.GUID) + sessionLogic.persist() + sessionLogic.turnCounterFunc(player.GUID) val (position, angle, velocity, notMountedState) = continent.GUID(obj.MountedIn) match { case Some(v: Vehicle) => - sessionData.updateBlockMap(obj, pos) + sessionLogic.updateBlockMap(obj, pos) (pos, v.Orientation - Vector3.z(value = 90f) * Vehicles.CargoOrientation(obj).toFloat, v.Velocity, false) case _ => (pos, ang, vel, true) @@ -186,7 +186,7 @@ class VehicleOperations( unkA ) ) - sessionData.squad.updateSquad() + 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 @@ -197,13 +197,13 @@ class VehicleOperations( case _ => ; } if (player.death_by == -1) { - sessionData.kickedByAdministration() + sessionLogic.kickedByAdministration() } } def handleChildObjectState(pkt: ChildObjectStateMessage): Unit = { val ChildObjectStateMessage(object_guid, pitch, yaw) = pkt - val (o, tools) = sessionData.shooting.FindContainedWeapon + val (o, tools) = sessionLogic.shooting.FindContainedWeapon //is COSM our primary upstream packet? (o match { case Some(mount: Mountable) => (o, mount.PassengerInSeat(player)) @@ -211,8 +211,8 @@ class VehicleOperations( }) match { case (None, None) | (_, None) | (Some(_: Vehicle), Some(0)) => ; case _ => - sessionData.persist() - sessionData.turnCounterFunc(player.GUID) + 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 { @@ -231,19 +231,19 @@ class VehicleOperations( } //TODO status condition of "playing getting out of vehicle to allow for late packets without warning if (player.death_by == -1) { - sessionData.kickedByAdministration() + sessionLogic.kickedByAdministration() } } def handleVehicleSubState(pkt: VehicleSubStateMessage): Unit = { val VehicleSubStateMessage(vehicle_guid, _, pos, ang, vel, unk1, _) = pkt - sessionData.validObject(vehicle_guid, decorator = "VehicleSubState") match { + 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 - sessionData.updateBlockMap(obj, pos) + sessionLogic.updateBlockMap(obj, pos) obj.zoneInteractions() continent.VehicleEvents ! VehicleServiceMessage( continent.id, @@ -268,7 +268,7 @@ class VehicleOperations( def handleMountVehicle(pkt: MountVehicleMsg): Unit = { val MountVehicleMsg(_, mountable_guid, entry_point) = pkt - sessionData.validObject(mountable_guid, decorator = "MountVehicle").collect { + sessionLogic.validObject(mountable_guid, decorator = "MountVehicle").collect { case obj: Mountable => obj.Actor ! Mountable.TryMount(player, entry_point) case _ => @@ -283,7 +283,7 @@ class VehicleOperations( //common warning for this section if (player.GUID == player_guid) { //normally disembarking from a mount - (sessionData.zoning.interstellarFerry.orElse(continent.GUID(player.VehicleSeated)) match { + (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 @@ -300,7 +300,7 @@ class VehicleOperations( 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 + 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. @@ -327,8 +327,8 @@ class VehicleOperations( case Some(obj_guid) => ( ( - sessionData.validObject(obj_guid, decorator = "DismountVehicle/Vehicle"), - sessionData.validObject(player_guid, decorator = "DismountVehicle/Player") + 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) @@ -508,7 +508,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) } diff --git a/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala b/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala index 4e1d27387..156fe18bf 100644 --- a/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala @@ -15,7 +15,7 @@ import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future import scala.concurrent.duration._ // -import net.psforever.actors.session.{AvatarActor, ChatActor, SessionActor} +import net.psforever.actors.session.{AvatarActor, ChatActor} import net.psforever.login.WorldSession.{CountAmmunition, CountGrenades, FindAmmoBoxThatUses, FindEquipmentStock, FindToolThatUses, PutEquipmentInInventoryOrDrop, PutNewEquipmentInInventoryOrDrop, RemoveOldEquipmentFromInventory} import net.psforever.objects.avatar.scoring.EquipmentStat import net.psforever.objects.ballistics.{Projectile, ProjectileQuality} @@ -42,10 +42,10 @@ import net.psforever.types.{ExoSuitType, PlanetSideGUID, Vector3} import net.psforever.util.Config private[support] class WeaponAndProjectileOperations( - val sessionData: SessionData, - avatarActor: typed.ActorRef[AvatarActor.Command], - chatActor: typed.ActorRef[ChatActor.Command], - implicit val context: ActorContext + val sessionLogic: SessionLogic, + avatarActor: typed.ActorRef[AvatarActor.Command], + chatActor: typed.ActorRef[ChatActor.Command], + implicit val context: ActorContext ) extends CommonSessionInterfacingFunctionality { var shooting: mutable.Set[PlanetSideGUID] = mutable.Set.empty //ChangeFireStateMessage_Start var prefire: mutable.Set[PlanetSideGUID] = mutable.Set.empty //if WeaponFireMessage precedes ChangeFireStateMessage_Start @@ -111,8 +111,8 @@ private[support] class WeaponAndProjectileOperations( val WeaponLazeTargetPositionMessage(_, _, _) = pkt //do not need to handle the progress bar animation/state on the server //laze waypoint is requested by client upon completion (see SquadWaypointRequest) - val purpose = if (sessionData.squad.squad_supplement_id > 0) { - s" for ${player.Sex.possessive} squad (#${sessionData.squad.squad_supplement_id -1})" + val purpose = if (sessionLogic.squad.squad_supplement_id > 0) { + s" for ${player.Sex.possessive} squad (#${sessionLogic.squad.squad_supplement_id -1})" } else { " ..." } @@ -128,7 +128,7 @@ private[support] class WeaponAndProjectileOperations( def handleChangeFireStateStart(pkt: ChangeFireStateMessage_Start)(implicit context: ActorContext): Unit = { val ChangeFireStateMessage_Start(item_guid) = pkt if (shooting.isEmpty) { - sessionData.findEquipment(item_guid) match { + sessionLogic.findEquipment(item_guid) match { case Some(tool: Tool) if player.VehicleSeated.isEmpty => fireStateStartWhenPlayer(tool, item_guid) case Some(tool: Tool) => @@ -151,7 +151,7 @@ private[support] class WeaponAndProjectileOperations( prefire -= item_guid shootingStop += item_guid -> now shooting -= item_guid - sessionData.findEquipment(item_guid) match { + sessionLogic.findEquipment(item_guid) match { case Some(tool: Tool) if player.VehicleSeated.isEmpty => fireStateStopWhenPlayer(tool, item_guid) case Some(tool: Tool) => @@ -169,8 +169,8 @@ private[support] class WeaponAndProjectileOperations( case _ => log.warn(s"ChangeFireState_Stop: can not find $item_guid") } - sessionData.progressBarUpdate.cancel() - sessionData.progressBarValue = None + sessionLogic.general.progressBarUpdate.cancel() + sessionLogic.general.progressBarValue = None } def handleReload(pkt: ReloadMessage): Unit = { @@ -187,7 +187,7 @@ private[support] class WeaponAndProjectileOperations( def handleChangeAmmo(pkt: ChangeAmmoMessage): Unit = { val ChangeAmmoMessage(item_guid, _) = pkt - val (thing, equipment) = sessionData.findContainedEquipment() + val (thing, equipment) = sessionLogic.findContainedEquipment() if (equipment.isEmpty) { log.warn(s"ChangeAmmo: either can not find $item_guid or the object found was not Equipment") } else { @@ -216,7 +216,7 @@ private[support] class WeaponAndProjectileOperations( def handleChangeFireMode(pkt: ChangeFireModeMessage): Unit = { val ChangeFireModeMessage(item_guid, _/*fire_mode*/) = pkt - sessionData.findEquipment(item_guid) match { + sessionLogic.findEquipment(item_guid) match { case Some(obj: PlanetSideGameObject with FireModeSwitch[_]) => val originalModeIndex = obj.FireModeIndex if (obj match { @@ -306,7 +306,7 @@ private[support] class WeaponAndProjectileOperations( (hit_info match { case Some(hitInfo) => val hitPos = hitInfo.hit_pos - sessionData.validObject(hitInfo.hitobject_guid, decorator = "Hit/hitInfo") match { + sessionLogic.validObject(hitInfo.hitobject_guid, decorator = "Hit/hitInfo") match { case _ if projectile.profile == GlobalDefinitions.flail_projectile => val radius = projectile.profile.DamageRadius * projectile.profile.DamageRadius val targets = Zone.findAllTargets(continent, player, hitPos, projectile.profile) @@ -340,7 +340,7 @@ private[support] class WeaponAndProjectileOperations( ) => ResolveProjectileInteraction(proj, DamageResolution.Hit, target, hitPos).collect { resprojectile => addShotsLanded(resprojectile.cause.attribution, shots = 1) - sessionData.handleDealingDamage(target, resprojectile) + sessionLogic.handleDealingDamage(target, resprojectile) } case _ => () } @@ -371,23 +371,23 @@ private[support] class WeaponAndProjectileOperations( (DamageResolution.Splash, DamageResolution.Splash) } //direct_victim_uid - sessionData.validObject(direct_victim_uid, decorator = "SplashHit/direct_victim") match { + 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) - sessionData.handleDealingDamage(target, resprojectile) + sessionLogic.handleDealingDamage(target, resprojectile) } case _ => () } //other victims targets.foreach(elem => { - sessionData.validObject(elem.uid, decorator = "SplashHit/other_victims") match { + 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) - sessionData.handleDealingDamage(target, resprojectile) + sessionLogic.handleDealingDamage(target, resprojectile) } case _ => () } @@ -421,13 +421,13 @@ private[support] class WeaponAndProjectileOperations( def handleLashHit(pkt: LashMessage): Unit = { val LashMessage(_, _, victim_guid, projectile_guid, hit_pos, _) = pkt - sessionData.validObject(victim_guid, decorator = "Lash") match { + sessionLogic.validObject(victim_guid, decorator = "Lash") match { case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) => CheckForHitPositionDiscrepancy(projectile_guid, hit_pos, target) ResolveProjectileInteraction(projectile_guid, DamageResolution.Lash, target, hit_pos).foreach { resprojectile => addShotsLanded(resprojectile.cause.attribution, shots = 1) - sessionData.handleDealingDamage(target, resprojectile) + sessionLogic.handleDealingDamage(target, resprojectile) } case _ => () } @@ -454,7 +454,7 @@ private[support] class WeaponAndProjectileOperations( None }).collect { case target: AutomatedTurret.Target => - sessionData.validObject(attackerGuid, decorator = "AIDamage/AutomatedTurret") + sessionLogic.validObject(attackerGuid, decorator = "AIDamage/AutomatedTurret") .collect { case turret: AutomatedTurret if turret.Target.isEmpty => turret.Actor ! AutomatedTurretBehavior.ConfirmShot(target) @@ -468,10 +468,10 @@ private[support] class WeaponAndProjectileOperations( } .orElse { //occasionally, something that is not technically a turret's natural target may be attacked - sessionData.validObject(targetGuid, decorator = "AIDamage/Target") + sessionLogic.validObject(targetGuid, decorator = "AIDamage/Target") .collect { case target: PlanetSideServerObject with FactionAffinity with Vitality => - sessionData.validObject(attackerGuid, decorator = "AIDamage/Attacker") + sessionLogic.validObject(attackerGuid, decorator = "AIDamage/Attacker") .collect { case turret: AutomatedTurret if turret.Target.nonEmpty => //the turret must be shooting at something (else) first @@ -587,11 +587,11 @@ private[support] class WeaponAndProjectileOperations( projectileGUID: PlanetSideGUID ): (Option[PlanetSideGameObject with Container], Option[Tool]) = { if (player.ZoningRequest != Zoning.Method.None) { - sessionData.zoning.CancelZoningProcessWithDescriptiveReason("cancel_fire") + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_fire") } if (player.isShielded) { // Cancel NC MAX shield if it's active - sessionData.toggleMaxSpecialState(enable = false) + sessionLogic.general.toggleMaxSpecialState(enable = false) } val (o, tools) = FindContainedWeapon val (_, enabledTools) = FindEnabledWeaponsToHandleWeaponFireAccountability(o, tools) @@ -893,7 +893,7 @@ private[support] class WeaponAndProjectileOperations( case Some(_) => stowFunc(previousBox) case None => - sessionData.normalItemDrop(player, continent)(previousBox) + sessionLogic.general.normalItemDrop(player, continent)(previousBox) } AmmoBox.Split(previousBox) match { case Nil | List(_) => () //done (the former case is technically not possible) @@ -919,7 +919,7 @@ private[support] class WeaponAndProjectileOperations( */ def FindDetectedProjectileTargets(targets: Iterable[PlanetSideGUID]): Iterable[String] = { targets - .map { sessionData.validObject(_, decorator="FindDetectedProjectileTargets") } + .map { sessionLogic.validObject(_, decorator="FindDetectedProjectileTargets") } .flatMap { case Some(obj: Vehicle) if !obj.Cloaked => //TODO hint: vehicleService ! VehicleServiceMessage(s"${obj.Actor}", VehicleAction.ProjectileAutoLockAwareness(mode)) @@ -1163,7 +1163,7 @@ private[support] class WeaponAndProjectileOperations( * the second value is an `Tool` object in the former */ def FindContainedWeapon: (Option[PlanetSideGameObject with Container], Set[Tool]) = { - sessionData.findContainedEquipment() match { + sessionLogic.findContainedEquipment() match { case (container, equipment) => (container, equipment collect { case t: Tool => t }) case _ => @@ -1207,11 +1207,11 @@ private[support] class WeaponAndProjectileOperations( //charge ammunition drain tool.FireMode match { case mode: ChargeFireModeDefinition => - sessionData.progressBarValue = Some(0f) - sessionData.progressBarUpdate = context.system.scheduler.scheduleOnce( + sessionLogic.general.progressBarValue = Some(0f) + sessionLogic.general.progressBarUpdate = context.system.scheduler.scheduleOnce( (mode.Time + mode.DrainInterval) milliseconds, context.self, - SessionActor.ProgressEvent(1f, () => {}, Tools.ChargeFireMode(player, tool), mode.DrainInterval) + CommonMessages.ProgressEvent(1f, () => {}, Tools.ChargeFireMode(player, tool), mode.DrainInterval) ) case _ => () } @@ -1225,7 +1225,7 @@ private[support] class WeaponAndProjectileOperations( } private def fireStateStartMountedMessages(itemGuid: PlanetSideGUID): Unit = { - sessionData.findContainedEquipment()._1.collect { + sessionLogic.findContainedEquipment()._1.collect { case turret: FacilityTurret if continent.map.cavern => turret.Actor ! VanuSentry.ChangeFireStart } @@ -1291,7 +1291,7 @@ private[support] class WeaponAndProjectileOperations( } private def fireStateStopMountedMessages(itemGuid: PlanetSideGUID): Unit = { - sessionData.findContainedEquipment()._1.collect { + sessionLogic.findContainedEquipment()._1.collect { case turret: FacilityTurret if continent.map.cavern => turret.Actor ! VanuSentry.ChangeFireStop } @@ -1498,7 +1498,7 @@ private[support] class WeaponAndProjectileOperations( val hitPos = target.Position + Vector3.z(value = 1f) ResolveProjectileInteraction(proj, DamageResolution.Hit, target, hitPos).collect { resprojectile => addShotsLanded(resprojectile.cause.attribution, shots = 1) - sessionData.handleDealingDamage(target, resprojectile) + sessionLogic.handleDealingDamage(target, resprojectile) } } } @@ -1585,7 +1585,21 @@ private[support] class WeaponAndProjectileOperations( }.filter(p => Vector3.DistanceSquared(start, p) <= a) } - override protected[session] def stop(): Unit = { + override protected[support] def actionsToCancel(): Unit = { + shootingStart.clear() + shootingStop.clear() + (prefire ++ shooting).foreach { guid => + sendResponse(ChangeFireStateMessage_Stop(guid)) + continent.AvatarEvents ! AvatarServiceMessage( + continent.id, + AvatarAction.ChangeFireState_Stop(player.GUID, guid) + ) + } + prefire.clear() + shooting.clear() + } + + override protected[support] def stop(): Unit = { if (player != null && player.HasGUID) { (prefire ++ shooting).foreach { guid => //do I need to do this? (maybe) diff --git a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala index 54b00181b..ee4dbf6f0 100644 --- a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala @@ -73,6 +73,11 @@ import net.psforever.util.{Config, DefinitionUtil} import net.psforever.zones.Zones object ZoningOperations { + private[support] 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,7 +106,7 @@ object ZoningOperations { } class ZoningOperations( - val sessionData: SessionData, + val sessionLogic: SessionLogic, avatarActor: typed.ActorRef[AvatarActor.Command], galaxyService: ActorRef, cluster: typed.ActorRef[ICS.Command], @@ -147,7 +152,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 _ => @@ -202,7 +207,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 +492,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 +661,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 +674,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 +704,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 +714,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 +754,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 +864,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 +1148,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 +1221,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 +1341,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 +1359,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 +1485,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 +1497,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 +1519,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 +1552,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 +1601,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.
@@ -1730,7 +1817,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 +1829,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,7 +1923,7 @@ 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() @@ -1847,14 +1934,14 @@ class ZoningOperations( ) 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 +1952,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 +1987,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 +2026,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 +2041,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 +2054,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 +2062,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 +2091,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( @@ -2087,7 +2174,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 +2188,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 +2234,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 +2341,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 +2358,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 +2404,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 +2423,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 +2493,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 +2876,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 } @@ -2792,7 +2906,7 @@ class ZoningOperations( log.trace(s"HandleSetCurrentAvatar - ${tplayer.Name}") session = session.copy(player = tplayer) val guid = tplayer.GUID - sessionData.updateDeployableUIElements(Deployables.InitializeDeployableUIElements(avatar)) + sessionLogic.general.updateDeployableUIElements(Deployables.InitializeDeployableUIElements(avatar)) 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? @@ -2851,7 +2965,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) @@ -2875,7 +2989,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( @@ -2923,7 +3037,7 @@ class ZoningOperations( case _ => ; } interstellarFerryTopLevelGUID = None - if (loadConfZone && sessionData.connectionState == 100) { + if (loadConfZone && sessionLogic.connectionState == 100) { configZone(continent) loadConfZone = false } @@ -2934,7 +3048,7 @@ class ZoningOperations( player.Actor ! Player.Die() } else { AvatarActor.savePlayerData(player) - sessionData.displayCharSavedMsgThenRenewTimer( + sessionLogic.general.displayCharSavedMsgThenRenewTimer( Config.app.game.savedMsg.short.fixed, Config.app.game.savedMsg.short.variable ) @@ -3167,7 +3281,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 +3315,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 +3340,7 @@ class ZoningOperations( loginChatMessage.foreach { msg => sendResponse(ChatMsg(zoningChatMessageType, wideContents=false, "", msg, None)) } loginChatMessage.clear() CancelZoningProcess() - sessionData.turnCounterFunc = NormalTurnCounter + sessionLogic.turnCounterFunc = NormalTurnCounter } /** @@ -3403,7 +3517,7 @@ class ZoningOperations( context.system.scheduler.scheduleOnce( delay.milliseconds, context.self, - SessionActor.AvatarAwardMessageBundle(xs, delay) + ZoningOperations.AvatarAwardMessageBundle(xs, delay) ) } } @@ -3413,8 +3527,8 @@ class ZoningOperations( * Set to `persist` when (new) player is loaded. */ def UpdatePersistenceAndRefs(): Unit = { - sessionData.persistFunc() - sessionData.updateOldRefsMap() + sessionLogic.persistFunc() + sessionLogic.updateOldRefsMap() } /** @@ -3425,6 +3539,34 @@ 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 + } + } } override protected[session] def stop(): Unit = { diff --git a/src/main/scala/net/psforever/objects/serverobject/CommonMessages.scala b/src/main/scala/net/psforever/objects/serverobject/CommonMessages.scala index 50a07a9d2..7f19cf8d2 100644 --- a/src/main/scala/net/psforever/objects/serverobject/CommonMessages.scala +++ b/src/main/scala/net/psforever/objects/serverobject/CommonMessages.scala @@ -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.