diff --git a/src/main/scala/net/psforever/actors/session/csr/AvatarHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/csr/AvatarHandlerLogic.scala index 4833ce14e..9d716b3ba 100644 --- a/src/main/scala/net/psforever/actors/session/csr/AvatarHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/csr/AvatarHandlerLogic.scala @@ -261,10 +261,6 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A sendResponse(ItemTransactionResultMessage(terminalGuid, action, result)) sessionLogic.terminals.lastTerminalOrderFulfillment = true AvatarActor.savePlayerData(player) - sessionLogic.general.renewCharSavedTimer( - Config.app.game.savedMsg.interruptedByAction.fixed, - Config.app.game.savedMsg.interruptedByAction.variable - ) case AvatarResponse.TerminalOrderResult(terminalGuid, action, result) => sendResponse(ItemTransactionResultMessage(terminalGuid, action, result)) diff --git a/src/main/scala/net/psforever/actors/session/csr/ChatLogic.scala b/src/main/scala/net/psforever/actors/session/csr/ChatLogic.scala index 781ea7e50..864e9738b 100644 --- a/src/main/scala/net/psforever/actors/session/csr/ChatLogic.scala +++ b/src/main/scala/net/psforever/actors/session/csr/ChatLogic.scala @@ -2,8 +2,11 @@ package net.psforever.actors.session.csr import akka.actor.ActorContext +import net.psforever.actors.session.SessionActor +import net.psforever.actors.session.normal.NormalMode import net.psforever.actors.session.support.{ChatFunctions, ChatOperations, SessionData} import net.psforever.objects.Session +import net.psforever.objects.avatar.ModePermissions import net.psforever.packet.game.{ChatMsg, SetChatFilterMessage} import net.psforever.services.chat.DefaultChannel import net.psforever.types.ChatMessageType @@ -30,10 +33,9 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext case (CMT_FLY, recipient, contents) => ops.commandFly(contents, recipient) - case (CMT_ANONYMOUS, _, _) => - // ? + case (CMT_ANONYMOUS, _, _) => () - case (CMT_TOGGLE_GM, _, contents)=> + case (CMT_TOGGLE_GM, _, contents) => customCommandModerator(contents) case (CMT_CULLWATERMARK, _, contents) => @@ -196,7 +198,6 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext case "ntu" => ops.customCommandNtu(session, params) case "zonerotate" => ops.customCommandZonerotate(params) case "nearby" => ops.customCommandNearby(session) - case "csr" | "gm" | "op" => customCommandModerator(params.headOption.getOrElse("")) case _ => // command was not handled sendResponse( @@ -216,22 +217,28 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext } def commandToggleSpectatorMode(contents: String): Unit = { -// val currentSpectatorActivation = (if (avatar != null) avatar.permissions else ModePermissions()).canSpectate -// contents.toLowerCase() match { -// case "on" | "o" | "" if !currentSpectatorActivation => -// context.self ! SessionActor.SetMode(SessionSpectatorMode) -// case "off" | "of" if currentSpectatorActivation => -// context.self ! SessionActor.SetMode(SessionCustomerServiceRepresentativeMode) -// case _ => () -// } + val currentSpectatorActivation = (if (avatar != null) avatar.permissions else ModePermissions()).canSpectate + contents.toLowerCase() match { + case "on" | "o" | "" if currentSpectatorActivation && player.spectator => + context.self ! SessionActor.SetMode(SpectateAsCustomerServiceRepresentativeMode) + case "off" | "of" if currentSpectatorActivation && !player.spectator => + context.self ! SessionActor.SetMode(CustomerServiceRepresentativeMode) + case _ => () + } } def customCommandModerator(contents : String): Boolean = { - if (player.spectator) { - sendResponse(ChatMsg(ChatMessageType.UNK_225, "CSR SPECTATOR MODE")) - sendResponse(ChatMsg(ChatMessageType.UNK_227, "Disable spectator mode first.")) + if (sessionLogic.zoning.maintainInitialGmState) { + sessionLogic.zoning.maintainInitialGmState = false } else { - ops.customCommandModerator(contents) + contents.toLowerCase() match { + case "off" | "of" if player.spectator => + context.self ! SessionActor.SetMode(CustomerServiceRepresentativeMode) + context.self ! SessionActor.SetMode(NormalMode) + case "off" | "of" => + context.self ! SessionActor.SetMode(NormalMode) + case _ => () + } } true } diff --git a/src/main/scala/net/psforever/actors/session/csr/CustomerServiceRepresentativeMode.scala b/src/main/scala/net/psforever/actors/session/csr/CustomerServiceRepresentativeMode.scala index 41c530b4c..60d4933ed 100644 --- a/src/main/scala/net/psforever/actors/session/csr/CustomerServiceRepresentativeMode.scala +++ b/src/main/scala/net/psforever/actors/session/csr/CustomerServiceRepresentativeMode.scala @@ -1,7 +1,7 @@ // Copyright (c) 2024 PSForever package net.psforever.actors.session.csr -import net.psforever.actors.session.support.{ChatFunctions, GeneralFunctions, LocalHandlerFunctions, ModeLogic, MountHandlerFunctions, PlayerMode, SessionData, SquadHandlerFunctions, TerminalHandlerFunctions, VehicleFunctions, VehicleHandlerFunctions, WeaponAndProjectileFunctions} +import net.psforever.actors.session.support.{ChatFunctions, GalaxyHandlerFunctions, GeneralFunctions, LocalHandlerFunctions, ModeLogic, MountHandlerFunctions, PlayerMode, SessionData, SquadHandlerFunctions, TerminalHandlerFunctions, VehicleFunctions, VehicleHandlerFunctions, WeaponAndProjectileFunctions} import net.psforever.actors.zone.ZoneActor import net.psforever.objects.Session import net.psforever.packet.PlanetSidePacket @@ -11,15 +11,15 @@ import net.psforever.types.ChatMessageType class CustomerServiceRepresentativeMode(data: SessionData) extends ModeLogic { val avatarResponse: AvatarHandlerLogic = AvatarHandlerLogic(data.avatarResponse) val chat: ChatFunctions = ChatLogic(data.chat) - val galaxy: GalaxyHandlerLogic = GalaxyHandlerLogic(data.galaxyResponseHandlers) + val galaxy: GalaxyHandlerFunctions = net.psforever.actors.session.normal.GalaxyHandlerLogic(data.galaxyResponseHandlers) val general: GeneralFunctions = GeneralLogic(data.general) - val local: LocalHandlerFunctions = LocalHandlerLogic(data.localResponse) + val local: LocalHandlerFunctions = net.psforever.actors.session.normal.LocalHandlerLogic(data.localResponse) val mountResponse: MountHandlerFunctions = MountHandlerLogic(data.mountResponse) val shooting: WeaponAndProjectileFunctions = WeaponAndProjectileLogic(data.shooting) val squad: SquadHandlerFunctions = SquadHandlerLogic(data.squad) val terminals: TerminalHandlerFunctions = TerminalHandlerLogic(data.terminals) val vehicles: VehicleFunctions = VehicleLogic(data.vehicles) - val vehicleResponse: VehicleHandlerFunctions = VehicleHandlerLogic(data.vehicleResponseOperations) + val vehicleResponse: VehicleHandlerFunctions = net.psforever.actors.session.normal.VehicleHandlerLogic(data.vehicleResponseOperations) override def switchTo(session: Session): Unit = { val player = session.player diff --git a/src/main/scala/net/psforever/actors/session/csr/GalaxyHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/csr/GalaxyHandlerLogic.scala deleted file mode 100644 index cf9b9932b..000000000 --- a/src/main/scala/net/psforever/actors/session/csr/GalaxyHandlerLogic.scala +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright (c) 2024 PSForever -package net.psforever.actors.session.csr - -import akka.actor.{ActorContext, ActorRef, typed} -import net.psforever.actors.session.AvatarActor -import net.psforever.actors.session.support.{GalaxyHandlerFunctions, SessionGalaxyHandlers, SessionData} -import net.psforever.packet.game.{BroadcastWarpgateUpdateMessage, FriendsResponse, HotSpotUpdateMessage, ZoneInfoMessage, ZonePopulationUpdateMessage, HotSpotInfo => PacketHotSpotInfo} -import net.psforever.services.galaxy.{GalaxyAction, GalaxyResponse, GalaxyServiceMessage} -import net.psforever.types.{MemberAction, PlanetSideEmpire} - -object GalaxyHandlerLogic { - def apply(ops: SessionGalaxyHandlers): GalaxyHandlerLogic = { - new GalaxyHandlerLogic(ops, ops.context) - } -} - -class GalaxyHandlerLogic(val ops: SessionGalaxyHandlers, implicit val context: ActorContext) extends GalaxyHandlerFunctions { - def sessionLogic: SessionData = ops.sessionLogic - - private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor - - private val galaxyService: ActorRef = ops.galaxyService - - /* packets */ - - def handleUpdateIgnoredPlayers(pkt: FriendsResponse): Unit = { - sendResponse(pkt) - pkt.friends.foreach { f => - galaxyService ! GalaxyServiceMessage(GalaxyAction.LogStatusChange(f.name)) - } - } - - /* response handlers */ - - def handle(reply: GalaxyResponse.Response): Unit = { - reply match { - case GalaxyResponse.HotSpotUpdate(zone_index, priority, hot_spot_info) => - sendResponse( - HotSpotUpdateMessage( - zone_index, - priority, - hot_spot_info.map { spot => PacketHotSpotInfo(spot.DisplayLocation.x, spot.DisplayLocation.y, 40) } - ) - ) - - case GalaxyResponse.MapUpdate(msg) => - sendResponse(msg) - - case GalaxyResponse.UpdateBroadcastPrivileges(zoneId, gateMapId, fromFactions, toFactions) => - val faction = player.Faction - val from = fromFactions.contains(faction) - val to = toFactions.contains(faction) - if (from && !to) { - sendResponse(BroadcastWarpgateUpdateMessage(zoneId, gateMapId, PlanetSideEmpire.NEUTRAL)) - } else if (!from && to) { - sendResponse(BroadcastWarpgateUpdateMessage(zoneId, gateMapId, faction)) - } - - case GalaxyResponse.FlagMapUpdate(msg) => - sendResponse(msg) - - case GalaxyResponse.TransferPassenger(temp_channel, vehicle, _, manifest) => - sessionLogic.zoning.handleTransferPassenger(temp_channel, vehicle, manifest) - - case GalaxyResponse.LockedZoneUpdate(zone, time) => - sendResponse(ZoneInfoMessage(zone.Number, empire_status=false, lock_time=time)) - - case GalaxyResponse.UnlockedZoneUpdate(zone) => - sendResponse(ZoneInfoMessage(zone.Number, empire_status=true, lock_time=0L)) - val popBO = 0 - val pop = zone.LivePlayers.distinctBy(_.CharId) - val popTR = pop.count(_.Faction == PlanetSideEmpire.TR) - val popNC = pop.count(_.Faction == PlanetSideEmpire.NC) - val popVS = pop.count(_.Faction == PlanetSideEmpire.VS) - sendResponse(ZonePopulationUpdateMessage(zone.Number, 414, 138, popTR, 138, popNC, 138, popVS, 138, popBO)) - - case GalaxyResponse.LogStatusChange(name) if avatar.people.friend.exists(_.name.equals(name)) => - avatarActor ! AvatarActor.MemberListRequest(MemberAction.UpdateFriend, name) - - case GalaxyResponse.SendResponse(msg) => - sendResponse(msg) - - case _ => () - } - } -} diff --git a/src/main/scala/net/psforever/actors/session/csr/GeneralLogic.scala b/src/main/scala/net/psforever/actors/session/csr/GeneralLogic.scala index 61c5e942a..a6732f51e 100644 --- a/src/main/scala/net/psforever/actors/session/csr/GeneralLogic.scala +++ b/src/main/scala/net/psforever/actors/session/csr/GeneralLogic.scala @@ -1,22 +1,19 @@ // Copyright (c) 2024 PSForever package net.psforever.actors.session.csr -import akka.actor.typed.scaladsl.adapter._ import akka.actor.{ActorContext, ActorRef, typed} -import net.psforever.actors.session.{AvatarActor, SessionActor} +import net.psforever.actors.session.AvatarActor import net.psforever.actors.session.support.{GeneralFunctions, GeneralOperations, SessionData} -import net.psforever.login.WorldSession.{CallBackForTask, ContainableMoveItem, DropEquipmentFromInventory, PickUpEquipmentFromGround, RemoveOldEquipmentFromInventory} -import net.psforever.objects.{Account, BoomerDeployable, BoomerTrigger, ConstructionItem, Deployables, GlobalDefinitions, Kit, LivePlayerList, PlanetSideGameObject, Player, SensorDeployable, ShieldGeneratorDeployable, SpecialEmp, TelepadDeployable, Tool, TrapDeployable, TurretDeployable, Vehicle} -import net.psforever.objects.avatar.{Avatar, PlayerControl, SpecialCarry} +import net.psforever.login.WorldSession.{ContainableMoveItem, DropEquipmentFromInventory, PickUpEquipmentFromGround, RemoveOldEquipmentFromInventory} +import net.psforever.objects.{Account, BoomerDeployable, BoomerTrigger, ConstructionItem, GlobalDefinitions, LivePlayerList, Player, SensorDeployable, ShieldGeneratorDeployable, SpecialEmp, TelepadDeployable, Tool, TrapDeployable, TurretDeployable, Vehicle} +import net.psforever.objects.avatar.{Avatar, PlayerControl} import net.psforever.objects.ballistics.Projectile -import net.psforever.objects.ce.{Deployable, DeployedItem, TelepadLike} +import net.psforever.objects.ce.{Deployable, DeployedItem} import net.psforever.objects.definition.{BasicDefinition, KitDefinition, SpecialExoSuitDefinition} import net.psforever.objects.entity.WorldEntity import net.psforever.objects.equipment.Equipment -import net.psforever.objects.guid.{GUIDTask, TaskBundle, TaskWorkflow} import net.psforever.objects.inventory.Container import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject, ServerObject} -import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.serverobject.containable.Containable import net.psforever.objects.serverobject.doors.Door import net.psforever.objects.serverobject.generator.Generator @@ -24,33 +21,21 @@ import net.psforever.objects.serverobject.llu.CaptureFlag import net.psforever.objects.serverobject.locks.IFFLock import net.psforever.objects.serverobject.mblocker.Locker import net.psforever.objects.serverobject.resourcesilo.ResourceSilo -import net.psforever.objects.serverobject.structures.{Building, WarpGate} +import net.psforever.objects.serverobject.structures.WarpGate import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal -import net.psforever.objects.serverobject.terminals.{MatrixTerminalDefinition, ProximityUnit, Terminal} +import net.psforever.objects.serverobject.terminals.{ProximityUnit, Terminal} import net.psforever.objects.serverobject.terminals.implant.ImplantTerminalMech import net.psforever.objects.serverobject.tube.SpawnTube import net.psforever.objects.serverobject.turret.FacilityTurret -import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource} -import net.psforever.objects.vehicles.{AccessPermissionGroup, Utility, UtilityType, VehicleLockState} -import net.psforever.objects.vehicles.Utility.InternalTelepad -import net.psforever.objects.vital.{VehicleDismountActivity, VehicleMountActivity, Vitality} -import net.psforever.objects.vital.collision.{CollisionReason, CollisionWithReason} -import net.psforever.objects.vital.etc.SuicideReason -import net.psforever.objects.vital.interaction.DamageInteraction -import net.psforever.objects.zones.blockmap.BlockMapEntity -import net.psforever.objects.zones.{Zone, ZoneProjectile, Zoning} +import net.psforever.objects.vehicles.Utility +import net.psforever.objects.vital.Vitality +import net.psforever.objects.zones.{ZoneProjectile, Zoning} import net.psforever.packet.PlanetSideGamePacket -import net.psforever.packet.game.objectcreate.ObjectClass -import net.psforever.packet.game.{ActionCancelMessage, ActionResultMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, BindStatus, BugReportMessage, ChangeFireModeMessage, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestAction, CharacterRequestMessage, ChatMsg, CollisionIs, ConnectToWorldRequestMessage, CreateShortcutMessage, DeadState, DeployObjectMessage, DisplayedAwardMessage, DropItemMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FriendsRequest, GenericAction, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, ImplantAction, InvalidTerrainMessage, ItemTransactionMessage, LootItemMessage, MoveItemMessage, ObjectDeleteMessage, ObjectDetectedMessage, ObjectHeldMessage, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, PlayerStateShiftMessage, RequestDestroyMessage, ShiftState, TargetInfo, TargetingImplantRequest, TargetingInfoMessage, TerrainCondition, TradeMessage, UnuseItemMessage, UseItemMessage, VoiceHostInfo, VoiceHostRequest, ZipLineMessage} +import net.psforever.packet.game.{ActionCancelMessage, ActionResultMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, BugReportMessage, ChangeFireModeMessage, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestMessage, CollisionIs, ConnectToWorldRequestMessage, CreateShortcutMessage, DeadState, DeployObjectMessage, DisplayedAwardMessage, DropItemMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FriendsRequest, GenericAction, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, ImplantAction, InvalidTerrainMessage, LootItemMessage, MoveItemMessage, ObjectDetectedMessage, ObjectHeldMessage, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, PlayerStateShiftMessage, RequestDestroyMessage, ShiftState, TargetInfo, TargetingImplantRequest, TargetingInfoMessage, TradeMessage, UnuseItemMessage, UseItemMessage, VoiceHostInfo, VoiceHostRequest, ZipLineMessage} import net.psforever.services.RemoverActor -import net.psforever.services.account.{AccountPersistenceService, RetrieveAccountData} +import net.psforever.services.account.AccountPersistenceService import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} -import net.psforever.services.local.{LocalAction, LocalServiceMessage} -import net.psforever.services.local.support.CaptureFlagManager -import net.psforever.types.{CapacitorStateType, ChatMessageType, Cosmetic, DriveState, ExoSuitType, ImplantType, PlanetSideEmpire, PlanetSideGUID, SpawnGroup, TransactionType, Vector3} -import net.psforever.util.Config - -import scala.concurrent.duration._ +import net.psforever.types.{CapacitorStateType, Cosmetic, ExoSuitType, PlanetSideEmpire, PlanetSideGUID, Vector3} object GeneralLogic { def apply(ops: GeneralOperations): GeneralLogic = { @@ -63,30 +48,11 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor - def handleConnectToWorldRequest(pkt: ConnectToWorldRequestMessage): Unit = { - val ConnectToWorldRequestMessage(_, token, majorVersion, minorVersion, revision, buildDate, _, _) = pkt - log.trace( - s"ConnectToWorldRequestMessage: client with versioning $majorVersion.$minorVersion.$revision, $buildDate has sent a token to the server" - ) - sendResponse(ChatMsg(ChatMessageType.CMT_CULLWATERMARK, wideContents=false, "", "", None)) - context.self ! SessionActor.StartHeartbeat - sessionLogic.accountIntermediary ! RetrieveAccountData(token) - } + def handleConnectToWorldRequest(pkt: ConnectToWorldRequestMessage): Unit = { /* intentionally blank */ } - def handleCharacterCreateRequest(pkt: CharacterCreateRequestMessage): Unit = { - val CharacterCreateRequestMessage(name, head, voice, gender, empire) = pkt - avatarActor ! AvatarActor.CreateAvatar(name, head, voice, gender, empire) - } + def handleCharacterCreateRequest(pkt: CharacterCreateRequestMessage): Unit = { /* intentionally blank */ } - def handleCharacterRequest(pkt: CharacterRequestMessage): Unit = { - val CharacterRequestMessage(charId, action) = pkt - action match { - case CharacterRequestAction.Delete => - avatarActor ! AvatarActor.DeleteAvatar(charId.toInt) - case CharacterRequestAction.Select => - avatarActor ! AvatarActor.SelectAvatar(charId.toInt, context.self) - } - } + def handleCharacterRequest(pkt: CharacterRequestMessage): Unit = { /* intentionally blank */ } def handlePlayerStateUpstream(pkt: PlayerStateMessageUpstream): Unit = { val PlayerStateMessageUpstream( @@ -107,14 +73,21 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex ) = pkt sessionLogic.persist() sessionLogic.turnCounterFunc(avatarGuid) - sessionLogic.updateBlockMap(player, pos) - //below half health, fully heal + //below half health, full heal val maxHealth = player.MaxHealth.toLong - if (player.Health < maxHealth * 0.5) { + if (player.Health < maxHealth * 0.5f) { player.Health = maxHealth.toInt + player.LogActivity(player.ClearHistory().head) sendResponse(PlanetsideAttributeMessage(avatarGuid, 0, maxHealth)) continent.AvatarEvents ! AvatarServiceMessage(continent.id, AvatarAction.PlanetsideAttribute(avatarGuid, 0, maxHealth)) } + //below half stamina, full stamina + val avatar = player.avatar + val maxStamina = avatar.maxStamina + if (avatar.stamina < maxStamina * 0.5f) { + session = session.copy(avatar = avatar.copy(stamina = maxStamina)) + sendResponse(PlanetsideAttributeMessage(player.GUID, 2, maxStamina.toLong)) + } //expected val isMoving = WorldEntity.isMoving(vel) val isMovingPlus = isMoving || isJumping || jumpThrust @@ -122,7 +95,7 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex if (sessionLogic.zoning.zoningStatus == Zoning.Status.Deconstructing) { sessionLogic.zoning.spawn.stopDeconstructing() } else if (sessionLogic.zoning.zoningStatus != Zoning.Status.None) { - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_motion") + sessionLogic.zoning.CancelZoningProcess() } } ops.fallHeightTracker(pos.z) @@ -136,10 +109,10 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex player.Crouching = isCrouching player.Jumping = isJumping if (isCloaking && !player.Cloaked) { - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_cloak") + sessionLogic.zoning.CancelZoningProcess() } player.Cloaked = player.ExoSuit == ExoSuitType.Infiltration && isCloaking - maxCapacitorTick(jumpThrust) + maxCapacitorTick() if (isMovingPlus && sessionLogic.terminals.usingMedicalTerminal.isDefined) { continent.GUID(sessionLogic.terminals.usingMedicalTerminal) match { case Some(term: Terminal with ProximityUnit) => @@ -150,52 +123,40 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex ops.accessedContainer match { // Ensure we don't unload the contents of the vehicle trunk for players seated in the vehicle. // This can happen if PSUM arrives during the mounting process - case Some(veh: Vehicle) if player.VehicleSeated.isEmpty || player.VehicleSeated.get != veh.GUID => - if (isMoving || veh.isMoving(test = 1) || Vector3.DistanceSquared(player.Position, veh.TrunkLocation) > 9) { - val guid = player.GUID - sendResponse(UnuseItemMessage(guid, veh.GUID)) - sendResponse(UnuseItemMessage(guid, guid)) - ops.unaccessContainer(veh) - } - case Some(container) => //just in case - if (isMovingPlus && (player.VehicleSeated.isEmpty || player.VehicleSeated.get != container.GUID)) { + case Some(container) + if !container.HasGUID && (player.VehicleSeated.isEmpty || player.VehicleSeated.get != container.GUID) => //just in case // Ensure we don't close the container if the player is seated in it val guid = player.GUID // If the container is a corpse and gets removed just as this runs it can cause a client disconnect, so we'll check the container has a GUID first. - if (container.HasGUID) { - sendResponse(UnuseItemMessage(guid, container.GUID)) - } sendResponse(UnuseItemMessage(guid, guid)) ops.unaccessContainer(container) - } case None => () } - val eagleEye: Boolean = ops.canSeeReallyFar - val isNotVisible: Boolean = sessionLogic.zoning.zoningStatus == Zoning.Status.Deconstructing || - (player.isAlive && sessionLogic.zoning.spawn.deadState == DeadState.RespawnTime) - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.PlayerState( - avatarGuid, - player.Position, - player.Velocity, - yaw, - pitch, - yawUpper, - seqTime, - isCrouching, - isJumping, - jumpThrust, - isCloaking, - isNotVisible, - eagleEye + if (!player.spectator) { + sessionLogic.updateBlockMap(player, pos) + val eagleEye: Boolean = ops.canSeeReallyFar + val isNotVisible: Boolean = sessionLogic.zoning.zoningStatus == Zoning.Status.Deconstructing || + (player.isAlive && sessionLogic.zoning.spawn.deadState == DeadState.RespawnTime) + continent.AvatarEvents ! AvatarServiceMessage( + continent.id, + AvatarAction.PlayerState( + avatarGuid, + player.Position, + player.Velocity, + yaw, + pitch, + yawUpper, + seqTime, + isCrouching, + isJumping, + jumpThrust, + isCloaking, + isNotVisible, + eagleEye + ) ) - ) - sessionLogic.squad.updateSquad() - if (player.death_by == -1) { - sessionLogic.kickedByAdministration() } - player.zoneInteractions() + sessionLogic.squad.updateSquad() } def handleVoiceHostRequest(pkt: VoiceHostRequest): Unit = { @@ -216,11 +177,11 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex (sessionLogic.validObject(itemGuid, decorator = "DropItem"), player.FreeHand.Equipment) match { case (Some(anItem: Equipment), Some(heldItem)) if (anItem eq heldItem) && continent.GUID(player.VehicleSeated).nonEmpty => - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + sessionLogic.zoning.CancelZoningProcess() RemoveOldEquipmentFromInventory(player)(heldItem) case (Some(anItem: Equipment), Some(heldItem)) if anItem eq heldItem => - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + sessionLogic.zoning.CancelZoningProcess() DropEquipmentFromInventory(player)(heldItem) case (Some(anItem: Equipment), _) if continent.GUID(player.VehicleSeated).isEmpty => @@ -248,11 +209,7 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex player.Actor ! PlayerControl.ObjectHeld(heldHolsters) } - def handleAvatarJump(pkt: AvatarJumpMessage): Unit = { - val AvatarJumpMessage(_) = pkt - avatarActor ! AvatarActor.ConsumeStamina(10) - avatarActor ! AvatarActor.SuspendStaminaRegeneration(2.5 seconds) - } + def handleAvatarJump(pkt: AvatarJumpMessage): Unit = { /* no stamina loss */ } def handleZipLine(pkt: ZipLineMessage): Unit = { val ZipLineMessage(playerGuid, forwards, action, pathId, pos) = pkt @@ -290,13 +247,7 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex //make sure this is the correct response for all cases sessionLogic.validObject(objectGuid, decorator = "RequestDestroy") match { case Some(vehicle: Vehicle) => - /* vehicle is not mounted in anything or, if it is, its seats are empty */ - if (vehicle.MountedIn.isEmpty || !vehicle.Seats.values.exists(_.isOccupied)) { //todo kick out to delete? - vehicle.Actor ! Vehicle.Deconstruct() - //log.info(s"RequestDestroy: vehicle $vehicle") - } else { - log.warn(s"RequestDestroy: ${player.Name} must own vehicle in order to deconstruct it") - } + vehicle.Actor ! Vehicle.Deconstruct() case Some(obj: Projectile) => if (!obj.isResolved) { @@ -305,7 +256,7 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex continent.Projectile ! ZoneProjectile.Remove(objectGuid) case Some(obj: BoomerTrigger) => - if (findEquipmentToDelete(objectGuid, obj)) { + if (ops.findEquipmentToDelete(objectGuid, obj)) { continent.GUID(obj.Companion) match { case Some(boomer: BoomerDeployable) => boomer.Trigger = None @@ -320,10 +271,16 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex obj.Actor ! Deployable.Deconstruct() case Some(obj: Equipment) => - findEquipmentToDelete(objectGuid, obj) + ops.findEquipmentToDelete(objectGuid, obj) - case Some(thing) => - log.warn(s"RequestDestroy: not allowed to delete this ${thing.Definition.Name}") + case Some(obj: Player) if obj.isBackpack => + obj.Position = Vector3.Zero + continent.AvatarEvents ! AvatarServiceMessage.Corpse(RemoverActor.ClearSpecific(List(obj), continent)) + + case Some(obj: Player) => + sessionLogic.general.suicide(obj) + + case Some(_) => () case None => () } @@ -411,7 +368,7 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex sendResponse(PlanetsideAttributeMessage(player.GUID, 28, implant.definition.implantType.value * 2)) } } else { - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_implant") + sessionLogic.zoning.CancelZoningProcess() avatar.implants(slot) match { case Some(implant) => if (status == 1) { @@ -439,45 +396,49 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex case Some(door: Door) => handleUseDoor(door, equipment) case Some(resourceSilo: ResourceSilo) => - handleUseResourceSilo(resourceSilo, equipment) + ops.handleUseResourceSilo(resourceSilo, equipment) case Some(panel: IFFLock) => - handleUseGeneralEntity(panel, equipment) + ops.handleUseGeneralEntity(panel, equipment) case Some(obj: Player) => - handleUsePlayer(obj, equipment, pkt) + ops.handleUsePlayer(obj, equipment, pkt) case Some(locker: Locker) => - handleUseLocker(locker, equipment, pkt) + ops.handleUseLocker(locker, equipment, pkt) case Some(gen: Generator) => - handleUseGeneralEntity(gen, equipment) + ops.handleUseGeneralEntity(gen, equipment) case Some(mech: ImplantTerminalMech) => - handleUseGeneralEntity(mech, equipment) + ops.handleUseGeneralEntity(mech, equipment) case Some(captureTerminal: CaptureTerminal) => - handleUseCaptureTerminal(captureTerminal, equipment) + ops.handleUseCaptureTerminal(captureTerminal, equipment) case Some(obj: FacilityTurret) => - handleUseFacilityTurret(obj, equipment, pkt) + ops.handleUseFacilityTurret(obj, equipment, pkt) case Some(obj: Vehicle) => - handleUseVehicle(obj, equipment, pkt) + ops.handleUseVehicle(obj, equipment, pkt) case Some(terminal: Terminal) => - handleUseTerminal(terminal, equipment, pkt) + ops.handleUseTerminal(terminal, equipment, pkt) case Some(obj: SpawnTube) => - handleUseSpawnTube(obj, equipment) + ops.handleUseSpawnTube(obj, equipment) case Some(obj: SensorDeployable) => - handleUseGeneralEntity(obj, equipment) + ops.handleUseGeneralEntity(obj, equipment) case Some(obj: TurretDeployable) => - handleUseGeneralEntity(obj, equipment) + ops.handleUseGeneralEntity(obj, equipment) case Some(obj: TrapDeployable) => - handleUseGeneralEntity(obj, equipment) + ops.handleUseGeneralEntity(obj, equipment) case Some(obj: ShieldGeneratorDeployable) => - handleUseGeneralEntity(obj, equipment) + ops.handleUseGeneralEntity(obj, equipment) + case Some(obj: TelepadDeployable) if player.spectator => + ops.handleUseTelepadDeployable(obj, equipment, pkt, ops.useRouterTelepadSystemSecretly) + case Some(obj: Utility.InternalTelepad) if player.spectator => + ops.handleUseInternalTelepad (obj, pkt, ops.useRouterTelepadSystemSecretly) case Some(obj: TelepadDeployable) => - handleUseTelepadDeployable(obj, equipment, pkt) + ops.handleUseTelepadDeployable(obj, equipment, pkt, ops.useRouterTelepadSystem) case Some(obj: Utility.InternalTelepad) => - handleUseInternalTelepad(obj, pkt) + ops.handleUseInternalTelepad (obj, pkt, ops.useRouterTelepadSystem) case Some(obj: CaptureFlag) => - handleUseCaptureFlag(obj) + ops.handleUseCaptureFlag(obj) case Some(_: WarpGate) => - handleUseWarpGate(equipment) + ops.handleUseWarpGate(equipment) case Some(obj) => - handleUseDefaultEntity(obj, equipment) + ops.handleUseDefaultEntity(obj, equipment) case None => () } } @@ -506,21 +467,8 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex case DeployedItem.portable_manned_turret => GlobalDefinitions.PortableMannedTurret(player.Faction).Item case dtype => dtype } - log.info(s"${player.Name} is constructing a $ammoType deployable") - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") - val dObj: Deployable = Deployables.Make(ammoType)() - dObj.Position = pos - dObj.Orientation = orient - dObj.WhichSide = player.WhichSide - dObj.Faction = player.Faction - dObj.AssignOwnership(player) - val tasking: TaskBundle = dObj match { - case turret: TurretDeployable => - GUIDTask.registerDeployableTurret(continent.GUID, turret) - case _ => - GUIDTask.registerObject(continent.GUID, dObj) - } - TaskWorkflow.execute(CallBackForTask(tasking, continent.Deployables, Zone.Deployable.BuildByOwner(dObj, player, obj), context.self)) + sessionLogic.zoning.CancelZoningProcess() + ops.handleDeployObject(continent, ammoType, pos, orient, player.WhichSide, PlanetSideEmpire.NEUTRAL, None) case Some(obj) => log.warn(s"DeployObject: what is $obj, ${player.Name}? It's not a construction tool!") case None => @@ -531,10 +479,8 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex def handlePlanetsideAttribute(pkt: PlanetsideAttributeMessage): Unit = { val PlanetsideAttributeMessage(objectGuid, attributeType, attributeValue) = pkt 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) => - log.warn(s"PlanetsideAttribute: ${player.Name} does not own vehicle ${vehicle.GUID} and can not change it") + vehicle.Actor ! ServerObject.AttributeMsg(attributeType, attributeValue) // Cosmetics options case Some(_: Player) if attributeType == 106 => avatarActor ! AvatarActor.SetCosmetics(Cosmetic.valuesFromAttributeValue(attributeValue)) @@ -557,7 +503,7 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex ) { //maelstrom primary fire mode discharge (no target) //aphelion_laser discharge (no target) - sessionLogic.shooting.HandleWeaponFireAccountability(objectGuid, PlanetSideGUID(Projectile.baseUID)) + sessionLogic.shooting.handleWeaponFireAccountability(objectGuid, PlanetSideGUID(Projectile.baseUID)) } else { sessionLogic.validObject(player.VehicleSeated, decorator = "GenericObjectAction/Vehicle") collect { case vehicle: Vehicle @@ -591,11 +537,7 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex def handleGenericAction(pkt: GenericActionMessage): Unit = { val GenericActionMessage(action) = pkt - if (player == null) { - if (action == GenericAction.AwayFromKeyboard_RCV) { - log.debug("GenericObjectState: AFK state reported during login") - } - } else { + if (player != null) { val (toolOpt, definition) = player.Slot(0).Equipment match { case Some(tool: Tool) => (Some(tool), tool.Definition) @@ -606,7 +548,6 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex case GenericAction.DropSpecialItem => ops.dropSpecialSlotItem() case GenericAction.MaxAnchorsExtend_RCV => - log.info(s"${player.Name} has anchored ${player.Sex.pronounObject}self to the ground") player.UsingSpecial = SpecialExoSuitDefinition.Mode.Anchored continent.AvatarEvents ! AvatarServiceMessage( continent.id, @@ -627,7 +568,6 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex log.warn(s"GenericObject: ${player.Name} is a MAX with an unexpected attachment - ${definition.Name}") } case GenericAction.MaxAnchorsRelease_RCV => - log.info(s"${player.Name} has released the anchors") player.UsingSpecial = SpecialExoSuitDefinition.Mode.Normal continent.AvatarEvents ! AvatarServiceMessage( continent.id, @@ -664,23 +604,16 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex log.warn(s"GenericActionMessage: ${player.Name} can't stop MAX special effect") } case GenericAction.AwayFromKeyboard_RCV => - log.info(s"${player.Name} is AFK") AvatarActor.savePlayerLocation(player) - ops.displayCharSavedMsgThenRenewTimer(fixedLen=1800L, varLen=0L) //~30min player.AwayFromKeyboard = true case GenericAction.BackInGame_RCV => - log.info(s"${player.Name} is back") player.AwayFromKeyboard = false - ops.renewCharSavedTimer( - Config.app.game.savedMsg.renewal.fixed, - Config.app.game.savedMsg.renewal.variable - ) case GenericAction.LookingForSquad_RCV => //Looking For Squad ON - if (!avatar.lookingForSquad && (sessionLogic.squad.squadUI.isEmpty || sessionLogic.squad.squadUI(player.CharId).index == 0)) { - avatarActor ! AvatarActor.SetLookingForSquad(true) + if (!avatar.lookingForSquad) { + avatarActor ! AvatarActor.SetLookingForSquad(false) } case GenericAction.NotLookingForSquad_RCV => //Looking For Squad OFF - if (avatar.lookingForSquad && (sessionLogic.squad.squadUI.isEmpty || sessionLogic.squad.squadUI(player.CharId).index == 0)) { + if (avatar.lookingForSquad) { avatarActor ! AvatarActor.SetLookingForSquad(false) } case _ => @@ -690,99 +623,31 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex } def handleGenericCollision(pkt: GenericCollisionMsg): Unit = { - val GenericCollisionMsg(ctype, p, _, ppos, pv, t, _, tpos, tv, _, _, _) = pkt - val fallHeight = { - if (pv.z * pv.z >= (pv.x * pv.x + pv.y * pv.y) * 0.5f) { - if (ops.heightTrend) { - val fall = ops.heightLast - ops.heightHistory - ops.heightHistory = ops.heightLast - fall - } - else { - val fall = ops.heightHistory - ops.heightLast - ops.heightLast = ops.heightHistory - fall - } - } else { - 0f + val GenericCollisionMsg(ctype, p, _, _, pv, _, _, _, _, _, _, _) = pkt + if (pv.z * pv.z >= (pv.x * pv.x + pv.y * pv.y) * 0.5f) { + if (ops.heightTrend) { + ops.heightHistory = ops.heightLast + } + else { + ops.heightLast = ops.heightHistory } } - val (target1, target2, bailProtectStatus, velocity) = (ctype, sessionLogic.validObject(p, decorator = "GenericCollision/Primary")) match { - case (CollisionIs.OfInfantry, out @ Some(user: Player)) + (ctype, sessionLogic.validObject(p, decorator = "GenericCollision/Primary")) match { + case (CollisionIs.OfInfantry, Some(user: Player)) if user == player => - val bailStatus = session.flying || session.speed > 1f || player.BailProtection player.BailProtection = false - val v = if (player.avatar.implants.exists { - case Some(implant) => implant.definition.implantType == ImplantType.Surge && implant.active - case _ => false - }) { - Vector3.Zero - } else { - pv - } - (out, None, bailStatus, v) - case (CollisionIs.OfGroundVehicle, out @ Some(v: Vehicle)) + case (CollisionIs.OfGroundVehicle, Some(v: Vehicle)) if v.Seats(0).occupant.contains(player) => - val bailStatus = v.BailProtection v.BailProtection = false - (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, sessionLogic.validObject(t, decorator = "GenericCollision/Aircraft"), false, pv) + case (CollisionIs.OfAircraft, Some(v: Vehicle)) + if v.Definition.CanFly && v.Seats(0).occupant.contains(player) => () case (CollisionIs.BetweenThings, _) => log.warn("GenericCollision: CollisionIs.BetweenThings detected - no handling case") - (None, None, false, Vector3.Zero) - case _ => - (None, None, false, Vector3.Zero) - } - val curr = System.currentTimeMillis() - (target1, t, target2) match { - case (None, _, _) => () - - case (Some(us: PlanetSideServerObject with Vitality with FactionAffinity), PlanetSideGUID(0), _) => - if (updateCollisionHistoryForTarget(us, curr)) { - if (!bailProtectStatus) { - sessionLogic.handleDealingDamage( - us, - DamageInteraction( - SourceEntry(us), - CollisionReason(velocity, fallHeight, us.DamageModel), - ppos - ) - ) - } - } - - case (Some(us: Vehicle), _, Some(victim: SensorDeployable)) => - collisionBetweenVehicleAndFragileDeployable(us, ppos, victim, tpos, velocity - tv, fallHeight, curr) - - case (Some(us: Vehicle), _, Some(victim: TurretDeployable)) if victim.Seats.isEmpty => - collisionBetweenVehicleAndFragileDeployable(us, ppos, victim, tpos, velocity - tv, fallHeight, curr) - - case ( - Some(us: PlanetSideServerObject with Vitality with FactionAffinity), _, - Some(victim: PlanetSideServerObject with Vitality with FactionAffinity) - ) => - if (updateCollisionHistoryForTarget(victim, curr)) { - val usSource = SourceEntry(us) - val victimSource = SourceEntry(victim) - //we take damage from the collision - if (!bailProtectStatus) { - performCollisionWithSomethingDamage(us, usSource, ppos, victimSource, fallHeight, velocity - tv) - } - //get dealt damage from our own collision (no protection) - ops.collisionHistory.put(us.Actor, curr) - performCollisionWithSomethingDamage(victim, victimSource, tpos, usSource, fallHeight = 0f, tv - velocity) - } - case _ => () } } - def handleAvatarFirstTimeEvent(pkt: AvatarFirstTimeEventMessage): Unit = { - val AvatarFirstTimeEventMessage(_, _, _, eventName) = pkt - avatarActor ! AvatarActor.AddFirstTimeEvent(eventName) - } + def handleAvatarFirstTimeEvent(pkt: AvatarFirstTimeEventMessage): Unit = { /* no speedrunning fte's */ } def handleBugReport(pkt: PlanetSideGamePacket): Unit = { val BugReportMessage( @@ -809,24 +674,16 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex .foreach { case obj: Vitality if obj.Destroyed => () //some entities will try to charge even if destroyed case obj: Vehicle if obj.MountedIn.nonEmpty => () //cargo vehicles need to be excluded - case obj: Vehicle => - commonFacilityShieldCharging(obj) - case obj: TurretDeployable => - commonFacilityShieldCharging(obj) - case _ if vehicleGuid.nonEmpty => - log.warn( - s"FacilityBenefitShieldChargeRequest: ${player.Name} can not find chargeable entity ${vehicleGuid.get.guid} in ${continent.id}" - ) - case _ => - log.warn(s"FacilityBenefitShieldChargeRequest: ${player.Name} is not seated in anything") + case obj: Vehicle => ops.commonFacilityShieldCharging(obj) + case obj: TurretDeployable => ops.commonFacilityShieldCharging(obj) + case _ if vehicleGuid.nonEmpty => () + case _ => () } } def handleBattleplan(pkt: BattleplanMessage): Unit = { - val BattleplanMessage(_, name, _, _) = pkt - val lament: String = s"$name has a brilliant idea that no one will ever see" - log.info(lament) - log.debug(s"Battleplan: $lament - $pkt") + /* can not draw battleplan */ + //todo csr exclusive battleplan channel } def handleBindPlayer(pkt: BindPlayerMessage): Unit = { @@ -852,25 +709,7 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex avatarActor ! AvatarActor.MemberListRequest(action, name) } - def handleInvalidTerrain(pkt: InvalidTerrainMessage): Unit = { - val InvalidTerrainMessage(_, vehicleGuid, alert, _) = pkt - (continent.GUID(vehicleGuid), continent.GUID(player.VehicleSeated)) match { - case (Some(packetVehicle: Vehicle), Some(playerVehicle: Vehicle)) if packetVehicle eq playerVehicle => - if (alert == TerrainCondition.Unsafe) { - log.info(s"${player.Name}'s ${packetVehicle.Definition.Name} is approaching terrain unsuitable for idling") - } - case (Some(packetVehicle: Vehicle), Some(_: Vehicle)) => - if (alert == TerrainCondition.Unsafe) { - log.info(s"${packetVehicle.Definition.Name}@${packetVehicle.GUID} is approaching terrain unsuitable for idling, but is not ${player.Name}'s vehicle") - } - case (Some(_: Vehicle), _) => - log.warn(s"InvalidTerrain: ${player.Name} is not seated in a(ny) vehicle near unsuitable terrain") - case (Some(packetThing), _) => - log.warn(s"InvalidTerrain: ${player.Name} thinks that ${packetThing.Definition.Name}@${packetThing.GUID} is near unsuitable terrain") - case _ => - log.error(s"InvalidTerrain: ${player.Name} is complaining about a thing@$vehicleGuid that can not be found") - } - } + def handleInvalidTerrain(pkt: InvalidTerrainMessage): Unit = { /* csr does not have to worry about invalid terrain */ } def handleActionCancel(pkt: ActionCancelMessage): Unit = { val ActionCancelMessage(_, _, _) = pkt @@ -931,19 +770,9 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex /* messages */ - def handleRenewCharSavedTimer(): Unit = { - ops.renewCharSavedTimer( - Config.app.game.savedMsg.interruptedByAction.fixed, - Config.app.game.savedMsg.interruptedByAction.variable - ) - } + def handleRenewCharSavedTimer(): Unit = { /* */ } - def handleRenewCharSavedTimerMsg(): Unit = { - ops.displayCharSavedMsgThenRenewTimer( - Config.app.game.savedMsg.interruptedByAction.fixed, - Config.app.game.savedMsg.interruptedByAction.variable - ) - } + def handleRenewCharSavedTimerMsg(): Unit = { /* */ } def handleSetAvatar(avatar: Avatar): Unit = { session = session.copy(avatar = avatar) @@ -953,21 +782,14 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex LivePlayerList.Update(avatar.id, avatar) } - def handleReceiveAccountData(account: Account): Unit = { - log.trace(s"ReceiveAccountData $account") - session = session.copy(account = account) - avatarActor ! AvatarActor.SetAccount(account) - } + def handleReceiveAccountData(account: Account): Unit = { /* no need */ } def handleUseCooldownRenew: BasicDefinition => Unit = { case _: KitDefinition => ops.kitToBeUsed = None case _ => () } - def handleAvatarResponse(avatar: Avatar): Unit = { - session = session.copy(avatar = avatar) - sessionLogic.accountPersistence ! AccountPersistenceService.Login(avatar.name, avatar.id) - } + def handleAvatarResponse(avatar: Avatar): Unit = { /* no need */ } def handleSetSpeed(speed: Float): Unit = { session = session.copy(speed = speed) @@ -986,9 +808,7 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex sessionLogic.accountPersistence ! AccountPersistenceService.Kick(player.Name, time) } - def handleSilenced(isSilenced: Boolean): Unit = { - player.silenced = isSilenced - } + def handleSilenced(isSilenced: Boolean): Unit = { /* can not be silenced */ } def handleItemPutInSlot(msg: Containable.ItemPutInSlot): Unit = { log.debug(s"ItemPutInSlot: $msg") @@ -1004,471 +824,44 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex /* supporting functions */ - private def handleUseDoor(door: Door, equipment: Option[Equipment]): Unit = { + def handleUseDoor(door: Door, equipment: Option[Equipment]): Unit = { equipment match { case Some(tool: Tool) if tool.Definition == GlobalDefinitions.medicalapplicator => - val distance: Float = math.max( - Config.app.game.doorsCanBeOpenedByMedAppFromThisDistance, - door.Definition.initialOpeningDistance - ) - door.Actor ! CommonMessages.Use(player, Some(distance)) + door.Actor ! CommonMessages.Use(player, Some(Float.MaxValue)) case _ => door.Actor ! CommonMessages.Use(player) } } - private def handleUseResourceSilo(resourceSilo: ResourceSilo, equipment: Option[Equipment]): Unit = { - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") - val vehicleOpt = continent.GUID(player.avatar.vehicle) - (vehicleOpt, equipment) match { - case (Some(vehicle: Vehicle), Some(item)) - if GlobalDefinitions.isBattleFrameVehicle(vehicle.Definition) && - GlobalDefinitions.isBattleFrameNTUSiphon(item.Definition) => - resourceSilo.Actor ! CommonMessages.Use(player, Some(vehicle)) - case (Some(vehicle: Vehicle), _) - if vehicle.Definition == GlobalDefinitions.ant && - vehicle.DeploymentState == DriveState.Deployed && - Vector3.DistanceSquared(resourceSilo.Position.xy, vehicle.Position.xy) < math.pow(resourceSilo.Definition.UseRadius, 2) => - resourceSilo.Actor ! CommonMessages.Use(player, Some(vehicle)) - case _ => () - } - } - - private def handleUsePlayer(obj: Player, equipment: Option[Equipment], msg: UseItemMessage): Unit = { - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") - if (obj.isBackpack) { - if (equipment.isEmpty) { - log.info(s"${player.Name} is looting the corpse of ${obj.Name}") - sendResponse(msg) - ops.accessContainer(obj) - } - } else if (!msg.unk3 && player.isAlive) { //potential kit use - (continent.GUID(msg.item_used_guid), ops.kitToBeUsed) match { - case (Some(kit: Kit), None) => - ops.kitToBeUsed = Some(msg.item_used_guid) - player.Actor ! CommonMessages.Use(player, Some(kit)) - case (Some(_: Kit), Some(_)) | (None, Some(_)) => - //a kit is already queued to be used; ignore this request - sendResponse(ChatMsg(ChatMessageType.UNK_225, wideContents=false, "", "Please wait ...", None)) - case (Some(item), _) => - log.error(s"UseItem: ${player.Name} looking for Kit to use, but found $item instead") - case (None, None) => - log.warn(s"UseItem: anticipated a Kit ${msg.item_used_guid} for ${player.Name}, but can't find it") } - } else if (msg.object_id == ObjectClass.avatar && msg.unk3) { - equipment match { - case Some(tool: Tool) if tool.Definition == GlobalDefinitions.bank => - obj.Actor ! CommonMessages.Use(player, equipment) - - case Some(tool: Tool) if tool.Definition == GlobalDefinitions.medicalapplicator => - obj.Actor ! CommonMessages.Use(player, equipment) - case _ => () - } - } - } - - private def handleUseLocker(locker: Locker, equipment: Option[Equipment], msg: UseItemMessage): Unit = { - equipment match { - case Some(item) => - sendUseGeneralEntityMessage(locker, item) - case None if locker.Faction == player.Faction || locker.HackedBy.nonEmpty => - log.info(s"${player.Name} is accessing a locker") - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") - val playerLocker = player.avatar.locker - sendResponse(msg.copy(object_guid = playerLocker.GUID, object_id = 456)) - ops.accessContainer(playerLocker) - case _ => () - } - } - - private def handleUseCaptureTerminal(captureTerminal: CaptureTerminal, equipment: Option[Equipment]): Unit = { - equipment match { - case Some(item) => - sendUseGeneralEntityMessage(captureTerminal, item) - case _ if ops.specialItemSlotGuid.nonEmpty => - continent.GUID(ops.specialItemSlotGuid) match { - case Some(llu: CaptureFlag) => - if (llu.Target.GUID == captureTerminal.Owner.GUID) { - continent.LocalEvents ! LocalServiceMessage(continent.id, LocalAction.LluCaptured(llu)) - } else { - log.info( - s"LLU target is not this base. Target GUID: ${llu.Target.GUID} This base: ${captureTerminal.Owner.GUID}" - ) - } - case _ => log.warn("Item in specialItemSlotGuid is not registered with continent or is not a LLU") - } - case _ => () - } - } - - private def handleUseFacilityTurret(obj: FacilityTurret, equipment: Option[Equipment], msg: UseItemMessage): Unit = { - equipment.foreach { item => - sendUseGeneralEntityMessage(obj, item) - obj.Actor ! CommonMessages.Use(player, Some((item, msg.unk2.toInt))) //try upgrade path - } - } - - private def handleUseVehicle(obj: Vehicle, equipment: Option[Equipment], msg: UseItemMessage): Unit = { - equipment match { - case Some(item) => - sendUseGeneralEntityMessage(obj, item) - case None if player.Faction == obj.Faction => - //access to trunk - if ( - obj.AccessingTrunk.isEmpty && - (!obj.PermissionGroup(AccessPermissionGroup.Trunk.id).contains(VehicleLockState.Locked) || obj.OwnerGuid - .contains(player.GUID)) - ) { - log.info(s"${player.Name} is looking in the ${obj.Definition.Name}'s trunk") - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") - obj.AccessingTrunk = player.GUID - ops.accessContainer(obj) - sendResponse(msg) - } - case _ => () - } - } - - private def handleUseTerminal(terminal: Terminal, equipment: Option[Equipment], msg: UseItemMessage): Unit = { - equipment match { - case Some(item) => - sendUseGeneralEntityMessage(terminal, item) - case None - if terminal.Owner == Building.NoBuilding || terminal.Faction == player.Faction || - terminal.HackedBy.nonEmpty || terminal.Faction == PlanetSideEmpire.NEUTRAL => - val tdef = terminal.Definition - if (tdef.isInstanceOf[MatrixTerminalDefinition]) { - //TODO matrix spawn point; for now, just blindly bind to show work (and hope nothing breaks) - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") - sendResponse( - BindPlayerMessage(BindStatus.Bind, "", display_icon=true, logging=true, SpawnGroup.Sanctuary, 0, 0, terminal.Position) - ) - } else if ( - tdef == GlobalDefinitions.multivehicle_rearm_terminal || tdef == GlobalDefinitions.bfr_rearm_terminal || - tdef == GlobalDefinitions.air_rearm_terminal || tdef == GlobalDefinitions.ground_rearm_terminal - ) { - findLocalVehicle match { - case Some(vehicle) => - log.info( - s"${player.Name} is accessing a ${terminal.Definition.Name} for ${player.Sex.possessive} ${vehicle.Definition.Name}" - ) - sendResponse(msg) - sendResponse(msg.copy(object_guid = vehicle.GUID, object_id = vehicle.Definition.ObjectId)) - case None => - log.error(s"UseItem: Expecting a seated vehicle, ${player.Name} found none") - } - } else if (tdef == GlobalDefinitions.teleportpad_terminal) { - //explicit request - log.info(s"${player.Name} is purchasing a router telepad") - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") - terminal.Actor ! Terminal.Request( - player, - ItemTransactionMessage(msg.object_guid, TransactionType.Buy, 0, "router_telepad", 0, PlanetSideGUID(0)) - ) - } else if (tdef == GlobalDefinitions.targeting_laser_dispenser) { - //explicit request - log.info(s"${player.Name} is purchasing a targeting laser") - 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}") - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") - sendResponse(msg) - } - case _ => () - } - } - - private def handleUseSpawnTube(obj: SpawnTube, equipment: Option[Equipment]): Unit = { - equipment match { - case Some(item) => - sendUseGeneralEntityMessage(obj, item) - case None if player.Faction == obj.Faction => - //deconstruction - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") - sessionLogic.actionsToCancel() - sessionLogic.terminals.CancelAllProximityUnits() - sessionLogic.zoning.spawn.startDeconstructing(obj) - case _ => () - } - } - - private def handleUseTelepadDeployable(obj: TelepadDeployable, equipment: Option[Equipment], msg: UseItemMessage): Unit = { - if (equipment.isEmpty) { - (continent.GUID(obj.Router) match { - case Some(vehicle: Vehicle) => Some((vehicle, vehicle.Utility(UtilityType.internal_router_telepad_deployable))) - case Some(vehicle) => Some(vehicle, None) - case None => None - }) match { - case Some((vehicle: Vehicle, Some(util: Utility.InternalTelepad))) => - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel") - player.WhichSide = vehicle.WhichSide - useRouterTelepadSystem( - router = vehicle, - internalTelepad = util, - remoteTelepad = obj, - src = obj, - dest = util - ) - case Some((vehicle: Vehicle, None)) => - log.error( - s"telepad@${msg.object_guid.guid} is not linked to a router - ${vehicle.Definition.Name}" - ) - case Some((o, _)) => - log.error( - s"telepad@${msg.object_guid.guid} is linked to wrong kind of object - ${o.Definition.Name}, ${obj.Router}" - ) - obj.Actor ! Deployable.Deconstruct() - case _ => () - } - } - } - - private def handleUseInternalTelepad(obj: InternalTelepad, msg: UseItemMessage): Unit = { - continent.GUID(obj.Telepad) match { - case Some(pad: TelepadDeployable) => - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel") - player.WhichSide = pad.WhichSide - useRouterTelepadSystem( - router = obj.Owner.asInstanceOf[Vehicle], - internalTelepad = obj, - remoteTelepad = pad, - src = obj, - dest = pad - ) - case Some(o) => - log.error( - s"internal telepad@${msg.object_guid.guid} is not linked to a remote telepad - ${o.Definition.Name}@${o.GUID.guid}" - ) - case None => () - } - } - - private def handleUseCaptureFlag(obj: CaptureFlag): Unit = { - // LLU can normally only be picked up the faction that owns it - ops.specialItemSlotGuid match { - case None if obj.Faction == player.Faction => - ops.specialItemSlotGuid = Some(obj.GUID) - player.Carrying = SpecialCarry.CaptureFlag - continent.LocalEvents ! CaptureFlagManager.PickupFlag(obj, player) - case None => - log.warn(s"${player.Faction} player ${player.toString} tried to pick up a ${obj.Faction} LLU - ${obj.GUID}") - case Some(guid) if guid != obj.GUID => - // Ignore duplicate pickup requests - log.warn( - s"${player.Faction} player ${player.toString} tried to pick up a ${obj.Faction} LLU, but their special slot already contains $guid" - ) - case _ => () - } - } - - private def handleUseWarpGate(equipment: Option[Equipment]): Unit = { - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") - (continent.GUID(player.VehicleSeated), equipment) match { - case (Some(vehicle: Vehicle), Some(item)) - if GlobalDefinitions.isBattleFrameVehicle(vehicle.Definition) && - GlobalDefinitions.isBattleFrameNTUSiphon(item.Definition) => - vehicle.Actor ! CommonMessages.Use(player, equipment) - case _ => () - } - } - - private def handleUseGeneralEntity(obj: PlanetSideServerObject, equipment: Option[Equipment]): Unit = { - equipment.foreach { item => - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") - obj.Actor ! CommonMessages.Use(player, Some(item)) - } - } - - private def sendUseGeneralEntityMessage(obj: PlanetSideServerObject, equipment: Equipment): Unit = { - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") - obj.Actor ! CommonMessages.Use(player, Some(equipment)) - } - - private def handleUseDefaultEntity(obj: PlanetSideGameObject, equipment: Option[Equipment]): Unit = { - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") - equipment match { - case Some(item) - if GlobalDefinitions.isBattleFrameArmorSiphon(item.Definition) || - GlobalDefinitions.isBattleFrameNTUSiphon(item.Definition) => () - case _ => - log.warn(s"UseItem: ${player.Name} does not know how to handle $obj") - } - } - - /** - * Get the current `Vehicle` object that the player is riding/driving. - * The vehicle must be found solely through use of `player.VehicleSeated`. - * @return the vehicle - */ - private def findLocalVehicle: Option[Vehicle] = { - continent.GUID(player.VehicleSeated) match { - case Some(obj: Vehicle) => Some(obj) - case _ => None - } - } - - /** - * A simple object searching algorithm that is limited to containers currently known and accessible by the player. - * If all relatively local containers are checked and the object is not found, - * the player's locker inventory will be checked, and then - * the game environment (items on the ground) will be checked too. - * If the target object is discovered, it is removed from its current location and is completely destroyed. - * @see `RequestDestroyMessage` - * @see `Zone.ItemIs.Where` - * @param objectGuid the target object's globally unique identifier; - * it is not expected that the object will be unregistered, but it is also not gauranteed - * @param obj the target object - * @return `true`, if the target object was discovered and removed; - * `false`, otherwise - */ - private def findEquipmentToDelete(objectGuid: PlanetSideGUID, obj: Equipment): Boolean = { - val findFunc - : PlanetSideServerObject with Container => Option[(PlanetSideServerObject with Container, Option[Int])] = - ops.findInLocalContainer(objectGuid) - - findFunc(player) - .orElse(ops.accessedContainer match { - case Some(parent: PlanetSideServerObject) => - findFunc(parent) - case _ => - None - }) - .orElse(findLocalVehicle match { - case Some(parent: PlanetSideServerObject) => - findFunc(parent) - case _ => - None - }) match { - case Some((parent, Some(_))) => - obj.Position = Vector3.Zero - RemoveOldEquipmentFromInventory(parent)(obj) - true - case _ if player.avatar.locker.Inventory.Remove(objectGuid) => - sendResponse(ObjectDeleteMessage(objectGuid, 0)) - true - case _ if continent.EquipmentOnGround.contains(obj) => - obj.Position = Vector3.Zero - continent.Ground ! Zone.Ground.RemoveItem(objectGuid) - continent.AvatarEvents ! AvatarServiceMessage.Ground(RemoverActor.ClearSpecific(List(obj), continent)) - true - case _ => - Zone.EquipmentIs.Where(obj, objectGuid, continent) match { - case None => - true - case Some(Zone.EquipmentIs.Orphaned()) if obj.HasGUID => - TaskWorkflow.execute(GUIDTask.unregisterEquipment(continent.GUID, obj)) - true - case Some(Zone.EquipmentIs.Orphaned()) => - true - case _ => - log.warn(s"RequestDestroy: equipment $obj exists, but ${player.Name} can not reach it to dispose of it") - false - } - } - } - - /** - * A player uses a fully-linked Router teleportation system. - * @param router the Router vehicle - * @param internalTelepad the internal telepad within the Router vehicle - * @param remoteTelepad the remote telepad that is currently associated with this Router - * @param src the origin of the teleportation (where the player starts) - * @param dest the destination of the teleportation (where the player is going) - */ - private def useRouterTelepadSystem( - router: Vehicle, - internalTelepad: InternalTelepad, - remoteTelepad: TelepadDeployable, - src: PlanetSideGameObject with TelepadLike, - dest: PlanetSideGameObject with TelepadLike - ): Unit = { - val time = System.currentTimeMillis() - if ( - time - ops.recentTeleportAttempt > 2000L && router.DeploymentState == DriveState.Deployed && - internalTelepad.Active && - remoteTelepad.Active - ) { - val pguid = player.GUID - val sguid = src.GUID - val dguid = dest.GUID - sendResponse(PlayerStateShiftMessage(ShiftState(0, dest.Position, player.Orientation.z))) - ops.useRouterTelepadEffect(pguid, sguid, dguid) - continent.LocalEvents ! LocalServiceMessage( - continent.id, - LocalAction.RouterTelepadTransport(pguid, pguid, sguid, dguid) - ) - val vSource = VehicleSource(router) - val zoneNumber = continent.Number - player.LogActivity(VehicleMountActivity(vSource, PlayerSource(player), zoneNumber)) - player.Position = dest.Position - player.LogActivity(VehicleDismountActivity(vSource, PlayerSource(player), zoneNumber)) - } else { - log.warn(s"UseRouterTelepadSystem: ${player.Name} can not teleport") - } - ops.recentTeleportAttempt = time - } - - private def maxCapacitorTick(jumpThrust: Boolean): Unit = { + private def maxCapacitorTick(): Unit = { if (player.ExoSuit == ExoSuitType.MAX) { - val activate = (jumpThrust || player.isOverdrived || player.isShielded) && player.Capacitor > 0 player.CapacitorState match { - case CapacitorStateType.Idle => maxCapacitorTickIdle(activate) - case CapacitorStateType.Discharging => maxCapacitorTickDischarging(activate) - case CapacitorStateType.ChargeDelay => maxCapacitorTickChargeDelay(activate) - case CapacitorStateType.Charging => maxCapacitorTickCharging(activate) + case CapacitorStateType.ChargeDelay => maxCapacitorTickChargeDelay() + case CapacitorStateType.Charging => maxCapacitorTickCharging() + case _ => maxCapacitorTickIdle() } } else if (player.CapacitorState != CapacitorStateType.Idle) { player.CapacitorState = CapacitorStateType.Idle } } - private def maxCapacitorTickIdle(activate: Boolean): Unit = { - if (activate) { - player.CapacitorState = CapacitorStateType.Discharging - //maxCapacitorTickDischarging(activate) - } else if (player.Capacitor < player.ExoSuitDef.MaxCapacitor) { + private def maxCapacitorTickIdle(): Unit = { + if (player.Capacitor < player.ExoSuitDef.MaxCapacitor) { player.CapacitorState = CapacitorStateType.ChargeDelay - maxCapacitorTickChargeDelay(activate) + maxCapacitorTickChargeDelay() } } - private def maxCapacitorTickDischarging(activate: Boolean): Unit = { - if (activate) { - val timeDiff = (System.currentTimeMillis() - player.CapacitorLastUsedMillis).toFloat / 1000 - val drainAmount = player.ExoSuitDef.CapacitorDrainPerSecond.toFloat * timeDiff - player.Capacitor -= drainAmount - sendResponse(PlanetsideAttributeMessage(player.GUID, 7, player.Capacitor.toInt)) - } else if (player.Capacitor < player.ExoSuitDef.MaxCapacitor) { - if (player.Faction != PlanetSideEmpire.VS) { - ops.toggleMaxSpecialState(enable = false) - } - player.CapacitorState = CapacitorStateType.ChargeDelay - maxCapacitorTickChargeDelay(activate) - } else { - player.CapacitorState = CapacitorStateType.Idle - } - } - - private def maxCapacitorTickChargeDelay(activate: Boolean): Unit = { - if (activate) { - player.CapacitorState = CapacitorStateType.Discharging - //maxCapacitorTickDischarging(activate) - } else if (player.Capacitor == player.ExoSuitDef.MaxCapacitor) { + private def maxCapacitorTickChargeDelay(): Unit = { + if (player.Capacitor == player.ExoSuitDef.MaxCapacitor) { player.CapacitorState = CapacitorStateType.Idle } else if (System.currentTimeMillis() - player.CapacitorLastUsedMillis > player.ExoSuitDef.CapacitorRechargeDelayMillis) { player.CapacitorState = CapacitorStateType.Charging - //maxCapacitorTickCharging(activate) } } - private def maxCapacitorTickCharging(activate: Boolean): Unit = { - if (activate) { - player.CapacitorState = CapacitorStateType.Discharging - //maxCapacitorTickDischarging(activate) - } else if (player.Capacitor < player.ExoSuitDef.MaxCapacitor) { + private def maxCapacitorTickCharging(): Unit = { + if (player.Capacitor < player.ExoSuitDef.MaxCapacitor) { val timeDiff = (System.currentTimeMillis() - player.CapacitorLastChargedMillis).toFloat / 1000 val chargeAmount = player.ExoSuitDef.CapacitorRechargePerSecond * timeDiff player.Capacitor += chargeAmount @@ -1477,64 +870,4 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex player.CapacitorState = CapacitorStateType.Idle } } - - private def updateCollisionHistoryForTarget( - target: PlanetSideServerObject with Vitality with FactionAffinity, - curr: Long - ): Boolean = { - ops.collisionHistory.get(target.Actor) match { - case Some(lastCollision) if curr - lastCollision <= 1000L => - false - case _ => - ops.collisionHistory.put(target.Actor, curr) - true - } - } - - private def collisionBetweenVehicleAndFragileDeployable( - 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 - ops.collisionHistory.put(vehicle.Actor, collisionTime) - sessionLogic.handleDealingDamage( - smallDeployable, - DamageInteraction(smallDeployableSource, SuicideReason(), smallDeployablePosition) - ) - } - } - - private def performCollisionWithSomethingDamage( - target: PlanetSideServerObject with Vitality with FactionAffinity, - targetSource: SourceEntry, - targetPosition: Vector3, - victimSource: SourceEntry, - fallHeight: Float, - velocity: Vector3 - ): Unit = { - sessionLogic.handleDealingDamage( - target, - DamageInteraction( - targetSource, - CollisionWithReason(CollisionReason(velocity, fallHeight, target.DamageModel), victimSource), - targetPosition - ) - ) - } - - private def commonFacilityShieldCharging(obj: PlanetSideServerObject with BlockMapEntity): Unit = { - obj.Actor ! CommonMessages.ChargeShields( - 15, - Some(continent.blockMap.sector(obj).buildingList.maxBy(_.Definition.SOIRadius)) - ) - } } diff --git a/src/main/scala/net/psforever/actors/session/csr/LocalHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/csr/LocalHandlerLogic.scala deleted file mode 100644 index 37d7c2f62..000000000 --- a/src/main/scala/net/psforever/actors/session/csr/LocalHandlerLogic.scala +++ /dev/null @@ -1,268 +0,0 @@ -// Copyright (c) 2024 PSForever -package net.psforever.actors.session.csr - -import akka.actor.ActorContext -import net.psforever.actors.session.support.{LocalHandlerFunctions, SessionData, SessionLocalHandlers} -import net.psforever.objects.ce.Deployable -import net.psforever.objects.serverobject.doors.Door -import net.psforever.objects.vehicles.MountableWeapons -import net.psforever.objects.{BoomerDeployable, ExplosiveDeployable, TelepadDeployable, Tool, TurretDeployable} -import net.psforever.packet.game.{ChatMsg, DeployableObjectsInfoMessage, GenericActionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HackMessage, HackState, HackState1, InventoryStateMessage, ObjectAttachMessage, ObjectCreateMessage, ObjectDeleteMessage, ObjectDetachMessage, OrbitalShuttleTimeMsg, PadAndShuttlePair, PlanetsideAttributeMessage, ProximityTerminalUseMessage, SetEmpireMessage, TriggerEffectMessage, TriggerSoundMessage, TriggeredSound, VehicleStateMessage} -import net.psforever.services.Service -import net.psforever.services.local.LocalResponse -import net.psforever.types.{ChatMessageType, PlanetSideGUID, Vector3} - -object LocalHandlerLogic { - def apply(ops: SessionLocalHandlers): LocalHandlerLogic = { - new LocalHandlerLogic(ops, ops.context) - } -} - -class LocalHandlerLogic(val ops: SessionLocalHandlers, implicit val context: ActorContext) extends LocalHandlerFunctions { - def sessionLogic: SessionData = ops.sessionLogic - - /* messages */ - - def handleTurretDeployableIsDismissed(obj: TurretDeployable): Unit = { - ops.handleTurretDeployableIsDismissed(obj) - } - - def handleDeployableIsDismissed(obj: Deployable): Unit = { - ops.handleDeployableIsDismissed(obj) - } - - /* response handlers */ - - /** - * na - * @param toChannel na - * @param guid na - * @param reply na - */ - def handle(toChannel: String, guid: PlanetSideGUID, reply: LocalResponse.Response): Unit = { - val resolvedPlayerGuid = if (player.HasGUID) { - player.GUID - } else { - Service.defaultPlayerGUID - } - val isNotSameTarget = resolvedPlayerGuid != guid - reply match { - case LocalResponse.DeployableMapIcon(behavior, deployInfo) if isNotSameTarget => - sendResponse(DeployableObjectsInfoMessage(behavior, deployInfo)) - - case LocalResponse.DeployableUIFor(item) => - sessionLogic.general.updateDeployableUIElements(avatar.deployables.UpdateUIElement(item)) - - case LocalResponse.Detonate(dguid, _: BoomerDeployable) => - sendResponse(TriggerEffectMessage(dguid, "detonate_boomer")) - sendResponse(PlanetsideAttributeMessage(dguid, attribute_type=29, attribute_value=1)) - sendResponse(ObjectDeleteMessage(dguid, unk1=0)) - - case LocalResponse.Detonate(dguid, _: ExplosiveDeployable) => - sendResponse(GenericObjectActionMessage(dguid, code=19)) - sendResponse(PlanetsideAttributeMessage(dguid, attribute_type=29, attribute_value=1)) - sendResponse(ObjectDeleteMessage(dguid, unk1=0)) - - case LocalResponse.Detonate(_, obj) => - log.warn(s"LocalResponse.Detonate: ${obj.Definition.Name} not configured to explode correctly") - - case LocalResponse.DoorOpens(doorGuid) if isNotSameTarget => - val pos = player.Position.xy - val range = ops.doorLoadRange() - val foundDoor = continent - .blockMap - .sector(pos, range) - .amenityList - .collect { case door: Door => door } - .find(_.GUID == doorGuid) - val doorExistsInRange: Boolean = foundDoor.nonEmpty - if (doorExistsInRange) { - sendResponse(GenericObjectStateMsg(doorGuid, state=16)) - } - - case LocalResponse.DoorCloses(doorGuid) => //door closes for everyone - sendResponse(GenericObjectStateMsg(doorGuid, state=17)) - - case LocalResponse.EliminateDeployable(obj: TurretDeployable, dguid, _, _) if obj.Destroyed => - sendResponse(ObjectDeleteMessage(dguid, unk1=0)) - - case LocalResponse.EliminateDeployable(obj: TurretDeployable, dguid, pos, _) => - obj.Destroyed = true - DeconstructDeployable( - obj, - dguid, - pos, - obj.Orientation, - deletionType= if (obj.MountPoints.isEmpty) { 2 } else { 1 } - ) - - case LocalResponse.EliminateDeployable(obj: ExplosiveDeployable, dguid, _, _) - if obj.Destroyed || obj.Jammed || obj.Health == 0 => - sendResponse(ObjectDeleteMessage(dguid, unk1=0)) - - case LocalResponse.EliminateDeployable(obj: ExplosiveDeployable, dguid, pos, effect) => - obj.Destroyed = true - DeconstructDeployable(obj, dguid, pos, obj.Orientation, effect) - - case LocalResponse.EliminateDeployable(obj: TelepadDeployable, dguid, _, _) if obj.Active && obj.Destroyed => - //if active, deactivate - obj.Active = false - ops.deactivateTelpadDeployableMessages(dguid) - //standard deployable elimination behavior - sendResponse(ObjectDeleteMessage(dguid, unk1=0)) - - case LocalResponse.EliminateDeployable(obj: TelepadDeployable, dguid, pos, _) if obj.Active => - //if active, deactivate - obj.Active = false - ops.deactivateTelpadDeployableMessages(dguid) - //standard deployable elimination behavior - obj.Destroyed = true - DeconstructDeployable(obj, dguid, pos, obj.Orientation, deletionType=2) - - case LocalResponse.EliminateDeployable(obj: TelepadDeployable, dguid, _, _) if obj.Destroyed => - //standard deployable elimination behavior - sendResponse(ObjectDeleteMessage(dguid, unk1=0)) - - case LocalResponse.EliminateDeployable(obj: TelepadDeployable, dguid, pos, _) => - //standard deployable elimination behavior - obj.Destroyed = true - DeconstructDeployable(obj, dguid, pos, obj.Orientation, deletionType=2) - - case LocalResponse.EliminateDeployable(obj, dguid, _, _) if obj.Destroyed => - sendResponse(ObjectDeleteMessage(dguid, unk1=0)) - - case LocalResponse.EliminateDeployable(obj, dguid, pos, effect) => - obj.Destroyed = true - DeconstructDeployable(obj, dguid, pos, obj.Orientation, effect) - - case LocalResponse.SendHackMessageHackCleared(targetGuid, unk1, unk2) => - sendResponse(HackMessage(HackState1.Unk0, targetGuid, guid, progress=0, unk1.toFloat, HackState.HackCleared, unk2)) - - case LocalResponse.HackObject(targetGuid, unk1, unk2) => - sessionLogic.general.hackObject(targetGuid, unk1, unk2) - - case LocalResponse.PlanetsideAttribute(targetGuid, attributeType, attributeValue) => - sessionLogic.general.sendPlanetsideAttributeMessage(targetGuid, attributeType, attributeValue) - - case LocalResponse.GenericObjectAction(targetGuid, actionNumber) => - sendResponse(GenericObjectActionMessage(targetGuid, actionNumber)) - - case LocalResponse.GenericActionMessage(actionNumber) => - sendResponse(GenericActionMessage(actionNumber)) - - case LocalResponse.ChatMessage(msg) => - sendResponse(msg) - - case LocalResponse.SendPacket(packet) => - sendResponse(packet) - - case LocalResponse.LluSpawned(llu) => - // Create LLU on client - sendResponse(ObjectCreateMessage( - llu.Definition.ObjectId, - llu.GUID, - llu.Definition.Packet.ConstructorData(llu).get - )) - sendResponse(TriggerSoundMessage(TriggeredSound.LLUMaterialize, llu.Position, unk=20, volume=0.8000001f)) - - case LocalResponse.LluDespawned(lluGuid, position) => - sendResponse(TriggerSoundMessage(TriggeredSound.LLUDeconstruct, position, unk=20, volume=0.8000001f)) - sendResponse(ObjectDeleteMessage(lluGuid, unk1=0)) - // If the player was holding the LLU, remove it from their tracked special item slot - sessionLogic.general.specialItemSlotGuid.collect { case guid if guid == lluGuid => - sessionLogic.general.specialItemSlotGuid = None - player.Carrying = None - } - - case LocalResponse.ObjectDelete(objectGuid, unk) if isNotSameTarget => - sendResponse(ObjectDeleteMessage(objectGuid, unk)) - - case LocalResponse.ProximityTerminalEffect(object_guid, true) => - sendResponse(ProximityTerminalUseMessage(Service.defaultPlayerGUID, object_guid, unk=true)) - - case LocalResponse.ProximityTerminalEffect(objectGuid, false) => - sendResponse(ProximityTerminalUseMessage(Service.defaultPlayerGUID, objectGuid, unk=false)) - sessionLogic.terminals.ForgetAllProximityTerminals(objectGuid) - - case LocalResponse.RouterTelepadMessage(msg) => - sendResponse(ChatMsg(ChatMessageType.UNK_229, wideContents=false, recipient="", msg, note=None)) - - case LocalResponse.RouterTelepadTransport(passengerGuid, srcGuid, destGuid) => - sessionLogic.general.useRouterTelepadEffect(passengerGuid, srcGuid, destGuid) - - case LocalResponse.SendResponse(msg) => - sendResponse(msg) - - case LocalResponse.SetEmpire(objectGuid, empire) => - sendResponse(SetEmpireMessage(objectGuid, empire)) - - case LocalResponse.ShuttleEvent(ev) => - val msg = OrbitalShuttleTimeMsg( - ev.u1, - ev.u2, - ev.t1, - ev.t2, - ev.t3, - pairs=ev.pairs.map { case ((a, b), c) => PadAndShuttlePair(a, b, c) } - ) - sendResponse(msg) - - case LocalResponse.ShuttleDock(pguid, sguid, slot) => - sendResponse(ObjectAttachMessage(pguid, sguid, slot)) - - case LocalResponse.ShuttleUndock(pguid, sguid, pos, orient) => - sendResponse(ObjectDetachMessage(pguid, sguid, pos, orient)) - - case LocalResponse.ShuttleState(sguid, pos, orient, state) => - sendResponse(VehicleStateMessage(sguid, unk1=0, pos, orient, vel=None, Some(state), unk3=0, unk4=0, wheel_direction=15, is_decelerating=false, is_cloaked=false)) - - case LocalResponse.ToggleTeleportSystem(router, systemPlan) => - sessionLogic.general.toggleTeleportSystem(router, systemPlan) - - case LocalResponse.TriggerEffect(targetGuid, effect, effectInfo, triggerLocation) => - sendResponse(TriggerEffectMessage(targetGuid, effect, effectInfo, triggerLocation)) - - case LocalResponse.TriggerSound(sound, pos, unk, volume) => - sendResponse(TriggerSoundMessage(sound, pos, unk, volume)) - - case LocalResponse.UpdateForceDomeStatus(buildingGuid, true) => - sendResponse(GenericObjectActionMessage(buildingGuid, 11)) - - case LocalResponse.UpdateForceDomeStatus(buildingGuid, false) => - sendResponse(GenericObjectActionMessage(buildingGuid, 12)) - - case LocalResponse.RechargeVehicleWeapon(vehicleGuid, weaponGuid) if resolvedPlayerGuid == guid => - continent.GUID(vehicleGuid) - .collect { case vehicle: MountableWeapons => (vehicle, vehicle.PassengerInSeat(player)) } - .collect { case (vehicle, Some(seat_num)) => vehicle.WeaponControlledFromSeat(seat_num) } - .getOrElse(Set.empty) - .collect { case weapon: Tool if weapon.GUID == weaponGuid => - sendResponse(InventoryStateMessage(weapon.AmmoSlot.Box.GUID, weapon.GUID, weapon.Magazine)) - } - - case _ => () - } - } - - /* support functions */ - - /** - * Common behavior for deconstructing deployables in the game environment. - * @param obj the deployable - * @param guid the globally unique identifier for the deployable - * @param pos the previous position of the deployable - * @param orient the previous orientation of the deployable - * @param deletionType the value passed to `ObjectDeleteMessage` concerning the deconstruction animation - */ - def DeconstructDeployable( - obj: Deployable, - guid: PlanetSideGUID, - pos: Vector3, - orient: Vector3, - deletionType: Int - ): Unit = { - sendResponse(TriggerEffectMessage("spawn_object_failed_effect", pos, orient)) - sendResponse(PlanetsideAttributeMessage(guid, 29, 1)) //make deployable vanish - sendResponse(ObjectDeleteMessage(guid, deletionType)) - } -} diff --git a/src/main/scala/net/psforever/actors/session/csr/MountHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/csr/MountHandlerLogic.scala index 91e470beb..94ba103a7 100644 --- a/src/main/scala/net/psforever/actors/session/csr/MountHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/csr/MountHandlerLogic.scala @@ -1,27 +1,24 @@ // Copyright (c) 2024 PSForever package net.psforever.actors.session.csr -import akka.actor.{ActorContext, typed} -import net.psforever.actors.session.AvatarActor +import akka.actor.ActorContext import net.psforever.actors.session.support.{MountHandlerFunctions, SessionData, SessionMountHandlers} import net.psforever.actors.zone.ZoneActor import net.psforever.objects.{GlobalDefinitions, PlanetSideGameObject, Player, Vehicle, Vehicles} -import net.psforever.objects.definition.{BasicDefinition, ObjectDefinition} import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.serverobject.environment.interaction.ResetAllEnvironmentInteractions import net.psforever.objects.serverobject.hackable.GenericHackables import net.psforever.objects.serverobject.mount.Mountable +import net.psforever.objects.serverobject.structures.WarpGate import net.psforever.objects.serverobject.terminals.implant.ImplantTerminalMech import net.psforever.objects.serverobject.turret.{FacilityTurret, WeaponTurret} -import net.psforever.objects.vehicles.{AccessPermissionGroup, CargoBehavior} +import net.psforever.objects.vehicles.AccessPermissionGroup import net.psforever.objects.vital.InGameHistory -import net.psforever.packet.game.{ChatMsg, DelayedPathMountMsg, DismountVehicleCargoMsg, DismountVehicleMsg, GenericObjectActionMessage, MountVehicleCargoMsg, MountVehicleMsg, ObjectAttachMessage, ObjectDetachMessage, PlanetsideAttributeMessage, PlayerStasisMessage, PlayerStateShiftMessage, ShiftState} +import net.psforever.packet.game.{ChatMsg, DelayedPathMountMsg, DismountVehicleCargoMsg, DismountVehicleMsg, GenericObjectActionMessage, MountVehicleCargoMsg, MountVehicleMsg, ObjectDetachMessage, PlanetsideAttributeMessage, PlayerStasisMessage, PlayerStateShiftMessage, ShiftState} import net.psforever.services.Service import net.psforever.services.local.{LocalAction, LocalServiceMessage} import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} -import net.psforever.types.{BailType, ChatMessageType, PlanetSideGUID, Vector3} - -import scala.concurrent.duration._ +import net.psforever.types.{BailType, ChatMessageType, DriveState, PlanetSideGUID, Vector3} object MountHandlerLogic { def apply(ops: SessionMountHandlers): MountHandlerLogic = { @@ -32,128 +29,28 @@ object MountHandlerLogic { class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: ActorContext) extends MountHandlerFunctions { def sessionLogic: SessionData = ops.sessionLogic - private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor - /* packets */ def handleMountVehicle(pkt: MountVehicleMsg): Unit = { - val MountVehicleMsg(_, mountable_guid, entry_point) = pkt - sessionLogic.validObject(mountable_guid, decorator = "MountVehicle").collect { - case obj: Mountable => - obj.Actor ! Mountable.TryMount(player, entry_point) - case _ => - log.error(s"MountVehicleMsg: object ${mountable_guid.guid} not a mountable thing, ${player.Name}") + //can only mount vehicle when not in csr spectator mode + if (!player.spectator) { + ops.handleMountVehicle(pkt) } } def handleDismountVehicle(pkt: DismountVehicleMsg): Unit = { - val DismountVehicleMsg(player_guid, bailType, wasKickedByDriver) = pkt - val dError: (String, Player)=> Unit = dismountError(bailType, wasKickedByDriver) - //TODO optimize this later - //common warning for this section - if (player.GUID == player_guid) { - //normally disembarking from a mount - (sessionLogic.zoning.interstellarFerry.orElse(continent.GUID(player.VehicleSeated)) match { - case out @ Some(obj: Vehicle) => - continent.GUID(obj.MountedIn) match { - case Some(_: Vehicle) => None //cargo vehicle - case _ => out //arrangement "may" be permissible - } - case out @ Some(_: Mountable) => - out - case _ => - dError(s"DismountVehicleMsg: player ${player.Name} not considered seated in a mountable entity", player) - None - }) match { - case Some(obj: Mountable) => - obj.PassengerInSeat(player) match { - case Some(seat_num) => - obj.Actor ! Mountable.TryDismount(player, seat_num, bailType) - //short-circuit the temporary channel for transferring between zones, the player is no longer doing that - sessionLogic.zoning.interstellarFerry = None - // Deconstruct the vehicle if the driver has bailed out and the vehicle is capable of flight - //todo: implement auto landing procedure if the pilot bails but passengers are still present instead of deconstructing the vehicle - //todo: continue flight path until aircraft crashes if no passengers present (or no passenger seats), then deconstruct. - //todo: kick cargo passengers out. To be added after PR #216 is merged - obj match { - case v: Vehicle - if bailType == BailType.Bailed && - v.SeatPermissionGroup(seat_num).contains(AccessPermissionGroup.Driver) && - v.isFlying => - v.Actor ! Vehicle.Deconstruct(None) //immediate deconstruction - case _ => () - } - - case None => - dError(s"DismountVehicleMsg: can not find where player ${player.Name}_guid is seated in mountable ${player.VehicleSeated}", player) - } - case _ => - dError(s"DismountVehicleMsg: can not find mountable entity ${player.VehicleSeated}", player) - } - } else { - //kicking someone else out of a mount; need to own that mount/mountable - val dWarn: (String, Player)=> Unit = dismountWarning(bailType, wasKickedByDriver) - player.avatar.vehicle match { - case Some(obj_guid) => - ( - ( - sessionLogic.validObject(obj_guid, decorator = "DismountVehicle/Vehicle"), - sessionLogic.validObject(player_guid, decorator = "DismountVehicle/Player") - ) match { - case (vehicle @ Some(obj: Vehicle), tplayer) => - if (obj.MountedIn.isEmpty) (vehicle, tplayer) else (None, None) - case (mount @ Some(_: Mountable), tplayer) => - (mount, tplayer) - case _ => - (None, None) - }) match { - case (Some(obj: Mountable), Some(tplayer: Player)) => - obj.PassengerInSeat(tplayer) match { - case Some(seat_num) => - obj.Actor ! Mountable.TryDismount(tplayer, seat_num, bailType) - case None => - dError(s"DismountVehicleMsg: can not find where other player ${tplayer.Name} is seated in mountable $obj_guid", tplayer) - } - case (None, _) => - dWarn(s"DismountVehicleMsg: ${player.Name} can not find his vehicle", player) - case (_, None) => - dWarn(s"DismountVehicleMsg: player $player_guid could not be found to kick, ${player.Name}", player) - case _ => - dWarn(s"DismountVehicleMsg: object is either not a Mountable or not a Player", player) - } - case None => - dWarn(s"DismountVehicleMsg: ${player.Name} does not own a vehicle", player) - } - } + //can't do this if we're not in vehicle, so also not csr spectator + ops.handleDismountVehicle(pkt.copy(bailType = BailType.Bailed)) } def handleMountVehicleCargo(pkt: MountVehicleCargoMsg): Unit = { - val MountVehicleCargoMsg(_, cargo_guid, carrier_guid, _) = pkt - (continent.GUID(cargo_guid), continent.GUID(carrier_guid)) match { - case (Some(cargo: Vehicle), Some(carrier: Vehicle)) => - carrier.CargoHolds.find({ case (_, hold) => !hold.isOccupied }) match { - case Some((mountPoint, _)) => - cargo.Actor ! CargoBehavior.StartCargoMounting(carrier_guid, mountPoint) - case _ => - log.warn( - s"MountVehicleCargoMsg: ${player.Name} trying to load cargo into a ${carrier.Definition.Name} which oes not have a cargo hold" - ) - } - case (None, _) | (Some(_), None) => - log.warn( - s"MountVehicleCargoMsg: ${player.Name} lost a vehicle while working with cargo - either $carrier_guid or $cargo_guid" - ) - case _ => () - } + //can't do this if we're not in vehicle, so also not csr spectator + ops.handleMountVehicleCargo(pkt) } def handleDismountVehicleCargo(pkt: DismountVehicleCargoMsg): Unit = { - val DismountVehicleCargoMsg(_, cargo_guid, bailed, _, kicked) = pkt - continent.GUID(cargo_guid) match { - case Some(cargo: Vehicle) => - cargo.Actor ! CargoBehavior.StartCargoDismounting(bailed || kicked) - case _ => () - } + //can't do this if we're not in vehicle, so also not csr spectator + ops.handleDismountVehicleCargo(pkt.copy(bailed = true)) } /* response handlers */ @@ -167,24 +64,21 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act def handle(tplayer: Player, reply: Mountable.Exchange): Unit = { reply match { case Mountable.CanMount(obj: ImplantTerminalMech, seatNumber, _) => - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") - log.info(s"${player.Name} mounts an implant terminal") + sessionLogic.zoning.CancelZoningProcess() sessionLogic.terminals.CancelAllProximityUnits() - MountingAction(tplayer, obj, seatNumber) + ops.MountingAction(tplayer, obj, seatNumber) sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistence case Mountable.CanMount(obj: Vehicle, seatNumber, _) if obj.Definition == GlobalDefinitions.orbital_shuttle => - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount") - log.info(s"${player.Name} mounts the orbital shuttle") + sessionLogic.zoning.CancelZoningProcess() sessionLogic.terminals.CancelAllProximityUnits() - MountingAction(tplayer, obj, seatNumber) + ops.MountingAction(tplayer, obj, seatNumber) sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistence case Mountable.CanMount(obj: Vehicle, seatNumber, _) if obj.Definition == GlobalDefinitions.ant => - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount") - log.info(s"${player.Name} mounts the driver seat of the ${obj.Definition.Name}") + sessionLogic.zoning.CancelZoningProcess() val obj_guid: PlanetSideGUID = obj.GUID sessionLogic.terminals.CancelAllProximityUnits() sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health)) @@ -193,12 +87,11 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act sendResponse(GenericObjectActionMessage(obj_guid, code=11)) sessionLogic.general.accessContainer(obj) tplayer.Actor ! ResetAllEnvironmentInteractions - MountingAction(tplayer, obj, seatNumber) + ops.MountingAction(tplayer, obj, seatNumber) case Mountable.CanMount(obj: Vehicle, seatNumber, _) if obj.Definition == GlobalDefinitions.quadstealth => - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount") - log.info(s"${player.Name} mounts the driver seat of the ${obj.Definition.Name}") + sessionLogic.zoning.CancelZoningProcess() val obj_guid: PlanetSideGUID = obj.GUID sessionLogic.terminals.CancelAllProximityUnits() sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health)) @@ -209,12 +102,11 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act sendResponse(GenericObjectActionMessage(obj_guid, code=11)) sessionLogic.general.accessContainer(obj) tplayer.Actor ! ResetAllEnvironmentInteractions - MountingAction(tplayer, obj, seatNumber) + ops.MountingAction(tplayer, obj, seatNumber) case Mountable.CanMount(obj: Vehicle, seatNumber, _) if seatNumber == 0 && obj.Definition.MaxCapacitor > 0 => - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount") - log.info(s"${player.Name} mounts the driver seat of the ${obj.Definition.Name}") + sessionLogic.zoning.CancelZoningProcess() val obj_guid: PlanetSideGUID = obj.GUID sessionLogic.terminals.CancelAllProximityUnits() sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health)) @@ -224,12 +116,11 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act sessionLogic.general.accessContainer(obj) ops.updateWeaponAtSeatPosition(obj, seatNumber) tplayer.Actor ! ResetAllEnvironmentInteractions - MountingAction(tplayer, obj, seatNumber) + ops.MountingAction(tplayer, obj, seatNumber) case Mountable.CanMount(obj: Vehicle, seatNumber, _) if seatNumber == 0 => - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount") - log.info(s"${player.Name} mounts the driver seat of the ${obj.Definition.Name}") + sessionLogic.zoning.CancelZoningProcess() val obj_guid: PlanetSideGUID = obj.GUID sessionLogic.terminals.CancelAllProximityUnits() sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health)) @@ -238,17 +129,11 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act sessionLogic.general.accessContainer(obj) ops.updateWeaponAtSeatPosition(obj, seatNumber) tplayer.Actor ! ResetAllEnvironmentInteractions - MountingAction(tplayer, obj, seatNumber) + ops.MountingAction(tplayer, obj, seatNumber) case Mountable.CanMount(obj: Vehicle, seatNumber, _) if obj.Definition.MaxCapacitor > 0 => - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount") - log.info(s"${player.Name} mounts ${ - obj.SeatPermissionGroup(seatNumber) match { - case Some(seatType) => s"a $seatType seat (#$seatNumber)" - case None => "a seat" - } - } of the ${obj.Definition.Name}") + sessionLogic.zoning.CancelZoningProcess() val obj_guid: PlanetSideGUID = obj.GUID sessionLogic.terminals.CancelAllProximityUnits() sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health)) @@ -258,16 +143,10 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act ops.updateWeaponAtSeatPosition(obj, seatNumber) sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistence tplayer.Actor ! ResetAllEnvironmentInteractions - MountingAction(tplayer, obj, seatNumber) + ops.MountingAction(tplayer, obj, seatNumber) case Mountable.CanMount(obj: Vehicle, seatNumber, _) => - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount") - log.info(s"${player.Name} mounts the ${ - obj.SeatPermissionGroup(seatNumber) match { - case Some(seatType) => s"a $seatType seat (#$seatNumber)" - case None => "a seat" - } - } of the ${obj.Definition.Name}") + sessionLogic.zoning.CancelZoningProcess() val obj_guid: PlanetSideGUID = obj.GUID sessionLogic.terminals.CancelAllProximityUnits() sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health)) @@ -276,51 +155,46 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act ops.updateWeaponAtSeatPosition(obj, seatNumber) sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistence tplayer.Actor ! ResetAllEnvironmentInteractions - MountingAction(tplayer, obj, seatNumber) + ops.MountingAction(tplayer, obj, seatNumber) case Mountable.CanMount(obj: FacilityTurret, seatNumber, _) if obj.Definition == GlobalDefinitions.vanu_sentry_turret => - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount") - log.info(s"${player.Name} mounts the ${obj.Definition.Name}") + sessionLogic.zoning.CancelZoningProcess() obj.Zone.LocalEvents ! LocalServiceMessage(obj.Zone.id, LocalAction.SetEmpire(obj.GUID, player.Faction)) sendResponse(PlanetsideAttributeMessage(obj.GUID, attribute_type=0, obj.Health)) ops.updateWeaponAtSeatPosition(obj, seatNumber) - MountingAction(tplayer, obj, seatNumber) + ops.MountingAction(tplayer, obj, seatNumber) case Mountable.CanMount(obj: FacilityTurret, seatNumber, _) if !obj.isUpgrading || System.currentTimeMillis() - GenericHackables.getTurretUpgradeTime >= 1500L => obj.setMiddleOfUpgrade(false) - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount") - log.info(s"${player.Name} mounts the ${obj.Definition.Name}") + sessionLogic.zoning.CancelZoningProcess() sendResponse(PlanetsideAttributeMessage(obj.GUID, attribute_type=0, obj.Health)) ops.updateWeaponAtSeatPosition(obj, seatNumber) - MountingAction(tplayer, obj, seatNumber) + ops.MountingAction(tplayer, obj, seatNumber) case Mountable.CanMount(obj: FacilityTurret, _, _) => - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount") + sessionLogic.zoning.CancelZoningProcess() log.warn( s"MountVehicleMsg: ${tplayer.Name} wants to mount turret ${obj.GUID.guid}, but needs to wait until it finishes updating" ) case Mountable.CanMount(obj: PlanetSideGameObject with FactionAffinity with WeaponTurret with InGameHistory, seatNumber, _) => - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount") - log.info(s"${player.Name} mounts the ${obj.Definition.asInstanceOf[BasicDefinition].Name}") + sessionLogic.zoning.CancelZoningProcess() sendResponse(PlanetsideAttributeMessage(obj.GUID, attribute_type=0, obj.Health)) ops.updateWeaponAtSeatPosition(obj, seatNumber) - MountingAction(tplayer, obj, seatNumber) + ops.MountingAction(tplayer, obj, seatNumber) case Mountable.CanMount(obj: Mountable, _, _) => log.warn(s"MountVehicleMsg: $obj is some kind of mountable object but nothing will happen for ${player.Name}") case Mountable.CanDismount(obj: ImplantTerminalMech, seatNum, _) => - log.info(s"${tplayer.Name} dismounts the implant terminal") - DismountAction(tplayer, obj, seatNum) + ops.DismountAction(tplayer, obj, seatNum) case Mountable.CanDismount(obj: Vehicle, _, mountPoint) if obj.Definition == GlobalDefinitions.orbital_shuttle && obj.MountedIn.nonEmpty => //dismount to hart lobby val pguid = player.GUID - log.info(s"${tplayer.Name} dismounts the orbital shuttle into the lobby") val sguid = obj.GUID val (pos, zang) = Vehicles.dismountShuttle(obj, mountPoint) tplayer.Position = pos @@ -336,8 +210,7 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act //get ready for orbital drop val pguid = player.GUID val events = continent.VehicleEvents - log.info(s"${player.Name} is prepped for dropping") - DismountAction(tplayer, obj, seatNum) + ops.DismountAction(tplayer, obj, seatNum) continent.actor ! ZoneActor.RemoveFromBlockMap(player) //character doesn't need it //DismountAction(...) uses vehicle service, so use that service to coordinate the remainder of the messages events ! VehicleServiceMessage( @@ -363,24 +236,24 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act case Mountable.CanDismount(obj: Vehicle, seatNum, _) if obj.Definition == GlobalDefinitions.droppod => - log.info(s"${tplayer.Name} has landed on ${continent.id}") sessionLogic.general.unaccessContainer(obj) - DismountAction(tplayer, obj, seatNum) + ops.DismountAction(tplayer, obj, seatNum) obj.Actor ! Vehicle.Deconstruct() + case Mountable.CanDismount(obj: Vehicle, seatNum, _) + if tplayer.GUID == player.GUID && + obj.isFlying && + obj.SeatPermissionGroup(seatNum).contains(AccessPermissionGroup.Driver) => + // Deconstruct the vehicle if the driver has bailed out and the vehicle is capable of flight + //todo: implement auto landing procedure if the pilot bails but passengers are still present instead of deconstructing the vehicle + //todo: continue flight path until aircraft crashes if no passengers present (or no passenger seats), then deconstruct. + //todo: kick cargo passengers out. To be added after PR #216 is merged + ops.DismountVehicleAction(tplayer, obj, seatNum) + obj.Actor ! Vehicle.Deconstruct(None) //immediate deconstruction + case Mountable.CanDismount(obj: Vehicle, seatNum, _) if tplayer.GUID == player.GUID => - //disembarking self - log.info(s"${player.Name} dismounts the ${obj.Definition.Name}'s ${ - obj.SeatPermissionGroup(seatNum) match { - case Some(AccessPermissionGroup.Driver) => "driver seat" - case Some(seatType) => s"$seatType seat (#$seatNum)" - case None => "seat" - } - }") - sessionLogic.vehicles.ConditionalDriverVehicleControl(obj) - sessionLogic.general.unaccessContainer(obj) - DismountVehicleAction(tplayer, obj, seatNum) + ops.DismountVehicleAction(tplayer, obj, seatNum) case Mountable.CanDismount(obj: Vehicle, seat_num, _) => continent.VehicleEvents ! VehicleServiceMessage( @@ -389,8 +262,7 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act ) case Mountable.CanDismount(obj: PlanetSideGameObject with PlanetSideGameObject with Mountable with FactionAffinity with InGameHistory, seatNum, _) => - log.info(s"${tplayer.Name} dismounts a ${obj.Definition.asInstanceOf[ObjectDefinition].Name}") - DismountAction(tplayer, obj, seatNum) + ops.DismountAction(tplayer, obj, seatNum) case Mountable.CanDismount(obj: Mountable, _, _) => log.warn(s"DismountVehicleMsg: $obj is some dismountable object but nothing will happen for ${player.Name}") @@ -407,114 +279,52 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act case Mountable.CanNotMount(obj: Mountable, seatNumber) => log.warn(s"MountVehicleMsg: ${tplayer.Name} attempted to mount $obj's seat $seatNumber, but was not allowed") - case Mountable.CanNotDismount(obj, seatNum) => + case Mountable.CanNotDismount(obj: Vehicle, _, BailType.Normal) + if obj.DeploymentState == DriveState.AutoPilot => + if (!player.spectator) { + sendResponse(ChatMsg(ChatMessageType.UNK_224, "@SA_CannotDismountAtThisTime")) + } + + case Mountable.CanNotDismount(obj: Vehicle, _, BailType.Bailed) + if obj.Definition == GlobalDefinitions.droppod => + if (!player.spectator) { + sendResponse(ChatMsg(ChatMessageType.UNK_224, "@CannotBailFromDroppod")) + } + + case Mountable.CanNotDismount(obj: Vehicle, _, BailType.Bailed) + if obj.DeploymentState == DriveState.AutoPilot => + if (!player.spectator) { + sendResponse(ChatMsg(ChatMessageType.UNK_224, "@SA_CannotBailAtThisTime")) + } + + case Mountable.CanNotDismount(obj: Vehicle, _, BailType.Bailed) + if { + continent + .blockMap + .sector(obj) + .buildingList + .exists { + case wg: WarpGate => + Vector3.DistanceSquared(obj.Position, wg.Position) < math.pow(wg.Definition.SOIRadius, 2) + case _ => + false + } + } => + if (!player.spectator) { + sendResponse(ChatMsg(ChatMessageType.UNK_227, "@Vehicle_CannotBailInWarpgateEnvelope")) + } + + case Mountable.CanNotDismount(obj: Vehicle, _, _) + if obj.isMoving(test = 1f) => + ops.handleDismountVehicle(DismountVehicleMsg(player.GUID, BailType.Bailed, wasKickedByDriver=true)) + if (!player.spectator) { + sendResponse(ChatMsg(ChatMessageType.UNK_224, "@TooFastToDismount")) + } + + case Mountable.CanNotDismount(obj, seatNum, _) => log.warn(s"DismountVehicleMsg: ${tplayer.Name} attempted to dismount $obj's mount $seatNum, but was not allowed") } } /* support functions */ - - private def dismountWarning( - bailAs: BailType.Value, - kickedByDriver: Boolean - ) - ( - note: String, - player: Player - ): Unit = { - log.warn(note) - player.VehicleSeated = None - sendResponse(DismountVehicleMsg(player.GUID, bailAs, kickedByDriver)) - } - - private def dismountError( - bailAs: BailType.Value, - kickedByDriver: Boolean - ) - ( - note: String, - player: Player - ): Unit = { - log.error(s"$note; some vehicle might not know that ${player.Name} is no longer sitting in it") - player.VehicleSeated = None - sendResponse(DismountVehicleMsg(player.GUID, bailAs, kickedByDriver)) - } - - /** - * Common activities/procedure when a player mounts a valid object. - * @param tplayer the player - * @param obj the mountable object - * @param seatNum the mount into which the player is mounting - */ - private def MountingAction(tplayer: Player, obj: PlanetSideGameObject with FactionAffinity with InGameHistory, seatNum: Int): Unit = { - val playerGuid: PlanetSideGUID = tplayer.GUID - val objGuid: PlanetSideGUID = obj.GUID - sessionLogic.actionsToCancel() - avatarActor ! AvatarActor.DeactivateActiveImplants - avatarActor ! AvatarActor.SuspendStaminaRegeneration(3.seconds) - sendResponse(ObjectAttachMessage(objGuid, playerGuid, seatNum)) - continent.VehicleEvents ! VehicleServiceMessage( - continent.id, - VehicleAction.MountVehicle(playerGuid, objGuid, seatNum) - ) - } - - /** - * Common activities/procedure when a player dismounts a valid mountable object. - * @param tplayer the player - * @param obj the mountable object - * @param seatNum the mount out of which which the player is disembarking - */ - private def DismountVehicleAction(tplayer: Player, obj: PlanetSideGameObject with FactionAffinity with InGameHistory, seatNum: Int): Unit = { - DismountAction(tplayer, obj, seatNum) - //until vehicles maintain synchronized momentum without a driver - obj match { - case v: Vehicle - if seatNum == 0 && Vector3.MagnitudeSquared(v.Velocity.getOrElse(Vector3.Zero)) > 0f => - sessionLogic.vehicles.serverVehicleControlVelocity.collect { _ => - sessionLogic.vehicles.ServerVehicleOverrideStop(v) - } - v.Velocity = Vector3.Zero - continent.VehicleEvents ! VehicleServiceMessage( - continent.id, - VehicleAction.VehicleState( - tplayer.GUID, - v.GUID, - unk1 = 0, - v.Position, - v.Orientation, - vel = None, - v.Flying, - unk3 = 0, - unk4 = 0, - wheel_direction = 15, - unk5 = false, - unk6 = v.Cloaked - ) - ) - case _ => () - } - } - - /** - * Common activities/procedure when a player dismounts a valid mountable object. - * @param tplayer the player - * @param obj the mountable object - * @param seatNum the mount out of which which the player is disembarking - */ - private def DismountAction(tplayer: Player, obj: PlanetSideGameObject with FactionAffinity with InGameHistory, seatNum: Int): Unit = { - val playerGuid: PlanetSideGUID = tplayer.GUID - tplayer.ContributionFrom(obj) - sessionLogic.keepAliveFunc = sessionLogic.zoning.NormalKeepAlive - val bailType = if (tplayer.BailProtection) { - BailType.Bailed - } else { - BailType.Normal - } - sendResponse(DismountVehicleMsg(playerGuid, bailType, wasKickedByDriver = false)) - continent.VehicleEvents ! VehicleServiceMessage( - continent.id, - VehicleAction.DismountVehicle(playerGuid, bailType, unk2 = false) - ) - } } diff --git a/src/main/scala/net/psforever/actors/session/csr/SpectateAsCustomerServiceRepresentativeMode.scala b/src/main/scala/net/psforever/actors/session/csr/SpectateAsCustomerServiceRepresentativeMode.scala index 665e60cf9..602375a4c 100644 --- a/src/main/scala/net/psforever/actors/session/csr/SpectateAsCustomerServiceRepresentativeMode.scala +++ b/src/main/scala/net/psforever/actors/session/csr/SpectateAsCustomerServiceRepresentativeMode.scala @@ -6,29 +6,26 @@ import net.psforever.actors.zone.ZoneActor import net.psforever.objects.serverobject.ServerObject import net.psforever.objects.{Session, Vehicle} import net.psforever.packet.PlanetSidePacket -import net.psforever.packet.game.ObjectDeleteMessage -import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.chat.SpectatorChannel import net.psforever.services.teamwork.{SquadAction, SquadServiceMessage} -import net.psforever.types.{CapacitorStateType, ChatMessageType, SquadRequestType} +import net.psforever.types.{ChatMessageType, SquadRequestType} // import net.psforever.actors.session.support.{ModeLogic, PlayerMode, SessionData} -import net.psforever.objects.serverobject.terminals.{ProximityUnit, Terminal} -import net.psforever.packet.game.{ChatMsg, UnuseItemMessage} +import net.psforever.packet.game.ChatMsg class SpectatorCSRModeLogic(data: SessionData) extends ModeLogic { val avatarResponse: AvatarHandlerFunctions = AvatarHandlerLogic(data.avatarResponse) val chat: ChatFunctions = ChatLogic(data.chat) - val galaxy: GalaxyHandlerFunctions = GalaxyHandlerLogic(data.galaxyResponseHandlers) + val galaxy: GalaxyHandlerFunctions = net.psforever.actors.session.normal.GalaxyHandlerLogic(data.galaxyResponseHandlers) val general: GeneralFunctions = GeneralLogic(data.general) - val local: LocalHandlerFunctions = LocalHandlerLogic(data.localResponse) + val local: LocalHandlerFunctions = net.psforever.actors.session.normal.LocalHandlerLogic(data.localResponse) val mountResponse: MountHandlerFunctions = MountHandlerLogic(data.mountResponse) val shooting: WeaponAndProjectileFunctions = WeaponAndProjectileLogic(data.shooting) val squad: SquadHandlerFunctions = SquadHandlerLogic(data.squad) val terminals: TerminalHandlerFunctions = TerminalHandlerLogic(data.terminals) val vehicles: VehicleFunctions = VehicleLogic(data.vehicles) - val vehicleResponse: VehicleHandlerFunctions = VehicleHandlerLogic(data.vehicleResponseOperations) + val vehicleResponse: VehicleHandlerFunctions = net.psforever.actors.session.normal.VehicleHandlerLogic(data.vehicleResponseOperations) override def switchTo(session: Session): Unit = { val player = session.player @@ -37,53 +34,17 @@ class SpectatorCSRModeLogic(data: SessionData) extends ModeLogic { val sendResponse: PlanetSidePacket=>Unit = data.sendResponse // continent.actor ! ZoneActor.RemoveFromBlockMap(player) - continent - .GUID(data.terminals.usingMedicalTerminal) - .foreach { case term: Terminal with ProximityUnit => - data.terminals.StopUsingProximityUnit(term) - } - data.general.accessedContainer - .collect { - case veh: Vehicle if player.VehicleSeated.isEmpty || player.VehicleSeated.get != veh.GUID => - sendResponse(UnuseItemMessage(pguid, veh.GUID)) - sendResponse(UnuseItemMessage(pguid, pguid)) - data.general.unaccessContainer(veh) - case container => //just in case - if (player.VehicleSeated.isEmpty || player.VehicleSeated.get != container.GUID) { - // Ensure we don't close the container if the player is seated in it - // If the container is a corpse and gets removed just as this runs it can cause a client disconnect, so we'll check the container has a GUID first. - if (container.HasGUID) { - sendResponse(UnuseItemMessage(pguid, container.GUID)) - } - sendResponse(UnuseItemMessage(pguid, pguid)) - data.general.unaccessContainer(container) - } - } - player.CapacitorState = CapacitorStateType.Idle - player.Capacitor = 0f - player.Inventory.Items - .foreach { entry => sendResponse(ObjectDeleteMessage(entry.GUID, 0)) } - sendResponse(ObjectDeleteMessage(player.avatar.locker.GUID, 0)) - continent.AvatarEvents ! AvatarServiceMessage(continent.id, AvatarAction.ObjectDelete(pguid, pguid)) - player.Holsters() - .collect { case slot if slot.Equipment.nonEmpty => sendResponse(ObjectDeleteMessage(slot.Equipment.get.GUID, 0)) } data.vehicles.GetMountableAndSeat(None, player, continent) match { case (Some(obj: Vehicle), Some(seatNum)) if seatNum == 0 => data.vehicles.ServerVehicleOverrideStop(obj) obj.Actor ! ServerObject.AttributeMsg(10, 3) //faction-accessible driver seat obj.Seat(seatNum).foreach(_.unmount(player)) player.VehicleSeated = None - Some(ObjectCreateMessageParent(obj.GUID, seatNum)) case (Some(obj), Some(seatNum)) => obj.Seat(seatNum).foreach(_.unmount(player)) player.VehicleSeated = None - Some(ObjectCreateMessageParent(obj.GUID, seatNum)) case _ => () } - data.general.dropSpecialSlotItem() - data.general.toggleMaxSpecialState(enable = false) - data.terminals.CancelAllProximityUnits() - data.terminals.lastTerminalOrderFulfillment = true data.squadService ! SquadServiceMessage( player, continent, @@ -94,22 +55,30 @@ class SpectatorCSRModeLogic(data: SessionData) extends ModeLogic { } // player.spectator = true + player.bops = true + continent.AvatarEvents ! AvatarServiceMessage(continent.id, AvatarAction.ObjectDelete(pguid, pguid)) data.chat.JoinChannel(SpectatorChannel) sendResponse(ChatMsg(ChatMessageType.CMT_TOGGLESPECTATORMODE, "on")) - sendResponse(ChatMsg(ChatMessageType.UNK_225, "CSR SPECTATOR MODE")) - data.session = session.copy(player = player) + sendResponse(ChatMsg(ChatMessageType.UNK_225, "CSR SPECTATOR MODE ON")) } override def switchFrom(session: Session): Unit = { val player = data.player + val pguid = player.GUID + val continent = data.continent + val avatarId = player.Definition.ObjectId val sendResponse: PlanetSidePacket => Unit = data.sendResponse // - data.continent.actor ! ZoneActor.AddToBlockMap(player, player.Position) - data.general.stop() data.chat.LeaveChannel(SpectatorChannel) player.spectator = false + player.bops = false + continent.AvatarEvents ! AvatarServiceMessage( + continent.id, + AvatarAction.LoadPlayer(pguid, avatarId, pguid, player.Definition.Packet.ConstructorData(player).get, None) + ) + data.continent.actor ! ZoneActor.AddToBlockMap(player, player.Position) sendResponse(ChatMsg(ChatMessageType.CMT_TOGGLESPECTATORMODE, "off")) - sendResponse(ChatMsg(ChatMessageType.UNK_227, "@SpectatorDisabled")) + sendResponse(ChatMsg(ChatMessageType.UNK_225, "CSR SPECTATOR MODE OFF")) } } diff --git a/src/main/scala/net/psforever/actors/session/csr/SquadHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/csr/SquadHandlerLogic.scala index fae51f9f7..f6be56232 100644 --- a/src/main/scala/net/psforever/actors/session/csr/SquadHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/csr/SquadHandlerLogic.scala @@ -25,34 +25,38 @@ class SquadHandlerLogic(val ops: SessionSquadHandlers, implicit val context: Act private val squadService: ActorRef = ops.squadService - private var waypointCooldown: Long = 0L - /* packet */ def handleSquadDefinitionAction(pkt: SquadDefinitionActionMessage): Unit = { - val SquadDefinitionActionMessage(u1, u2, action) = pkt - squadService ! SquadServiceMessage(player, continent, SquadServiceAction.Definition(u1, u2, action)) + if (!player.spectator) { + val SquadDefinitionActionMessage(u1, u2, action) = pkt + squadService ! SquadServiceMessage(player, continent, SquadServiceAction.Definition(u1, u2, action)) + } } def handleSquadMemberRequest(pkt: SquadMembershipRequest): Unit = { - val SquadMembershipRequest(request_type, char_id, unk3, player_name, unk5) = pkt - squadService ! SquadServiceMessage( - player, - continent, - SquadServiceAction.Membership(request_type, char_id, unk3, player_name, unk5) - ) + if (!player.spectator) { + val SquadMembershipRequest(request_type, char_id, unk3, player_name, unk5) = pkt + squadService ! SquadServiceMessage( + player, + continent, + SquadServiceAction.Membership(request_type, char_id, unk3, player_name, unk5) + ) + } } def handleSquadWaypointRequest(pkt: SquadWaypointRequest): Unit = { - val SquadWaypointRequest(request, _, wtype, unk, info) = pkt - val time = System.currentTimeMillis() - val subtype = wtype.subtype - if(subtype == WaypointSubtype.Squad) { - squadService ! SquadServiceMessage(player, continent, SquadServiceAction.Waypoint(request, wtype, unk, info)) - } else if (subtype == WaypointSubtype.Laze && time - waypointCooldown > 1000) { - //guarding against duplicating laze waypoints - waypointCooldown = time - squadService ! SquadServiceMessage(player, continent, SquadServiceAction.Waypoint(request, wtype, unk, info)) + if (!player.spectator) { + val SquadWaypointRequest(request, _, wtype, unk, info) = pkt + val time = System.currentTimeMillis() + val subtype = wtype.subtype + if (subtype == WaypointSubtype.Squad) { + squadService ! SquadServiceMessage(player, continent, SquadServiceAction.Waypoint(request, wtype, unk, info)) + } else if (subtype == WaypointSubtype.Laze && time - waypointCooldown > 1000) { + //guarding against duplicating laze waypoints + waypointCooldown = time + squadService ! SquadServiceMessage(player, continent, SquadServiceAction.Waypoint(request, wtype, unk, info)) + } } } diff --git a/src/main/scala/net/psforever/actors/session/csr/TerminalHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/csr/TerminalHandlerLogic.scala index ba6843ceb..f5b42446e 100644 --- a/src/main/scala/net/psforever/actors/session/csr/TerminalHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/csr/TerminalHandlerLogic.scala @@ -5,14 +5,16 @@ import akka.actor.{ActorContext, typed} import net.psforever.actors.session.AvatarActor import net.psforever.actors.session.support.{SessionData, SessionTerminalHandlers, TerminalHandlerFunctions} import net.psforever.login.WorldSession.{BuyNewEquipmentPutInInventory, SellEquipmentFromInventory} +import net.psforever.objects.definition.VehicleDefinition import net.psforever.objects.{GlobalDefinitions, Player, Vehicle} import net.psforever.objects.guid.TaskWorkflow import net.psforever.objects.serverobject.pad.VehicleSpawnPad -import net.psforever.objects.serverobject.terminals.{ProximityUnit, Terminal} +import net.psforever.objects.serverobject.terminals.Terminal import net.psforever.objects.sourcing.AmenitySource import net.psforever.objects.vital.TerminalUsedActivity -import net.psforever.packet.game.{FavoritesAction, FavoritesRequest, ItemTransactionMessage, ItemTransactionResultMessage, ProximityTerminalUseMessage, UnuseItemMessage} +import net.psforever.packet.game.{FavoritesRequest, ItemTransactionMessage, ItemTransactionResultMessage, ProximityTerminalUseMessage, UnuseItemMessage} import net.psforever.types.{TransactionType, Vector3} +import net.psforever.util.DefinitionUtil object TerminalHandlerLogic { def apply(ops: SessionTerminalHandlers): TerminalHandlerLogic = { @@ -26,46 +28,23 @@ class TerminalHandlerLogic(val ops: SessionTerminalHandlers, implicit val contex private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor def handleItemTransaction(pkt: ItemTransactionMessage): Unit = { - val ItemTransactionMessage(terminalGuid, transactionType, _, itemName, _, _) = pkt - continent.GUID(terminalGuid) match { - case Some(term: Terminal) if ops.lastTerminalOrderFulfillment => - val msg: String = if (itemName.nonEmpty) s" of $itemName" else "" - log.info(s"${player.Name} is submitting an order - a $transactionType from a ${term.Definition.Name}$msg") - ops.lastTerminalOrderFulfillment = false - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") - term.Actor ! Terminal.Request(player, pkt) - case Some(_: Terminal) => - log.warn(s"Please Wait until your previous order has been fulfilled, ${player.Name}") - case Some(obj) => - log.error(s"ItemTransaction: ${obj.Definition.Name} is not a terminal, ${player.Name}") + val ItemTransactionMessage(_, transactionType, _, itemName, _, _) = pkt + DefinitionUtil.fromString(itemName) match { + case _: VehicleDefinition if transactionType == TransactionType.Buy && player.spectator => + () //can not buy vehicle as csr spectator case _ => - log.error(s"ItemTransaction: entity with guid=${terminalGuid.guid} does not exist, ${player.Name}") + sessionLogic.zoning.CancelZoningProcess() + ops.handleItemTransaction(pkt) } } def handleProximityTerminalUse(pkt: ProximityTerminalUseMessage): Unit = { - val ProximityTerminalUseMessage(_, objectGuid, _) = pkt - continent.GUID(objectGuid) match { - case Some(obj: Terminal with ProximityUnit) => - ops.HandleProximityTerminalUse(obj) - case Some(obj) => - log.warn(s"ProximityTerminalUse: ${obj.Definition.Name} guid=${objectGuid.guid} is not ready to implement proximity effects") - case None => - log.error(s"ProximityTerminalUse: ${player.Name} can not find an object with guid ${objectGuid.guid}") - } + ops.handleProximityTerminalUse(pkt) } def handleFavoritesRequest(pkt: FavoritesRequest): Unit = { - val FavoritesRequest(_, loadoutType, action, line, label) = pkt - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") - action match { - case FavoritesAction.Save => - avatarActor ! AvatarActor.SaveLoadout(player, loadoutType, label, line) - case FavoritesAction.Delete => - avatarActor ! AvatarActor.DeleteLoadout(player, loadoutType, line) - case FavoritesAction.Unknown => - log.warn(s"FavoritesRequest: ${player.Name} requested an unknown favorites action") - } + sessionLogic.zoning.CancelZoningProcess() + ops.handleFavoritesRequest(pkt) } /** @@ -112,54 +91,12 @@ class TerminalHandlerLogic(val ops: SessionTerminalHandlers, implicit val contex ops.lastTerminalOrderFulfillment = true case Terminal.BuyVehicle(vehicle, _, _) - if tplayer.avatar.purchaseCooldown(vehicle.Definition).nonEmpty => + if tplayer.avatar.purchaseCooldown(vehicle.Definition).nonEmpty || player.spectator => sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, success = false)) ops.lastTerminalOrderFulfillment = true case Terminal.BuyVehicle(vehicle, weapons, trunk) => - continent.map.terminalToSpawnPad - .find { case (termid, _) => termid == msg.terminal_guid.guid } - .map { case (a: Int, b: Int) => (continent.GUID(a), continent.GUID(b)) } - .collect { case (Some(term: Terminal), Some(pad: VehicleSpawnPad)) => - avatarActor ! AvatarActor.UpdatePurchaseTime(vehicle.Definition) - vehicle.Faction = tplayer.Faction - vehicle.Position = pad.Position - vehicle.Orientation = pad.Orientation + Vector3.z(pad.Definition.VehicleCreationZOrientOffset) - //default loadout, weapons - val vWeapons = vehicle.Weapons - weapons.foreach { entry => - vWeapons.get(entry.start) match { - case Some(slot) => - entry.obj.Faction = tplayer.Faction - slot.Equipment = None - slot.Equipment = entry.obj - case None => - log.warn( - s"BuyVehicle: ${player.Name} tries to apply default loadout to $vehicle on spawn, but can not find a mounted weapon for ${entry.start}" - ) - } - } - //default loadout, trunk - val vTrunk = vehicle.Trunk - vTrunk.Clear() - trunk.foreach { entry => - entry.obj.Faction = tplayer.Faction - vTrunk.InsertQuickly(entry.start, entry.obj) - } - TaskWorkflow.execute(ops.registerVehicleFromSpawnPad(vehicle, pad, term)) - sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, success = true)) - if (GlobalDefinitions.isBattleFrameVehicle(vehicle.Definition)) { - sendResponse(UnuseItemMessage(player.GUID, msg.terminal_guid)) - } - player.LogActivity(TerminalUsedActivity(AmenitySource(term), msg.transaction_type)) - } - .orElse { - log.error( - s"${tplayer.Name} wanted to spawn a vehicle, but there was no spawn pad associated with terminal ${msg.terminal_guid} to accept it" - ) - sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, success = false)) - None - } + ops.buyVehicle(msg.terminal_guid, msg.transaction_type, vehicle, weapons, trunk) ops.lastTerminalOrderFulfillment = true case Terminal.NoDeal() if msg != null => diff --git a/src/main/scala/net/psforever/actors/session/csr/VehicleHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/csr/VehicleHandlerLogic.scala deleted file mode 100644 index 7bacfbda7..000000000 --- a/src/main/scala/net/psforever/actors/session/csr/VehicleHandlerLogic.scala +++ /dev/null @@ -1,399 +0,0 @@ -// Copyright (c) 2024 PSForever -package net.psforever.actors.session.csr - -import akka.actor.{ActorContext, ActorRef, typed} -import net.psforever.actors.session.AvatarActor -import net.psforever.actors.session.support.{SessionData, SessionVehicleHandlers, VehicleHandlerFunctions} -import net.psforever.objects.{GlobalDefinitions, Player, Tool, Vehicle, Vehicles} -import net.psforever.objects.equipment.{Equipment, JammableMountedWeapons, JammableUnit} -import net.psforever.objects.guid.{GUIDTask, TaskWorkflow} -import net.psforever.objects.serverobject.mount.Mountable -import net.psforever.objects.serverobject.pad.VehicleSpawnPad -import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent -import net.psforever.packet.game.{ChangeAmmoMessage, ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, ChatMsg, ChildObjectStateMessage, DeadState, DeployRequestMessage, DismountVehicleMsg, FrameVehicleStateMessage, GenericObjectActionMessage, HitHint, InventoryStateMessage, ObjectAttachMessage, ObjectCreateDetailedMessage, ObjectCreateMessage, ObjectDeleteMessage, ObjectDetachMessage, PlanetsideAttributeMessage, ReloadMessage, ServerVehicleOverrideMsg, VehicleStateMessage, WeaponDryFireMessage} -import net.psforever.services.Service -import net.psforever.services.vehicle.{VehicleResponse, VehicleServiceResponse} -import net.psforever.types.{BailType, ChatMessageType, PlanetSideGUID, Vector3} - -object VehicleHandlerLogic { - def apply(ops: SessionVehicleHandlers): VehicleHandlerLogic = { - new VehicleHandlerLogic(ops, ops.context) - } -} - -class VehicleHandlerLogic(val ops: SessionVehicleHandlers, implicit val context: ActorContext) extends VehicleHandlerFunctions { - def sessionLogic: SessionData = ops.sessionLogic - - private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor - - private val galaxyService: ActorRef = ops.galaxyService - - /** - * na - * - * @param toChannel na - * @param guid na - * @param reply na - */ - def handle(toChannel: String, guid: PlanetSideGUID, reply: VehicleResponse.Response): Unit = { - val resolvedPlayerGuid = if (player.HasGUID) { - player.GUID - } else { - PlanetSideGUID(-1) - } - val isNotSameTarget = resolvedPlayerGuid != guid - reply match { - case VehicleResponse.VehicleState( - vehicleGuid, - unk1, - pos, - orient, - vel, - unk2, - unk3, - unk4, - wheelDirection, - unk5, - unk6 - ) if isNotSameTarget && player.VehicleSeated.contains(vehicleGuid) => - //player who is also in the vehicle (not driver) - sendResponse(VehicleStateMessage(vehicleGuid, unk1, pos, orient, vel, unk2, unk3, unk4, wheelDirection, unk5, unk6)) - player.Position = pos - player.Orientation = orient - player.Velocity = vel - sessionLogic.updateLocalBlockMap(pos) - - case VehicleResponse.VehicleState( - vehicleGuid, - unk1, - pos, - ang, - vel, - unk2, - unk3, - unk4, - wheelDirection, - unk5, - unk6 - ) if isNotSameTarget => - //player who is watching the vehicle from the outside - sendResponse(VehicleStateMessage(vehicleGuid, unk1, pos, ang, vel, unk2, unk3, unk4, wheelDirection, unk5, unk6)) - - case VehicleResponse.ChildObjectState(objectGuid, pitch, yaw) if isNotSameTarget => - sendResponse(ChildObjectStateMessage(objectGuid, pitch, yaw)) - - case VehicleResponse.FrameVehicleState(vguid, u1, pos, oient, vel, u2, u3, u4, is_crouched, u6, u7, u8, u9, uA) - if isNotSameTarget => - sendResponse(FrameVehicleStateMessage(vguid, u1, pos, oient, vel, u2, u3, u4, is_crouched, u6, u7, u8, u9, uA)) - - case VehicleResponse.ChangeFireState_Start(weaponGuid) if isNotSameTarget => - sendResponse(ChangeFireStateMessage_Start(weaponGuid)) - - case VehicleResponse.ChangeFireState_Stop(weaponGuid) if isNotSameTarget => - sendResponse(ChangeFireStateMessage_Stop(weaponGuid)) - - case VehicleResponse.Reload(itemGuid) if isNotSameTarget => - sendResponse(ReloadMessage(itemGuid, ammo_clip=1, unk1=0)) - - case VehicleResponse.ChangeAmmo(weapon_guid, weapon_slot, previous_guid, ammo_id, ammo_guid, ammo_data) if isNotSameTarget => - sendResponse(ObjectDetachMessage(weapon_guid, previous_guid, Vector3.Zero, 0)) - //TODO? sendResponse(ObjectDeleteMessage(previousAmmoGuid, 0)) - sendResponse( - ObjectCreateMessage( - ammo_id, - ammo_guid, - ObjectCreateMessageParent(weapon_guid, weapon_slot), - ammo_data - ) - ) - sendResponse(ChangeAmmoMessage(weapon_guid, 1)) - - case VehicleResponse.WeaponDryFire(weaponGuid) if isNotSameTarget => - continent.GUID(weaponGuid).collect { - case tool: Tool if tool.Magazine == 0 => - // check that the magazine is still empty before sending WeaponDryFireMessage - // if it has been reloaded since then, other clients will not see it firing - sendResponse(WeaponDryFireMessage(weaponGuid)) - } - - case VehicleResponse.DismountVehicle(bailType, wasKickedByDriver) if isNotSameTarget => - sendResponse(DismountVehicleMsg(guid, bailType, wasKickedByDriver)) - - case VehicleResponse.MountVehicle(vehicleGuid, seat) if isNotSameTarget => - sendResponse(ObjectAttachMessage(vehicleGuid, guid, seat)) - - case VehicleResponse.DeployRequest(objectGuid, state, unk1, unk2, pos) if isNotSameTarget => - sendResponse(DeployRequestMessage(guid, objectGuid, state, unk1, unk2, pos)) - - case VehicleResponse.SendResponse(msg) => - sendResponse(msg) - - case VehicleResponse.AttachToRails(vehicleGuid, padGuid) => - sendResponse(ObjectAttachMessage(padGuid, vehicleGuid, slot=3)) - - case VehicleResponse.ConcealPlayer(playerGuid) => - sendResponse(GenericObjectActionMessage(playerGuid, code=9)) - - case VehicleResponse.DetachFromRails(vehicleGuid, padGuid, padPosition, padOrientationZ) => - val pad = continent.GUID(padGuid).get.asInstanceOf[VehicleSpawnPad].Definition - sendResponse( - ObjectDetachMessage( - padGuid, - vehicleGuid, - padPosition + Vector3.z(pad.VehicleCreationZOffset), - padOrientationZ + pad.VehicleCreationZOrientOffset - ) - ) - - case VehicleResponse.EquipmentInSlot(pkt) if isNotSameTarget => - sendResponse(pkt) - - case VehicleResponse.GenericObjectAction(objectGuid, action) if isNotSameTarget => - sendResponse(GenericObjectActionMessage(objectGuid, action)) - - case VehicleResponse.HitHint(sourceGuid) if player.isAlive => - sendResponse(HitHint(sourceGuid, player.GUID)) - - case VehicleResponse.InventoryState(obj, parentGuid, start, conData) if isNotSameTarget => - //TODO prefer ObjectDetachMessage, but how to force ammo pools to update properly? - val objGuid = obj.GUID - sendResponse(ObjectDeleteMessage(objGuid, unk1=0)) - sendResponse(ObjectCreateDetailedMessage( - obj.Definition.ObjectId, - objGuid, - ObjectCreateMessageParent(parentGuid, start), - conData - )) - - case VehicleResponse.KickPassenger(_, wasKickedByDriver, vehicleGuid) if resolvedPlayerGuid == guid => - //seat number (first field) seems to be correct if passenger is kicked manually by driver - //but always seems to return 4 if user is kicked by mount permissions changing - sendResponse(DismountVehicleMsg(guid, BailType.Kicked, wasKickedByDriver)) - val typeOfRide = continent.GUID(vehicleGuid) match { - case Some(obj: Vehicle) => - sessionLogic.general.unaccessContainer(obj) - s"the ${obj.Definition.Name}'s seat by ${obj.OwnerName.getOrElse("the pilot")}" - case _ => - s"${player.Sex.possessive} ride" - } - log.info(s"${player.Name} has been kicked from $typeOfRide!") - - case VehicleResponse.KickPassenger(_, wasKickedByDriver, _) => - //seat number (first field) seems to be correct if passenger is kicked manually by driver - //but always seems to return 4 if user is kicked by mount permissions changing - sendResponse(DismountVehicleMsg(guid, BailType.Kicked, wasKickedByDriver)) - - case VehicleResponse.InventoryState2(objGuid, parentGuid, value) if isNotSameTarget => - sendResponse(InventoryStateMessage(objGuid, unk=0, parentGuid, value)) - - case VehicleResponse.LoadVehicle(vehicle, vtype, vguid, vdata) if isNotSameTarget => - //this is not be suitable for vehicles with people who are seated in it before it spawns (if that is possible) - sendResponse(ObjectCreateMessage(vtype, vguid, vdata)) - Vehicles.ReloadAccessPermissions(vehicle, player.Name) - - case VehicleResponse.ObjectDelete(itemGuid) if isNotSameTarget => - sendResponse(ObjectDeleteMessage(itemGuid, unk1=0)) - - case VehicleResponse.Ownership(vehicleGuid) if resolvedPlayerGuid == guid => - //Only the player that owns this vehicle needs the ownership packet - avatarActor ! AvatarActor.SetVehicle(Some(vehicleGuid)) - sendResponse(PlanetsideAttributeMessage(resolvedPlayerGuid, attribute_type=21, vehicleGuid)) - - case VehicleResponse.PlanetsideAttribute(vehicleGuid, attributeType, attributeValue) if isNotSameTarget => - sendResponse(PlanetsideAttributeMessage(vehicleGuid, attributeType, attributeValue)) - - case VehicleResponse.ResetSpawnPad(padGuid) => - sendResponse(GenericObjectActionMessage(padGuid, code=23)) - - case VehicleResponse.RevealPlayer(playerGuid) => - sendResponse(GenericObjectActionMessage(playerGuid, code=10)) - - case VehicleResponse.SeatPermissions(vehicleGuid, seatGroup, permission) if isNotSameTarget => - sendResponse(PlanetsideAttributeMessage(vehicleGuid, seatGroup, permission)) - - case VehicleResponse.StowEquipment(vehicleGuid, slot, itemType, itemGuid, itemData) if isNotSameTarget => - //TODO prefer ObjectAttachMessage, but how to force ammo pools to update properly? - sendResponse(ObjectCreateDetailedMessage(itemType, itemGuid, ObjectCreateMessageParent(vehicleGuid, slot), itemData)) - - case VehicleResponse.UnloadVehicle(_, vehicleGuid) => - sendResponse(ObjectDeleteMessage(vehicleGuid, unk1=0)) - - case VehicleResponse.UnstowEquipment(itemGuid) if isNotSameTarget => - //TODO prefer ObjectDetachMessage, but how to force ammo pools to update properly? - sendResponse(ObjectDeleteMessage(itemGuid, unk1=0)) - - case VehicleResponse.UpdateAmsSpawnPoint(list) => - sessionLogic.zoning.spawn.amsSpawnPoints = list.filter(tube => tube.Faction == player.Faction) - sessionLogic.zoning.spawn.DrawCurrentAmsSpawnPoint() - - case VehicleResponse.TransferPassengerChannel(oldChannel, tempChannel, vehicle, vehicleToDelete) if isNotSameTarget => - sessionLogic.zoning.interstellarFerry = Some(vehicle) - sessionLogic.zoning.interstellarFerryTopLevelGUID = Some(vehicleToDelete) - continent.VehicleEvents ! Service.Leave(Some(oldChannel)) //old vehicle-specific channel (was s"${vehicle.Actor}") - galaxyService ! Service.Join(tempChannel) //temporary vehicle-specific channel - log.debug(s"TransferPassengerChannel: ${player.Name} now subscribed to $tempChannel for vehicle gating") - - case VehicleResponse.KickCargo(vehicle, speed, delay) - if player.VehicleSeated.nonEmpty && sessionLogic.zoning.spawn.deadState == DeadState.Alive && speed > 0 => - val strafe = 1 + Vehicles.CargoOrientation(vehicle) - val reverseSpeed = if (strafe > 1) { 0 } else { speed } - //strafe or reverse, not both - sessionLogic.vehicles.ServerVehicleOverrideWithPacket( - vehicle, - ServerVehicleOverrideMsg( - lock_accelerator=true, - lock_wheel=true, - reverse=true, - unk4=false, - lock_vthrust=0, - strafe, - reverseSpeed, - unk8=Some(0) - ) - ) - import scala.concurrent.ExecutionContext.Implicits.global - import scala.concurrent.duration._ - context.system.scheduler.scheduleOnce( - delay milliseconds, - context.self, - VehicleServiceResponse(toChannel, PlanetSideGUID(0), VehicleResponse.KickCargo(vehicle, speed=0, delay)) - ) - - case VehicleResponse.KickCargo(cargo, _, _) - if player.VehicleSeated.nonEmpty && sessionLogic.zoning.spawn.deadState == DeadState.Alive => - sessionLogic.vehicles.TotalDriverVehicleControl(cargo) - - case VehicleResponse.StartPlayerSeatedInVehicle(vehicle, _) - if player.VisibleSlots.contains(player.DrawnSlot) => - player.DrawnSlot = Player.HandsDownSlot - startPlayerSeatedInVehicle(vehicle) - - case VehicleResponse.StartPlayerSeatedInVehicle(vehicle, _) => - startPlayerSeatedInVehicle(vehicle) - - case VehicleResponse.PlayerSeatedInVehicle(vehicle, _) => - Vehicles.ReloadAccessPermissions(vehicle, player.Name) - sessionLogic.vehicles.ServerVehicleOverrideWithPacket( - vehicle, - ServerVehicleOverrideMsg( - lock_accelerator=true, - lock_wheel=true, - reverse=true, - unk4=false, - lock_vthrust=1, - lock_strafe=0, - movement_speed=0, - unk8=Some(0) - ) - ) - sessionLogic.vehicles.serverVehicleControlVelocity = Some(0) - - case VehicleResponse.ServerVehicleOverrideStart(vehicle, _) => - val vdef = vehicle.Definition - sessionLogic.vehicles.ServerVehicleOverrideWithPacket( - vehicle, - ServerVehicleOverrideMsg( - lock_accelerator=true, - lock_wheel=true, - reverse=false, - unk4=false, - lock_vthrust=if (GlobalDefinitions.isFlightVehicle(vdef)) { 1 } else { 0 }, - lock_strafe=0, - movement_speed=vdef.AutoPilotSpeed1, - unk8=Some(0) - ) - ) - - case VehicleResponse.ServerVehicleOverrideEnd(vehicle, _) => - sessionLogic.vehicles.ServerVehicleOverrideStop(vehicle) - - case VehicleResponse.PeriodicReminder(VehicleSpawnPad.Reminders.Blocked, data) => - sendResponse(ChatMsg( - ChatMessageType.CMT_OPEN, - wideContents=true, - recipient="", - s"The vehicle spawn where you placed your order is blocked. ${data.getOrElse("")}", - note=None - )) - - case VehicleResponse.PeriodicReminder(_, data) => - val (isType, flag, msg): (ChatMessageType, Boolean, String) = data match { - case Some(msg: String) if msg.startsWith("@") => (ChatMessageType.UNK_227, false, msg) - case Some(msg: String) => (ChatMessageType.CMT_OPEN, true, msg) - case _ => (ChatMessageType.CMT_OPEN, true, "Your vehicle order has been cancelled.") - } - sendResponse(ChatMsg(isType, flag, recipient="", msg, None)) - - case VehicleResponse.ChangeLoadout(target, oldWeapons, addedWeapons, oldInventory, newInventory) - if player.avatar.vehicle.contains(target) => - //TODO when vehicle weapons can be changed without visual glitches, rewrite this - continent.GUID(target).collect { case vehicle: Vehicle => - import net.psforever.login.WorldSession.boolToInt - //owner: must unregister old equipment, and register and install new equipment - (oldWeapons ++ oldInventory).foreach { - case (obj, eguid) => - sendResponse(ObjectDeleteMessage(eguid, unk1=0)) - TaskWorkflow.execute(GUIDTask.unregisterEquipment(continent.GUID, obj)) - } - sessionLogic.general.applyPurchaseTimersBeforePackingLoadout(player, vehicle, addedWeapons ++ newInventory) - //jammer or unjamm new weapons based on vehicle status - val vehicleJammered = vehicle.Jammed - addedWeapons - .map { _.obj } - .collect { - case jamItem: JammableUnit if jamItem.Jammed != vehicleJammered => - jamItem.Jammed = vehicleJammered - JammableMountedWeapons.JammedWeaponStatus(vehicle.Zone, jamItem, vehicleJammered) - } - changeLoadoutDeleteOldEquipment(vehicle, oldWeapons, oldInventory) - } - - case VehicleResponse.ChangeLoadout(target, oldWeapons, _, oldInventory, _) - if sessionLogic.general.accessedContainer.map(_.GUID).contains(target) => - //TODO when vehicle weapons can be changed without visual glitches, rewrite this - continent.GUID(target).collect { case vehicle: Vehicle => - //external participant: observe changes to equipment - (oldWeapons ++ oldInventory).foreach { case (_, eguid) => sendResponse(ObjectDeleteMessage(eguid, unk1=0)) } - changeLoadoutDeleteOldEquipment(vehicle, oldWeapons, oldInventory) - } - - case VehicleResponse.ChangeLoadout(target, oldWeapons, _, oldInventory, _) => - //TODO when vehicle weapons can be changed without visual glitches, rewrite this - continent.GUID(target).collect { case vehicle: Vehicle => - changeLoadoutDeleteOldEquipment(vehicle, oldWeapons, oldInventory) - } - - case _ => () - } - } - - private def changeLoadoutDeleteOldEquipment( - vehicle: Vehicle, - oldWeapons: Iterable[(Equipment, PlanetSideGUID)], - oldInventory: Iterable[(Equipment, PlanetSideGUID)] - ): Unit = { - vehicle.PassengerInSeat(player) match { - case Some(seatNum) => - //participant: observe changes to equipment - (oldWeapons ++ oldInventory).foreach { - case (_, eguid) => sendResponse(ObjectDeleteMessage(eguid, unk1=0)) - } - sessionLogic.mountResponse.updateWeaponAtSeatPosition(vehicle, seatNum) - case None => - //observer: observe changes to external equipment - oldWeapons.foreach { case (_, eguid) => sendResponse(ObjectDeleteMessage(eguid, unk1=0)) } - } - } - - private def startPlayerSeatedInVehicle(vehicle: Vehicle): Unit = { - val vehicle_guid = vehicle.GUID - sessionLogic.actionsToCancel() - sessionLogic.terminals.CancelAllProximityUnits() - sessionLogic.vehicles.serverVehicleControlVelocity = Some(0) - sendResponse(PlanetsideAttributeMessage(vehicle_guid, attribute_type=22, attribute_value=1L)) //mount points off - sendResponse(PlanetsideAttributeMessage(player.GUID, attribute_type=21, vehicle_guid)) //ownership - vehicle.MountPoints.find { case (_, mp) => mp.seatIndex == 0 }.collect { - case (mountPoint, _) => vehicle.Actor ! Mountable.TryMount(player, mountPoint) - } - } -} diff --git a/src/main/scala/net/psforever/actors/session/csr/VehicleLogic.scala b/src/main/scala/net/psforever/actors/session/csr/VehicleLogic.scala index c9eac4b88..afd433b85 100644 --- a/src/main/scala/net/psforever/actors/session/csr/VehicleLogic.scala +++ b/src/main/scala/net/psforever/actors/session/csr/VehicleLogic.scala @@ -10,7 +10,8 @@ import net.psforever.objects.serverobject.deploy.Deployment import net.psforever.objects.serverobject.mount.Mountable import net.psforever.objects.vehicles.control.BfrFlight import net.psforever.objects.zones.Zone -import net.psforever.packet.game.{ChildObjectStateMessage, DeployRequestMessage, FrameVehicleStateMessage, VehicleStateMessage, VehicleSubStateMessage} +import net.psforever.packet.game.{ChildObjectStateMessage, DeployRequestMessage, FrameVehicleStateMessage, PlanetsideAttributeMessage, VehicleStateMessage, VehicleSubStateMessage} +import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} import net.psforever.types.{DriveState, Vector3} @@ -46,6 +47,7 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex //we're driving the vehicle sessionLogic.persist() sessionLogic.turnCounterFunc(player.GUID) + topOffHealth(obj) sessionLogic.general.fallHeightTracker(pos.z) if (obj.MountedIn.isEmpty) { sessionLogic.updateBlockMap(obj, pos) @@ -129,6 +131,7 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex //we're driving the vehicle sessionLogic.persist() sessionLogic.turnCounterFunc(player.GUID) + topOffHealth(obj) val (position, angle, velocity, notMountedState) = continent.GUID(obj.MountedIn) match { case Some(v: Vehicle) => sessionLogic.updateBlockMap(obj, pos) @@ -217,6 +220,7 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex case _ => sessionLogic.persist() sessionLogic.turnCounterFunc(player.GUID) + topOffHealthOfPlayer() } //the majority of the following check retrieves information to determine if we are in control of the child tools.find { _.GUID == object_guid } match { @@ -275,15 +279,7 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex continent.GUID(vehicle_guid) .collect { case obj: Vehicle => - val vehicle = player.avatar.vehicle - if (!vehicle.contains(vehicle_guid)) { - log.warn(s"DeployRequest: ${player.Name} does not own the would-be-deploying ${obj.Definition.Name}") - } else if (vehicle != player.VehicleSeated) { - log.warn(s"${player.Name} must be mounted as the driver to request a deployment change") - } else { - log.info(s"${player.Name} is requesting a deployment change for ${obj.Definition.Name} - $deploy_state") - continent.Transport ! Zone.Vehicle.TryDeploymentChange(obj, deploy_state) - } + continent.Transport ! Zone.Vehicle.TryDeploymentChange(obj, deploy_state) obj case obj => log.error(s"DeployRequest: ${player.Name} expected a vehicle, but found a ${obj.Definition.Name} instead") @@ -299,31 +295,19 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex /* messages */ def handleCanDeploy(obj: Deployment.DeploymentObject, state: DriveState.Value): Unit = { - if (state == DriveState.Deploying) { - log.trace(s"DeployRequest: $obj transitioning to deploy state") - } else if (state == DriveState.Deployed) { - log.trace(s"DeployRequest: $obj has been Deployed") - } else { + if (!Deployment.CheckForDeployState(state)) { CanNotChangeDeployment(obj, state, "incorrect deploy state") } } def handleCanUndeploy(obj: Deployment.DeploymentObject, state: DriveState.Value): Unit = { - if (state == DriveState.Undeploying) { - log.trace(s"DeployRequest: $obj transitioning to undeploy state") - } else if (state == DriveState.Mobile) { - log.trace(s"DeployRequest: $obj is Mobile") - } else { + if (!Deployment.CheckForUndeployState(state)) { CanNotChangeDeployment(obj, state, "incorrect undeploy state") } } def handleCanNotChangeDeployment(obj: Deployment.DeploymentObject, state: DriveState.Value, reason: String): Unit = { - if (Deployment.CheckForDeployState(state) && !Deployment.AngleCheck(obj)) { - CanNotChangeDeployment(obj, state, reason = "ground too steep") - } else { - CanNotChangeDeployment(obj, state, reason) - } + CanNotChangeDeployment(obj, state, reason) } /* support functions */ @@ -339,17 +323,35 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex state: DriveState.Value, reason: String ): Unit = { - val mobileShift: String = if (obj.DeploymentState != DriveState.Mobile) { + 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") + } + + private def topOffHealthOfPlayer(): Unit = { + //driver below half health, full heal + val maxHealthOfPlayer = player.MaxHealth.toLong + if (player.Health < maxHealthOfPlayer * 0.5f) { + player.Health = maxHealthOfPlayer.toInt + player.LogActivity(player.ClearHistory().head) + sendResponse(PlanetsideAttributeMessage(player.GUID, 0, maxHealthOfPlayer)) + continent.AvatarEvents ! AvatarServiceMessage(continent.id, AvatarAction.PlanetsideAttribute(player.GUID, 0, maxHealthOfPlayer)) + } + } + + private def topOffHealth(vehicle: Vehicle): Unit = { + topOffHealthOfPlayer() + //vehicle below half health, full heal + val maxHealthOfVehicle = vehicle.MaxHealth.toLong + if (vehicle.Health < maxHealthOfVehicle * 0.5f) { + vehicle.Health = maxHealthOfVehicle.toInt + sendResponse(PlanetsideAttributeMessage(vehicle.GUID, 0, maxHealthOfVehicle)) + continent.AvatarEvents ! AvatarServiceMessage(continent.id, AvatarAction.PlanetsideAttribute(vehicle.GUID, 0, maxHealthOfVehicle)) + } } } diff --git a/src/main/scala/net/psforever/actors/session/csr/WeaponAndProjectileLogic.scala b/src/main/scala/net/psforever/actors/session/csr/WeaponAndProjectileLogic.scala index 22cf517e3..a99067d89 100644 --- a/src/main/scala/net/psforever/actors/session/csr/WeaponAndProjectileLogic.scala +++ b/src/main/scala/net/psforever/actors/session/csr/WeaponAndProjectileLogic.scala @@ -1,217 +1,54 @@ // Copyright (c) 2024 PSForever package net.psforever.actors.session.csr -import akka.actor.{ActorContext, typed} -import net.psforever.actors.session.AvatarActor +import akka.actor.ActorContext import net.psforever.actors.session.support.{SessionData, WeaponAndProjectileFunctions, WeaponAndProjectileOperations} -import net.psforever.login.WorldSession.{CountAmmunition, CountGrenades, FindAmmoBoxThatUses, FindEquipmentStock, FindToolThatUses, PutEquipmentInInventoryOrDrop, PutNewEquipmentInInventoryOrDrop, RemoveOldEquipmentFromInventory} -import net.psforever.objects.ballistics.{Projectile, ProjectileQuality} -import net.psforever.objects.definition.ProjectileDefinition -import net.psforever.objects.entity.SimpleWorldEntity -import net.psforever.objects.equipment.{ChargeFireModeDefinition, Equipment, EquipmentSize, FireModeSwitch} -import net.psforever.objects.guid.{GUIDTask, TaskBundle, TaskWorkflow} +import net.psforever.login.WorldSession.{CountGrenades, FindEquipmentStock, FindToolThatUses, RemoveOldEquipmentFromInventory} +import net.psforever.objects.equipment.ChargeFireModeDefinition import net.psforever.objects.inventory.Container -import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} -import net.psforever.objects.serverobject.doors.InteriorDoorPassage -import net.psforever.objects.{AmmoBox, BoomerDeployable, BoomerTrigger, ConstructionItem, Deployables, DummyExplodingEntity, GlobalDefinitions, OwnableByPlayer, PlanetSideGameObject, Player, SpecialEmp, Tool, Tools, Vehicle} -import net.psforever.objects.serverobject.interior.Sidedness -import net.psforever.objects.serverobject.mount.Mountable -import net.psforever.objects.serverobject.structures.Amenity +import net.psforever.objects.{BoomerDeployable, BoomerTrigger, GlobalDefinitions, Player, SpecialEmp, Tool, Tools, Vehicle} import net.psforever.objects.serverobject.turret.{FacilityTurret, VanuSentry} -import net.psforever.objects.serverobject.turret.auto.{AutomatedTurret, AutomatedTurretBehavior} -import net.psforever.objects.sourcing.{PlayerSource, SourceEntry} -import net.psforever.objects.vital.Vitality import net.psforever.objects.vital.base.{DamageResolution, DamageType} -import net.psforever.objects.vital.etc.OicwLilBuddyReason -import net.psforever.objects.vital.interaction.DamageInteraction -import net.psforever.objects.vital.projectile.ProjectileReason import net.psforever.objects.zones.{Zone, ZoneProjectile} -import net.psforever.packet.game.{AIDamage, AvatarGrenadeStateMessage, ChainLashMessage, ChangeAmmoMessage, ChangeFireModeMessage, ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, HitMessage, InventoryStateMessage, LashMessage, LongRangeProjectileInfoMessage, ObjectAttachMessage, ObjectDeleteMessage, ObjectDetachMessage, ProjectileStateMessage, QuantityUpdateMessage, ReloadMessage, SplashHitMessage, UplinkRequest, WeaponDelayFireMessage, WeaponDryFireMessage, WeaponFireMessage, WeaponLazeTargetPositionMessage} +import net.psforever.packet.game.{AIDamage, AvatarGrenadeStateMessage, ChangeAmmoMessage, ChangeFireModeMessage, ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, HitMessage, LashMessage, LongRangeProjectileInfoMessage, ProjectileStateMessage, QuantityUpdateMessage, ReloadMessage, SplashHitMessage, UplinkRequest, WeaponDelayFireMessage, WeaponDryFireMessage, WeaponFireMessage, WeaponLazeTargetPositionMessage} import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} import net.psforever.types.{PlanetSideGUID, Vector3} -import net.psforever.util.Config import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.Future import scala.concurrent.duration._ object WeaponAndProjectileLogic { def apply(ops: WeaponAndProjectileOperations): WeaponAndProjectileLogic = { new WeaponAndProjectileLogic(ops, ops.context) } - - /** - * Does a line segment line intersect with a sphere?
- * This most likely belongs in `Geometry` or `GeometryForm` or somehow in association with the `\objects\geometry\` package. - * @param start first point of the line segment - * @param end second point of the line segment - * @param center center of the sphere - * @param radius radius of the sphere - * @return list of all points of intersection, if any - * @see `Vector3.DistanceSquared` - * @see `Vector3.MagnitudeSquared` - */ - private def quickLineSphereIntersectionPoints( - start: Vector3, - end: Vector3, - center: Vector3, - radius: Float - ): Iterable[Vector3] = { - /* - Algorithm adapted from code found on https://paulbourke.net/geometry/circlesphere/index.html#linesphere, - because I kept messing up proper substitution of the line formula and the circle formula into the quadratic equation. - */ - val Vector3(cx, cy, cz) = center - val Vector3(sx, sy, sz) = start - val vector = end - start - //speed our way through a quadratic equation - val (a, b) = { - val Vector3(dx, dy, dz) = vector - ( - dx * dx + dy * dy + dz * dz, - 2f * (dx * (sx - cx) + dy * (sy - cy) + dz * (sz - cz)) - ) - } - val c = Vector3.MagnitudeSquared(center) + Vector3.MagnitudeSquared(start) - 2f * (cx * sx + cy * sy + cz * sz) - radius * radius - val result = b * b - 4 * a * c - if (result < 0f) { - //negative, no intersection - Seq() - } else if (result < 0.00001f) { - //zero-ish, one intersection point - Seq(start - vector * (b / (2f * a))) - } else { - //positive, two intersection points - val sqrt = math.sqrt(result).toFloat - val endStart = vector / (2f * a) - Seq(start + endStart * (sqrt - b), start + endStart * (b + sqrt) * -1f) - }.filter(p => Vector3.DistanceSquared(start, p) <= a) - } - - /** - * Preparation for explosion damage that utilizes the Scorpion's little buddy sub-projectiles. - * The main difference from "normal" server-side explosion - * is that the owner of the projectile must be clarified explicitly. - * @see `Zone::serverSideDamage` - * @param zone where the explosion is taking place - * (`source` contains the coordinate location) - * @param source a game object that represents the source of the explosion - * @param owner who or what to accredit damage from the explosion to; - * clarifies a normal `SourceEntry(source)` accreditation - */ - private def detonateLittleBuddy( - zone: Zone, - source: PlanetSideGameObject with FactionAffinity with Vitality, - proxy: Projectile, - owner: SourceEntry - )(): Unit = { - Zone.serverSideDamage(zone, source, littleBuddyExplosionDamage(owner, proxy.id, source.Position)) - } - - /** - * Preparation for explosion damage that utilizes the Scorpion's little buddy sub-projectiles. - * The main difference from "normal" server-side explosion - * is that the owner of the projectile must be clarified explicitly. - * The sub-projectiles will be the product of a normal projectile rather than a standard game object - * so a custom `source` entity must wrap around it and fulfill the requirements of the field. - * @see `Zone::explosionDamage` - * @param owner who or what to accredit damage from the explosion to - * @param explosionPosition where the explosion will be positioned in the game world - * @param source a game object that represents the source of the explosion - * @param target a game object that is affected by the explosion - * @return a `DamageInteraction` object - */ - private def littleBuddyExplosionDamage( - owner: SourceEntry, - projectileId: Long, - explosionPosition: Vector3 - ) - ( - source: PlanetSideGameObject with FactionAffinity with Vitality, - target: PlanetSideGameObject with FactionAffinity with Vitality - ): DamageInteraction = { - DamageInteraction(SourceEntry(target), OicwLilBuddyReason(owner, projectileId, target.DamageModel), explosionPosition) - } } class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit val context: ActorContext) extends WeaponAndProjectileFunctions { def sessionLogic: SessionData = ops.sessionLogic - private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor + //private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor /* packets */ def handleWeaponFire(pkt: WeaponFireMessage): Unit = { - val WeaponFireMessage( - _, - weapon_guid, - projectile_guid, - shot_origin, - _, - _, - _, - _/*max_distance,*/, - _, - _/*projectile_type,*/, - thrown_projectile_vel - ) = pkt - HandleWeaponFireOperations(weapon_guid, projectile_guid, shot_origin, thrown_projectile_vel.flatten) + ops.handleWeaponFireOperations(pkt) } def handleWeaponDelayFire(pkt: WeaponDelayFireMessage): Unit = { val WeaponDelayFireMessage(_, _) = pkt - log.info(s"${player.Name} - $pkt") } def handleWeaponDryFire(pkt: WeaponDryFireMessage): Unit = { - val WeaponDryFireMessage(weapon_guid) = pkt - val (containerOpt, tools) = ops.FindContainedWeapon - tools - .find { _.GUID == weapon_guid } - .orElse { continent.GUID(weapon_guid) } - .collect { - case _: Equipment if containerOpt.exists(_.isInstanceOf[Player]) => - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.WeaponDryFire(player.GUID, weapon_guid) - ) - case _: Equipment => - continent.VehicleEvents ! VehicleServiceMessage( - continent.id, - VehicleAction.WeaponDryFire(player.GUID, weapon_guid) - ) - } - .orElse { - log.warn( - s"WeaponDryFire: ${player.Name}'s weapon ${weapon_guid.guid} is either not a weapon or does not exist" - ) - None - } + ops.handleWeaponDryFire(pkt) } - def handleWeaponLazeTargetPosition(pkt: WeaponLazeTargetPositionMessage): Unit = { - 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 (sessionLogic.squad.squad_supplement_id > 0) { - s" for ${player.Sex.possessive} squad (#${sessionLogic.squad.squad_supplement_id -1})" - } else { - " ..." - } - log.info(s"${player.Name} is lazing a position$purpose") - } + def handleWeaponLazeTargetPosition(pkt: WeaponLazeTargetPositionMessage): Unit = { /* laze is handled elsewhere */ } - def handleUplinkRequest(packet: UplinkRequest): Unit = { - sessionLogic.administrativeKick(player) - } + def handleUplinkRequest(packet: UplinkRequest): Unit = { /* CUD not implemented yet */ } - def handleAvatarGrenadeState(pkt: AvatarGrenadeStateMessage): Unit = { - val AvatarGrenadeStateMessage(_, state) = pkt - //TODO I thought I had this working? - log.info(s"${player.Name} has $state ${player.Sex.possessive} grenade") - } + def handleAvatarGrenadeState(pkt: AvatarGrenadeStateMessage): Unit = { /* grenades are handled elsewhere */ } def handleChangeFireStateStart(pkt: ChangeFireStateMessage_Start): Unit = { val ChangeFireStateMessage_Start(item_guid) = pkt @@ -274,98 +111,15 @@ class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit } def handleChangeAmmo(pkt: ChangeAmmoMessage): Unit = { - val ChangeAmmoMessage(item_guid, _) = pkt - 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 { - equipment foreach { - case obj: ConstructionItem => - if (Deployables.performConstructionItemAmmoChange(player.avatar.certifications, obj, obj.AmmoTypeIndex)) { - log.info( - s"${player.Name} switched ${player.Sex.possessive} ${obj.Definition.Name} to construct ${obj.AmmoType} (option #${obj.FireModeIndex})" - ) - sendResponse(ChangeAmmoMessage(obj.GUID, obj.AmmoTypeIndex)) - } - case tool: Tool => - thing match { - case Some(player: Player) => - PerformToolAmmoChange(tool, player, ModifyAmmunition(player)) - case Some(mountable: PlanetSideServerObject with Container) => - PerformToolAmmoChange(tool, mountable, ModifyAmmunitionInMountable(mountable)) - case _ => - log.warn(s"ChangeAmmo: the ${thing.get.Definition.Name} in ${player.Name}'s is not the correct type") - } - case obj => - log.warn(s"ChangeAmmo: the ${obj.Definition.Name} in ${player.Name}'s hands does not contain ammunition") - } - } + ops.handleChangeAmmo(pkt) } def handleChangeFireMode(pkt: ChangeFireModeMessage): Unit = { - val ChangeFireModeMessage(item_guid, _/*fire_mode*/) = pkt - sessionLogic.findEquipment(item_guid) match { - case Some(obj: PlanetSideGameObject with FireModeSwitch[_]) => - val originalModeIndex = obj.FireModeIndex - if (obj match { - case citem: ConstructionItem => - val modeChanged = Deployables.performConstructionItemFireModeChange( - player.avatar.certifications, - citem, - originalModeIndex - ) - modeChanged - case _ => - obj.NextFireMode - obj.FireModeIndex != originalModeIndex - }) { - val modeIndex = obj.FireModeIndex - obj match { - case citem: ConstructionItem => - log.info(s"${player.Name} switched ${player.Sex.possessive} ${obj.Definition.Name} to construct ${citem.AmmoType} (mode #$modeIndex)") - case _ => - log.info(s"${player.Name} changed ${player.Sex.possessive} her ${obj.Definition.Name}'s fire mode to #$modeIndex") - } - sendResponse(ChangeFireModeMessage(item_guid, modeIndex)) - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.ChangeFireMode(player.GUID, item_guid, modeIndex) - ) - } - case Some(_) => - log.warn(s"ChangeFireMode: the object that was found for $item_guid does not possess fire modes") - case None => - log.warn(s"ChangeFireMode: can not find $item_guid") - } + ops.handleChangeFireMode(pkt) } def handleProjectileState(pkt: ProjectileStateMessage): Unit = { - val ProjectileStateMessage(projectile_guid, shot_pos, shot_vel, shot_orient, seq, end, target_guid) = pkt - val index = projectile_guid.guid - Projectile.baseUID - ops.projectiles(index) match { - case Some(projectile) if projectile.HasGUID => - val projectileGlobalUID = projectile.GUID - projectile.Position = shot_pos - projectile.Orientation = shot_orient - projectile.Velocity = shot_vel - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.ProjectileState( - player.GUID, - projectileGlobalUID, - shot_pos, - shot_vel, - shot_orient, - seq, - end, - target_guid - ) - ) - case _ if seq == 0 => - /* missing the first packet in the sequence is permissible */ - case _ => - log.warn(s"ProjectileState: constructed projectile ${projectile_guid.guid} can not be found") - } + ops.handleProjectileState(pkt) } def handleLongRangeProjectileState(pkt: LongRangeProjectileInfoMessage): Unit = { @@ -378,296 +132,100 @@ class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit } def handleDirectHit(pkt: HitMessage): Unit = { - val HitMessage( - _, - projectile_guid, - _, - hit_info, - _, - _, - _ - ) = pkt - //find defined projectile - ops.FindProjectileEntry(projectile_guid) match { - case Some(projectile) => - //find target(s) - (hit_info match { - case Some(hitInfo) => - val hitPos = hitInfo.hit_pos - 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) - .filter { target => - Vector3.DistanceSquared(target.Position, hitPos) <= radius - } - targets.map { target => - CheckForHitPositionDiscrepancy(projectile_guid, hitPos, target) - (target, projectile, hitPos, target.Position) - } - - case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) => - CheckForHitPositionDiscrepancy(projectile_guid, hitPos, target) - List((target, projectile, hitInfo.shot_origin, hitPos)) - - case None => - HandleDamageProxy(projectile, projectile_guid, hitPos) - - case _ => - Nil - } - case None => - Nil - }) - .foreach { - case ( - target: PlanetSideGameObject with FactionAffinity with Vitality, - proj: Projectile, - _: Vector3, - hitPos: Vector3 - ) => - ResolveProjectileInteraction(proj, DamageResolution.Hit, target, hitPos).collect { resprojectile => - addShotsLanded(resprojectile.cause.attribution, shots = 1) - sessionLogic.handleDealingDamage(target, resprojectile) - } - case _ => () - } - case None => - log.warn(s"ResolveProjectile: expected projectile, but ${projectile_guid.guid} not found") + val list = ops.composeDirectDamageInformation(pkt) + if (!player.spectator) { + list.foreach { + case (target, projectile, _, _) => + ops.resolveProjectileInteraction(target, projectile, DamageResolution.Hit, target.Position) + } + //... + if (list.isEmpty) { + val proxyList = ops + .FindProjectileEntry(pkt.projectile_guid) + .map(projectile => ops.resolveDamageProxy(projectile, projectile.GUID, pkt.hit_info.map(_.hit_pos).getOrElse(Vector3.Zero))) + .getOrElse(Nil) + proxyList.collectFirst { + case (_, proxy, _, _) if proxy.tool_def == GlobalDefinitions.oicw => + ops.performLittleBuddyExplosion(proxyList.map(_._2)) + } + } } } def handleSplashHit(pkt: SplashHitMessage): Unit = { - val SplashHitMessage( - _, - projectile_guid, - explosion_pos, - direct_victim_uid, - _, - projectile_vel, - _, - targets - ) = pkt - ops.FindProjectileEntry(projectile_guid) match { - case Some(projectile) => - val profile = projectile.profile - projectile.Velocity = projectile_vel + val list = ops.composeSplashDamageInformation(pkt) + if (list.nonEmpty) { + val projectile = list.head._2 + val explosionPosition = projectile.Position + val projectileGuid = projectile.GUID + val profile = projectile.profile + if (!player.spectator) { val (resolution1, resolution2) = profile.Aggravated match { case Some(_) if profile.ProjectileDamageTypes.contains(DamageType.Aggravated) => (DamageResolution.AggravatedDirect, DamageResolution.AggravatedSplash) case _ => (DamageResolution.Splash, DamageResolution.Splash) } - //direct_victim_uid - sessionLogic.validObject(direct_victim_uid, decorator = "SplashHit/direct_victim") match { - case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) => - CheckForHitPositionDiscrepancy(projectile_guid, explosion_pos, target) - ResolveProjectileInteraction(projectile, resolution1, target, target.Position).collect { resprojectile => - addShotsLanded(resprojectile.cause.attribution, shots = 1) - sessionLogic.handleDealingDamage(target, resprojectile) - } - case _ => () - } - //other victims - targets.foreach(elem => { - sessionLogic.validObject(elem.uid, decorator = "SplashHit/other_victims") match { - case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) => - CheckForHitPositionDiscrepancy(projectile_guid, explosion_pos, target) - ResolveProjectileInteraction(projectile, resolution2, target, explosion_pos).collect { resprojectile => - addShotsLanded(resprojectile.cause.attribution, shots = 1) - sessionLogic.handleDealingDamage(target, resprojectile) - } - case _ => () - } - }) //... - HandleDamageProxy(projectile, projectile_guid, explosion_pos) + val (direct, others) = list.partition { case (_, _, hitPos, targetPos) => hitPos == targetPos } + direct.foreach { + case (target, _, _, _) => + ops.resolveProjectileInteraction(target, projectile, resolution1, target.Position) + } + others.foreach { + case (target, _, _, _) => + ops.resolveProjectileInteraction(target, projectile, resolution2, target.Position) + } + //... + val proxyList = ops.resolveDamageProxy(projectile, projectileGuid, explosionPosition).map(_._2) + if (profile == GlobalDefinitions.oicw_projectile) { + ops.performLittleBuddyExplosion(proxyList) //normal damage radius + } + //... if ( - projectile.profile.HasJammedEffectDuration || - projectile.profile.JammerProjectile || - projectile.profile.SympatheticExplosion + profile.HasJammedEffectDuration || + profile.JammerProjectile || + profile.SympatheticExplosion ) { - //can also substitute 'projectile.profile' for 'SpecialEmp.emp' + //can also substitute 'profile' for 'SpecialEmp.emp' Zone.serverSideDamage( continent, player, SpecialEmp.emp, - SpecialEmp.createEmpInteraction(SpecialEmp.emp, explosion_pos), - SpecialEmp.prepareDistanceCheck(player, explosion_pos, player.Faction), + SpecialEmp.createEmpInteraction(SpecialEmp.emp, explosionPosition), + SpecialEmp.prepareDistanceCheck(player, explosionPosition, player.Faction), SpecialEmp.findAllBoomers(profile.DamageRadius) ) } - if (profile.ExistsOnRemoteClients && projectile.HasGUID) { - //cleanup - continent.Projectile ! ZoneProjectile.Remove(projectile.GUID) - } - case None => () + } + if (profile.ExistsOnRemoteClients && projectile.HasGUID) { + continent.Projectile ! ZoneProjectile.Remove(projectileGuid) + } } } def handleLashHit(pkt: LashMessage): Unit = { - val LashMessage(_, _, victim_guid, projectile_guid, hit_pos, _) = pkt - 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) - sessionLogic.handleDealingDamage(target, resprojectile) - } - case _ => () + val list = ops.composeLashDamageInformation(pkt) + if (!player.spectator) { + list.foreach { + case (target, projectile, _, targetPosition) => + ops.resolveProjectileInteraction(target, projectile, DamageResolution.Lash, targetPosition) + } } } def handleAIDamage(pkt: AIDamage): Unit = { - val AIDamage(targetGuid, attackerGuid, projectileTypeId, _, _) = pkt - (continent.GUID(player.VehicleSeated) match { - case Some(tobj: PlanetSideServerObject with FactionAffinity with Vitality with OwnableByPlayer) - if tobj.GUID == targetGuid && - tobj.OwnerGuid.contains(player.GUID) => - //deployable turrets - Some(tobj) - case Some(tobj: PlanetSideServerObject with FactionAffinity with Vitality with Mountable) - if tobj.GUID == targetGuid && - tobj.Seats.values.flatMap(_.occupants.map(_.GUID)).toSeq.contains(player.GUID) => - //facility turrets, etc. - Some(tobj) - case _ - if player.GUID == targetGuid => - //player avatars - Some(player) - case _ => - None - }).collect { - case target: AutomatedTurret.Target => - sessionLogic.validObject(attackerGuid, decorator = "AIDamage/AutomatedTurret") - .collect { - case turret: AutomatedTurret if turret.Target.isEmpty => - turret.Actor ! AutomatedTurretBehavior.ConfirmShot(target) - Some(target) - - case turret: AutomatedTurret => - turret.Actor ! AutomatedTurretBehavior.ConfirmShot(target) - HandleAIDamage(target, CompileAutomatedTurretDamageData(turret, projectileTypeId)) - Some(target) - } - } - .orElse { - //occasionally, something that is not technically a turret's natural target may be attacked - sessionLogic.validObject(targetGuid, decorator = "AIDamage/Target") - .collect { - case target: PlanetSideServerObject with FactionAffinity with Vitality => - sessionLogic.validObject(attackerGuid, decorator = "AIDamage/Attacker") - .collect { - case turret: AutomatedTurret if turret.Target.nonEmpty => - //the turret must be shooting at something (else) first - HandleAIDamage(target, CompileAutomatedTurretDamageData(turret, projectileTypeId)) - } - Some(target) - } + val list = ops.composeAIDamageInformation(pkt) + if (!player.spectator && ops.confirmAIDamageTarget(pkt, list.map(_._1))) { + list.foreach { + case (target, projectile, _, targetPosition) => + ops.resolveProjectileInteraction(target, projectile, DamageResolution.Hit, targetPosition) } + } } /* support code */ - private def HandleWeaponFireOperations( - weaponGUID: PlanetSideGUID, - projectileGUID: PlanetSideGUID, - shotOrigin: Vector3, - shotVelocity: Option[Vector3] - ): Unit = { - ops.HandleWeaponFireAccountability(weaponGUID, projectileGUID) match { - case (Some(obj), Some(tool)) => - val projectileIndex = projectileGUID.guid - Projectile.baseUID - val projectilePlace = ops.projectiles(projectileIndex) - if ( - projectilePlace match { - case Some(projectile) => - !projectile.isResolved && System.currentTimeMillis() - projectile.fire_time < projectile.profile.Lifespan.toLong - case None => - false - } - ) { - log.debug( - s"WeaponFireMessage: overwriting unresolved projectile ${projectileGUID.guid}, known to ${player.Name}" - ) - } - val (angle, attribution, acceptableDistanceToOwner) = obj match { - case p: Player => - ( - SimpleWorldEntity.validateOrientationEntry( - p.Orientation + Vector3.z(p.FacingYawUpper) - ), - tool.Definition.ObjectId, - 10f + (if (p.Velocity.nonEmpty) { - 5f - } else { - 0f - }) - ) - case v: Vehicle if v.Definition.CanFly => - (tool.Orientation, obj.Definition.ObjectId, 1000f) //TODO this is too simplistic to find proper angle - case _: Vehicle => - (tool.Orientation, obj.Definition.ObjectId, 225f) //TODO this is too simplistic to find proper angle - case _ => - (obj.Orientation, obj.Definition.ObjectId, 300f) - } - val distanceToOwner = Vector3.DistanceSquared(shotOrigin, player.Position) - if (distanceToOwner <= acceptableDistanceToOwner) { - val projectile_info = tool.Projectile - val wguid = weaponGUID.guid - val mountedIn = (continent.turretToWeapon - .find { case (guid, _) => guid == wguid } match { - case Some((_, turretGuid)) => Some(( - turretGuid, - continent.GUID(turretGuid).collect { case o: PlanetSideGameObject with FactionAffinity => SourceEntry(o) } - )) - case _ => None - }) match { - case Some((guid, Some(entity))) => Some((guid, entity)) - case _ => None - } - val projectile = new Projectile( - projectile_info, - tool.Definition, - tool.FireMode, - mountedIn, - PlayerSource(player), - attribution, - shotOrigin, - angle, - shotVelocity - ) - val initialQuality = tool.FireMode match { - case mode: ChargeFireModeDefinition => - ProjectileQuality.Modified( - { - val timeInterval = projectile.fire_time - ops.shootingStart.getOrElse(tool.GUID, System.currentTimeMillis()) - timeInterval.toFloat / mode.Time.toFloat - } - ) - case _ => - ProjectileQuality.Normal - } - val qualityprojectile = projectile.quality(initialQuality) - qualityprojectile.WhichSide = player.WhichSide - ops.projectiles(projectileIndex) = Some(qualityprojectile) - if (projectile_info.ExistsOnRemoteClients) { - log.trace( - s"WeaponFireMessage: ${player.Name}'s ${projectile_info.Name} is a remote projectile" - ) - continent.Projectile ! ZoneProjectile.Add(player.GUID, qualityprojectile) - } - } else { - log.warn( - s"WeaponFireMessage: ${player.Name}'s ${tool.Definition.Name} projectile is too far from owner position at time of discharge ($distanceToOwner > $acceptableDistanceToOwner); suspect" - ) - } - - case _ => () - } - } - /** * After a weapon has finished shooting, determine if it needs to be sorted in a special way. * @param tool a weapon @@ -679,7 +237,6 @@ class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit val ammoType = tool.AmmoType FindEquipmentStock(player, FindToolThatUses(ammoType), 3, CountGrenades).reverse match { //do not search sidearm holsters case Nil => - log.info(s"${player.Name} has no more $ammoType grenades to throw") RemoveOldEquipmentFromInventory(player)(tool) case x :: xs => //this is similar to ReloadMessage @@ -691,11 +248,10 @@ class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit RemoveOldEquipmentFromInventory(player)(x.obj) sumReloadValue } else { - ModifyAmmunition(player)(box.AmmoSlot.Box, 3 - tailReloadValue) + ops.modifyAmmunition(player)(box.AmmoSlot.Box, 3 - tailReloadValue) 3 } - log.info(s"${player.Name} found $actualReloadValue more $ammoType grenades to throw") - ModifyAmmunition(player)( + ops.modifyAmmunition(player)( tool.AmmoSlot.Box, -actualReloadValue ) //grenade item already in holster (negative because empty) @@ -706,354 +262,6 @@ class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit } } - /** - * Given an object that contains a box of amunition in its `Inventory` at a certain location, - * change the amount of ammunition within that box. - * @param obj the `Container` - * @param box an `AmmoBox` to modify - * @param reloadValue the value to modify the `AmmoBox`; - * subtracted from the current `Capacity` of `Box` - */ - private def ModifyAmmunition(obj: PlanetSideGameObject with Container)(box: AmmoBox, reloadValue: Int): Unit = { - val capacity = box.Capacity - reloadValue - box.Capacity = capacity - sendResponse(InventoryStateMessage(box.GUID, obj.GUID, capacity)) - } - - /** - * Given a vehicle that contains a box of ammunition in its `Trunk` at a certain location, - * change the amount of ammunition within that box. - * @param obj the `Container` - * @param box an `AmmoBox` to modify - * @param reloadValue the value to modify the `AmmoBox`; - * subtracted from the current `Capacity` of `Box` - */ - private def ModifyAmmunitionInMountable(obj: PlanetSideServerObject with Container)(box: AmmoBox, reloadValue: Int): Unit = { - ModifyAmmunition(obj)(box, reloadValue) - obj.Find(box).collect { index => - continent.VehicleEvents ! VehicleServiceMessage( - s"${obj.Actor}", - VehicleAction.InventoryState( - player.GUID, - box, - obj.GUID, - index, - box.Definition.Packet.DetailedConstructorData(box).get - ) - ) - } - } - - /** - * na - * @param tool na - * @param obj na - */ - private def PerformToolAmmoChange( - tool: Tool, - obj: PlanetSideServerObject with Container, - modifyFunc: (AmmoBox, Int) => Unit - ): Unit = { - val originalAmmoType = tool.AmmoType - do { - val requestedAmmoType = tool.NextAmmoType - val fullMagazine = tool.MaxMagazine - if (requestedAmmoType != tool.AmmoSlot.Box.AmmoType) { - FindEquipmentStock(obj, FindAmmoBoxThatUses(requestedAmmoType), fullMagazine, CountAmmunition).reverse match { - case Nil => () - case x :: xs => - val stowNewFunc: Equipment => TaskBundle = PutNewEquipmentInInventoryOrDrop(obj) - val stowFunc: Equipment => Future[Any] = PutEquipmentInInventoryOrDrop(obj) - - xs.foreach(item => { - obj.Inventory -= item.start - sendResponse(ObjectDeleteMessage(item.obj.GUID, 0)) - TaskWorkflow.execute(GUIDTask.unregisterObject(continent.GUID, item.obj)) - }) - - //box will be the replacement ammo; give it the discovered magazine and load it into the weapon - val box = x.obj.asInstanceOf[AmmoBox] - //previousBox is the current magazine in tool; it will be removed from the weapon - val previousBox = tool.AmmoSlot.Box - val originalBoxCapacity = box.Capacity - val tailReloadValue: Int = if (xs.isEmpty) { - 0 - } else { - xs.map(_.obj.asInstanceOf[AmmoBox].Capacity).sum - } - val sumReloadValue: Int = originalBoxCapacity + tailReloadValue - val ammoSlotIndex = tool.FireMode.AmmoSlotIndex - val box_guid = box.GUID - val tool_guid = tool.GUID - obj.Inventory -= x.start //remove replacement ammo from inventory - tool.AmmoSlots(ammoSlotIndex).Box = box //put replacement ammo in tool - sendResponse(ObjectDetachMessage(tool_guid, previousBox.GUID, Vector3.Zero, 0f)) - sendResponse(ObjectDetachMessage(obj.GUID, box_guid, Vector3.Zero, 0f)) - sendResponse(ObjectAttachMessage(tool_guid, box_guid, ammoSlotIndex)) - - //announce swapped ammunition box in weapon - val previous_box_guid = previousBox.GUID - val boxDef = box.Definition - sendResponse(ChangeAmmoMessage(tool_guid, box.Capacity)) - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.ChangeAmmo( - player.GUID, - tool_guid, - ammoSlotIndex, - previous_box_guid, - boxDef.ObjectId, - box.GUID, - boxDef.Packet.ConstructorData(box).get - ) - ) - - //handle inventory contents - box.Capacity = if (sumReloadValue <= fullMagazine) { - sumReloadValue - } else { - val splitReloadAmmo: Int = sumReloadValue - fullMagazine - log.trace( - s"PerformToolAmmoChange: ${player.Name} takes ${originalBoxCapacity - splitReloadAmmo} from a box of $originalBoxCapacity $requestedAmmoType ammo" - ) - val boxForInventory = AmmoBox(box.Definition, splitReloadAmmo) - TaskWorkflow.execute(stowNewFunc(boxForInventory)) - fullMagazine - } - sendResponse( - InventoryStateMessage(box.GUID, tool.GUID, box.Capacity) - ) //should work for both players and vehicles - log.info(s"${player.Name} loads ${box.Capacity} $requestedAmmoType into the ${tool.Definition.Name}") - if (previousBox.Capacity > 0) { - //divide capacity across other existing and not full boxes of that ammo type - var capacity = previousBox.Capacity - val iter = obj.Inventory.Items - .filter(entry => { - entry.obj match { - case item: AmmoBox => - item.AmmoType == originalAmmoType && item.FullCapacity != item.Capacity - case _ => - false - } - }) - .sortBy(_.start) - .iterator - while (capacity > 0 && iter.hasNext) { - val entry = iter.next() - val item: AmmoBox = entry.obj.asInstanceOf[AmmoBox] - val ammoAllocated = math.min(item.FullCapacity - item.Capacity, capacity) - log.info(s"${player.Name} put $ammoAllocated back into a box of ${item.Capacity} $originalAmmoType") - capacity -= ammoAllocated - modifyFunc(item, -ammoAllocated) - } - previousBox.Capacity = capacity - } - - if (previousBox.Capacity > 0) { - //split previousBox into AmmoBox objects of appropriate max capacity, e.g., 100 9mm -> 2 x 50 9mm - obj.Inventory.Fit(previousBox) match { - case Some(_) => - stowFunc(previousBox) - case None => - sessionLogic.general.normalItemDrop(player, continent)(previousBox) - } - AmmoBox.Split(previousBox) match { - case Nil | List(_) => () //done (the former case is technically not possible) - case _ :: toUpdate => - modifyFunc(previousBox, 0) //update to changed capacity value - toUpdate.foreach(box => { TaskWorkflow.execute(stowNewFunc(box)) }) - } - } else { - TaskWorkflow.execute(GUIDTask.unregisterObject(continent.GUID, previousBox)) - } - } - } - } while (tool.AmmoType != originalAmmoType && tool.AmmoType != tool.AmmoSlot.Box.AmmoType) - } - - private def CheckForHitPositionDiscrepancy( - projectile_guid: PlanetSideGUID, - hitPos: Vector3, - target: PlanetSideGameObject with FactionAffinity with Vitality - ): Unit = { - val hitPositionDiscrepancy = Vector3.DistanceSquared(hitPos, target.Position) - if (hitPositionDiscrepancy > Config.app.antiCheat.hitPositionDiscrepancyThreshold) { - // If the target position on the server does not match the position where the projectile landed within reason there may be foul play - log.warn( - s"${player.Name}'s shot #${projectile_guid.guid} has hit discrepancy with target. Target: ${target.Position}, Reported: $hitPos, Distance: $hitPositionDiscrepancy / ${math.sqrt(hitPositionDiscrepancy).toFloat}; suspect" - ) - } - } - - /** - * Find a projectile with the given globally unique identifier and mark it as a resolved shot. - * A `Resolved` shot has either encountered an obstacle or is being cleaned up for not finding an obstacle. - * @param projectile_guid the projectile GUID - * @param resolution the resolution status to promote the projectile - * @return the projectile - */ - private def ResolveProjectileInteraction( - projectile_guid: PlanetSideGUID, - resolution: DamageResolution.Value, - target: PlanetSideGameObject with FactionAffinity with Vitality, - pos: Vector3 - ): Option[DamageInteraction] = { - ops.FindProjectileEntry(projectile_guid) match { - case Some(projectile) => - ResolveProjectileInteraction(projectile, resolution, target, pos) - case None => - log.trace(s"ResolveProjectile: ${player.Name} expected projectile, but ${projectile_guid.guid} not found") - None - } - } - - /** - * na - * @param projectile the projectile object - * @param resolution the resolution status to promote the projectile - * @return a copy of the projectile - */ - private def ResolveProjectileInteraction( - projectile: Projectile, - resolution: DamageResolution.Value, - target: PlanetSideGameObject with FactionAffinity with Vitality, - pos: Vector3 - ): Option[DamageInteraction] = { - if (projectile.isMiss) { - log.warn("expected projectile was already counted as a missed shot; can not resolve any further") - None - } else { - val outProjectile = ProjectileQuality.modifiers(projectile, resolution, target, pos, Some(player)) - if (projectile.tool_def.Size == EquipmentSize.Melee && outProjectile.quality == ProjectileQuality.Modified(25)) { - avatarActor ! AvatarActor.ConsumeStamina(10) - } - Some(DamageInteraction(SourceEntry(target), ProjectileReason(resolution, outProjectile, target.DamageModel), pos)) - } - } - - /** - * Take a projectile that was introduced into the game world and - * determine if it generates a secondary damage projectile or - * an method of damage causation that requires additional management. - * @param projectile the projectile - * @param pguid the client-local projectile identifier - * @param hitPos the game world position where the projectile is being recorded - * @return a for all affected targets, a combination of projectiles, projectile location, and the target's location; - * nothing if no targets were affected - */ - private def HandleDamageProxy( - projectile: Projectile, - pguid: PlanetSideGUID, - hitPos: Vector3 - ): List[(PlanetSideGameObject with FactionAffinity with Vitality, Projectile, Vector3, Vector3)] = { - GlobalDefinitions.getDamageProxy(projectile, hitPos) match { - case Nil => - Nil - case list if list.isEmpty => - Nil - case list => - HandleDamageProxySetupLittleBuddy(list, hitPos) - UpdateProjectileSidednessAfterHit(projectile, hitPos) - val projectileSide = projectile.WhichSide - list.flatMap { proxy => - if (proxy.profile.ExistsOnRemoteClients) { - proxy.Position = hitPos - proxy.WhichSide = projectileSide - continent.Projectile ! ZoneProjectile.Add(player.GUID, proxy) - Nil - } else if (proxy.tool_def == GlobalDefinitions.maelstrom) { - //server-side maelstrom grenade target selection - val radius = proxy.profile.LashRadius * proxy.profile.LashRadius - val targets = Zone.findAllTargets(continent, hitPos, proxy.profile.LashRadius, { _.livePlayerList }) - .filter { target => - Vector3.DistanceSquared(target.Position, hitPos) <= radius - } - //chainlash is separated from the actual damage application for convenience - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.SendResponse( - PlanetSideGUID(0), - ChainLashMessage( - hitPos, - projectile.profile.ObjectId, - targets.map { _.GUID } - ) - ) - ) - targets.map { target => - CheckForHitPositionDiscrepancy(pguid, hitPos, target) - (target, proxy, hitPos, target.Position) - } - } else { - Nil - } - } - } - } - - private def HandleDamageProxySetupLittleBuddy(listOfProjectiles: List[Projectile], detonationPosition: Vector3): Boolean = { - val listOfLittleBuddies: List[Projectile] = listOfProjectiles.filter { _.tool_def == GlobalDefinitions.oicw } - val size: Int = listOfLittleBuddies.size - if (size > 0) { - val desiredDownwardsProjectiles: Int = 2 - val firstHalf: Int = math.min(size, desiredDownwardsProjectiles) //number that fly straight down - val secondHalf: Int = math.max(size - firstHalf, 0) //number that are flared out - val z: Float = player.Orientation.z //player's standing direction - val north: Vector3 = Vector3(0,1,0) //map North - val speed: Float = 144f //speed (packet discovered) - val dist: Float = 25 //distance (client defined) - val downwardsAngle: Float = -85f - val flaredAngle: Float = -70f - //angle of separation for downwards, degrees from vertical for flared out - val (smallStep, smallAngle): (Float, Float) = if (firstHalf > 1) { - (360f / firstHalf, downwardsAngle) - } else { - (0f, 0f) - } - val (largeStep, largeAngle): (Float, Float) = if (secondHalf > 1) { - (360f / secondHalf, flaredAngle) - } else { - (0f, 0f) - } - val smallRotOffset: Float = z + 90f - val largeRotOffset: Float = z + math.random().toFloat * 45f - val verticalCorrection = Vector3.z(dist - dist * math.sin(math.toRadians(90 - smallAngle + largeAngle)).toFloat) - //downwards projectiles - var i: Int = 0 - listOfLittleBuddies.take(firstHalf).foreach { proxy => - val facing = (smallRotOffset + smallStep * i.toFloat) % 360 - val dir = north.Rx(smallAngle).Rz(facing) - proxy.Position = detonationPosition + dir.xy + verticalCorrection - proxy.Velocity = dir * speed - proxy.Orientation = Vector3(0, (360f + smallAngle) % 360, facing) - HandleDamageProxyLittleBuddyExplosion(proxy, dir, dist) - i += 1 - } - //flared out projectiles - i = 0 - listOfLittleBuddies.drop(firstHalf).foreach { proxy => - val facing = (largeRotOffset + largeStep * i.toFloat) % 360 - val dir = north.Rx(largeAngle).Rz(facing) - proxy.Position = detonationPosition + dir - proxy.Velocity = dir * speed - proxy.Orientation = Vector3(0, (360f + largeAngle) % 360, facing) - HandleDamageProxyLittleBuddyExplosion(proxy, dir, dist) - i += 1 - } - true - } else { - false - } - } - - private def HandleDamageProxyLittleBuddyExplosion(proxy: Projectile, orientation: Vector3, distance: Float): Unit = { - //explosion - val obj = new DummyExplodingEntity(proxy, proxy.owner.Faction) - obj.Position = obj.Position + orientation * distance - val explosionFunc: ()=>Unit = WeaponAndProjectileLogic.detonateLittleBuddy(continent, obj, proxy, proxy.owner) - context.system.scheduler.scheduleOnce(500.milliseconds) { explosionFunc() } - } - /* used by ChangeFireStateMessage_Start handling */ @@ -1078,10 +286,12 @@ class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit } private def fireStateStartPlayerMessages(itemGuid: PlanetSideGUID): Unit = { - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.ChangeFireState_Start(player.GUID, itemGuid) - ) + if (!player.spectator) { + continent.AvatarEvents ! AvatarServiceMessage( + continent.id, + AvatarAction.ChangeFireState_Start(player.GUID, itemGuid) + ) + } } private def fireStateStartMountedMessages(itemGuid: PlanetSideGUID): Unit = { @@ -1103,7 +313,7 @@ class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit log.warn( s"ChangeFireState_Start: ${player.Name}'s ${tool.Definition.Name} magazine was empty before trying to shoot" ) - ops.EmptyMagazine(itemGuid, tool) + ops.emptyMagazine(itemGuid, tool) } private def fireStateStartWhenPlayer(tool: Tool, itemGuid: PlanetSideGUID): Unit = { @@ -1165,10 +375,12 @@ class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit used by ReloadMessage handling */ private def reloadPlayerMessages(itemGuid: PlanetSideGUID): Unit = { - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.Reload(player.GUID, itemGuid) - ) + if (!player.spectator) { + continent.AvatarEvents ! AvatarServiceMessage( + continent.id, + AvatarAction.Reload(player.GUID, itemGuid) + ) + } } private def reloadVehicleMessages(itemGuid: PlanetSideGUID): Unit = { @@ -1178,70 +390,19 @@ class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit ) } - private def handleReloadProcedure( - itemGuid: PlanetSideGUID, - obj: PlanetSideGameObject with Container, - tools: Set[Tool], - unk1: Int, - deleteFunc: Equipment => Future[Any], - modifyFunc: (AmmoBox, Int) => Unit, - messageFunc: PlanetSideGUID => Unit - ): Unit = { - tools - .filter { _.GUID == itemGuid } - .foreach { tool => - val currentMagazine : Int = tool.Magazine - val magazineSize : Int = tool.MaxMagazine - val reloadValue : Int = magazineSize - currentMagazine - if (magazineSize > 0 && reloadValue > 0) { - FindEquipmentStock(obj, FindAmmoBoxThatUses(tool.AmmoType), reloadValue, CountAmmunition).reverse match { - case Nil => () - case x :: xs => - xs.foreach { item => deleteFunc(item.obj) } - val box = x.obj.asInstanceOf[AmmoBox] - val tailReloadValue : Int = if (xs.isEmpty) { - 0 - } - else { - xs.map(_.obj.asInstanceOf[AmmoBox].Capacity).sum - } - val sumReloadValue : Int = box.Capacity + tailReloadValue - val actualReloadValue = if (sumReloadValue <= reloadValue) { - deleteFunc(box) - sumReloadValue - } - else { - modifyFunc(box, reloadValue - tailReloadValue) - reloadValue - } - val finalReloadValue = actualReloadValue + currentMagazine - log.info( - s"${player.Name} successfully reloaded $reloadValue ${tool.AmmoType} into ${tool.Definition.Name}" - ) - tool.Magazine = finalReloadValue - sendResponse(ReloadMessage(itemGuid, finalReloadValue, unk1)) - messageFunc(itemGuid) - } - } else { - //the weapon can not reload due to full magazine; the UI for the magazine is obvious bugged, so fix it - sendResponse(QuantityUpdateMessage(tool.AmmoSlot.Box.GUID, magazineSize)) - } - } - } - private def handleReloadWhenPlayer( itemGuid: PlanetSideGUID, obj: Player, tools: Set[Tool], unk1: Int ): Unit = { - handleReloadProcedure( + ops.handleReloadProcedure( itemGuid, obj, tools, unk1, RemoveOldEquipmentFromInventory(obj)(_), - ModifyAmmunition(obj)(_, _), + ops.modifyAmmunition(obj)(_, _), reloadPlayerMessages ) } @@ -1252,128 +413,14 @@ class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit tools: Set[Tool], unk1: Int ): Unit = { - handleReloadProcedure( + ops.handleReloadProcedure( itemGuid, obj, tools, unk1, RemoveOldEquipmentFromInventory(obj)(_), - ModifyAmmunitionInMountable(obj)(_, _), + ops.modifyAmmunitionInMountable(obj)(_, _), reloadVehicleMessages ) } - - //noinspection SameParameterValue - private def addShotsLanded(weaponId: Int, shots: Int): Unit = { - ops.addShotsToMap(ops.shotsLanded, weaponId, shots) - } - - private def CompileAutomatedTurretDamageData( - turret: AutomatedTurret, - projectileTypeId: Long - ): Option[(AutomatedTurret, Tool, SourceEntry, ProjectileDefinition)] = { - turret match { - case tOwner: OwnableByPlayer => - CompileAutomatedTurretDamageData( - turret, - CompileAutomatedTurretOwnableBlame(tOwner), - projectileTypeId - ) - case tAmenity: Amenity => - CompileAutomatedTurretDamageData( - turret, - CompileAutomatedTurretAmenityBlame(tAmenity), - projectileTypeId - ) - case _ => - None - } - } - - private def CompileAutomatedTurretOwnableBlame(turret: AutomatedTurret with OwnableByPlayer): SourceEntry = { - Deployables.AssignBlameTo(continent, turret.OwnerName, SourceEntry(turret)) - } - - private def CompileAutomatedTurretAmenityBlame(turret: AutomatedTurret with Amenity): SourceEntry = { - turret - .Seats - .values - .flatMap(_.occupant) - .collectFirst(SourceEntry(_)) - .getOrElse(SourceEntry(turret.Owner)) - } - - private def CompileAutomatedTurretDamageData( - turret: AutomatedTurret, - blame: SourceEntry, - projectileTypeId: Long - ): Option[(AutomatedTurret, Tool, SourceEntry, ProjectileDefinition)] = { - turret.Weapons - .values - .flatMap { _.Equipment } - .collect { - case weapon: Tool => (turret, weapon, blame, weapon.Projectile) - } - .find { case (_, _, _, p) => p.ObjectId == projectileTypeId } - } - - private def HandleAIDamage( - target: PlanetSideServerObject with FactionAffinity with Vitality, - results: Option[(AutomatedTurret, Tool, SourceEntry, ProjectileDefinition)] - ): Unit = { - results.collect { - case (obj, tool, owner, projectileInfo) => - val angle = Vector3.Unit(target.Position - obj.Position) - val proj = new Projectile( - projectileInfo, - tool.Definition, - tool.FireMode, - None, - owner, - obj.Definition.ObjectId, - obj.Position + Vector3.z(value = 1f), - angle, - Some(angle * projectileInfo.FinalVelocity) - ) - val hitPos = target.Position + Vector3.z(value = 1f) - ResolveProjectileInteraction(proj, DamageResolution.Hit, target, hitPos).collect { resprojectile => - addShotsLanded(resprojectile.cause.attribution, shots = 1) - sessionLogic.handleDealingDamage(target, resprojectile) - } - } - } - - private def UpdateProjectileSidednessAfterHit(projectile: Projectile, hitPosition: Vector3): Unit = { - val origin = projectile.Position - val distance = Vector3.Magnitude(hitPosition - origin) - continent.blockMap - .sector(hitPosition, distance) - .environmentList - .collect { case o: InteriorDoorPassage => - val door = o.door - val intersectTest = WeaponAndProjectileLogic.quickLineSphereIntersectionPoints( - origin, - hitPosition, - door.Position, - door.Definition.UseRadius + 0.1f - ) - (door, intersectTest) - } - .collect { case (door, intersectionTest) if intersectionTest.nonEmpty => - (door, Vector3.Magnitude(hitPosition - door.Position), intersectionTest) - } - .minByOption { case (_, dist, _) => dist } - .foreach { case (door, _, intersects) => - val strictly = if (Vector3.DotProduct(Vector3.Unit(hitPosition - door.Position), door.Outwards) > 0f) { - Sidedness.OutsideOf - } else { - Sidedness.InsideOf - } - projectile.WhichSide = if (intersects.size == 1) { - Sidedness.InBetweenSides(door, strictly) - } else { - strictly - } - } - } } diff --git a/src/main/scala/net/psforever/actors/session/normal/ChatLogic.scala b/src/main/scala/net/psforever/actors/session/normal/ChatLogic.scala index b4fb47ed2..4d74b4e08 100644 --- a/src/main/scala/net/psforever/actors/session/normal/ChatLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/ChatLogic.scala @@ -2,9 +2,11 @@ package net.psforever.actors.session.normal import akka.actor.ActorContext +import net.psforever.actors.session.SessionActor import net.psforever.actors.session.spectator.SpectatorMode import net.psforever.actors.session.support.{ChatFunctions, ChatOperations, SessionData} import net.psforever.objects.Session +import net.psforever.objects.avatar.ModePermissions import net.psforever.packet.game.{ChatMsg, SetChatFilterMessage} import net.psforever.services.chat.DefaultChannel import net.psforever.types.ChatMessageType @@ -28,17 +30,16 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext case (_, _, contents) if contents.startsWith("!") && customCommandMessages(message, session) => () - case (CMT_ANONYMOUS, _, _) => - // ? + case (CMT_ANONYMOUS, _, _) => () case (CMT_TOGGLE_GM, _, contents) => - ops.customCommandModerator(contents) + customCommandModerator(contents) case (CMT_CULLWATERMARK, _, contents) => ops.commandWatermark(contents) case (CMT_TOGGLESPECTATORMODE, _, contents) if isAlive => - ops.commandToggleSpectatorMode(contents) + commandToggleSpectatorMode(contents) case (CMT_RECALL, _, _) => ops.commandRecall(session) @@ -135,7 +136,6 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext case "grenade" => ops.customCommandGrenade(session, log) case "macro" => ops.customCommandMacro(session, params) case "progress" => ops.customCommandProgress(session, params) - case "csr" | "gm" | "op" => ops.customCommandModerator(params.headOption.getOrElse("")) case _ => // command was not handled sendResponse( @@ -147,10 +147,30 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext message.note ) ) - false + true } } else { false } } + + def commandToggleSpectatorMode(contents: String): Unit = { + val currentSpectatorActivation = (if (avatar != null) avatar.permissions else ModePermissions()).canSpectate + contents.toLowerCase() match { + case "on" | "o" | "" if currentSpectatorActivation && !player.spectator => + context.self ! SessionActor.SetMode(ops.SpectatorMode) + case _ => () + } + } + + def customCommandModerator(contents: String): Boolean = { + val currentCsrActivation = (if (avatar != null) avatar.permissions else ModePermissions()).canGM + contents.toLowerCase() match { + case "on" | "o" | "" if currentCsrActivation => + import net.psforever.actors.session.csr.CustomerServiceRepresentativeMode + context.self ! SessionActor.SetMode(CustomerServiceRepresentativeMode) + case _ => () + } + true + } } diff --git a/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala b/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala index a19a401a0..c89179572 100644 --- a/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala @@ -5,17 +5,16 @@ import akka.actor.typed.scaladsl.adapter._ import akka.actor.{ActorContext, ActorRef, typed} import net.psforever.actors.session.{AvatarActor, SessionActor} import net.psforever.actors.session.support.{GeneralFunctions, GeneralOperations, SessionData} -import net.psforever.login.WorldSession.{CallBackForTask, ContainableMoveItem, DropEquipmentFromInventory, PickUpEquipmentFromGround, RemoveOldEquipmentFromInventory} -import net.psforever.objects.{Account, BoomerDeployable, BoomerTrigger, ConstructionItem, Deployables, GlobalDefinitions, Kit, LivePlayerList, PlanetSideGameObject, Player, SensorDeployable, ShieldGeneratorDeployable, SpecialEmp, TelepadDeployable, Tool, TrapDeployable, TurretDeployable, Vehicle} +import net.psforever.login.WorldSession.{ContainableMoveItem, DropEquipmentFromInventory, PickUpEquipmentFromGround, RemoveOldEquipmentFromInventory} +import net.psforever.objects.{Account, BoomerDeployable, BoomerTrigger, ConstructionItem, GlobalDefinitions, LivePlayerList, Player, SensorDeployable, ShieldGeneratorDeployable, SpecialEmp, TelepadDeployable, Tool, TrapDeployable, TurretDeployable, Vehicle} import net.psforever.objects.avatar.{Avatar, PlayerControl, SpecialCarry} import net.psforever.objects.ballistics.Projectile -import net.psforever.objects.ce.{Deployable, DeployedItem, TelepadLike} +import net.psforever.objects.ce.{Deployable, DeployedItem} import net.psforever.objects.definition.{BasicDefinition, KitDefinition, SpecialExoSuitDefinition} import net.psforever.objects.entity.WorldEntity import net.psforever.objects.equipment.Equipment -import net.psforever.objects.guid.{GUIDTask, TaskBundle, TaskWorkflow} import net.psforever.objects.inventory.Container -import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject, ServerObject} +import net.psforever.objects.serverobject.{PlanetSideServerObject, ServerObject} import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.serverobject.containable.Containable import net.psforever.objects.serverobject.doors.Door @@ -24,30 +23,25 @@ import net.psforever.objects.serverobject.llu.CaptureFlag import net.psforever.objects.serverobject.locks.IFFLock import net.psforever.objects.serverobject.mblocker.Locker import net.psforever.objects.serverobject.resourcesilo.ResourceSilo -import net.psforever.objects.serverobject.structures.{Building, WarpGate} +import net.psforever.objects.serverobject.structures.WarpGate import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal -import net.psforever.objects.serverobject.terminals.{MatrixTerminalDefinition, ProximityUnit, Terminal} +import net.psforever.objects.serverobject.terminals.{ProximityUnit, Terminal} import net.psforever.objects.serverobject.terminals.implant.ImplantTerminalMech import net.psforever.objects.serverobject.tube.SpawnTube import net.psforever.objects.serverobject.turret.FacilityTurret -import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource} -import net.psforever.objects.vehicles.{AccessPermissionGroup, Utility, UtilityType, VehicleLockState} -import net.psforever.objects.vehicles.Utility.InternalTelepad -import net.psforever.objects.vital.{VehicleDismountActivity, VehicleMountActivity, Vitality} +import net.psforever.objects.sourcing.SourceEntry +import net.psforever.objects.vehicles.Utility +import net.psforever.objects.vital.Vitality import net.psforever.objects.vital.collision.{CollisionReason, CollisionWithReason} import net.psforever.objects.vital.etc.SuicideReason import net.psforever.objects.vital.interaction.DamageInteraction -import net.psforever.objects.zones.blockmap.BlockMapEntity -import net.psforever.objects.zones.{Zone, ZoneProjectile, Zoning} +import net.psforever.objects.zones.{ZoneProjectile, Zoning} import net.psforever.packet.PlanetSideGamePacket -import net.psforever.packet.game.objectcreate.ObjectClass -import net.psforever.packet.game.{ActionCancelMessage, ActionResultMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, BindStatus, BugReportMessage, ChangeFireModeMessage, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestAction, CharacterRequestMessage, ChatMsg, CollisionIs, ConnectToWorldRequestMessage, CreateShortcutMessage, DeadState, DeployObjectMessage, DisplayedAwardMessage, DropItemMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FriendsRequest, GenericAction, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, ImplantAction, InvalidTerrainMessage, ItemTransactionMessage, LootItemMessage, MoveItemMessage, ObjectDeleteMessage, ObjectDetectedMessage, ObjectHeldMessage, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, PlayerStateShiftMessage, RequestDestroyMessage, ShiftState, TargetInfo, TargetingImplantRequest, TargetingInfoMessage, TerrainCondition, TradeMessage, UnuseItemMessage, UseItemMessage, VoiceHostInfo, VoiceHostRequest, ZipLineMessage} -import net.psforever.services.RemoverActor +import net.psforever.packet.game.{ActionCancelMessage, ActionResultMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, BugReportMessage, ChangeFireModeMessage, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestAction, CharacterRequestMessage, ChatMsg, CollisionIs, ConnectToWorldRequestMessage, CreateShortcutMessage, DeadState, DeployObjectMessage, DisplayedAwardMessage, DropItemMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FriendsRequest, GenericAction, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, ImplantAction, InvalidTerrainMessage, LootItemMessage, MoveItemMessage, ObjectDetectedMessage, ObjectHeldMessage, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, PlayerStateShiftMessage, RequestDestroyMessage, ShiftState, TargetInfo, TargetingImplantRequest, TargetingInfoMessage, TerrainCondition, TradeMessage, UnuseItemMessage, UseItemMessage, VoiceHostInfo, VoiceHostRequest, ZipLineMessage} import net.psforever.services.account.{AccountPersistenceService, RetrieveAccountData} import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} -import net.psforever.services.local.{LocalAction, LocalServiceMessage} import net.psforever.services.local.support.CaptureFlagManager -import net.psforever.types.{CapacitorStateType, ChatMessageType, Cosmetic, DriveState, ExoSuitType, ImplantType, PlanetSideEmpire, PlanetSideGUID, SpawnGroup, TransactionType, Vector3} +import net.psforever.types.{CapacitorStateType, ChatMessageType, Cosmetic, ExoSuitType, ImplantType, PlanetSideEmpire, PlanetSideGUID, Vector3} import net.psforever.util.Config import scala.concurrent.duration._ @@ -311,7 +305,7 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex continent.Projectile ! ZoneProjectile.Remove(objectGuid) case Some(obj: BoomerTrigger) => - if (findEquipmentToDelete(objectGuid, obj)) { + if (ops.findEquipmentToDelete(objectGuid, obj)) { continent.GUID(obj.Companion) match { case Some(boomer: BoomerDeployable) => boomer.Trigger = None @@ -330,7 +324,7 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex } case Some(obj: Equipment) => - findEquipmentToDelete(objectGuid, obj) + ops.findEquipmentToDelete(objectGuid, obj) case Some(thing) => log.warn(s"RequestDestroy: not allowed to delete this ${thing.Definition.Name}") @@ -447,47 +441,47 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex } sessionLogic.validObject(pkt.object_guid, decorator = "UseItem") match { case Some(door: Door) => - handleUseDoor(door, equipment) + ops.handleUseDoor(door, equipment) case Some(resourceSilo: ResourceSilo) => - handleUseResourceSilo(resourceSilo, equipment) + ops.handleUseResourceSilo(resourceSilo, equipment) case Some(panel: IFFLock) => - handleUseGeneralEntity(panel, equipment) + ops.handleUseGeneralEntity(panel, equipment) case Some(obj: Player) => - handleUsePlayer(obj, equipment, pkt) + ops.handleUsePlayer(obj, equipment, pkt) case Some(locker: Locker) => - handleUseLocker(locker, equipment, pkt) + ops.handleUseLocker(locker, equipment, pkt) case Some(gen: Generator) => - handleUseGeneralEntity(gen, equipment) + ops.handleUseGeneralEntity(gen, equipment) case Some(mech: ImplantTerminalMech) => - handleUseGeneralEntity(mech, equipment) + ops.handleUseGeneralEntity(mech, equipment) case Some(captureTerminal: CaptureTerminal) => - handleUseCaptureTerminal(captureTerminal, equipment) + ops.handleUseCaptureTerminal(captureTerminal, equipment) case Some(obj: FacilityTurret) => - handleUseFacilityTurret(obj, equipment, pkt) + ops.handleUseFacilityTurret(obj, equipment, pkt) case Some(obj: Vehicle) => - handleUseVehicle(obj, equipment, pkt) + ops.handleUseVehicle(obj, equipment, pkt) case Some(terminal: Terminal) => - handleUseTerminal(terminal, equipment, pkt) + ops.handleUseTerminal(terminal, equipment, pkt) case Some(obj: SpawnTube) => - handleUseSpawnTube(obj, equipment) + ops.handleUseSpawnTube(obj, equipment) case Some(obj: SensorDeployable) => - handleUseGeneralEntity(obj, equipment) + ops.handleUseGeneralEntity(obj, equipment) case Some(obj: TurretDeployable) => - handleUseGeneralEntity(obj, equipment) + ops.handleUseGeneralEntity(obj, equipment) case Some(obj: TrapDeployable) => - handleUseGeneralEntity(obj, equipment) + ops.handleUseGeneralEntity(obj, equipment) case Some(obj: ShieldGeneratorDeployable) => - handleUseGeneralEntity(obj, equipment) + ops.handleUseGeneralEntity(obj, equipment) case Some(obj: TelepadDeployable) => - handleUseTelepadDeployable(obj, equipment, pkt) + ops.handleUseTelepadDeployable(obj, equipment, pkt, ops.useRouterTelepadSystem) case Some(obj: Utility.InternalTelepad) => - handleUseInternalTelepad(obj, pkt) + ops.handleUseInternalTelepad(obj, pkt, ops.useRouterTelepadSystem) case Some(obj: CaptureFlag) => - handleUseCaptureFlag(obj) + ops.handleUseCaptureFlag(obj) case Some(_: WarpGate) => - handleUseWarpGate(equipment) + ops.handleUseWarpGate(equipment) case Some(obj) => - handleUseDefaultEntity(obj, equipment) + ops.handleUseDefaultEntity(obj, equipment) case None => () } } @@ -518,19 +512,7 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex } log.info(s"${player.Name} is constructing a $ammoType deployable") sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") - val dObj: Deployable = Deployables.Make(ammoType)() - dObj.Position = pos - dObj.Orientation = orient - dObj.WhichSide = player.WhichSide - dObj.Faction = player.Faction - dObj.AssignOwnership(player) - val tasking: TaskBundle = dObj match { - case turret: TurretDeployable => - GUIDTask.registerDeployableTurret(continent.GUID, turret) - case _ => - GUIDTask.registerObject(continent.GUID, dObj) - } - TaskWorkflow.execute(CallBackForTask(tasking, continent.Deployables, Zone.Deployable.BuildByOwner(dObj, player, obj), context.self)) + ops.handleDeployObject(continent, ammoType, pos, orient, player.WhichSide, player.Faction, Some((player, obj))) case Some(obj) => log.warn(s"DeployObject: what is $obj, ${player.Name}? It's not a construction tool!") case None => @@ -567,7 +549,7 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex ) { //maelstrom primary fire mode discharge (no target) //aphelion_laser discharge (no target) - sessionLogic.shooting.HandleWeaponFireAccountability(objectGuid, PlanetSideGUID(Projectile.baseUID)) + sessionLogic.shooting.handleWeaponFireAccountability(objectGuid, PlanetSideGUID(Projectile.baseUID)) } else { sessionLogic.validObject(player.VehicleSeated, decorator = "GenericObjectAction/Vehicle") collect { case vehicle: Vehicle @@ -819,10 +801,8 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex .foreach { case obj: Vitality if obj.Destroyed => () //some entities will try to charge even if destroyed case obj: Vehicle if obj.MountedIn.nonEmpty => () //cargo vehicles need to be excluded - case obj: Vehicle => - commonFacilityShieldCharging(obj) - case obj: TurretDeployable => - commonFacilityShieldCharging(obj) + case obj: Vehicle => ops.commonFacilityShieldCharging(obj) + case obj: TurretDeployable => ops.commonFacilityShieldCharging(obj) case _ if vehicleGuid.nonEmpty => log.warn( s"FacilityBenefitShieldChargeRequest: ${player.Name} can not find chargeable entity ${vehicleGuid.get.guid} in ${continent.id}" @@ -1014,413 +994,6 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex /* supporting functions */ - private def handleUseDoor(door: Door, equipment: Option[Equipment]): Unit = { - equipment match { - case Some(tool: Tool) if tool.Definition == GlobalDefinitions.medicalapplicator => - val distance: Float = math.max( - Config.app.game.doorsCanBeOpenedByMedAppFromThisDistance, - door.Definition.initialOpeningDistance - ) - door.Actor ! CommonMessages.Use(player, Some(distance)) - case _ => - door.Actor ! CommonMessages.Use(player) - } - } - - private def handleUseResourceSilo(resourceSilo: ResourceSilo, equipment: Option[Equipment]): Unit = { - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") - val vehicleOpt = continent.GUID(player.avatar.vehicle) - (vehicleOpt, equipment) match { - case (Some(vehicle: Vehicle), Some(item)) - if GlobalDefinitions.isBattleFrameVehicle(vehicle.Definition) && - GlobalDefinitions.isBattleFrameNTUSiphon(item.Definition) => - resourceSilo.Actor ! CommonMessages.Use(player, Some(vehicle)) - case (Some(vehicle: Vehicle), _) - if vehicle.Definition == GlobalDefinitions.ant && - vehicle.DeploymentState == DriveState.Deployed && - Vector3.DistanceSquared(resourceSilo.Position.xy, vehicle.Position.xy) < math.pow(resourceSilo.Definition.UseRadius, 2) => - resourceSilo.Actor ! CommonMessages.Use(player, Some(vehicle)) - case _ => () - } - } - - private def handleUsePlayer(obj: Player, equipment: Option[Equipment], msg: UseItemMessage): Unit = { - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") - if (obj.isBackpack) { - if (equipment.isEmpty) { - log.info(s"${player.Name} is looting the corpse of ${obj.Name}") - sendResponse(msg) - ops.accessContainer(obj) - } - } else if (!msg.unk3 && player.isAlive) { //potential kit use - (continent.GUID(msg.item_used_guid), ops.kitToBeUsed) match { - case (Some(kit: Kit), None) => - ops.kitToBeUsed = Some(msg.item_used_guid) - player.Actor ! CommonMessages.Use(player, Some(kit)) - case (Some(_: Kit), Some(_)) | (None, Some(_)) => - //a kit is already queued to be used; ignore this request - sendResponse(ChatMsg(ChatMessageType.UNK_225, wideContents=false, "", "Please wait ...", None)) - case (Some(item), _) => - log.error(s"UseItem: ${player.Name} looking for Kit to use, but found $item instead") - case (None, None) => - log.warn(s"UseItem: anticipated a Kit ${msg.item_used_guid} for ${player.Name}, but can't find it") } - } else if (msg.object_id == ObjectClass.avatar && msg.unk3) { - equipment match { - case Some(tool: Tool) if tool.Definition == GlobalDefinitions.bank => - obj.Actor ! CommonMessages.Use(player, equipment) - - case Some(tool: Tool) if tool.Definition == GlobalDefinitions.medicalapplicator => - obj.Actor ! CommonMessages.Use(player, equipment) - case _ => () - } - } - } - - private def handleUseLocker(locker: Locker, equipment: Option[Equipment], msg: UseItemMessage): Unit = { - equipment match { - case Some(item) => - sendUseGeneralEntityMessage(locker, item) - case None if locker.Faction == player.Faction || locker.HackedBy.nonEmpty => - log.info(s"${player.Name} is accessing a locker") - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") - val playerLocker = player.avatar.locker - sendResponse(msg.copy(object_guid = playerLocker.GUID, object_id = 456)) - ops.accessContainer(playerLocker) - case _ => () - } - } - - private def handleUseCaptureTerminal(captureTerminal: CaptureTerminal, equipment: Option[Equipment]): Unit = { - equipment match { - case Some(item) => - sendUseGeneralEntityMessage(captureTerminal, item) - case _ if ops.specialItemSlotGuid.nonEmpty => - continent.GUID(ops.specialItemSlotGuid) match { - case Some(llu: CaptureFlag) => - if (llu.Target.GUID == captureTerminal.Owner.GUID) { - continent.LocalEvents ! LocalServiceMessage(continent.id, LocalAction.LluCaptured(llu)) - } else { - log.info( - s"LLU target is not this base. Target GUID: ${llu.Target.GUID} This base: ${captureTerminal.Owner.GUID}" - ) - } - case _ => log.warn("Item in specialItemSlotGuid is not registered with continent or is not a LLU") - } - case _ => () - } - } - - private def handleUseFacilityTurret(obj: FacilityTurret, equipment: Option[Equipment], msg: UseItemMessage): Unit = { - equipment.foreach { item => - sendUseGeneralEntityMessage(obj, item) - obj.Actor ! CommonMessages.Use(player, Some((item, msg.unk2.toInt))) //try upgrade path - } - } - - private def handleUseVehicle(obj: Vehicle, equipment: Option[Equipment], msg: UseItemMessage): Unit = { - equipment match { - case Some(item) => - sendUseGeneralEntityMessage(obj, item) - case None if player.Faction == obj.Faction => - //access to trunk - if ( - obj.AccessingTrunk.isEmpty && - (!obj.PermissionGroup(AccessPermissionGroup.Trunk.id).contains(VehicleLockState.Locked) || obj.OwnerGuid - .contains(player.GUID)) - ) { - log.info(s"${player.Name} is looking in the ${obj.Definition.Name}'s trunk") - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") - obj.AccessingTrunk = player.GUID - ops.accessContainer(obj) - sendResponse(msg) - } - case _ => () - } - } - - private def handleUseTerminal(terminal: Terminal, equipment: Option[Equipment], msg: UseItemMessage): Unit = { - equipment match { - case Some(item) => - sendUseGeneralEntityMessage(terminal, item) - case None - if terminal.Owner == Building.NoBuilding || terminal.Faction == player.Faction || - terminal.HackedBy.nonEmpty || terminal.Faction == PlanetSideEmpire.NEUTRAL => - val tdef = terminal.Definition - if (tdef.isInstanceOf[MatrixTerminalDefinition]) { - //TODO matrix spawn point; for now, just blindly bind to show work (and hope nothing breaks) - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") - sendResponse( - BindPlayerMessage(BindStatus.Bind, "", display_icon=true, logging=true, SpawnGroup.Sanctuary, 0, 0, terminal.Position) - ) - } else if ( - tdef == GlobalDefinitions.multivehicle_rearm_terminal || tdef == GlobalDefinitions.bfr_rearm_terminal || - tdef == GlobalDefinitions.air_rearm_terminal || tdef == GlobalDefinitions.ground_rearm_terminal - ) { - findLocalVehicle match { - case Some(vehicle) => - log.info( - s"${player.Name} is accessing a ${terminal.Definition.Name} for ${player.Sex.possessive} ${vehicle.Definition.Name}" - ) - sendResponse(msg) - sendResponse(msg.copy(object_guid = vehicle.GUID, object_id = vehicle.Definition.ObjectId)) - case None => - log.error(s"UseItem: Expecting a seated vehicle, ${player.Name} found none") - } - } else if (tdef == GlobalDefinitions.teleportpad_terminal) { - //explicit request - log.info(s"${player.Name} is purchasing a router telepad") - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") - terminal.Actor ! Terminal.Request( - player, - ItemTransactionMessage(msg.object_guid, TransactionType.Buy, 0, "router_telepad", 0, PlanetSideGUID(0)) - ) - } else if (tdef == GlobalDefinitions.targeting_laser_dispenser) { - //explicit request - log.info(s"${player.Name} is purchasing a targeting laser") - 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}") - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") - sendResponse(msg) - } - case _ => () - } - } - - private def handleUseSpawnTube(obj: SpawnTube, equipment: Option[Equipment]): Unit = { - equipment match { - case Some(item) => - sendUseGeneralEntityMessage(obj, item) - case None if player.Faction == obj.Faction => - //deconstruction - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") - sessionLogic.actionsToCancel() - sessionLogic.terminals.CancelAllProximityUnits() - sessionLogic.zoning.spawn.startDeconstructing(obj) - case _ => () - } - } - - private def handleUseTelepadDeployable(obj: TelepadDeployable, equipment: Option[Equipment], msg: UseItemMessage): Unit = { - if (equipment.isEmpty) { - (continent.GUID(obj.Router) match { - case Some(vehicle: Vehicle) => Some((vehicle, vehicle.Utility(UtilityType.internal_router_telepad_deployable))) - case Some(vehicle) => Some(vehicle, None) - case None => None - }) match { - case Some((vehicle: Vehicle, Some(util: Utility.InternalTelepad))) => - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel") - player.WhichSide = vehicle.WhichSide - useRouterTelepadSystem( - router = vehicle, - internalTelepad = util, - remoteTelepad = obj, - src = obj, - dest = util - ) - case Some((vehicle: Vehicle, None)) => - log.error( - s"telepad@${msg.object_guid.guid} is not linked to a router - ${vehicle.Definition.Name}" - ) - case Some((o, _)) => - log.error( - s"telepad@${msg.object_guid.guid} is linked to wrong kind of object - ${o.Definition.Name}, ${obj.Router}" - ) - obj.Actor ! Deployable.Deconstruct() - case _ => () - } - } - } - - private def handleUseInternalTelepad(obj: InternalTelepad, msg: UseItemMessage): Unit = { - continent.GUID(obj.Telepad) match { - case Some(pad: TelepadDeployable) => - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel") - player.WhichSide = pad.WhichSide - useRouterTelepadSystem( - router = obj.Owner.asInstanceOf[Vehicle], - internalTelepad = obj, - remoteTelepad = pad, - src = obj, - dest = pad - ) - case Some(o) => - log.error( - s"internal telepad@${msg.object_guid.guid} is not linked to a remote telepad - ${o.Definition.Name}@${o.GUID.guid}" - ) - case None => () - } - } - - private def handleUseCaptureFlag(obj: CaptureFlag): Unit = { - // LLU can normally only be picked up the faction that owns it - ops.specialItemSlotGuid match { - case None if obj.Faction == player.Faction => - ops.specialItemSlotGuid = Some(obj.GUID) - player.Carrying = SpecialCarry.CaptureFlag - continent.LocalEvents ! CaptureFlagManager.PickupFlag(obj, player) - case None => - log.warn(s"${player.Faction} player ${player.toString} tried to pick up a ${obj.Faction} LLU - ${obj.GUID}") - case Some(guid) if guid != obj.GUID => - // Ignore duplicate pickup requests - log.warn( - s"${player.Faction} player ${player.toString} tried to pick up a ${obj.Faction} LLU, but their special slot already contains $guid" - ) - case _ => () - } - } - - private def handleUseWarpGate(equipment: Option[Equipment]): Unit = { - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") - (continent.GUID(player.VehicleSeated), equipment) match { - case (Some(vehicle: Vehicle), Some(item)) - if GlobalDefinitions.isBattleFrameVehicle(vehicle.Definition) && - GlobalDefinitions.isBattleFrameNTUSiphon(item.Definition) => - vehicle.Actor ! CommonMessages.Use(player, equipment) - case _ => () - } - } - - private def handleUseGeneralEntity(obj: PlanetSideServerObject, equipment: Option[Equipment]): Unit = { - equipment.foreach { item => - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") - obj.Actor ! CommonMessages.Use(player, Some(item)) - } - } - - private def sendUseGeneralEntityMessage(obj: PlanetSideServerObject, equipment: Equipment): Unit = { - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") - obj.Actor ! CommonMessages.Use(player, Some(equipment)) - } - - private def handleUseDefaultEntity(obj: PlanetSideGameObject, equipment: Option[Equipment]): Unit = { - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") - equipment match { - case Some(item) - if GlobalDefinitions.isBattleFrameArmorSiphon(item.Definition) || - GlobalDefinitions.isBattleFrameNTUSiphon(item.Definition) => () - case _ => - log.warn(s"UseItem: ${player.Name} does not know how to handle $obj") - } - } - - /** - * Get the current `Vehicle` object that the player is riding/driving. - * The vehicle must be found solely through use of `player.VehicleSeated`. - * @return the vehicle - */ - private def findLocalVehicle: Option[Vehicle] = { - continent.GUID(player.VehicleSeated) match { - case Some(obj: Vehicle) => Some(obj) - case _ => None - } - } - - /** - * A simple object searching algorithm that is limited to containers currently known and accessible by the player. - * If all relatively local containers are checked and the object is not found, - * the player's locker inventory will be checked, and then - * the game environment (items on the ground) will be checked too. - * If the target object is discovered, it is removed from its current location and is completely destroyed. - * @see `RequestDestroyMessage` - * @see `Zone.ItemIs.Where` - * @param objectGuid the target object's globally unique identifier; - * it is not expected that the object will be unregistered, but it is also not gauranteed - * @param obj the target object - * @return `true`, if the target object was discovered and removed; - * `false`, otherwise - */ - private def findEquipmentToDelete(objectGuid: PlanetSideGUID, obj: Equipment): Boolean = { - val findFunc - : PlanetSideServerObject with Container => Option[(PlanetSideServerObject with Container, Option[Int])] = - ops.findInLocalContainer(objectGuid) - - findFunc(player) - .orElse(ops.accessedContainer match { - case Some(parent: PlanetSideServerObject) => - findFunc(parent) - case _ => - None - }) - .orElse(findLocalVehicle match { - case Some(parent: PlanetSideServerObject) => - findFunc(parent) - case _ => - None - }) match { - case Some((parent, Some(_))) => - obj.Position = Vector3.Zero - RemoveOldEquipmentFromInventory(parent)(obj) - true - case _ if player.avatar.locker.Inventory.Remove(objectGuid) => - sendResponse(ObjectDeleteMessage(objectGuid, 0)) - true - case _ if continent.EquipmentOnGround.contains(obj) => - obj.Position = Vector3.Zero - continent.Ground ! Zone.Ground.RemoveItem(objectGuid) - continent.AvatarEvents ! AvatarServiceMessage.Ground(RemoverActor.ClearSpecific(List(obj), continent)) - true - case _ => - Zone.EquipmentIs.Where(obj, objectGuid, continent) match { - case None => - true - case Some(Zone.EquipmentIs.Orphaned()) if obj.HasGUID => - TaskWorkflow.execute(GUIDTask.unregisterEquipment(continent.GUID, obj)) - true - case Some(Zone.EquipmentIs.Orphaned()) => - true - case _ => - log.warn(s"RequestDestroy: equipment $obj exists, but ${player.Name} can not reach it to dispose of it") - false - } - } - } - - /** - * A player uses a fully-linked Router teleportation system. - * @param router the Router vehicle - * @param internalTelepad the internal telepad within the Router vehicle - * @param remoteTelepad the remote telepad that is currently associated with this Router - * @param src the origin of the teleportation (where the player starts) - * @param dest the destination of the teleportation (where the player is going) - */ - private def useRouterTelepadSystem( - router: Vehicle, - internalTelepad: InternalTelepad, - remoteTelepad: TelepadDeployable, - src: PlanetSideGameObject with TelepadLike, - dest: PlanetSideGameObject with TelepadLike - ): Unit = { - val time = System.currentTimeMillis() - if ( - time - ops.recentTeleportAttempt > 2000L && router.DeploymentState == DriveState.Deployed && - internalTelepad.Active && - remoteTelepad.Active - ) { - val pguid = player.GUID - val sguid = src.GUID - val dguid = dest.GUID - sendResponse(PlayerStateShiftMessage(ShiftState(0, dest.Position, player.Orientation.z))) - ops.useRouterTelepadEffect(pguid, sguid, dguid) - continent.LocalEvents ! LocalServiceMessage( - continent.id, - LocalAction.RouterTelepadTransport(pguid, pguid, sguid, dguid) - ) - val vSource = VehicleSource(router) - val zoneNumber = continent.Number - player.LogActivity(VehicleMountActivity(vSource, PlayerSource(player), zoneNumber)) - player.Position = dest.Position - player.LogActivity(VehicleDismountActivity(vSource, PlayerSource(player), zoneNumber)) - } else { - log.warn(s"UseRouterTelepadSystem: ${player.Name} can not teleport") - } - ops.recentTeleportAttempt = time - } - private def maxCapacitorTick(jumpThrust: Boolean): Unit = { if (player.ExoSuit == ExoSuitType.MAX) { val activate = (jumpThrust || player.isOverdrived || player.isShielded) && player.Capacitor > 0 @@ -1540,11 +1113,4 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex ) ) } - - private def commonFacilityShieldCharging(obj: PlanetSideServerObject with BlockMapEntity): Unit = { - obj.Actor ! CommonMessages.ChargeShields( - 15, - Some(continent.blockMap.sector(obj).buildingList.maxBy(_.Definition.SOIRadius)) - ) - } } diff --git a/src/main/scala/net/psforever/actors/session/normal/LocalHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/normal/LocalHandlerLogic.scala index 09bd78579..3b7538a5a 100644 --- a/src/main/scala/net/psforever/actors/session/normal/LocalHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/LocalHandlerLogic.scala @@ -10,7 +10,7 @@ import net.psforever.objects.{BoomerDeployable, ExplosiveDeployable, TelepadDepl import net.psforever.packet.game.{ChatMsg, DeployableObjectsInfoMessage, GenericActionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HackMessage, HackState, HackState1, InventoryStateMessage, ObjectAttachMessage, ObjectCreateMessage, ObjectDeleteMessage, ObjectDetachMessage, OrbitalShuttleTimeMsg, PadAndShuttlePair, PlanetsideAttributeMessage, ProximityTerminalUseMessage, SetEmpireMessage, TriggerEffectMessage, TriggerSoundMessage, TriggeredSound, VehicleStateMessage} import net.psforever.services.Service import net.psforever.services.local.LocalResponse -import net.psforever.types.{ChatMessageType, PlanetSideGUID, Vector3} +import net.psforever.types.{ChatMessageType, PlanetSideGUID} object LocalHandlerLogic { def apply(ops: SessionLocalHandlers): LocalHandlerLogic = { @@ -88,7 +88,7 @@ class LocalHandlerLogic(val ops: SessionLocalHandlers, implicit val context: Act case LocalResponse.EliminateDeployable(obj: TurretDeployable, dguid, pos, _) => obj.Destroyed = true - DeconstructDeployable( + ops.DeconstructDeployable( obj, dguid, pos, @@ -102,7 +102,7 @@ class LocalHandlerLogic(val ops: SessionLocalHandlers, implicit val context: Act case LocalResponse.EliminateDeployable(obj: ExplosiveDeployable, dguid, pos, effect) => obj.Destroyed = true - DeconstructDeployable(obj, dguid, pos, obj.Orientation, effect) + ops.DeconstructDeployable(obj, dguid, pos, obj.Orientation, effect) case LocalResponse.EliminateDeployable(obj: TelepadDeployable, dguid, _, _) if obj.Active && obj.Destroyed => //if active, deactivate @@ -117,7 +117,7 @@ class LocalHandlerLogic(val ops: SessionLocalHandlers, implicit val context: Act ops.deactivateTelpadDeployableMessages(dguid) //standard deployable elimination behavior obj.Destroyed = true - DeconstructDeployable(obj, dguid, pos, obj.Orientation, deletionType=2) + ops.DeconstructDeployable(obj, dguid, pos, obj.Orientation, deletionType=2) case LocalResponse.EliminateDeployable(obj: TelepadDeployable, dguid, _, _) if obj.Destroyed => //standard deployable elimination behavior @@ -126,14 +126,14 @@ class LocalHandlerLogic(val ops: SessionLocalHandlers, implicit val context: Act case LocalResponse.EliminateDeployable(obj: TelepadDeployable, dguid, pos, _) => //standard deployable elimination behavior obj.Destroyed = true - DeconstructDeployable(obj, dguid, pos, obj.Orientation, deletionType=2) + ops.DeconstructDeployable(obj, dguid, pos, obj.Orientation, deletionType=2) case LocalResponse.EliminateDeployable(obj, dguid, _, _) if obj.Destroyed => sendResponse(ObjectDeleteMessage(dguid, unk1=0)) case LocalResponse.EliminateDeployable(obj, dguid, pos, effect) => obj.Destroyed = true - DeconstructDeployable(obj, dguid, pos, obj.Orientation, effect) + ops.DeconstructDeployable(obj, dguid, pos, obj.Orientation, effect) case LocalResponse.SendHackMessageHackCleared(targetGuid, unk1, unk2) => sendResponse(HackMessage(HackState1.Unk0, targetGuid, guid, progress=0, unk1.toFloat, HackState.HackCleared, unk2)) @@ -245,24 +245,4 @@ class LocalHandlerLogic(val ops: SessionLocalHandlers, implicit val context: Act } /* support functions */ - - /** - * Common behavior for deconstructing deployables in the game environment. - * @param obj the deployable - * @param guid the globally unique identifier for the deployable - * @param pos the previous position of the deployable - * @param orient the previous orientation of the deployable - * @param deletionType the value passed to `ObjectDeleteMessage` concerning the deconstruction animation - */ - def DeconstructDeployable( - obj: Deployable, - guid: PlanetSideGUID, - pos: Vector3, - orient: Vector3, - deletionType: Int - ): Unit = { - sendResponse(TriggerEffectMessage("spawn_object_failed_effect", pos, orient)) - sendResponse(PlanetsideAttributeMessage(guid, 29, 1)) //make deployable vanish - sendResponse(ObjectDeleteMessage(guid, deletionType)) - } } diff --git a/src/main/scala/net/psforever/actors/session/normal/MountHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/normal/MountHandlerLogic.scala index c2f1a8969..e7c6c238c 100644 --- a/src/main/scala/net/psforever/actors/session/normal/MountHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/MountHandlerLogic.scala @@ -1,8 +1,7 @@ // Copyright (c) 2024 PSForever package net.psforever.actors.session.normal -import akka.actor.{ActorContext, typed} -import net.psforever.actors.session.AvatarActor +import akka.actor.ActorContext import net.psforever.actors.session.support.{MountHandlerFunctions, SessionData, SessionMountHandlers} import net.psforever.actors.zone.ZoneActor import net.psforever.objects.{GlobalDefinitions, PlanetSideGameObject, Player, Vehicle, Vehicles} @@ -14,16 +13,14 @@ import net.psforever.objects.serverobject.mount.Mountable import net.psforever.objects.serverobject.structures.WarpGate import net.psforever.objects.serverobject.terminals.implant.ImplantTerminalMech import net.psforever.objects.serverobject.turret.{FacilityTurret, WeaponTurret} -import net.psforever.objects.vehicles.{AccessPermissionGroup, CargoBehavior} +import net.psforever.objects.vehicles.AccessPermissionGroup import net.psforever.objects.vital.InGameHistory -import net.psforever.packet.game.{ChatMsg, DelayedPathMountMsg, DismountVehicleCargoMsg, DismountVehicleMsg, GenericObjectActionMessage, MountVehicleCargoMsg, MountVehicleMsg, ObjectAttachMessage, ObjectDetachMessage, PlanetsideAttributeMessage, PlayerStasisMessage, PlayerStateShiftMessage, ShiftState} +import net.psforever.packet.game.{ChatMsg, DelayedPathMountMsg, DismountVehicleCargoMsg, DismountVehicleMsg, GenericObjectActionMessage, MountVehicleCargoMsg, MountVehicleMsg, ObjectDetachMessage, PlanetsideAttributeMessage, PlayerStasisMessage, PlayerStateShiftMessage, ShiftState} import net.psforever.services.Service import net.psforever.services.local.{LocalAction, LocalServiceMessage} import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} import net.psforever.types.{BailType, ChatMessageType, DriveState, PlanetSideGUID, Vector3} -import scala.concurrent.duration._ - object MountHandlerLogic { def apply(ops: SessionMountHandlers): MountHandlerLogic = { new MountHandlerLogic(ops, ops.context) @@ -33,116 +30,22 @@ object MountHandlerLogic { class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: ActorContext) extends MountHandlerFunctions { def sessionLogic: SessionData = ops.sessionLogic - private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor - /* packets */ def handleMountVehicle(pkt: MountVehicleMsg): Unit = { - val MountVehicleMsg(_, mountable_guid, entry_point) = pkt - sessionLogic.validObject(mountable_guid, decorator = "MountVehicle").collect { - case obj: Mountable => - obj.Actor ! Mountable.TryMount(player, entry_point) - case _ => - log.error(s"MountVehicleMsg: object ${mountable_guid.guid} not a mountable thing, ${player.Name}") - } + ops.handleMountVehicle(pkt) } def handleDismountVehicle(pkt: DismountVehicleMsg): Unit = { - val DismountVehicleMsg(player_guid, bailType, wasKickedByDriver) = pkt - val dError: (String, Player)=> Unit = dismountError(bailType, wasKickedByDriver) - //TODO optimize this later - //common warning for this section - if (player.GUID == player_guid) { - //normally disembarking from a mount - (sessionLogic.zoning.interstellarFerry.orElse(continent.GUID(player.VehicleSeated)) match { - case out @ Some(obj: Vehicle) => - continent.GUID(obj.MountedIn) match { - case Some(_: Vehicle) => None //cargo vehicle - case _ => out //arrangement "may" be permissible - } - case out @ Some(_: Mountable) => - out - case _ => - dError(s"DismountVehicleMsg: player ${player.Name} not considered seated in a mountable entity", player) - None - }) match { - case Some(obj: Mountable) => - obj.PassengerInSeat(player) match { - case Some(seat_num) => - obj.Actor ! Mountable.TryDismount(player, seat_num, bailType) - //short-circuit the temporary channel for transferring between zones, the player is no longer doing that - sessionLogic.zoning.interstellarFerry = None - - case None => - dError(s"DismountVehicleMsg: can not find where player ${player.Name}_guid is seated in mountable ${player.VehicleSeated}", player) - } - case _ => - dError(s"DismountVehicleMsg: can not find mountable entity ${player.VehicleSeated}", player) - } - } else { - //kicking someone else out of a mount; need to own that mount/mountable - val dWarn: (String, Player)=> Unit = dismountWarning(bailType, wasKickedByDriver) - player.avatar.vehicle match { - case Some(obj_guid) => - ( - ( - sessionLogic.validObject(obj_guid, decorator = "DismountVehicle/Vehicle"), - sessionLogic.validObject(player_guid, decorator = "DismountVehicle/Player") - ) match { - case (vehicle @ Some(obj: Vehicle), tplayer) => - if (obj.MountedIn.isEmpty) (vehicle, tplayer) else (None, None) - case (mount @ Some(_: Mountable), tplayer) => - (mount, tplayer) - case _ => - (None, None) - }) match { - case (Some(obj: Mountable), Some(tplayer: Player)) => - obj.PassengerInSeat(tplayer) match { - case Some(seat_num) => - obj.Actor ! Mountable.TryDismount(tplayer, seat_num, bailType) - case None => - dError(s"DismountVehicleMsg: can not find where other player ${tplayer.Name} is seated in mountable $obj_guid", tplayer) - } - case (None, _) => - dWarn(s"DismountVehicleMsg: ${player.Name} can not find his vehicle", player) - case (_, None) => - dWarn(s"DismountVehicleMsg: player $player_guid could not be found to kick, ${player.Name}", player) - case _ => - dWarn(s"DismountVehicleMsg: object is either not a Mountable or not a Player", player) - } - case None => - dWarn(s"DismountVehicleMsg: ${player.Name} does not own a vehicle", player) - } - } + ops.handleDismountVehicle(pkt) } def handleMountVehicleCargo(pkt: MountVehicleCargoMsg): Unit = { - val MountVehicleCargoMsg(_, cargo_guid, carrier_guid, _) = pkt - (continent.GUID(cargo_guid), continent.GUID(carrier_guid)) match { - case (Some(cargo: Vehicle), Some(carrier: Vehicle)) => - carrier.CargoHolds.find({ case (_, hold) => !hold.isOccupied }) match { - case Some((mountPoint, _)) => - cargo.Actor ! CargoBehavior.StartCargoMounting(carrier_guid, mountPoint) - case _ => - log.warn( - s"MountVehicleCargoMsg: ${player.Name} trying to load cargo into a ${carrier.Definition.Name} which oes not have a cargo hold" - ) - } - case (None, _) | (Some(_), None) => - log.warn( - s"MountVehicleCargoMsg: ${player.Name} lost a vehicle while working with cargo - either $carrier_guid or $cargo_guid" - ) - case _ => () - } + ops.handleMountVehicleCargo(pkt) } def handleDismountVehicleCargo(pkt: DismountVehicleCargoMsg): Unit = { - val DismountVehicleCargoMsg(_, cargo_guid, bailed, _, kicked) = pkt - continent.GUID(cargo_guid) match { - case Some(cargo: Vehicle) => - cargo.Actor ! CargoBehavior.StartCargoDismounting(bailed || kicked) - case _ => () - } + ops.handleDismountVehicleCargo(pkt) } /* response handlers */ @@ -156,24 +59,24 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act def handle(tplayer: Player, reply: Mountable.Exchange): Unit = { reply match { case Mountable.CanMount(obj: ImplantTerminalMech, seatNumber, _) => - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") log.info(s"${player.Name} mounts an implant terminal") + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") sessionLogic.terminals.CancelAllProximityUnits() - MountingAction(tplayer, obj, seatNumber) + ops.MountingAction(tplayer, obj, seatNumber) sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistence case Mountable.CanMount(obj: Vehicle, seatNumber, _) if obj.Definition == GlobalDefinitions.orbital_shuttle => - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount") log.info(s"${player.Name} mounts the orbital shuttle") + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount") sessionLogic.terminals.CancelAllProximityUnits() - MountingAction(tplayer, obj, seatNumber) + ops.MountingAction(tplayer, obj, seatNumber) sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistence case Mountable.CanMount(obj: Vehicle, seatNumber, _) if obj.Definition == GlobalDefinitions.ant => - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount") log.info(s"${player.Name} mounts the driver seat of the ${obj.Definition.Name}") + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount") val obj_guid: PlanetSideGUID = obj.GUID sessionLogic.terminals.CancelAllProximityUnits() sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health)) @@ -182,12 +85,12 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act sendResponse(GenericObjectActionMessage(obj_guid, code=11)) sessionLogic.general.accessContainer(obj) tplayer.Actor ! ResetAllEnvironmentInteractions - MountingAction(tplayer, obj, seatNumber) + ops.MountingAction(tplayer, obj, seatNumber) case Mountable.CanMount(obj: Vehicle, seatNumber, _) if obj.Definition == GlobalDefinitions.quadstealth => - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount") log.info(s"${player.Name} mounts the driver seat of the ${obj.Definition.Name}") + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount") val obj_guid: PlanetSideGUID = obj.GUID sessionLogic.terminals.CancelAllProximityUnits() sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health)) @@ -198,12 +101,12 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act sendResponse(GenericObjectActionMessage(obj_guid, code=11)) sessionLogic.general.accessContainer(obj) tplayer.Actor ! ResetAllEnvironmentInteractions - MountingAction(tplayer, obj, seatNumber) + ops.MountingAction(tplayer, obj, seatNumber) case Mountable.CanMount(obj: Vehicle, seatNumber, _) if seatNumber == 0 && obj.Definition.MaxCapacitor > 0 => - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount") log.info(s"${player.Name} mounts the driver seat of the ${obj.Definition.Name}") + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount") val obj_guid: PlanetSideGUID = obj.GUID sessionLogic.terminals.CancelAllProximityUnits() sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health)) @@ -213,12 +116,12 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act sessionLogic.general.accessContainer(obj) ops.updateWeaponAtSeatPosition(obj, seatNumber) tplayer.Actor ! ResetAllEnvironmentInteractions - MountingAction(tplayer, obj, seatNumber) + ops.MountingAction(tplayer, obj, seatNumber) case Mountable.CanMount(obj: Vehicle, seatNumber, _) if seatNumber == 0 => - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount") log.info(s"${player.Name} mounts the driver seat of the ${obj.Definition.Name}") + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount") val obj_guid: PlanetSideGUID = obj.GUID sessionLogic.terminals.CancelAllProximityUnits() sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health)) @@ -227,17 +130,17 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act sessionLogic.general.accessContainer(obj) ops.updateWeaponAtSeatPosition(obj, seatNumber) tplayer.Actor ! ResetAllEnvironmentInteractions - MountingAction(tplayer, obj, seatNumber) + ops.MountingAction(tplayer, obj, seatNumber) case Mountable.CanMount(obj: Vehicle, seatNumber, _) if obj.Definition.MaxCapacitor > 0 => - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount") log.info(s"${player.Name} mounts ${ obj.SeatPermissionGroup(seatNumber) match { case Some(seatType) => s"a $seatType seat (#$seatNumber)" case None => "a seat" } } of the ${obj.Definition.Name}") + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount") val obj_guid: PlanetSideGUID = obj.GUID sessionLogic.terminals.CancelAllProximityUnits() sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health)) @@ -247,16 +150,16 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act ops.updateWeaponAtSeatPosition(obj, seatNumber) sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistence tplayer.Actor ! ResetAllEnvironmentInteractions - MountingAction(tplayer, obj, seatNumber) + ops.MountingAction(tplayer, obj, seatNumber) case Mountable.CanMount(obj: Vehicle, seatNumber, _) => - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount") log.info(s"${player.Name} mounts the ${ obj.SeatPermissionGroup(seatNumber) match { case Some(seatType) => s"a $seatType seat (#$seatNumber)" case None => "a seat" } } of the ${obj.Definition.Name}") + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount") val obj_guid: PlanetSideGUID = obj.GUID sessionLogic.terminals.CancelAllProximityUnits() sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health)) @@ -265,51 +168,51 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act ops.updateWeaponAtSeatPosition(obj, seatNumber) sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistence tplayer.Actor ! ResetAllEnvironmentInteractions - MountingAction(tplayer, obj, seatNumber) + ops.MountingAction(tplayer, obj, seatNumber) case Mountable.CanMount(obj: FacilityTurret, seatNumber, _) if obj.Definition == GlobalDefinitions.vanu_sentry_turret => - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount") log.info(s"${player.Name} mounts the ${obj.Definition.Name}") + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount") obj.Zone.LocalEvents ! LocalServiceMessage(obj.Zone.id, LocalAction.SetEmpire(obj.GUID, player.Faction)) sendResponse(PlanetsideAttributeMessage(obj.GUID, attribute_type=0, obj.Health)) ops.updateWeaponAtSeatPosition(obj, seatNumber) - MountingAction(tplayer, obj, seatNumber) + ops.MountingAction(tplayer, obj, seatNumber) case Mountable.CanMount(obj: FacilityTurret, seatNumber, _) if !obj.isUpgrading || System.currentTimeMillis() - GenericHackables.getTurretUpgradeTime >= 1500L => + log.info(s"${player.Name} mounts the ${obj.Definition.Name}") obj.setMiddleOfUpgrade(false) sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount") - log.info(s"${player.Name} mounts the ${obj.Definition.Name}") sendResponse(PlanetsideAttributeMessage(obj.GUID, attribute_type=0, obj.Health)) ops.updateWeaponAtSeatPosition(obj, seatNumber) - MountingAction(tplayer, obj, seatNumber) + ops.MountingAction(tplayer, obj, seatNumber) case Mountable.CanMount(obj: FacilityTurret, _, _) => - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount") log.warn( s"MountVehicleMsg: ${tplayer.Name} wants to mount turret ${obj.GUID.guid}, but needs to wait until it finishes updating" ) + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount") case Mountable.CanMount(obj: PlanetSideGameObject with FactionAffinity with WeaponTurret with InGameHistory, seatNumber, _) => - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount") log.info(s"${player.Name} mounts the ${obj.Definition.asInstanceOf[BasicDefinition].Name}") + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount") sendResponse(PlanetsideAttributeMessage(obj.GUID, attribute_type=0, obj.Health)) ops.updateWeaponAtSeatPosition(obj, seatNumber) - MountingAction(tplayer, obj, seatNumber) + ops.MountingAction(tplayer, obj, seatNumber) case Mountable.CanMount(obj: Mountable, _, _) => log.warn(s"MountVehicleMsg: $obj is some kind of mountable object but nothing will happen for ${player.Name}") case Mountable.CanDismount(obj: ImplantTerminalMech, seatNum, _) => log.info(s"${tplayer.Name} dismounts the implant terminal") - DismountAction(tplayer, obj, seatNum) + ops.DismountAction(tplayer, obj, seatNum) case Mountable.CanDismount(obj: Vehicle, _, mountPoint) if obj.Definition == GlobalDefinitions.orbital_shuttle && obj.MountedIn.nonEmpty => + log.info(s"${tplayer.Name} dismounts the orbital shuttle into the lobby") //dismount to hart lobby val pguid = player.GUID - log.info(s"${tplayer.Name} dismounts the orbital shuttle into the lobby") val sguid = obj.GUID val (pos, zang) = Vehicles.dismountShuttle(obj, mountPoint) tplayer.Position = pos @@ -322,11 +225,11 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act case Mountable.CanDismount(obj: Vehicle, seatNum, _) if obj.Definition == GlobalDefinitions.orbital_shuttle => + log.info(s"${player.Name} is prepped for dropping") //get ready for orbital drop val pguid = player.GUID val events = continent.VehicleEvents - log.info(s"${player.Name} is prepped for dropping") - DismountAction(tplayer, obj, seatNum) + ops.DismountAction(tplayer, obj, seatNum) continent.actor ! ZoneActor.RemoveFromBlockMap(player) //character doesn't need it //DismountAction(...) uses vehicle service, so use that service to coordinate the remainder of the messages events ! VehicleServiceMessage( @@ -354,7 +257,7 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act if obj.Definition == GlobalDefinitions.droppod => log.info(s"${tplayer.Name} has landed on ${continent.id}") sessionLogic.general.unaccessContainer(obj) - DismountAction(tplayer, obj, seatNum) + ops.DismountAction(tplayer, obj, seatNum) obj.Actor ! Vehicle.Deconstruct() case Mountable.CanDismount(obj: Vehicle, seatNum, _) @@ -365,12 +268,12 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act //todo: implement auto landing procedure if the pilot bails but passengers are still present instead of deconstructing the vehicle //todo: continue flight path until aircraft crashes if no passengers present (or no passenger seats), then deconstruct. //todo: kick cargo passengers out. To be added after PR #216 is merged - DismountVehicleAction(tplayer, obj, seatNum) + ops.DismountVehicleAction(tplayer, obj, seatNum) obj.Actor ! Vehicle.Deconstruct(None) //immediate deconstruction case Mountable.CanDismount(obj: Vehicle, seatNum, _) if tplayer.GUID == player.GUID => - DismountVehicleAction(tplayer, obj, seatNum) + ops.DismountVehicleAction(tplayer, obj, seatNum) case Mountable.CanDismount(obj: Vehicle, seat_num, _) => continent.VehicleEvents ! VehicleServiceMessage( @@ -380,7 +283,7 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act case Mountable.CanDismount(obj: PlanetSideGameObject with Mountable with FactionAffinity with InGameHistory, seatNum, _) => log.info(s"${tplayer.Name} dismounts a ${obj.Definition.asInstanceOf[ObjectDefinition].Name}") - DismountAction(tplayer, obj, seatNum) + ops.DismountAction(tplayer, obj, seatNum) case Mountable.CanDismount(obj: Mountable, _, _) => log.warn(s"DismountVehicleMsg: $obj is some dismountable object but nothing will happen for ${player.Name}") @@ -434,118 +337,4 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act } /* support functions */ - - private def dismountWarning( - bailAs: BailType.Value, - kickedByDriver: Boolean - ) - ( - note: String, - player: Player - ): Unit = { - log.warn(note) - player.VehicleSeated = None - sendResponse(DismountVehicleMsg(player.GUID, bailAs, kickedByDriver)) - } - - private def dismountError( - bailAs: BailType.Value, - kickedByDriver: Boolean - ) - ( - note: String, - player: Player - ): Unit = { - log.error(s"$note; some vehicle might not know that ${player.Name} is no longer sitting in it") - player.VehicleSeated = None - sendResponse(DismountVehicleMsg(player.GUID, bailAs, kickedByDriver)) - } - - /** - * Common activities/procedure when a player mounts a valid object. - * @param tplayer the player - * @param obj the mountable object - * @param seatNum the mount into which the player is mounting - */ - private def MountingAction(tplayer: Player, obj: PlanetSideGameObject with FactionAffinity with InGameHistory, seatNum: Int): Unit = { - val playerGuid: PlanetSideGUID = tplayer.GUID - val objGuid: PlanetSideGUID = obj.GUID - sessionLogic.actionsToCancel() - avatarActor ! AvatarActor.DeactivateActiveImplants - avatarActor ! AvatarActor.SuspendStaminaRegeneration(3.seconds) - sendResponse(ObjectAttachMessage(objGuid, playerGuid, seatNum)) - continent.VehicleEvents ! VehicleServiceMessage( - continent.id, - VehicleAction.MountVehicle(playerGuid, objGuid, seatNum) - ) - } - - /** - * Common activities/procedure when a player dismounts a valid mountable object. - * @param tplayer the player - * @param obj the mountable object - * @param seatNum the mount out of which which the player is disembarking - */ - private def DismountVehicleAction(tplayer: Player, obj: Vehicle, seatNum: Int): Unit = { - //disembarking self - log.info(s"${player.Name} dismounts the ${obj.Definition.Name}'s ${ - obj.SeatPermissionGroup(seatNum) match { - case Some(AccessPermissionGroup.Driver) => "driver seat" - case Some(seatType) => s"$seatType seat (#$seatNum)" - case None => "seat" - } - }") - sessionLogic.vehicles.ConditionalDriverVehicleControl(obj) - sessionLogic.general.unaccessContainer(obj) - DismountAction(tplayer, obj, seatNum) - //until vehicles maintain synchronized momentum without a driver - obj match { - case v: Vehicle - if seatNum == 0 && Vector3.MagnitudeSquared(v.Velocity.getOrElse(Vector3.Zero)) > 0f => - sessionLogic.vehicles.serverVehicleControlVelocity.collect { _ => - sessionLogic.vehicles.ServerVehicleOverrideStop(v) - } - v.Velocity = Vector3.Zero - continent.VehicleEvents ! VehicleServiceMessage( - continent.id, - VehicleAction.VehicleState( - tplayer.GUID, - v.GUID, - unk1 = 0, - v.Position, - v.Orientation, - vel = None, - v.Flying, - unk3 = 0, - unk4 = 0, - wheel_direction = 15, - unk5 = false, - unk6 = v.Cloaked - ) - ) - case _ => () - } - } - - /** - * Common activities/procedure when a player dismounts a valid mountable object. - * @param tplayer the player - * @param obj the mountable object - * @param seatNum the mount out of which which the player is disembarking - */ - private def DismountAction(tplayer: Player, obj: PlanetSideGameObject with FactionAffinity with InGameHistory, seatNum: Int): Unit = { - val playerGuid: PlanetSideGUID = tplayer.GUID - tplayer.ContributionFrom(obj) - sessionLogic.keepAliveFunc = sessionLogic.zoning.NormalKeepAlive - val bailType = if (tplayer.BailProtection) { - BailType.Bailed - } else { - BailType.Normal - } - sendResponse(DismountVehicleMsg(playerGuid, bailType, wasKickedByDriver = false)) - continent.VehicleEvents ! VehicleServiceMessage( - continent.id, - VehicleAction.DismountVehicle(playerGuid, bailType, unk2 = false) - ) - } } diff --git a/src/main/scala/net/psforever/actors/session/normal/NormalMode.scala b/src/main/scala/net/psforever/actors/session/normal/NormalMode.scala index 1a88bf9f3..60d0012aa 100644 --- a/src/main/scala/net/psforever/actors/session/normal/NormalMode.scala +++ b/src/main/scala/net/psforever/actors/session/normal/NormalMode.scala @@ -1,13 +1,12 @@ // Copyright (c) 2024 PSForever package net.psforever.actors.session.normal -import net.psforever.actors.session.support.{ChatFunctions, GeneralFunctions, LocalHandlerFunctions, MountHandlerFunctions, SquadHandlerFunctions, TerminalHandlerFunctions, VehicleFunctions, VehicleHandlerFunctions, WeaponAndProjectileFunctions} -import net.psforever.actors.session.support.{ModeLogic, PlayerMode, SessionData} +import net.psforever.actors.session.support.{ChatFunctions, GalaxyHandlerFunctions, GeneralFunctions, LocalHandlerFunctions, ModeLogic, MountHandlerFunctions, PlayerMode, SessionData, SquadHandlerFunctions, TerminalHandlerFunctions, VehicleFunctions, VehicleHandlerFunctions, WeaponAndProjectileFunctions} class NormalModeLogic(data: SessionData) extends ModeLogic { val avatarResponse: AvatarHandlerLogic = AvatarHandlerLogic(data.avatarResponse) val chat: ChatFunctions = ChatLogic(data.chat) - val galaxy: GalaxyHandlerLogic = GalaxyHandlerLogic(data.galaxyResponseHandlers) + val galaxy: GalaxyHandlerFunctions = GalaxyHandlerLogic(data.galaxyResponseHandlers) val general: GeneralFunctions = GeneralLogic(data.general) val local: LocalHandlerFunctions = LocalHandlerLogic(data.localResponse) val mountResponse: MountHandlerFunctions = MountHandlerLogic(data.mountResponse) diff --git a/src/main/scala/net/psforever/actors/session/normal/SquadHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/normal/SquadHandlerLogic.scala index 9197211f8..71bbc20a0 100644 --- a/src/main/scala/net/psforever/actors/session/normal/SquadHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/SquadHandlerLogic.scala @@ -25,8 +25,6 @@ class SquadHandlerLogic(val ops: SessionSquadHandlers, implicit val context: Act private val squadService: ActorRef = ops.squadService - private var waypointCooldown: Long = 0L - /* packet */ def handleSquadDefinitionAction(pkt: SquadDefinitionActionMessage): Unit = { diff --git a/src/main/scala/net/psforever/actors/session/normal/TerminalHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/normal/TerminalHandlerLogic.scala index bf9eb1dc7..28eb63913 100644 --- a/src/main/scala/net/psforever/actors/session/normal/TerminalHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/TerminalHandlerLogic.scala @@ -8,10 +8,10 @@ import net.psforever.login.WorldSession.{BuyNewEquipmentPutInInventory, SellEqui import net.psforever.objects.{GlobalDefinitions, Player, Vehicle} import net.psforever.objects.guid.TaskWorkflow import net.psforever.objects.serverobject.pad.VehicleSpawnPad -import net.psforever.objects.serverobject.terminals.{ProximityUnit, Terminal} +import net.psforever.objects.serverobject.terminals.Terminal import net.psforever.objects.sourcing.AmenitySource import net.psforever.objects.vital.TerminalUsedActivity -import net.psforever.packet.game.{FavoritesAction, FavoritesRequest, ItemTransactionMessage, ItemTransactionResultMessage, ProximityTerminalUseMessage, UnuseItemMessage} +import net.psforever.packet.game.{FavoritesRequest, ItemTransactionMessage, ItemTransactionResultMessage, ProximityTerminalUseMessage, UnuseItemMessage} import net.psforever.types.{TransactionType, Vector3} object TerminalHandlerLogic { @@ -26,46 +26,17 @@ class TerminalHandlerLogic(val ops: SessionTerminalHandlers, implicit val contex private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor def handleItemTransaction(pkt: ItemTransactionMessage): Unit = { - val ItemTransactionMessage(terminalGuid, transactionType, _, itemName, _, _) = pkt - continent.GUID(terminalGuid) match { - case Some(term: Terminal) if ops.lastTerminalOrderFulfillment => - val msg: String = if (itemName.nonEmpty) s" of $itemName" else "" - log.info(s"${player.Name} is submitting an order - a $transactionType from a ${term.Definition.Name}$msg") - ops.lastTerminalOrderFulfillment = false - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") - term.Actor ! Terminal.Request(player, pkt) - case Some(_: Terminal) => - log.warn(s"Please Wait until your previous order has been fulfilled, ${player.Name}") - case Some(obj) => - log.error(s"ItemTransaction: ${obj.Definition.Name} is not a terminal, ${player.Name}") - case _ => - log.error(s"ItemTransaction: entity with guid=${terminalGuid.guid} does not exist, ${player.Name}") - } + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + ops.handleItemTransaction(pkt) } def handleProximityTerminalUse(pkt: ProximityTerminalUseMessage): Unit = { - val ProximityTerminalUseMessage(_, objectGuid, _) = pkt - continent.GUID(objectGuid) match { - case Some(obj: Terminal with ProximityUnit) => - ops.HandleProximityTerminalUse(obj) - case Some(obj) => - log.warn(s"ProximityTerminalUse: ${obj.Definition.Name} guid=${objectGuid.guid} is not ready to implement proximity effects") - case None => - log.error(s"ProximityTerminalUse: ${player.Name} can not find an object with guid ${objectGuid.guid}") - } + ops.handleProximityTerminalUse(pkt) } def handleFavoritesRequest(pkt: FavoritesRequest): Unit = { - val FavoritesRequest(_, loadoutType, action, line, label) = pkt sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") - action match { - case FavoritesAction.Save => - avatarActor ! AvatarActor.SaveLoadout(player, loadoutType, label, line) - case FavoritesAction.Delete => - avatarActor ! AvatarActor.DeleteLoadout(player, loadoutType, line) - case FavoritesAction.Unknown => - log.warn(s"FavoritesRequest: ${player.Name} requested an unknown favorites action") - } + ops.handleFavoritesRequest(pkt) } /** @@ -117,49 +88,7 @@ class TerminalHandlerLogic(val ops: SessionTerminalHandlers, implicit val contex ops.lastTerminalOrderFulfillment = true case Terminal.BuyVehicle(vehicle, weapons, trunk) => - continent.map.terminalToSpawnPad - .find { case (termid, _) => termid == msg.terminal_guid.guid } - .map { case (a: Int, b: Int) => (continent.GUID(a), continent.GUID(b)) } - .collect { case (Some(term: Terminal), Some(pad: VehicleSpawnPad)) => - avatarActor ! AvatarActor.UpdatePurchaseTime(vehicle.Definition) - vehicle.Faction = tplayer.Faction - vehicle.Position = pad.Position - vehicle.Orientation = pad.Orientation + Vector3.z(pad.Definition.VehicleCreationZOrientOffset) - //default loadout, weapons - val vWeapons = vehicle.Weapons - weapons.foreach { entry => - vWeapons.get(entry.start) match { - case Some(slot) => - entry.obj.Faction = tplayer.Faction - slot.Equipment = None - slot.Equipment = entry.obj - case None => - log.warn( - s"BuyVehicle: ${player.Name} tries to apply default loadout to $vehicle on spawn, but can not find a mounted weapon for ${entry.start}" - ) - } - } - //default loadout, trunk - val vTrunk = vehicle.Trunk - vTrunk.Clear() - trunk.foreach { entry => - entry.obj.Faction = tplayer.Faction - vTrunk.InsertQuickly(entry.start, entry.obj) - } - TaskWorkflow.execute(ops.registerVehicleFromSpawnPad(vehicle, pad, term)) - sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, success = true)) - if (GlobalDefinitions.isBattleFrameVehicle(vehicle.Definition)) { - sendResponse(UnuseItemMessage(player.GUID, msg.terminal_guid)) - } - player.LogActivity(TerminalUsedActivity(AmenitySource(term), msg.transaction_type)) - } - .orElse { - log.error( - s"${tplayer.Name} wanted to spawn a vehicle, but there was no spawn pad associated with terminal ${msg.terminal_guid} to accept it" - ) - sendResponse(ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Buy, success = false)) - None - } + ops.buyVehicle(msg.terminal_guid, msg.transaction_type, vehicle, weapons, trunk) ops.lastTerminalOrderFulfillment = true case Terminal.NoDeal() if msg != null => diff --git a/src/main/scala/net/psforever/actors/session/normal/WeaponAndProjectileLogic.scala b/src/main/scala/net/psforever/actors/session/normal/WeaponAndProjectileLogic.scala index 6d202dc8b..295da46fb 100644 --- a/src/main/scala/net/psforever/actors/session/normal/WeaponAndProjectileLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/WeaponAndProjectileLogic.scala @@ -1,163 +1,41 @@ // Copyright (c) 2024 PSForever package net.psforever.actors.session.normal -import akka.actor.{ActorContext, typed} -import net.psforever.actors.session.AvatarActor +import akka.actor.ActorContext +//import akka.actor.typed +//import net.psforever.actors.session.AvatarActor import net.psforever.actors.session.support.{SessionData, WeaponAndProjectileFunctions, WeaponAndProjectileOperations} -import net.psforever.login.WorldSession.{CountAmmunition, CountGrenades, FindAmmoBoxThatUses, FindEquipmentStock, FindToolThatUses, PutEquipmentInInventoryOrDrop, PutNewEquipmentInInventoryOrDrop, RemoveOldEquipmentFromInventory} -import net.psforever.objects.ballistics.{Projectile, ProjectileQuality} -import net.psforever.objects.definition.ProjectileDefinition -import net.psforever.objects.entity.SimpleWorldEntity -import net.psforever.objects.equipment.{ChargeFireModeDefinition, Equipment, EquipmentSize, FireModeSwitch} -import net.psforever.objects.guid.{GUIDTask, TaskBundle, TaskWorkflow} +import net.psforever.login.WorldSession.{CountGrenades, FindEquipmentStock, FindToolThatUses, RemoveOldEquipmentFromInventory} +import net.psforever.objects.equipment.ChargeFireModeDefinition import net.psforever.objects.inventory.Container -import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} -import net.psforever.objects.serverobject.doors.InteriorDoorPassage -import net.psforever.objects.{AmmoBox, BoomerDeployable, BoomerTrigger, ConstructionItem, Deployables, DummyExplodingEntity, GlobalDefinitions, OwnableByPlayer, PlanetSideGameObject, Player, SpecialEmp, Tool, Tools, Vehicle} -import net.psforever.objects.serverobject.interior.Sidedness -import net.psforever.objects.serverobject.mount.Mountable -import net.psforever.objects.serverobject.structures.Amenity +import net.psforever.objects.{BoomerDeployable, BoomerTrigger, GlobalDefinitions, Player, SpecialEmp, Tool, Tools, Vehicle} import net.psforever.objects.serverobject.turret.{FacilityTurret, VanuSentry} -import net.psforever.objects.serverobject.turret.auto.{AutomatedTurret, AutomatedTurretBehavior} -import net.psforever.objects.sourcing.{PlayerSource, SourceEntry} -import net.psforever.objects.vital.Vitality import net.psforever.objects.vital.base.{DamageResolution, DamageType} -import net.psforever.objects.vital.etc.OicwLilBuddyReason -import net.psforever.objects.vital.interaction.DamageInteraction -import net.psforever.objects.vital.projectile.ProjectileReason import net.psforever.objects.zones.{Zone, ZoneProjectile} -import net.psforever.packet.game.{AIDamage, AvatarGrenadeStateMessage, ChainLashMessage, ChangeAmmoMessage, ChangeFireModeMessage, ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, HitMessage, InventoryStateMessage, LashMessage, LongRangeProjectileInfoMessage, ObjectAttachMessage, ObjectDeleteMessage, ObjectDetachMessage, ProjectileStateMessage, QuantityUpdateMessage, ReloadMessage, SplashHitMessage, UplinkRequest, WeaponDelayFireMessage, WeaponDryFireMessage, WeaponFireMessage, WeaponLazeTargetPositionMessage} +import net.psforever.packet.game.{AIDamage, AvatarGrenadeStateMessage, ChangeAmmoMessage, ChangeFireModeMessage, ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, HitMessage, LashMessage, LongRangeProjectileInfoMessage, ProjectileStateMessage, QuantityUpdateMessage, ReloadMessage, SplashHitMessage, UplinkRequest, WeaponDelayFireMessage, WeaponDryFireMessage, WeaponFireMessage, WeaponLazeTargetPositionMessage} import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} import net.psforever.types.{PlanetSideGUID, Vector3} -import net.psforever.util.Config import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.Future import scala.concurrent.duration._ object WeaponAndProjectileLogic { def apply(ops: WeaponAndProjectileOperations): WeaponAndProjectileLogic = { new WeaponAndProjectileLogic(ops, ops.context) } - - /** - * Does a line segment line intersect with a sphere?
- * This most likely belongs in `Geometry` or `GeometryForm` or somehow in association with the `\objects\geometry\` package. - * @param start first point of the line segment - * @param end second point of the line segment - * @param center center of the sphere - * @param radius radius of the sphere - * @return list of all points of intersection, if any - * @see `Vector3.DistanceSquared` - * @see `Vector3.MagnitudeSquared` - */ - private def quickLineSphereIntersectionPoints( - start: Vector3, - end: Vector3, - center: Vector3, - radius: Float - ): Iterable[Vector3] = { - /* - Algorithm adapted from code found on https://paulbourke.net/geometry/circlesphere/index.html#linesphere, - because I kept messing up proper substitution of the line formula and the circle formula into the quadratic equation. - */ - val Vector3(cx, cy, cz) = center - val Vector3(sx, sy, sz) = start - val vector = end - start - //speed our way through a quadratic equation - val (a, b) = { - val Vector3(dx, dy, dz) = vector - ( - dx * dx + dy * dy + dz * dz, - 2f * (dx * (sx - cx) + dy * (sy - cy) + dz * (sz - cz)) - ) - } - val c = Vector3.MagnitudeSquared(center) + Vector3.MagnitudeSquared(start) - 2f * (cx * sx + cy * sy + cz * sz) - radius * radius - val result = b * b - 4 * a * c - if (result < 0f) { - //negative, no intersection - Seq() - } else if (result < 0.00001f) { - //zero-ish, one intersection point - Seq(start - vector * (b / (2f * a))) - } else { - //positive, two intersection points - val sqrt = math.sqrt(result).toFloat - val endStart = vector / (2f * a) - Seq(start + endStart * (sqrt - b), start + endStart * (b + sqrt) * -1f) - }.filter(p => Vector3.DistanceSquared(start, p) <= a) - } - - /** - * Preparation for explosion damage that utilizes the Scorpion's little buddy sub-projectiles. - * The main difference from "normal" server-side explosion - * is that the owner of the projectile must be clarified explicitly. - * @see `Zone::serverSideDamage` - * @param zone where the explosion is taking place - * (`source` contains the coordinate location) - * @param source a game object that represents the source of the explosion - * @param owner who or what to accredit damage from the explosion to; - * clarifies a normal `SourceEntry(source)` accreditation - */ - private def detonateLittleBuddy( - zone: Zone, - source: PlanetSideGameObject with FactionAffinity with Vitality, - proxy: Projectile, - owner: SourceEntry - )(): Unit = { - Zone.serverSideDamage(zone, source, littleBuddyExplosionDamage(owner, proxy.id, source.Position)) - } - - /** - * Preparation for explosion damage that utilizes the Scorpion's little buddy sub-projectiles. - * The main difference from "normal" server-side explosion - * is that the owner of the projectile must be clarified explicitly. - * The sub-projectiles will be the product of a normal projectile rather than a standard game object - * so a custom `source` entity must wrap around it and fulfill the requirements of the field. - * @see `Zone::explosionDamage` - * @param owner who or what to accredit damage from the explosion to - * @param explosionPosition where the explosion will be positioned in the game world - * @param source a game object that represents the source of the explosion - * @param target a game object that is affected by the explosion - * @return a `DamageInteraction` object - */ - private def littleBuddyExplosionDamage( - owner: SourceEntry, - projectileId: Long, - explosionPosition: Vector3 - ) - ( - source: PlanetSideGameObject with FactionAffinity with Vitality, - target: PlanetSideGameObject with FactionAffinity with Vitality - ): DamageInteraction = { - DamageInteraction(SourceEntry(target), OicwLilBuddyReason(owner, projectileId, target.DamageModel), explosionPosition) - } } class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit val context: ActorContext) extends WeaponAndProjectileFunctions { def sessionLogic: SessionData = ops.sessionLogic - private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor + //private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor /* packets */ def handleWeaponFire(pkt: WeaponFireMessage): Unit = { - val WeaponFireMessage( - _, - weapon_guid, - projectile_guid, - shot_origin, - _, - _, - _, - _/*max_distance,*/, - _, - _/*projectile_type,*/, - thrown_projectile_vel - ) = pkt - HandleWeaponFireOperations(weapon_guid, projectile_guid, shot_origin, thrown_projectile_vel.flatten) + ops.handleWeaponFireOperations(pkt) } def handleWeaponDelayFire(pkt: WeaponDelayFireMessage): Unit = { @@ -166,29 +44,7 @@ class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit } def handleWeaponDryFire(pkt: WeaponDryFireMessage): Unit = { - val WeaponDryFireMessage(weapon_guid) = pkt - val (containerOpt, tools) = ops.FindContainedWeapon - tools - .find { _.GUID == weapon_guid } - .orElse { continent.GUID(weapon_guid) } - .collect { - case _: Equipment if containerOpt.exists(_.isInstanceOf[Player]) => - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.WeaponDryFire(player.GUID, weapon_guid) - ) - case _: Equipment => - continent.VehicleEvents ! VehicleServiceMessage( - continent.id, - VehicleAction.WeaponDryFire(player.GUID, weapon_guid) - ) - } - .orElse { - log.warn( - s"WeaponDryFire: ${player.Name}'s weapon ${weapon_guid.guid} is either not a weapon or does not exist" - ) - None - } + ops.handleWeaponDryFire(pkt) } def handleWeaponLazeTargetPosition(pkt: WeaponLazeTargetPositionMessage): Unit = { @@ -208,8 +64,8 @@ class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit } def handleAvatarGrenadeState(pkt: AvatarGrenadeStateMessage): Unit = { + //grenades are handled elsewhere val AvatarGrenadeStateMessage(_, state) = pkt - //TODO I thought I had this working? log.info(s"${player.Name} has $state ${player.Sex.possessive} grenade") } @@ -274,98 +130,15 @@ class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit } def handleChangeAmmo(pkt: ChangeAmmoMessage): Unit = { - val ChangeAmmoMessage(item_guid, _) = pkt - 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 { - equipment foreach { - case obj: ConstructionItem => - if (Deployables.performConstructionItemAmmoChange(player.avatar.certifications, obj, obj.AmmoTypeIndex)) { - log.info( - s"${player.Name} switched ${player.Sex.possessive} ${obj.Definition.Name} to construct ${obj.AmmoType} (option #${obj.FireModeIndex})" - ) - sendResponse(ChangeAmmoMessage(obj.GUID, obj.AmmoTypeIndex)) - } - case tool: Tool => - thing match { - case Some(player: Player) => - PerformToolAmmoChange(tool, player, ModifyAmmunition(player)) - case Some(mountable: PlanetSideServerObject with Container) => - PerformToolAmmoChange(tool, mountable, ModifyAmmunitionInMountable(mountable)) - case _ => - log.warn(s"ChangeAmmo: the ${thing.get.Definition.Name} in ${player.Name}'s is not the correct type") - } - case obj => - log.warn(s"ChangeAmmo: the ${obj.Definition.Name} in ${player.Name}'s hands does not contain ammunition") - } - } + ops.handleChangeAmmo(pkt) } def handleChangeFireMode(pkt: ChangeFireModeMessage): Unit = { - val ChangeFireModeMessage(item_guid, _/*fire_mode*/) = pkt - sessionLogic.findEquipment(item_guid) match { - case Some(obj: PlanetSideGameObject with FireModeSwitch[_]) => - val originalModeIndex = obj.FireModeIndex - if (obj match { - case citem: ConstructionItem => - val modeChanged = Deployables.performConstructionItemFireModeChange( - player.avatar.certifications, - citem, - originalModeIndex - ) - modeChanged - case _ => - obj.NextFireMode - obj.FireModeIndex != originalModeIndex - }) { - val modeIndex = obj.FireModeIndex - obj match { - case citem: ConstructionItem => - log.info(s"${player.Name} switched ${player.Sex.possessive} ${obj.Definition.Name} to construct ${citem.AmmoType} (mode #$modeIndex)") - case _ => - log.info(s"${player.Name} changed ${player.Sex.possessive} her ${obj.Definition.Name}'s fire mode to #$modeIndex") - } - sendResponse(ChangeFireModeMessage(item_guid, modeIndex)) - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.ChangeFireMode(player.GUID, item_guid, modeIndex) - ) - } - case Some(_) => - log.warn(s"ChangeFireMode: the object that was found for $item_guid does not possess fire modes") - case None => - log.warn(s"ChangeFireMode: can not find $item_guid") - } + ops.handleChangeFireMode(pkt) } def handleProjectileState(pkt: ProjectileStateMessage): Unit = { - val ProjectileStateMessage(projectile_guid, shot_pos, shot_vel, shot_orient, seq, end, target_guid) = pkt - val index = projectile_guid.guid - Projectile.baseUID - ops.projectiles(index) match { - case Some(projectile) if projectile.HasGUID => - val projectileGlobalUID = projectile.GUID - projectile.Position = shot_pos - projectile.Orientation = shot_orient - projectile.Velocity = shot_vel - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.ProjectileState( - player.GUID, - projectileGlobalUID, - shot_pos, - shot_vel, - shot_orient, - seq, - end, - target_guid - ) - ) - case _ if seq == 0 => - /* missing the first packet in the sequence is permissible */ - case _ => - log.warn(s"ProjectileState: constructed projectile ${projectile_guid.guid} can not be found") - } + ops.handleProjectileState(pkt) } def handleLongRangeProjectileState(pkt: LongRangeProjectileInfoMessage): Unit = { @@ -378,296 +151,110 @@ class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit } def handleDirectHit(pkt: HitMessage): Unit = { - val HitMessage( - _, - projectile_guid, - _, - hit_info, - _, - _, - _ - ) = pkt - //find defined projectile - ops.FindProjectileEntry(projectile_guid) match { - case Some(projectile) => - //find target(s) - (hit_info match { - case Some(hitInfo) => - val hitPos = hitInfo.hit_pos - 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) - .filter { target => - Vector3.DistanceSquared(target.Position, hitPos) <= radius - } - targets.map { target => - CheckForHitPositionDiscrepancy(projectile_guid, hitPos, target) - (target, projectile, hitPos, target.Position) - } - - case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) => - CheckForHitPositionDiscrepancy(projectile_guid, hitPos, target) - List((target, projectile, hitInfo.shot_origin, hitPos)) - - case None => - HandleDamageProxy(projectile, projectile_guid, hitPos) - - case _ => - Nil - } - case None => - Nil - }) - .foreach { - case ( - target: PlanetSideGameObject with FactionAffinity with Vitality, - proj: Projectile, - _: Vector3, - hitPos: Vector3 - ) => - ResolveProjectileInteraction(proj, DamageResolution.Hit, target, hitPos).collect { resprojectile => - addShotsLanded(resprojectile.cause.attribution, shots = 1) - sessionLogic.handleDealingDamage(target, resprojectile) - } - case _ => () - } - case None => - log.warn(s"ResolveProjectile: expected projectile, but ${projectile_guid.guid} not found") + val list = ops.composeDirectDamageInformation(pkt) + .collect { + case (target, projectile, hitPos, _) => + ops.checkForHitPositionDiscrepancy(projectile.GUID, hitPos, target) + ops.resolveProjectileInteraction(target, projectile, DamageResolution.Hit, hitPos) + projectile + } + //... + if (list.isEmpty) { + val proxyList = ops + .FindProjectileEntry(pkt.projectile_guid) + .map(projectile => ops.resolveDamageProxy(projectile, projectile.GUID, pkt.hit_info.map(_.hit_pos).getOrElse(Vector3.Zero))) + .getOrElse(Nil) + proxyList.collectFirst { + case (_, proxy, _, _) if proxy.tool_def == GlobalDefinitions.oicw => + ops.performLittleBuddyExplosion(proxyList.map(_._2)) + } + proxyList.foreach { + case (target, proxy, hitPos, _) if proxy.tool_def == GlobalDefinitions.oicw => + ops.checkForHitPositionDiscrepancy(proxy.GUID, hitPos, target) + } } } def handleSplashHit(pkt: SplashHitMessage): Unit = { - val SplashHitMessage( - _, - projectile_guid, - explosion_pos, - direct_victim_uid, - _, - projectile_vel, - _, - targets - ) = pkt - ops.FindProjectileEntry(projectile_guid) match { - case Some(projectile) => - val profile = projectile.profile - projectile.Velocity = projectile_vel - val (resolution1, resolution2) = profile.Aggravated match { - case Some(_) if profile.ProjectileDamageTypes.contains(DamageType.Aggravated) => - (DamageResolution.AggravatedDirect, DamageResolution.AggravatedSplash) - case _ => - (DamageResolution.Splash, DamageResolution.Splash) + val list = ops.composeSplashDamageInformation(pkt) + if (list.nonEmpty) { + val projectile = list.head._2 + val explosionPosition = projectile.Position + val projectileGuid = projectile.GUID + val profile = projectile.profile + val (resolution1, resolution2) = profile.Aggravated match { + case Some(_) if profile.ProjectileDamageTypes.contains(DamageType.Aggravated) => + (DamageResolution.AggravatedDirect, DamageResolution.AggravatedSplash) + case _ => + (DamageResolution.Splash, DamageResolution.Splash) + } + //... + val (direct, others) = list.partition { case (_, _, hitPos, targetPos) => hitPos == targetPos } + direct.foreach { + case (target, _, hitPos, _) => + ops.checkForHitPositionDiscrepancy(projectile.GUID, hitPos, target) + ops.resolveProjectileInteraction(target, projectile, resolution1, hitPos) + } + others.foreach { + case (target, _, hitPos, _) => + ops.checkForHitPositionDiscrepancy(projectile.GUID, hitPos, target) + ops.resolveProjectileInteraction(target, projectile, resolution2, hitPos) + } + //... + val proxyList = ops.resolveDamageProxy(projectile, projectileGuid, explosionPosition) + if (proxyList.nonEmpty) { + proxyList.foreach { + case (target, _, hitPos, _) => ops.checkForHitPositionDiscrepancy(projectileGuid, hitPos, target) } - //direct_victim_uid - sessionLogic.validObject(direct_victim_uid, decorator = "SplashHit/direct_victim") match { - case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) => - CheckForHitPositionDiscrepancy(projectile_guid, explosion_pos, target) - ResolveProjectileInteraction(projectile, resolution1, target, target.Position).collect { resprojectile => - addShotsLanded(resprojectile.cause.attribution, shots = 1) - sessionLogic.handleDealingDamage(target, resprojectile) - } - case _ => () + if (profile == GlobalDefinitions.oicw_projectile) { + ops.performLittleBuddyExplosion(proxyList.map(_._2)) } - //other victims - targets.foreach(elem => { - sessionLogic.validObject(elem.uid, decorator = "SplashHit/other_victims") match { - case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) => - CheckForHitPositionDiscrepancy(projectile_guid, explosion_pos, target) - ResolveProjectileInteraction(projectile, resolution2, target, explosion_pos).collect { resprojectile => - addShotsLanded(resprojectile.cause.attribution, shots = 1) - sessionLogic.handleDealingDamage(target, resprojectile) - } - case _ => () - } - }) - //... - HandleDamageProxy(projectile, projectile_guid, explosion_pos) - if ( - projectile.profile.HasJammedEffectDuration || - projectile.profile.JammerProjectile || - projectile.profile.SympatheticExplosion - ) { - //can also substitute 'projectile.profile' for 'SpecialEmp.emp' - Zone.serverSideDamage( - continent, - player, - SpecialEmp.emp, - SpecialEmp.createEmpInteraction(SpecialEmp.emp, explosion_pos), - SpecialEmp.prepareDistanceCheck(player, explosion_pos, player.Faction), - SpecialEmp.findAllBoomers(profile.DamageRadius) - ) - } - if (profile.ExistsOnRemoteClients && projectile.HasGUID) { - //cleanup - continent.Projectile ! ZoneProjectile.Remove(projectile.GUID) - } - case None => () + } + //... + if ( + profile.HasJammedEffectDuration || + profile.JammerProjectile || + profile.SympatheticExplosion + ) { + //can also substitute 'profile' for 'SpecialEmp.emp' + Zone.serverSideDamage( + continent, + player, + SpecialEmp.emp, + SpecialEmp.createEmpInteraction(SpecialEmp.emp, explosionPosition), + SpecialEmp.prepareDistanceCheck(player, explosionPosition, player.Faction), + SpecialEmp.findAllBoomers(profile.DamageRadius) + ) + } + if (profile.ExistsOnRemoteClients && projectile.HasGUID) { + //cleanup + continent.Projectile ! ZoneProjectile.Remove(projectile.GUID) + } } } def handleLashHit(pkt: LashMessage): Unit = { - val LashMessage(_, _, victim_guid, projectile_guid, hit_pos, _) = pkt - 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) - sessionLogic.handleDealingDamage(target, resprojectile) - } - case _ => () + val list = ops.composeLashDamageInformation(pkt) + list.foreach { + case (target, projectile, hitPos, _) => + ops.checkForHitPositionDiscrepancy(projectile.GUID, hitPos, target) + ops.resolveProjectileInteraction(target, projectile, DamageResolution.Lash, hitPos) } } def handleAIDamage(pkt: AIDamage): Unit = { - val AIDamage(targetGuid, attackerGuid, projectileTypeId, _, _) = pkt - (continent.GUID(player.VehicleSeated) match { - case Some(tobj: PlanetSideServerObject with FactionAffinity with Vitality with OwnableByPlayer) - if tobj.GUID == targetGuid && - tobj.OwnerGuid.contains(player.GUID) => - //deployable turrets - Some(tobj) - case Some(tobj: PlanetSideServerObject with FactionAffinity with Vitality with Mountable) - if tobj.GUID == targetGuid && - tobj.Seats.values.flatMap(_.occupants.map(_.GUID)).toSeq.contains(player.GUID) => - //facility turrets, etc. - Some(tobj) - case _ - if player.GUID == targetGuid => - //player avatars - Some(player) - case _ => - None - }).collect { - case target: AutomatedTurret.Target => - sessionLogic.validObject(attackerGuid, decorator = "AIDamage/AutomatedTurret") - .collect { - case turret: AutomatedTurret if turret.Target.isEmpty => - turret.Actor ! AutomatedTurretBehavior.ConfirmShot(target) - Some(target) - - case turret: AutomatedTurret => - turret.Actor ! AutomatedTurretBehavior.ConfirmShot(target) - HandleAIDamage(target, CompileAutomatedTurretDamageData(turret, projectileTypeId)) - Some(target) - } - } - .orElse { - //occasionally, something that is not technically a turret's natural target may be attacked - continent.GUID(targetGuid) //AIDamage/Attacker - .collect { - case target: PlanetSideServerObject with FactionAffinity with Vitality => - sessionLogic.validObject(attackerGuid, decorator = "AIDamage/Attacker") - .collect { - case turret: AutomatedTurret if turret.Target.nonEmpty => - //the turret must be shooting at something (else) first - HandleAIDamage(target, CompileAutomatedTurretDamageData(turret, projectileTypeId)) - } - Some(target) - } + val list = ops.composeAIDamageInformation(pkt) + if (ops.confirmAIDamageTarget(pkt, list.map(_._1))) { + list.foreach { + case (target, projectile, hitPos, _) => + ops.checkForHitPositionDiscrepancy(projectile.GUID, hitPos, target) + ops.resolveProjectileInteraction(target, projectile, DamageResolution.Hit, hitPos) } + } } /* support code */ - private def HandleWeaponFireOperations( - weaponGUID: PlanetSideGUID, - projectileGUID: PlanetSideGUID, - shotOrigin: Vector3, - shotVelocity: Option[Vector3] - ): Unit = { - ops.HandleWeaponFireAccountability(weaponGUID, projectileGUID) match { - case (Some(obj), Some(tool)) => - val projectileIndex = projectileGUID.guid - Projectile.baseUID - val projectilePlace = ops.projectiles(projectileIndex) - if ( - projectilePlace match { - case Some(projectile) => - !projectile.isResolved && System.currentTimeMillis() - projectile.fire_time < projectile.profile.Lifespan.toLong - case None => - false - } - ) { - log.debug( - s"WeaponFireMessage: overwriting unresolved projectile ${projectileGUID.guid}, known to ${player.Name}" - ) - } - val (angle, attribution, acceptableDistanceToOwner) = obj match { - case p: Player => - ( - SimpleWorldEntity.validateOrientationEntry( - p.Orientation + Vector3.z(p.FacingYawUpper) - ), - tool.Definition.ObjectId, - 10f + (if (p.Velocity.nonEmpty) { - 5f - } else { - 0f - }) - ) - case v: Vehicle if v.Definition.CanFly => - (tool.Orientation, obj.Definition.ObjectId, 1000f) //TODO this is too simplistic to find proper angle - case _: Vehicle => - (tool.Orientation, obj.Definition.ObjectId, 225f) //TODO this is too simplistic to find proper angle - case _ => - (obj.Orientation, obj.Definition.ObjectId, 300f) - } - val distanceToOwner = Vector3.DistanceSquared(shotOrigin, player.Position) - if (distanceToOwner <= acceptableDistanceToOwner) { - val projectile_info = tool.Projectile - val wguid = weaponGUID.guid - val mountedIn = (continent.turretToWeapon - .find { case (guid, _) => guid == wguid } match { - case Some((_, turretGuid)) => Some(( - turretGuid, - continent.GUID(turretGuid).collect { case o: PlanetSideGameObject with FactionAffinity => SourceEntry(o) } - )) - case _ => None - }) match { - case Some((guid, Some(entity))) => Some((guid, entity)) - case _ => None - } - val projectile = new Projectile( - projectile_info, - tool.Definition, - tool.FireMode, - mountedIn, - PlayerSource(player), - attribution, - shotOrigin, - angle, - shotVelocity - ) - val initialQuality = tool.FireMode match { - case mode: ChargeFireModeDefinition => - ProjectileQuality.Modified( - { - val timeInterval = projectile.fire_time - ops.shootingStart.getOrElse(tool.GUID, System.currentTimeMillis()) - timeInterval.toFloat / mode.Time.toFloat - } - ) - case _ => - ProjectileQuality.Normal - } - val qualityprojectile = projectile.quality(initialQuality) - qualityprojectile.WhichSide = player.WhichSide - ops.projectiles(projectileIndex) = Some(qualityprojectile) - if (projectile_info.ExistsOnRemoteClients) { - log.trace( - s"WeaponFireMessage: ${player.Name}'s ${projectile_info.Name} is a remote projectile" - ) - continent.Projectile ! ZoneProjectile.Add(player.GUID, qualityprojectile) - } - } else { - log.warn( - s"WeaponFireMessage: ${player.Name}'s ${tool.Definition.Name} projectile is too far from owner position at time of discharge ($distanceToOwner > $acceptableDistanceToOwner); suspect" - ) - } - - case _ => () - } - } - /** * After a weapon has finished shooting, determine if it needs to be sorted in a special way. * @param tool a weapon @@ -691,11 +278,11 @@ class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit RemoveOldEquipmentFromInventory(player)(x.obj) sumReloadValue } else { - ModifyAmmunition(player)(box.AmmoSlot.Box, 3 - tailReloadValue) + ops.modifyAmmunition(player)(box.AmmoSlot.Box, 3 - tailReloadValue) 3 } log.info(s"${player.Name} found $actualReloadValue more $ammoType grenades to throw") - ModifyAmmunition(player)( + ops.modifyAmmunition(player)( tool.AmmoSlot.Box, -actualReloadValue ) //grenade item already in holster (negative because empty) @@ -706,354 +293,6 @@ class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit } } - /** - * Given an object that contains a box of amunition in its `Inventory` at a certain location, - * change the amount of ammunition within that box. - * @param obj the `Container` - * @param box an `AmmoBox` to modify - * @param reloadValue the value to modify the `AmmoBox`; - * subtracted from the current `Capacity` of `Box` - */ - private def ModifyAmmunition(obj: PlanetSideGameObject with Container)(box: AmmoBox, reloadValue: Int): Unit = { - val capacity = box.Capacity - reloadValue - box.Capacity = capacity - sendResponse(InventoryStateMessage(box.GUID, obj.GUID, capacity)) - } - - /** - * Given a vehicle that contains a box of ammunition in its `Trunk` at a certain location, - * change the amount of ammunition within that box. - * @param obj the `Container` - * @param box an `AmmoBox` to modify - * @param reloadValue the value to modify the `AmmoBox`; - * subtracted from the current `Capacity` of `Box` - */ - private def ModifyAmmunitionInMountable(obj: PlanetSideServerObject with Container)(box: AmmoBox, reloadValue: Int): Unit = { - ModifyAmmunition(obj)(box, reloadValue) - obj.Find(box).collect { index => - continent.VehicleEvents ! VehicleServiceMessage( - s"${obj.Actor}", - VehicleAction.InventoryState( - player.GUID, - box, - obj.GUID, - index, - box.Definition.Packet.DetailedConstructorData(box).get - ) - ) - } - } - - /** - * na - * @param tool na - * @param obj na - */ - private def PerformToolAmmoChange( - tool: Tool, - obj: PlanetSideServerObject with Container, - modifyFunc: (AmmoBox, Int) => Unit - ): Unit = { - val originalAmmoType = tool.AmmoType - do { - val requestedAmmoType = tool.NextAmmoType - val fullMagazine = tool.MaxMagazine - if (requestedAmmoType != tool.AmmoSlot.Box.AmmoType) { - FindEquipmentStock(obj, FindAmmoBoxThatUses(requestedAmmoType), fullMagazine, CountAmmunition).reverse match { - case Nil => () - case x :: xs => - val stowNewFunc: Equipment => TaskBundle = PutNewEquipmentInInventoryOrDrop(obj) - val stowFunc: Equipment => Future[Any] = PutEquipmentInInventoryOrDrop(obj) - - xs.foreach(item => { - obj.Inventory -= item.start - sendResponse(ObjectDeleteMessage(item.obj.GUID, 0)) - TaskWorkflow.execute(GUIDTask.unregisterObject(continent.GUID, item.obj)) - }) - - //box will be the replacement ammo; give it the discovered magazine and load it into the weapon - val box = x.obj.asInstanceOf[AmmoBox] - //previousBox is the current magazine in tool; it will be removed from the weapon - val previousBox = tool.AmmoSlot.Box - val originalBoxCapacity = box.Capacity - val tailReloadValue: Int = if (xs.isEmpty) { - 0 - } else { - xs.map(_.obj.asInstanceOf[AmmoBox].Capacity).sum - } - val sumReloadValue: Int = originalBoxCapacity + tailReloadValue - val ammoSlotIndex = tool.FireMode.AmmoSlotIndex - val box_guid = box.GUID - val tool_guid = tool.GUID - obj.Inventory -= x.start //remove replacement ammo from inventory - tool.AmmoSlots(ammoSlotIndex).Box = box //put replacement ammo in tool - sendResponse(ObjectDetachMessage(tool_guid, previousBox.GUID, Vector3.Zero, 0f)) - sendResponse(ObjectDetachMessage(obj.GUID, box_guid, Vector3.Zero, 0f)) - sendResponse(ObjectAttachMessage(tool_guid, box_guid, ammoSlotIndex)) - - //announce swapped ammunition box in weapon - val previous_box_guid = previousBox.GUID - val boxDef = box.Definition - sendResponse(ChangeAmmoMessage(tool_guid, box.Capacity)) - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.ChangeAmmo( - player.GUID, - tool_guid, - ammoSlotIndex, - previous_box_guid, - boxDef.ObjectId, - box.GUID, - boxDef.Packet.ConstructorData(box).get - ) - ) - - //handle inventory contents - box.Capacity = if (sumReloadValue <= fullMagazine) { - sumReloadValue - } else { - val splitReloadAmmo: Int = sumReloadValue - fullMagazine - log.trace( - s"PerformToolAmmoChange: ${player.Name} takes ${originalBoxCapacity - splitReloadAmmo} from a box of $originalBoxCapacity $requestedAmmoType ammo" - ) - val boxForInventory = AmmoBox(box.Definition, splitReloadAmmo) - TaskWorkflow.execute(stowNewFunc(boxForInventory)) - fullMagazine - } - sendResponse( - InventoryStateMessage(box.GUID, tool.GUID, box.Capacity) - ) //should work for both players and vehicles - log.info(s"${player.Name} loads ${box.Capacity} $requestedAmmoType into the ${tool.Definition.Name}") - if (previousBox.Capacity > 0) { - //divide capacity across other existing and not full boxes of that ammo type - var capacity = previousBox.Capacity - val iter = obj.Inventory.Items - .filter(entry => { - entry.obj match { - case item: AmmoBox => - item.AmmoType == originalAmmoType && item.FullCapacity != item.Capacity - case _ => - false - } - }) - .sortBy(_.start) - .iterator - while (capacity > 0 && iter.hasNext) { - val entry = iter.next() - val item: AmmoBox = entry.obj.asInstanceOf[AmmoBox] - val ammoAllocated = math.min(item.FullCapacity - item.Capacity, capacity) - log.info(s"${player.Name} put $ammoAllocated back into a box of ${item.Capacity} $originalAmmoType") - capacity -= ammoAllocated - modifyFunc(item, -ammoAllocated) - } - previousBox.Capacity = capacity - } - - if (previousBox.Capacity > 0) { - //split previousBox into AmmoBox objects of appropriate max capacity, e.g., 100 9mm -> 2 x 50 9mm - obj.Inventory.Fit(previousBox) match { - case Some(_) => - stowFunc(previousBox) - case None => - sessionLogic.general.normalItemDrop(player, continent)(previousBox) - } - AmmoBox.Split(previousBox) match { - case Nil | List(_) => () //done (the former case is technically not possible) - case _ :: toUpdate => - modifyFunc(previousBox, 0) //update to changed capacity value - toUpdate.foreach(box => { TaskWorkflow.execute(stowNewFunc(box)) }) - } - } else { - TaskWorkflow.execute(GUIDTask.unregisterObject(continent.GUID, previousBox)) - } - } - } - } while (tool.AmmoType != originalAmmoType && tool.AmmoType != tool.AmmoSlot.Box.AmmoType) - } - - private def CheckForHitPositionDiscrepancy( - projectile_guid: PlanetSideGUID, - hitPos: Vector3, - target: PlanetSideGameObject with FactionAffinity with Vitality - ): Unit = { - val hitPositionDiscrepancy = Vector3.DistanceSquared(hitPos, target.Position) - if (hitPositionDiscrepancy > Config.app.antiCheat.hitPositionDiscrepancyThreshold) { - // If the target position on the server does not match the position where the projectile landed within reason there may be foul play - log.warn( - s"${player.Name}'s shot #${projectile_guid.guid} has hit discrepancy with target. Target: ${target.Position}, Reported: $hitPos, Distance: $hitPositionDiscrepancy / ${math.sqrt(hitPositionDiscrepancy).toFloat}; suspect" - ) - } - } - - /** - * Find a projectile with the given globally unique identifier and mark it as a resolved shot. - * A `Resolved` shot has either encountered an obstacle or is being cleaned up for not finding an obstacle. - * @param projectile_guid the projectile GUID - * @param resolution the resolution status to promote the projectile - * @return the projectile - */ - private def ResolveProjectileInteraction( - projectile_guid: PlanetSideGUID, - resolution: DamageResolution.Value, - target: PlanetSideGameObject with FactionAffinity with Vitality, - pos: Vector3 - ): Option[DamageInteraction] = { - ops.FindProjectileEntry(projectile_guid) match { - case Some(projectile) => - ResolveProjectileInteraction(projectile, resolution, target, pos) - case None => - log.trace(s"ResolveProjectile: ${player.Name} expected projectile, but ${projectile_guid.guid} not found") - None - } - } - - /** - * na - * @param projectile the projectile object - * @param resolution the resolution status to promote the projectile - * @return a copy of the projectile - */ - private def ResolveProjectileInteraction( - projectile: Projectile, - resolution: DamageResolution.Value, - target: PlanetSideGameObject with FactionAffinity with Vitality, - pos: Vector3 - ): Option[DamageInteraction] = { - if (projectile.isMiss) { - log.warn("expected projectile was already counted as a missed shot; can not resolve any further") - None - } else { - val outProjectile = ProjectileQuality.modifiers(projectile, resolution, target, pos, Some(player)) - if (projectile.tool_def.Size == EquipmentSize.Melee && outProjectile.quality == ProjectileQuality.Modified(25)) { - avatarActor ! AvatarActor.ConsumeStamina(10) - } - Some(DamageInteraction(SourceEntry(target), ProjectileReason(resolution, outProjectile, target.DamageModel), pos)) - } - } - - /** - * Take a projectile that was introduced into the game world and - * determine if it generates a secondary damage projectile or - * an method of damage causation that requires additional management. - * @param projectile the projectile - * @param pguid the client-local projectile identifier - * @param hitPos the game world position where the projectile is being recorded - * @return a for all affected targets, a combination of projectiles, projectile location, and the target's location; - * nothing if no targets were affected - */ - private def HandleDamageProxy( - projectile: Projectile, - pguid: PlanetSideGUID, - hitPos: Vector3 - ): List[(PlanetSideGameObject with FactionAffinity with Vitality, Projectile, Vector3, Vector3)] = { - GlobalDefinitions.getDamageProxy(projectile, hitPos) match { - case Nil => - Nil - case list if list.isEmpty => - Nil - case list => - HandleDamageProxySetupLittleBuddy(list, hitPos) - UpdateProjectileSidednessAfterHit(projectile, hitPos) - val projectileSide = projectile.WhichSide - list.flatMap { proxy => - if (proxy.profile.ExistsOnRemoteClients) { - proxy.Position = hitPos - proxy.WhichSide = projectileSide - continent.Projectile ! ZoneProjectile.Add(player.GUID, proxy) - Nil - } else if (proxy.tool_def == GlobalDefinitions.maelstrom) { - //server-side maelstrom grenade target selection - val radius = proxy.profile.LashRadius * proxy.profile.LashRadius - val targets = Zone.findAllTargets(continent, hitPos, proxy.profile.LashRadius, { _.livePlayerList }) - .filter { target => - Vector3.DistanceSquared(target.Position, hitPos) <= radius - } - //chainlash is separated from the actual damage application for convenience - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.SendResponse( - PlanetSideGUID(0), - ChainLashMessage( - hitPos, - projectile.profile.ObjectId, - targets.map { _.GUID } - ) - ) - ) - targets.map { target => - CheckForHitPositionDiscrepancy(pguid, hitPos, target) - (target, proxy, hitPos, target.Position) - } - } else { - Nil - } - } - } - } - - private def HandleDamageProxySetupLittleBuddy(listOfProjectiles: List[Projectile], detonationPosition: Vector3): Boolean = { - val listOfLittleBuddies: List[Projectile] = listOfProjectiles.filter { _.tool_def == GlobalDefinitions.oicw } - val size: Int = listOfLittleBuddies.size - if (size > 0) { - val desiredDownwardsProjectiles: Int = 2 - val firstHalf: Int = math.min(size, desiredDownwardsProjectiles) //number that fly straight down - val secondHalf: Int = math.max(size - firstHalf, 0) //number that are flared out - val z: Float = player.Orientation.z //player's standing direction - val north: Vector3 = Vector3(0,1,0) //map North - val speed: Float = 144f //speed (packet discovered) - val dist: Float = 25 //distance (client defined) - val downwardsAngle: Float = -85f - val flaredAngle: Float = -70f - //angle of separation for downwards, degrees from vertical for flared out - val (smallStep, smallAngle): (Float, Float) = if (firstHalf > 1) { - (360f / firstHalf, downwardsAngle) - } else { - (0f, 0f) - } - val (largeStep, largeAngle): (Float, Float) = if (secondHalf > 1) { - (360f / secondHalf, flaredAngle) - } else { - (0f, 0f) - } - val smallRotOffset: Float = z + 90f - val largeRotOffset: Float = z + math.random().toFloat * 45f - val verticalCorrection = Vector3.z(dist - dist * math.sin(math.toRadians(90 - smallAngle + largeAngle)).toFloat) - //downwards projectiles - var i: Int = 0 - listOfLittleBuddies.take(firstHalf).foreach { proxy => - val facing = (smallRotOffset + smallStep * i.toFloat) % 360 - val dir = north.Rx(smallAngle).Rz(facing) - proxy.Position = detonationPosition + dir.xy + verticalCorrection - proxy.Velocity = dir * speed - proxy.Orientation = Vector3(0, (360f + smallAngle) % 360, facing) - HandleDamageProxyLittleBuddyExplosion(proxy, dir, dist) - i += 1 - } - //flared out projectiles - i = 0 - listOfLittleBuddies.drop(firstHalf).foreach { proxy => - val facing = (largeRotOffset + largeStep * i.toFloat) % 360 - val dir = north.Rx(largeAngle).Rz(facing) - proxy.Position = detonationPosition + dir - proxy.Velocity = dir * speed - proxy.Orientation = Vector3(0, (360f + largeAngle) % 360, facing) - HandleDamageProxyLittleBuddyExplosion(proxy, dir, dist) - i += 1 - } - true - } else { - false - } - } - - private def HandleDamageProxyLittleBuddyExplosion(proxy: Projectile, orientation: Vector3, distance: Float): Unit = { - //explosion - val obj = new DummyExplodingEntity(proxy, proxy.owner.Faction) - obj.Position = obj.Position + orientation * distance - val explosionFunc: ()=>Unit = WeaponAndProjectileLogic.detonateLittleBuddy(continent, obj, proxy, proxy.owner) - context.system.scheduler.scheduleOnce(500.milliseconds) { explosionFunc() } - } - /* used by ChangeFireStateMessage_Start handling */ @@ -1095,19 +334,15 @@ class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit ) } - private def allowFireStateChangeStart(tool: Tool, itemGuid: PlanetSideGUID): Boolean = { - tool.FireMode.RoundsPerShot == 0 || tool.Magazine > 0 || ops.prefire.contains(itemGuid) - } - private def enforceEmptyMagazine(tool: Tool, itemGuid: PlanetSideGUID): Unit = { log.warn( s"ChangeFireState_Start: ${player.Name}'s ${tool.Definition.Name} magazine was empty before trying to shoot" ) - ops.EmptyMagazine(itemGuid, tool) + ops.emptyMagazine(itemGuid, tool) } private def fireStateStartWhenPlayer(tool: Tool, itemGuid: PlanetSideGUID): Unit = { - if (allowFireStateChangeStart(tool, itemGuid)) { + if (ops.allowFireStateChangeStart(tool, itemGuid)) { fireStateStartSetup(itemGuid) //special case - suppress the decimator's alternate fire mode, by projectile if (tool.Projectile != GlobalDefinitions.phoenix_missile_guided_projectile) { @@ -1120,7 +355,7 @@ class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit } private def fireStateStartWhenMounted(tool: Tool, itemGuid: PlanetSideGUID): Unit = { - if (allowFireStateChangeStart(tool, itemGuid)) { + if (ops.allowFireStateChangeStart(tool, itemGuid)) { fireStateStartSetup(itemGuid) fireStateStartMountedMessages(itemGuid) fireStateStartChargeMode(tool) @@ -1178,70 +413,19 @@ class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit ) } - private def handleReloadProcedure( - itemGuid: PlanetSideGUID, - obj: PlanetSideGameObject with Container, - tools: Set[Tool], - unk1: Int, - deleteFunc: Equipment => Future[Any], - modifyFunc: (AmmoBox, Int) => Unit, - messageFunc: PlanetSideGUID => Unit - ): Unit = { - tools - .filter { _.GUID == itemGuid } - .foreach { tool => - val currentMagazine : Int = tool.Magazine - val magazineSize : Int = tool.MaxMagazine - val reloadValue : Int = magazineSize - currentMagazine - if (magazineSize > 0 && reloadValue > 0) { - FindEquipmentStock(obj, FindAmmoBoxThatUses(tool.AmmoType), reloadValue, CountAmmunition).reverse match { - case Nil => () - case x :: xs => - xs.foreach { item => deleteFunc(item.obj) } - val box = x.obj.asInstanceOf[AmmoBox] - val tailReloadValue : Int = if (xs.isEmpty) { - 0 - } - else { - xs.map(_.obj.asInstanceOf[AmmoBox].Capacity).sum - } - val sumReloadValue : Int = box.Capacity + tailReloadValue - val actualReloadValue = if (sumReloadValue <= reloadValue) { - deleteFunc(box) - sumReloadValue - } - else { - modifyFunc(box, reloadValue - tailReloadValue) - reloadValue - } - val finalReloadValue = actualReloadValue + currentMagazine - log.info( - s"${player.Name} successfully reloaded $reloadValue ${tool.AmmoType} into ${tool.Definition.Name}" - ) - tool.Magazine = finalReloadValue - sendResponse(ReloadMessage(itemGuid, finalReloadValue, unk1)) - messageFunc(itemGuid) - } - } else { - //the weapon can not reload due to full magazine; the UI for the magazine is obvious bugged, so fix it - sendResponse(QuantityUpdateMessage(tool.AmmoSlot.Box.GUID, magazineSize)) - } - } - } - private def handleReloadWhenPlayer( itemGuid: PlanetSideGUID, obj: Player, tools: Set[Tool], unk1: Int ): Unit = { - handleReloadProcedure( + ops.handleReloadProcedure( itemGuid, obj, tools, unk1, RemoveOldEquipmentFromInventory(obj)(_), - ModifyAmmunition(obj)(_, _), + ops.modifyAmmunition(obj)(_, _), reloadPlayerMessages ) } @@ -1252,128 +436,14 @@ class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit tools: Set[Tool], unk1: Int ): Unit = { - handleReloadProcedure( + ops.handleReloadProcedure( itemGuid, obj, tools, unk1, RemoveOldEquipmentFromInventory(obj)(_), - ModifyAmmunitionInMountable(obj)(_, _), + ops.modifyAmmunitionInMountable(obj)(_, _), reloadVehicleMessages ) } - - //noinspection SameParameterValue - private def addShotsLanded(weaponId: Int, shots: Int): Unit = { - ops.addShotsToMap(ops.shotsLanded, weaponId, shots) - } - - private def CompileAutomatedTurretDamageData( - turret: AutomatedTurret, - projectileTypeId: Long - ): Option[(AutomatedTurret, Tool, SourceEntry, ProjectileDefinition)] = { - turret match { - case tOwner: OwnableByPlayer => - CompileAutomatedTurretDamageData( - turret, - CompileAutomatedTurretOwnableBlame(tOwner), - projectileTypeId - ) - case tAmenity: Amenity => - CompileAutomatedTurretDamageData( - turret, - CompileAutomatedTurretAmenityBlame(tAmenity), - projectileTypeId - ) - case _ => - None - } - } - - private def CompileAutomatedTurretOwnableBlame(turret: AutomatedTurret with OwnableByPlayer): SourceEntry = { - Deployables.AssignBlameTo(continent, turret.OwnerName, SourceEntry(turret)) - } - - private def CompileAutomatedTurretAmenityBlame(turret: AutomatedTurret with Amenity): SourceEntry = { - turret - .Seats - .values - .flatMap(_.occupant) - .collectFirst(SourceEntry(_)) - .getOrElse(SourceEntry(turret.Owner)) - } - - private def CompileAutomatedTurretDamageData( - turret: AutomatedTurret, - blame: SourceEntry, - projectileTypeId: Long - ): Option[(AutomatedTurret, Tool, SourceEntry, ProjectileDefinition)] = { - turret.Weapons - .values - .flatMap { _.Equipment } - .collect { - case weapon: Tool => (turret, weapon, blame, weapon.Projectile) - } - .find { case (_, _, _, p) => p.ObjectId == projectileTypeId } - } - - private def HandleAIDamage( - target: PlanetSideServerObject with FactionAffinity with Vitality, - results: Option[(AutomatedTurret, Tool, SourceEntry, ProjectileDefinition)] - ): Unit = { - results.collect { - case (obj, tool, owner, projectileInfo) => - val angle = Vector3.Unit(target.Position - obj.Position) - val proj = new Projectile( - projectileInfo, - tool.Definition, - tool.FireMode, - None, - owner, - obj.Definition.ObjectId, - obj.Position + Vector3.z(value = 1f), - angle, - Some(angle * projectileInfo.FinalVelocity) - ) - val hitPos = target.Position + Vector3.z(value = 1f) - ResolveProjectileInteraction(proj, DamageResolution.Hit, target, hitPos).collect { resprojectile => - addShotsLanded(resprojectile.cause.attribution, shots = 1) - sessionLogic.handleDealingDamage(target, resprojectile) - } - } - } - - private def UpdateProjectileSidednessAfterHit(projectile: Projectile, hitPosition: Vector3): Unit = { - val origin = projectile.Position - val distance = Vector3.Magnitude(hitPosition - origin) - continent.blockMap - .sector(hitPosition, distance) - .environmentList - .collect { case o: InteriorDoorPassage => - val door = o.door - val intersectTest = WeaponAndProjectileLogic.quickLineSphereIntersectionPoints( - origin, - hitPosition, - door.Position, - door.Definition.UseRadius + 0.1f - ) - (door, intersectTest) - } - .collect { case (door, intersectionTest) if intersectionTest.nonEmpty => - (door, Vector3.Magnitude(hitPosition - door.Position), intersectionTest) - } - .minByOption { case (_, dist, _) => dist } - .foreach { case (door, _, intersects) => - val strictly = if (Vector3.DotProduct(Vector3.Unit(hitPosition - door.Position), door.Outwards) > 0f) { - Sidedness.OutsideOf - } else { - Sidedness.InsideOf - } - projectile.WhichSide = if (intersects.size == 1) { - Sidedness.InBetweenSides(door, strictly) - } else { - strictly - } - } - } } diff --git a/src/main/scala/net/psforever/actors/session/spectator/AvatarHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/spectator/AvatarHandlerLogic.scala index e6ef12b6d..67a569c23 100644 --- a/src/main/scala/net/psforever/actors/session/spectator/AvatarHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/spectator/AvatarHandlerLogic.scala @@ -235,10 +235,6 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A sendResponse(ItemTransactionResultMessage(terminalGuid, action, result)) sessionLogic.terminals.lastTerminalOrderFulfillment = true AvatarActor.savePlayerData(player) - sessionLogic.general.renewCharSavedTimer( - Config.app.game.savedMsg.interruptedByAction.fixed, - Config.app.game.savedMsg.interruptedByAction.variable - ) case AvatarResponse.TerminalOrderResult(terminalGuid, action, result) => sendResponse(ItemTransactionResultMessage(terminalGuid, action, result)) @@ -452,7 +448,6 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A sessionLogic.shooting.shotsWhileDead = 0 } sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason(msg = "cancel") - sessionLogic.general.renewCharSavedTimer(fixedLen = 1800L, varLen = 0L) //player state changes AvatarActor.updateToolDischargeFor(avatar) diff --git a/src/main/scala/net/psforever/actors/session/spectator/ChatLogic.scala b/src/main/scala/net/psforever/actors/session/spectator/ChatLogic.scala index a7e5884e2..4f5fbe3d3 100644 --- a/src/main/scala/net/psforever/actors/session/spectator/ChatLogic.scala +++ b/src/main/scala/net/psforever/actors/session/spectator/ChatLogic.scala @@ -31,11 +31,9 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext case (CMT_FLY, recipient, contents) => ops.commandFly(contents, recipient) - case (CMT_ANONYMOUS, _, _) => - // ? + case (CMT_ANONYMOUS, _, _) => () - case (CMT_TOGGLE_GM, _, _) => - // ? + case (CMT_TOGGLE_GM, _, _) => () case (CMT_CULLWATERMARK, _, contents) => ops.commandWatermark(contents) diff --git a/src/main/scala/net/psforever/actors/session/spectator/GalaxyHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/spectator/GalaxyHandlerLogic.scala deleted file mode 100644 index d3b6abf63..000000000 --- a/src/main/scala/net/psforever/actors/session/spectator/GalaxyHandlerLogic.scala +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright (c) 2024 PSForever -package net.psforever.actors.session.spectator - -import akka.actor.{ActorContext, ActorRef, typed} -import net.psforever.actors.session.AvatarActor -import net.psforever.actors.session.support.{GalaxyHandlerFunctions, SessionGalaxyHandlers, SessionData} -import net.psforever.packet.game.{BroadcastWarpgateUpdateMessage, FriendsResponse, HotSpotUpdateMessage, ZoneInfoMessage, ZonePopulationUpdateMessage, HotSpotInfo => PacketHotSpotInfo} -import net.psforever.services.galaxy.{GalaxyAction, GalaxyResponse, GalaxyServiceMessage} -import net.psforever.types.{MemberAction, PlanetSideEmpire} - -object GalaxyHandlerLogic { - def apply(ops: SessionGalaxyHandlers): GalaxyHandlerLogic = { - new GalaxyHandlerLogic(ops, ops.context) - } -} - -class GalaxyHandlerLogic(val ops: SessionGalaxyHandlers, implicit val context: ActorContext) extends GalaxyHandlerFunctions { - def sessionLogic: SessionData = ops.sessionLogic - - private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor - - private val galaxyService: ActorRef = ops.galaxyService - - /* packets */ - - def handleUpdateIgnoredPlayers(pkt: FriendsResponse): Unit = { - sendResponse(pkt) - pkt.friends.foreach { f => - galaxyService ! GalaxyServiceMessage(GalaxyAction.LogStatusChange(f.name)) - } - } - - /* response handlers */ - - def handle(reply: GalaxyResponse.Response): Unit = { - reply match { - case GalaxyResponse.HotSpotUpdate(zone_index, priority, hot_spot_info) => - sendResponse( - HotSpotUpdateMessage( - zone_index, - priority, - hot_spot_info.map { spot => PacketHotSpotInfo(spot.DisplayLocation.x, spot.DisplayLocation.y, 40) } - ) - ) - - case GalaxyResponse.MapUpdate(msg) => - sendResponse(msg) - - case GalaxyResponse.UpdateBroadcastPrivileges(zoneId, gateMapId, fromFactions, toFactions) => - val faction = player.Faction - val from = fromFactions.contains(faction) - val to = toFactions.contains(faction) - if (from && !to) { - sendResponse(BroadcastWarpgateUpdateMessage(zoneId, gateMapId, PlanetSideEmpire.NEUTRAL)) - } else if (!from && to) { - sendResponse(BroadcastWarpgateUpdateMessage(zoneId, gateMapId, faction)) - } - - case GalaxyResponse.FlagMapUpdate(msg) => - sendResponse(msg) - - case GalaxyResponse.TransferPassenger(temp_channel, vehicle, _, manifest) => - sessionLogic.zoning.handleTransferPassenger(temp_channel, vehicle, manifest) - - case GalaxyResponse.LockedZoneUpdate(zone, time) => - sendResponse(ZoneInfoMessage(zone.Number, empire_status=false, lock_time=time)) - - case GalaxyResponse.UnlockedZoneUpdate(zone) => - sendResponse(ZoneInfoMessage(zone.Number, empire_status=true, lock_time=0L)) - val popBO = 0 - val popTR = zone.Players.count(_.faction == PlanetSideEmpire.TR) - val popNC = zone.Players.count(_.faction == PlanetSideEmpire.NC) - val popVS = zone.Players.count(_.faction == PlanetSideEmpire.VS) - sendResponse(ZonePopulationUpdateMessage(zone.Number, 414, 138, popTR, 138, popNC, 138, popVS, 138, popBO)) - - case GalaxyResponse.LogStatusChange(name) if avatar.people.friend.exists(_.name.equals(name)) => - avatarActor ! AvatarActor.MemberListRequest(MemberAction.UpdateFriend, name) - - case GalaxyResponse.SendResponse(msg) => - sendResponse(msg) - - case _ => () - } - } -} diff --git a/src/main/scala/net/psforever/actors/session/spectator/GeneralLogic.scala b/src/main/scala/net/psforever/actors/session/spectator/GeneralLogic.scala index 806747a5a..c522b2f3c 100644 --- a/src/main/scala/net/psforever/actors/session/spectator/GeneralLogic.scala +++ b/src/main/scala/net/psforever/actors/session/spectator/GeneralLogic.scala @@ -4,24 +4,19 @@ package net.psforever.actors.session.spectator import akka.actor.{ActorContext, ActorRef, typed} import net.psforever.actors.session.AvatarActor import net.psforever.actors.session.support.{GeneralFunctions, GeneralOperations, SessionData} -import net.psforever.objects.{Account, GlobalDefinitions, LivePlayerList, PlanetSideGameObject, Player, TelepadDeployable, Tool, Vehicle} +import net.psforever.objects.{Account, GlobalDefinitions, LivePlayerList, Player, TelepadDeployable, Tool, Vehicle} import net.psforever.objects.avatar.{Avatar, Implant} import net.psforever.objects.ballistics.Projectile -import net.psforever.objects.ce.{Deployable, TelepadLike} import net.psforever.objects.definition.{BasicDefinition, KitDefinition, SpecialExoSuitDefinition} -import net.psforever.objects.equipment.Equipment -import net.psforever.objects.serverobject.CommonMessages import net.psforever.objects.serverobject.containable.Containable import net.psforever.objects.serverobject.doors.Door -import net.psforever.objects.vehicles.{Utility, UtilityType} -import net.psforever.objects.vehicles.Utility.InternalTelepad +import net.psforever.objects.vehicles.Utility import net.psforever.objects.zones.ZoneProjectile import net.psforever.packet.PlanetSideGamePacket import net.psforever.packet.game.{ActionCancelMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, BugReportMessage, ChangeFireModeMessage, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestMessage, ConnectToWorldRequestMessage, CreateShortcutMessage, DeployObjectMessage, DisplayedAwardMessage, DropItemMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FriendsRequest, GenericAction, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, ImplantAction, InvalidTerrainMessage, LootItemMessage, MoveItemMessage, ObjectDetectedMessage, ObjectHeldMessage, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, PlayerStateShiftMessage, RequestDestroyMessage, ShiftState, TargetInfo, TargetingImplantRequest, TargetingInfoMessage, TradeMessage, UnuseItemMessage, UseItemMessage, VoiceHostInfo, VoiceHostRequest, ZipLineMessage} import net.psforever.services.account.AccountPersistenceService import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} -import net.psforever.types.{DriveState, ExoSuitType, PlanetSideGUID, Vector3} -import net.psforever.util.Config +import net.psforever.types.{ExoSuitType, PlanetSideGUID, Vector3} object GeneralLogic { def apply(ops: GeneralOperations): GeneralLogic = { @@ -168,11 +163,11 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex def handleUseItem(pkt: UseItemMessage): Unit = { sessionLogic.validObject(pkt.object_guid, decorator = "UseItem") match { case Some(door: Door) => - handleUseDoor(door, None) + ops.handleUseDoor(door, None) case Some(obj: TelepadDeployable) => - handleUseTelepadDeployable(obj, None, pkt) + ops.handleUseTelepadDeployable(obj, None, pkt, ops.useRouterTelepadSystemSecretly) case Some(obj: Utility.InternalTelepad) => - handleUseInternalTelepad(obj, pkt) + ops.handleUseInternalTelepad(obj, pkt, ops.useRouterTelepadSystemSecretly) case _ => () } } @@ -267,15 +262,10 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex case GenericAction.AwayFromKeyboard_RCV => log.info(s"${player.Name} is AFK") AvatarActor.savePlayerLocation(player) - ops.displayCharSavedMsgThenRenewTimer(fixedLen=1800L, varLen=0L) //~30min player.AwayFromKeyboard = true case GenericAction.BackInGame_RCV => log.info(s"${player.Name} is back") player.AwayFromKeyboard = false - ops.renewCharSavedTimer( - Config.app.game.savedMsg.renewal.fixed, - Config.app.game.savedMsg.renewal.variable - ) case GenericAction.LookingForSquad_RCV => //Looking For Squad ON if (!avatar.lookingForSquad && (sessionLogic.squad.squadUI.isEmpty || sessionLogic.squad.squadUI(player.CharId).index == 0)) { avatarActor ! AvatarActor.SetLookingForSquad(true) @@ -460,101 +450,6 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex /* supporting functions */ - private def handleUseDoor(door: Door, equipment: Option[Equipment]): Unit = { - equipment match { - case Some(tool: Tool) if tool.Definition == GlobalDefinitions.medicalapplicator => - val distance: Float = math.max( - Config.app.game.doorsCanBeOpenedByMedAppFromThisDistance, - door.Definition.initialOpeningDistance - ) - door.Actor ! CommonMessages.Use(player, Some(distance)) - case _ => - door.Actor ! CommonMessages.Use(player) - } - } - - private def handleUseTelepadDeployable(obj: TelepadDeployable, equipment: Option[Equipment], msg: UseItemMessage): Unit = { - if (equipment.isEmpty) { - (continent.GUID(obj.Router) match { - case Some(vehicle: Vehicle) => Some((vehicle, vehicle.Utility(UtilityType.internal_router_telepad_deployable))) - case Some(vehicle) => Some(vehicle, None) - case None => None - }) match { - case Some((vehicle: Vehicle, Some(util: Utility.InternalTelepad))) => - player.WhichSide = vehicle.WhichSide - useRouterTelepadSystem( - router = vehicle, - internalTelepad = util, - remoteTelepad = obj, - src = obj, - dest = util - ) - case Some((vehicle: Vehicle, None)) => - log.error( - s"telepad@${msg.object_guid.guid} is not linked to a router - ${vehicle.Definition.Name}" - ) - case Some((o, _)) => - log.error( - s"telepad@${msg.object_guid.guid} is linked to wrong kind of object - ${o.Definition.Name}, ${obj.Router}" - ) - obj.Actor ! Deployable.Deconstruct() - case _ => () - } - } - } - - private def handleUseInternalTelepad(obj: InternalTelepad, msg: UseItemMessage): Unit = { - continent.GUID(obj.Telepad) match { - case Some(pad: TelepadDeployable) => - player.WhichSide = pad.WhichSide - useRouterTelepadSystem( - router = obj.Owner.asInstanceOf[Vehicle], - internalTelepad = obj, - remoteTelepad = pad, - src = obj, - dest = pad - ) - case Some(o) => - log.error( - s"internal telepad@${msg.object_guid.guid} is not linked to a remote telepad - ${o.Definition.Name}@${o.GUID.guid}" - ) - case None => () - } - } - - /** - * A player uses a fully-linked Router teleportation system. - * @param router the Router vehicle - * @param internalTelepad the internal telepad within the Router vehicle - * @param remoteTelepad the remote telepad that is currently associated with this Router - * @param src the origin of the teleportation (where the player starts) - * @param dest the destination of the teleportation (where the player is going) - */ - private def useRouterTelepadSystem( - router: Vehicle, - internalTelepad: InternalTelepad, - remoteTelepad: TelepadDeployable, - src: PlanetSideGameObject with TelepadLike, - dest: PlanetSideGameObject with TelepadLike - ): Unit = { - val time = System.currentTimeMillis() - if ( - time - ops.recentTeleportAttempt > 2000L && router.DeploymentState == DriveState.Deployed && - internalTelepad.Active && - remoteTelepad.Active - ) { - val pguid = player.GUID - val sguid = src.GUID - val dguid = dest.GUID - sendResponse(PlayerStateShiftMessage(ShiftState(0, dest.Position, player.Orientation.z))) - ops.useRouterTelepadEffect(pguid, sguid, dguid) - player.Position = dest.Position - } else { - log.warn(s"UseRouterTelepadSystem: ${player.Name} can not teleport") - } - ops.recentTeleportAttempt = time - } - private def administrativeKick(tplayer: Player): Unit = { log.warn(s"${tplayer.Name} has been kicked by ${player.Name}") tplayer.death_by = -1 diff --git a/src/main/scala/net/psforever/actors/session/spectator/LocalHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/spectator/LocalHandlerLogic.scala deleted file mode 100644 index f28ab0f38..000000000 --- a/src/main/scala/net/psforever/actors/session/spectator/LocalHandlerLogic.scala +++ /dev/null @@ -1,257 +0,0 @@ -// Copyright (c) 2024 PSForever -package net.psforever.actors.session.spectator - -import akka.actor.ActorContext -import net.psforever.actors.session.support.{LocalHandlerFunctions, SessionData, SessionLocalHandlers} -import net.psforever.objects.ce.Deployable -import net.psforever.objects.guid.{GUIDTask, TaskWorkflow} -import net.psforever.objects.vehicles.MountableWeapons -import net.psforever.objects.{BoomerDeployable, ExplosiveDeployable, TelepadDeployable, Tool, TurretDeployable} -import net.psforever.packet.game.{ChatMsg, DeployableObjectsInfoMessage, GenericActionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HackMessage, HackState, HackState1, InventoryStateMessage, ObjectAttachMessage, ObjectCreateMessage, ObjectDeleteMessage, ObjectDetachMessage, OrbitalShuttleTimeMsg, PadAndShuttlePair, PlanetsideAttributeMessage, ProximityTerminalUseMessage, SetEmpireMessage, TriggerEffectMessage, TriggerSoundMessage, TriggeredSound, VehicleStateMessage} -import net.psforever.services.Service -import net.psforever.services.local.LocalResponse -import net.psforever.types.{ChatMessageType, PlanetSideGUID, Vector3} - -object LocalHandlerLogic { - def apply(ops: SessionLocalHandlers): LocalHandlerLogic = { - new LocalHandlerLogic(ops, ops.context) - } -} - -class LocalHandlerLogic(val ops: SessionLocalHandlers, implicit val context: ActorContext) extends LocalHandlerFunctions { - def sessionLogic: SessionData = ops.sessionLogic - - /* messages */ - - def handleTurretDeployableIsDismissed(obj: TurretDeployable): Unit = { - TaskWorkflow.execute(GUIDTask.unregisterDeployableTurret(continent.GUID, obj)) - } - - def handleDeployableIsDismissed(obj: Deployable): Unit = { - TaskWorkflow.execute(GUIDTask.unregisterObject(continent.GUID, obj)) - } - - /* response handlers */ - - /** - * na - * @param toChannel na - * @param guid na - * @param reply na - */ - def handle(toChannel: String, guid: PlanetSideGUID, reply: LocalResponse.Response): Unit = { - val resolvedPlayerGuid = if (player.HasGUID) { - player.GUID - } else { - Service.defaultPlayerGUID - } - val isNotSameTarget = resolvedPlayerGuid != guid - reply match { - case LocalResponse.DeployableMapIcon(behavior, deployInfo) if isNotSameTarget => - sendResponse(DeployableObjectsInfoMessage(behavior, deployInfo)) - - case LocalResponse.DeployableUIFor(item) => - sessionLogic.general.updateDeployableUIElements(avatar.deployables.UpdateUIElement(item)) - - case LocalResponse.Detonate(dguid, _: BoomerDeployable) => - sendResponse(TriggerEffectMessage(dguid, "detonate_boomer")) - sendResponse(PlanetsideAttributeMessage(dguid, attribute_type=29, attribute_value=1)) - sendResponse(ObjectDeleteMessage(dguid, unk1=0)) - - case LocalResponse.Detonate(dguid, _: ExplosiveDeployable) => - sendResponse(GenericObjectActionMessage(dguid, code=19)) - sendResponse(PlanetsideAttributeMessage(dguid, attribute_type=29, attribute_value=1)) - sendResponse(ObjectDeleteMessage(dguid, unk1=0)) - - case LocalResponse.Detonate(_, obj) => - log.warn(s"LocalResponse.Detonate: ${obj.Definition.Name} not configured to explode correctly") - - case LocalResponse.DoorOpens(doorGuid) if isNotSameTarget => - sendResponse(GenericObjectStateMsg(doorGuid, state=16)) - - case LocalResponse.DoorCloses(doorGuid) => //door closes for everyone - sendResponse(GenericObjectStateMsg(doorGuid, state=17)) - - case LocalResponse.EliminateDeployable(obj: TurretDeployable, dguid, _, _) if obj.Destroyed => - sendResponse(ObjectDeleteMessage(dguid, unk1=0)) - - case LocalResponse.EliminateDeployable(obj: TurretDeployable, dguid, pos, _) => - obj.Destroyed = true - DeconstructDeployable( - obj, - dguid, - pos, - obj.Orientation, - deletionType= if (obj.MountPoints.isEmpty) { 2 } else { 1 } - ) - - case LocalResponse.EliminateDeployable(obj: ExplosiveDeployable, dguid, _, _) - if obj.Destroyed || obj.Jammed || obj.Health == 0 => - sendResponse(ObjectDeleteMessage(dguid, unk1=0)) - - case LocalResponse.EliminateDeployable(obj: ExplosiveDeployable, dguid, pos, effect) => - obj.Destroyed = true - DeconstructDeployable(obj, dguid, pos, obj.Orientation, effect) - - case LocalResponse.EliminateDeployable(obj: TelepadDeployable, dguid, _, _) if obj.Active && obj.Destroyed => - //if active, deactivate - obj.Active = false - ops.deactivateTelpadDeployableMessages(dguid) - //standard deployable elimination behavior - sendResponse(ObjectDeleteMessage(dguid, unk1=0)) - - case LocalResponse.EliminateDeployable(obj: TelepadDeployable, dguid, pos, _) if obj.Active => - //if active, deactivate - obj.Active = false - ops.deactivateTelpadDeployableMessages(dguid) - //standard deployable elimination behavior - obj.Destroyed = true - DeconstructDeployable(obj, dguid, pos, obj.Orientation, deletionType=2) - - case LocalResponse.EliminateDeployable(obj: TelepadDeployable, dguid, _, _) if obj.Destroyed => - //standard deployable elimination behavior - sendResponse(ObjectDeleteMessage(dguid, unk1=0)) - - case LocalResponse.EliminateDeployable(obj: TelepadDeployable, dguid, pos, _) => - //standard deployable elimination behavior - obj.Destroyed = true - DeconstructDeployable(obj, dguid, pos, obj.Orientation, deletionType=2) - - case LocalResponse.EliminateDeployable(obj, dguid, _, _) if obj.Destroyed => - sendResponse(ObjectDeleteMessage(dguid, unk1=0)) - - case LocalResponse.EliminateDeployable(obj, dguid, pos, effect) => - obj.Destroyed = true - DeconstructDeployable(obj, dguid, pos, obj.Orientation, effect) - - case LocalResponse.SendHackMessageHackCleared(targetGuid, unk1, unk2) => - sendResponse(HackMessage(HackState1.Unk0, targetGuid, guid, progress=0, unk1.toFloat, HackState.HackCleared, unk2)) - - case LocalResponse.HackObject(targetGuid, unk1, unk2) => - sessionLogic.general.hackObject(targetGuid, unk1, unk2) - - case LocalResponse.PlanetsideAttribute(targetGuid, attributeType, attributeValue) => - sessionLogic.general.sendPlanetsideAttributeMessage(targetGuid, attributeType, attributeValue) - - case LocalResponse.GenericObjectAction(targetGuid, actionNumber) => - sendResponse(GenericObjectActionMessage(targetGuid, actionNumber)) - - case LocalResponse.GenericActionMessage(actionNumber) => - sendResponse(GenericActionMessage(actionNumber)) - - case LocalResponse.ChatMessage(msg) => - sendResponse(msg) - - case LocalResponse.SendPacket(packet) => - sendResponse(packet) - - case LocalResponse.LluSpawned(llu) => - // Create LLU on client - sendResponse(ObjectCreateMessage( - llu.Definition.ObjectId, - llu.GUID, - llu.Definition.Packet.ConstructorData(llu).get - )) - sendResponse(TriggerSoundMessage(TriggeredSound.LLUMaterialize, llu.Position, unk=20, volume=0.8000001f)) - - case LocalResponse.LluDespawned(lluGuid, position) => - sendResponse(TriggerSoundMessage(TriggeredSound.LLUDeconstruct, position, unk=20, volume=0.8000001f)) - sendResponse(ObjectDeleteMessage(lluGuid, unk1=0)) - // If the player was holding the LLU, remove it from their tracked special item slot - sessionLogic.general.specialItemSlotGuid.collect { case guid if guid == lluGuid => - sessionLogic.general.specialItemSlotGuid = None - player.Carrying = None - } - - case LocalResponse.ObjectDelete(objectGuid, unk) if isNotSameTarget => - sendResponse(ObjectDeleteMessage(objectGuid, unk)) - - case LocalResponse.ProximityTerminalEffect(object_guid, true) => - sendResponse(ProximityTerminalUseMessage(Service.defaultPlayerGUID, object_guid, unk=true)) - - case LocalResponse.ProximityTerminalEffect(objectGuid, false) => - sendResponse(ProximityTerminalUseMessage(Service.defaultPlayerGUID, objectGuid, unk=false)) - sessionLogic.terminals.ForgetAllProximityTerminals(objectGuid) - - case LocalResponse.RouterTelepadMessage(msg) => - sendResponse(ChatMsg(ChatMessageType.UNK_229, wideContents=false, recipient="", msg, note=None)) - - case LocalResponse.RouterTelepadTransport(passengerGuid, srcGuid, destGuid) => - sessionLogic.general.useRouterTelepadEffect(passengerGuid, srcGuid, destGuid) - - case LocalResponse.SendResponse(msg) => - sendResponse(msg) - - case LocalResponse.SetEmpire(objectGuid, empire) => - sendResponse(SetEmpireMessage(objectGuid, empire)) - - case LocalResponse.ShuttleEvent(ev) => - val msg = OrbitalShuttleTimeMsg( - ev.u1, - ev.u2, - ev.t1, - ev.t2, - ev.t3, - pairs=ev.pairs.map { case ((a, b), c) => PadAndShuttlePair(a, b, c) } - ) - sendResponse(msg) - - case LocalResponse.ShuttleDock(pguid, sguid, slot) => - sendResponse(ObjectAttachMessage(pguid, sguid, slot)) - - case LocalResponse.ShuttleUndock(pguid, sguid, pos, orient) => - sendResponse(ObjectDetachMessage(pguid, sguid, pos, orient)) - - case LocalResponse.ShuttleState(sguid, pos, orient, state) => - sendResponse(VehicleStateMessage(sguid, unk1=0, pos, orient, vel=None, Some(state), unk3=0, unk4=0, wheel_direction=15, is_decelerating=false, is_cloaked=false)) - - case LocalResponse.ToggleTeleportSystem(router, systemPlan) => - sessionLogic.general.toggleTeleportSystem(router, systemPlan) - - case LocalResponse.TriggerEffect(targetGuid, effect, effectInfo, triggerLocation) => - sendResponse(TriggerEffectMessage(targetGuid, effect, effectInfo, triggerLocation)) - - case LocalResponse.TriggerSound(sound, pos, unk, volume) => - sendResponse(TriggerSoundMessage(sound, pos, unk, volume)) - - case LocalResponse.UpdateForceDomeStatus(buildingGuid, true) => - sendResponse(GenericObjectActionMessage(buildingGuid, 11)) - - case LocalResponse.UpdateForceDomeStatus(buildingGuid, false) => - sendResponse(GenericObjectActionMessage(buildingGuid, 12)) - - case LocalResponse.RechargeVehicleWeapon(vehicleGuid, weaponGuid) if resolvedPlayerGuid == guid => - continent.GUID(vehicleGuid) - .collect { case vehicle: MountableWeapons => (vehicle, vehicle.PassengerInSeat(player)) } - .collect { case (vehicle, Some(seat_num)) => vehicle.WeaponControlledFromSeat(seat_num) } - .getOrElse(Set.empty) - .collect { case weapon: Tool if weapon.GUID == weaponGuid => - sendResponse(InventoryStateMessage(weapon.AmmoSlot.Box.GUID, weapon.GUID, weapon.Magazine)) - } - - case _ => () - } - } - - /* support functions */ - - /** - * Common behavior for deconstructing deployables in the game environment. - * @param obj the deployable - * @param guid the globally unique identifier for the deployable - * @param pos the previous position of the deployable - * @param orient the previous orientation of the deployable - * @param deletionType the value passed to `ObjectDeleteMessage` concerning the deconstruction animation - */ - def DeconstructDeployable( - obj: Deployable, - guid: PlanetSideGUID, - pos: Vector3, - orient: Vector3, - deletionType: Int - ): Unit = { - sendResponse(TriggerEffectMessage("spawn_object_failed_effect", pos, orient)) - sendResponse(PlanetsideAttributeMessage(guid, 29, 1)) //make deployable vanish - sendResponse(ObjectDeleteMessage(guid, deletionType)) - } -} diff --git a/src/main/scala/net/psforever/actors/session/spectator/MountHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/spectator/MountHandlerLogic.scala index 2cf441e0f..42214bbd4 100644 --- a/src/main/scala/net/psforever/actors/session/spectator/MountHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/spectator/MountHandlerLogic.scala @@ -10,13 +10,11 @@ import net.psforever.objects.{GlobalDefinitions, PlanetSideGameObject, Player, V import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.serverobject.mount.Mountable import net.psforever.objects.serverobject.terminals.implant.ImplantTerminalMech -import net.psforever.objects.vehicles.{AccessPermissionGroup, CargoBehavior} import net.psforever.objects.vital.InGameHistory import net.psforever.packet.game.{DelayedPathMountMsg, DismountVehicleCargoMsg, DismountVehicleMsg, GenericObjectActionMessage, MountVehicleCargoMsg, MountVehicleMsg, ObjectDetachMessage, PlayerStasisMessage, PlayerStateShiftMessage, ShiftState} import net.psforever.services.Service import net.psforever.services.local.{LocalAction, LocalServiceMessage} import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} -import net.psforever.types.{BailType, PlanetSideGUID, Vector3} object MountHandlerLogic { def apply(ops: SessionMountHandlers): MountHandlerLogic = { @@ -29,98 +27,16 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act /* packets */ - def handleMountVehicle(pkt: MountVehicleMsg): Unit = { /* intentionally blank */ } + def handleMountVehicle(pkt: MountVehicleMsg): Unit = { /* can not mount as spectator */ } def handleDismountVehicle(pkt: DismountVehicleMsg): Unit = { - val DismountVehicleMsg(player_guid, bailType, wasKickedByDriver) = pkt - val dError: (String, Player)=> Unit = dismountError(bailType, wasKickedByDriver) - //TODO optimize this later - //common warning for this section - if (player.GUID == player_guid) { - //normally disembarking from a mount - (sessionLogic.zoning.interstellarFerry.orElse(continent.GUID(player.VehicleSeated)) match { - case out @ Some(obj: Vehicle) => - continent.GUID(obj.MountedIn) match { - case Some(_: Vehicle) => None //cargo vehicle - case _ => out //arrangement "may" be permissible - } - case out @ Some(_: Mountable) => - out - case _ => - dError(s"DismountVehicleMsg: player ${player.Name} not considered seated in a mountable entity", player) - None - }) match { - case Some(obj: Mountable) => - obj.PassengerInSeat(player) match { - case Some(seat_num) => - obj.Actor ! Mountable.TryDismount(player, seat_num, bailType) - //short-circuit the temporary channel for transferring between zones, the player is no longer doing that - sessionLogic.zoning.interstellarFerry = None - // Deconstruct the vehicle if the driver has bailed out and the vehicle is capable of flight - //todo: implement auto landing procedure if the pilot bails but passengers are still present instead of deconstructing the vehicle - //todo: continue flight path until aircraft crashes if no passengers present (or no passenger seats), then deconstruct. - //todo: kick cargo passengers out. To be added after PR #216 is merged - obj match { - case v: Vehicle - if bailType == BailType.Bailed && - v.SeatPermissionGroup(seat_num).contains(AccessPermissionGroup.Driver) && - v.isFlying => - v.Actor ! Vehicle.Deconstruct(None) //immediate deconstruction - case _ => () - } - - case None => - dError(s"DismountVehicleMsg: can not find where player ${player.Name}_guid is seated in mountable ${player.VehicleSeated}", player) - } - case _ => - dError(s"DismountVehicleMsg: can not find mountable entity ${player.VehicleSeated}", player) - } - } else { - //kicking someone else out of a mount; need to own that mount/mountable - val dWarn: (String, Player)=> Unit = dismountWarning(bailType, wasKickedByDriver) - player.avatar.vehicle match { - case Some(obj_guid) => - ( - ( - sessionLogic.validObject(obj_guid, decorator = "DismountVehicle/Vehicle"), - sessionLogic.validObject(player_guid, decorator = "DismountVehicle/Player") - ) match { - case (vehicle @ Some(obj: Vehicle), tplayer) => - if (obj.MountedIn.isEmpty) (vehicle, tplayer) else (None, None) - case (mount @ Some(_: Mountable), tplayer) => - (mount, tplayer) - case _ => - (None, None) - }) match { - case (Some(obj: Mountable), Some(tplayer: Player)) => - obj.PassengerInSeat(tplayer) match { - case Some(seat_num) => - obj.Actor ! Mountable.TryDismount(tplayer, seat_num, bailType) - case None => - dError(s"DismountVehicleMsg: can not find where other player ${tplayer.Name} is seated in mountable $obj_guid", tplayer) - } - case (None, _) => - dWarn(s"DismountVehicleMsg: ${player.Name} can not find his vehicle", player) - case (_, None) => - dWarn(s"DismountVehicleMsg: player $player_guid could not be found to kick, ${player.Name}", player) - case _ => - dWarn(s"DismountVehicleMsg: object is either not a Mountable or not a Player", player) - } - case None => - dWarn(s"DismountVehicleMsg: ${player.Name} does not own a vehicle", player) - } - } + ops.handleDismountVehicle(pkt) } - def handleMountVehicleCargo(pkt: MountVehicleCargoMsg): Unit = { /* intentionally blank */ } + def handleMountVehicleCargo(pkt: MountVehicleCargoMsg): Unit = { /* can not mount as spectator */ } def handleDismountVehicleCargo(pkt: DismountVehicleCargoMsg): Unit = { - val DismountVehicleCargoMsg(_, cargo_guid, bailed, _, kicked) = pkt - continent.GUID(cargo_guid) match { - case Some(cargo: Vehicle) => - cargo.Actor ! CargoBehavior.StartCargoDismounting(bailed || kicked) - case _ => () - } + ops.handleDismountVehicleCargo(pkt) } /* response handlers */ @@ -134,7 +50,7 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act def handle(tplayer: Player, reply: Mountable.Exchange): Unit = { reply match { case Mountable.CanDismount(obj: ImplantTerminalMech, seatNum, _) => - DismountAction(tplayer, obj, seatNum) + ops.DismountAction(tplayer, obj, seatNum) obj.Zone.actor ! ZoneActor.RemoveFromBlockMap(player) case Mountable.CanDismount(obj: Vehicle, _, mountPoint) @@ -157,7 +73,7 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act //get ready for orbital drop val pguid = player.GUID val events = continent.VehicleEvents - DismountAction(tplayer, obj, seatNum) + ops.DismountAction(tplayer, obj, seatNum) continent.actor ! ZoneActor.RemoveFromBlockMap(player) //character doesn't need it //DismountAction(...) uses vehicle service, so use that service to coordinate the remainder of the messages events ! VehicleServiceMessage( @@ -185,14 +101,14 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act case Mountable.CanDismount(obj: Vehicle, seatNum, _) if obj.Definition == GlobalDefinitions.droppod => sessionLogic.general.unaccessContainer(obj) - DismountAction(tplayer, obj, seatNum) + ops.DismountAction(tplayer, obj, seatNum) obj.Actor ! Vehicle.Deconstruct() case Mountable.CanDismount(obj: Vehicle, seatNum, _) if tplayer.GUID == player.GUID => sessionLogic.vehicles.ConditionalDriverVehicleControl(obj) sessionLogic.general.unaccessContainer(obj) - DismountVehicleAction(tplayer, obj, seatNum) + ops.DismountVehicleAction(tplayer, obj, seatNum) case Mountable.CanDismount(obj: Vehicle, seat_num, _) => continent.VehicleEvents ! VehicleServiceMessage( @@ -201,7 +117,7 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act ) case Mountable.CanDismount(obj: PlanetSideGameObject with PlanetSideGameObject with Mountable with FactionAffinity with InGameHistory, seatNum, _) => - DismountAction(tplayer, obj, seatNum) + ops.DismountAction(tplayer, obj, seatNum) case Mountable.CanDismount(_: Mountable, _, _) => () @@ -213,90 +129,4 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act } /* support functions */ - - private def dismountWarning( - bailAs: BailType.Value, - kickedByDriver: Boolean - ) - ( - note: String, - player: Player - ): Unit = { - log.warn(note) - player.VehicleSeated = None - sendResponse(DismountVehicleMsg(player.GUID, bailAs, kickedByDriver)) - } - - private def dismountError( - bailAs: BailType.Value, - kickedByDriver: Boolean - ) - ( - note: String, - player: Player - ): Unit = { - log.error(s"$note; some vehicle might not know that ${player.Name} is no longer sitting in it") - player.VehicleSeated = None - sendResponse(DismountVehicleMsg(player.GUID, bailAs, kickedByDriver)) - } - - /** - * Common activities/procedure when a player dismounts a valid mountable object. - * @param tplayer the player - * @param obj the mountable object - * @param seatNum the mount out of which which the player is disembarking - */ - private def DismountVehicleAction(tplayer: Player, obj: PlanetSideGameObject with FactionAffinity with InGameHistory, seatNum: Int): Unit = { - DismountAction(tplayer, obj, seatNum) - //until vehicles maintain synchronized momentum without a driver - obj match { - case v: Vehicle - if seatNum == 0 && Vector3.MagnitudeSquared(v.Velocity.getOrElse(Vector3.Zero)) > 0f => - sessionLogic.vehicles.serverVehicleControlVelocity.collect { _ => - sessionLogic.vehicles.ServerVehicleOverrideStop(v) - } - v.Velocity = Vector3.Zero - continent.VehicleEvents ! VehicleServiceMessage( - continent.id, - VehicleAction.VehicleState( - tplayer.GUID, - v.GUID, - unk1 = 0, - v.Position, - v.Orientation, - vel = None, - v.Flying, - unk3 = 0, - unk4 = 0, - wheel_direction = 15, - unk5 = false, - unk6 = v.Cloaked - ) - ) - v.Zone.actor ! ZoneActor.RemoveFromBlockMap(player) - case _ => () - } - } - - /** - * Common activities/procedure when a player dismounts a valid mountable object. - * @param tplayer the player - * @param obj the mountable object - * @param seatNum the mount out of which which the player is disembarking - */ - private def DismountAction(tplayer: Player, obj: PlanetSideGameObject with FactionAffinity with InGameHistory, seatNum: Int): Unit = { - val playerGuid: PlanetSideGUID = tplayer.GUID - tplayer.ContributionFrom(obj) - sessionLogic.keepAliveFunc = sessionLogic.zoning.NormalKeepAlive - val bailType = if (tplayer.BailProtection) { - BailType.Bailed - } else { - BailType.Normal - } - sendResponse(DismountVehicleMsg(playerGuid, bailType, wasKickedByDriver = false)) - continent.VehicleEvents ! VehicleServiceMessage( - continent.id, - VehicleAction.DismountVehicle(playerGuid, bailType, unk2 = false) - ) - } } diff --git a/src/main/scala/net/psforever/actors/session/spectator/SpectatorMode.scala b/src/main/scala/net/psforever/actors/session/spectator/SpectatorMode.scala index b950274db..89c0321e4 100644 --- a/src/main/scala/net/psforever/actors/session/spectator/SpectatorMode.scala +++ b/src/main/scala/net/psforever/actors/session/spectator/SpectatorMode.scala @@ -24,9 +24,9 @@ import net.psforever.packet.game.{ChatMsg, CreateShortcutMessage, UnuseItemMessa class SpectatorModeLogic(data: SessionData) extends ModeLogic { val avatarResponse: AvatarHandlerFunctions = AvatarHandlerLogic(data.avatarResponse) val chat: ChatFunctions = ChatLogic(data.chat) - val galaxy: GalaxyHandlerFunctions = GalaxyHandlerLogic(data.galaxyResponseHandlers) + val galaxy: GalaxyHandlerFunctions = net.psforever.actors.session.normal.GalaxyHandlerLogic(data.galaxyResponseHandlers) val general: GeneralFunctions = GeneralLogic(data.general) - val local: LocalHandlerFunctions = LocalHandlerLogic(data.localResponse) + val local: LocalHandlerFunctions = net.psforever.actors.session.normal.LocalHandlerLogic(data.localResponse) val mountResponse: MountHandlerFunctions = MountHandlerLogic(data.mountResponse) val shooting: WeaponAndProjectileFunctions = WeaponAndProjectileLogic(data.shooting) val squad: SquadHandlerFunctions = SquadHandlerLogic(data.squad) @@ -118,7 +118,6 @@ class SpectatorModeLogic(data: SessionData) extends ModeLogic { data.chat.commandIncomingSilence(session, ChatMsg(ChatMessageType.CMT_SILENCE, "player 0")) } // - player.spectator = true data.chat.JoinChannel(SpectatorChannel) val newPlayer = SpectatorModeLogic.spectatorCharacter(player) newPlayer.LogActivity(player.History.headOption) @@ -159,7 +158,6 @@ class SpectatorModeLogic(data: SessionData) extends ModeLogic { .map(CreateShortcutMessage(pguid, _, None)) .foreach(sendResponse) data.chat.LeaveChannel(SpectatorChannel) - player.spectator = false sendResponse(ObjectDeleteMessage(player.avatar.locker.GUID, 0)) //free up the slot sendResponse(ChatMsg(ChatMessageType.CMT_TOGGLESPECTATORMODE, "off")) sendResponse(ChatMsg(ChatMessageType.UNK_227, "@SpectatorDisabled")) @@ -212,6 +210,7 @@ object SpectatorModeLogic { newPlayer.Position = player.Position newPlayer.Orientation = player.Orientation newPlayer.spectator = true + newPlayer.bops = true newPlayer.Spawn() newPlayer } diff --git a/src/main/scala/net/psforever/actors/session/spectator/VehicleHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/spectator/VehicleHandlerLogic.scala index ee66f69e9..95bfdf352 100644 --- a/src/main/scala/net/psforever/actors/session/spectator/VehicleHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/spectator/VehicleHandlerLogic.scala @@ -308,13 +308,13 @@ class VehicleHandlerLogic(val ops: SessionVehicleHandlers, implicit val context: sessionLogic.vehicles.ServerVehicleOverrideStop(vehicle) case VehicleResponse.PeriodicReminder(VehicleSpawnPad.Reminders.Blocked, data) => - sendResponse(ChatMsg( - ChatMessageType.CMT_OPEN, - wideContents=true, - recipient="", - s"The vehicle spawn where you placed your order is blocked. ${data.getOrElse("")}", - note=None - )) + val str = s"${data.getOrElse("The vehicle spawn pad where you placed your order is blocked.")}" + val msg = if (str.contains("@")) { + ChatMsg(ChatMessageType.UNK_229, str) + } else { + ChatMsg(ChatMessageType.CMT_OPEN, wideContents = true, recipient = "", str, note = None) + } + sendResponse(msg) case VehicleResponse.PeriodicReminder(_, data) => val (isType, flag, msg): (ChatMessageType, Boolean, String) = data match { diff --git a/src/main/scala/net/psforever/actors/session/spectator/VehicleLogic.scala b/src/main/scala/net/psforever/actors/session/spectator/VehicleLogic.scala index 7ba9cca0b..c61499dd8 100644 --- a/src/main/scala/net/psforever/actors/session/spectator/VehicleLogic.scala +++ b/src/main/scala/net/psforever/actors/session/spectator/VehicleLogic.scala @@ -1,14 +1,11 @@ // Copyright (c) 2024 PSForever package net.psforever.actors.session.spectator -import akka.actor.{ActorContext, typed} -import net.psforever.actors.session.AvatarActor +import akka.actor.ActorContext import net.psforever.actors.session.support.{SessionData, VehicleFunctions, VehicleOperations} import net.psforever.objects.serverobject.PlanetSideServerObject -import net.psforever.objects.{Vehicle, Vehicles} +import net.psforever.objects.Vehicle import net.psforever.objects.serverobject.deploy.Deployment -import net.psforever.objects.serverobject.mount.Mountable -import net.psforever.objects.vehicles.control.BfrFlight import net.psforever.packet.game.{ChildObjectStateMessage, DeployRequestMessage, FrameVehicleStateMessage, VehicleStateMessage, VehicleSubStateMessage} import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} import net.psforever.types.{DriveState, Vector3} @@ -22,210 +19,15 @@ object VehicleLogic { class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContext) extends VehicleFunctions { def sessionLogic: SessionData = ops.sessionLogic - private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor + //private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor /* packets */ - def handleVehicleState(pkt: VehicleStateMessage): Unit = { - val VehicleStateMessage( - vehicle_guid, - unk1, - pos, - ang, - vel, - is_flying, - unk6, - unk7, - wheels, - is_decelerating, - is_cloaked - ) = pkt - ops.GetVehicleAndSeat() match { - case (Some(obj), Some(0)) => - //we're driving the vehicle - sessionLogic.persist() - sessionLogic.turnCounterFunc(player.GUID) - sessionLogic.general.fallHeightTracker(pos.z) - if (obj.MountedIn.isEmpty) { - sessionLogic.updateBlockMap(obj, pos) - } - player.Position = pos //convenient - if (obj.WeaponControlledFromSeat(0).isEmpty) { - player.Orientation = Vector3.z(ang.z) //convenient - } - obj.Position = pos - obj.Orientation = ang - if (obj.MountedIn.isEmpty) { - if (obj.DeploymentState != DriveState.Deployed) { - obj.Velocity = vel - } else { - obj.Velocity = Some(Vector3.Zero) - } - if (obj.Definition.CanFly) { - obj.Flying = is_flying //usually Some(7) - } - obj.Cloaked = obj.Definition.CanCloak && is_cloaked - } else { - obj.Velocity = None - obj.Flying = None - } - continent.VehicleEvents ! VehicleServiceMessage( - continent.id, - VehicleAction.VehicleState( - player.GUID, - vehicle_guid, - unk1, - obj.Position, - ang, - obj.Velocity, - if (obj.isFlying) { - is_flying - } else { - None - }, - unk6, - unk7, - wheels, - is_decelerating, - obj.Cloaked - ) - ) - sessionLogic.squad.updateSquad() - obj.zoneInteractions() - case (None, _) => - //log.error(s"VehicleState: no vehicle $vehicle_guid found in zone") - //TODO placing a "not driving" warning here may trigger as we are disembarking the vehicle - case (_, Some(index)) => - log.error( - s"VehicleState: ${player.Name} should not be dispatching this kind of packet from vehicle ${vehicle_guid.guid} when not the driver (actually, seat $index)" - ) - case _ => () - } - if (player.death_by == -1) { - sessionLogic.kickedByAdministration() - } - } + def handleVehicleState(pkt: VehicleStateMessage): Unit = { /* can not drive vehicle as spectator */ } - def handleFrameVehicleState(pkt: FrameVehicleStateMessage): Unit = { - val FrameVehicleStateMessage( - vehicle_guid, - unk1, - pos, - ang, - vel, - unk2, - unk3, - unk4, - is_crouched, - is_airborne, - ascending_flight, - flight_time, - unk9, - unkA - ) = pkt - ops.GetVehicleAndSeat() match { - case (Some(obj), Some(0)) => - //we're driving the vehicle - sessionLogic.persist() - sessionLogic.turnCounterFunc(player.GUID) - val (position, angle, velocity, notMountedState) = continent.GUID(obj.MountedIn) match { - case Some(v: Vehicle) => - sessionLogic.updateBlockMap(obj, pos) - (pos, v.Orientation - Vector3.z(value = 90f) * Vehicles.CargoOrientation(obj).toFloat, v.Velocity, false) - case _ => - (pos, ang, vel, true) - } - player.Position = position //convenient - if (obj.WeaponControlledFromSeat(seatNumber = 0).isEmpty) { - player.Orientation = Vector3.z(ang.z) //convenient - } - obj.Position = position - obj.Orientation = angle - obj.Velocity = velocity - // if (is_crouched && obj.DeploymentState != DriveState.Kneeling) { - // //dev stuff goes here - // } - // else - // if (!is_crouched && obj.DeploymentState == DriveState.Kneeling) { - // //dev stuff goes here - // } - obj.DeploymentState = if (is_crouched || !notMountedState) DriveState.Kneeling else DriveState.Mobile - if (notMountedState) { - if (obj.DeploymentState != DriveState.Kneeling) { - if (is_airborne) { - val flight = if (ascending_flight) flight_time else -flight_time - obj.Flying = Some(flight) - obj.Actor ! BfrFlight.Soaring(flight) - } else if (obj.Flying.nonEmpty) { - obj.Flying = None - obj.Actor ! BfrFlight.Landed - } - } else { - obj.Velocity = None - obj.Flying = None - } - obj.zoneInteractions() - } else { - obj.Velocity = None - obj.Flying = None - } - continent.VehicleEvents ! VehicleServiceMessage( - continent.id, - VehicleAction.FrameVehicleState( - player.GUID, - vehicle_guid, - unk1, - position, - angle, - velocity, - unk2, - unk3, - unk4, - is_crouched, - is_airborne, - ascending_flight, - flight_time, - unk9, - unkA - ) - ) - sessionLogic.squad.updateSquad() - case (None, _) => - //log.error(s"VehicleState: no vehicle $vehicle_guid found in zone") - //TODO placing a "not driving" warning here may trigger as we are disembarking the vehicle - case (_, Some(index)) => - log.error( - s"VehicleState: ${player.Name} should not be dispatching this kind of packet from vehicle ${vehicle_guid.guid} when not the driver (actually, seat $index)" - ) - case _ => () - } - if (player.death_by == -1) { - sessionLogic.kickedByAdministration() - } - } + def handleFrameVehicleState(pkt: FrameVehicleStateMessage): Unit = { /* can not drive vehicle as spectator */ } - def handleChildObjectState(pkt: ChildObjectStateMessage): Unit = { - val ChildObjectStateMessage(object_guid, pitch, yaw) = pkt - val (o, tools) = sessionLogic.shooting.FindContainedWeapon - //is COSM our primary upstream packet? - (o match { - case Some(mount: Mountable) => (o, mount.PassengerInSeat(player)) - case _ => (None, None) - }) match { - case (None, None) | (_, None) | (Some(_: Vehicle), Some(0)) => () - case _ => - sessionLogic.persist() - sessionLogic.turnCounterFunc(player.GUID) - } - //the majority of the following check retrieves information to determine if we are in control of the child - tools.find { _.GUID == object_guid } match { - case None => () - case Some(_) => player.Orientation = Vector3(0f, pitch, yaw) - } - if (player.death_by == -1) { - sessionLogic.kickedByAdministration() - } - } + def handleChildObjectState(pkt: ChildObjectStateMessage): Unit = { /* can not drive vehicle as spectator */ } def handleVehicleSubState(pkt: VehicleSubStateMessage): Unit = { val VehicleSubStateMessage(vehicle_guid, _, pos, ang, vel, unk1, _) = pkt @@ -258,22 +60,7 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex } } - def handleDeployRequest(pkt: DeployRequestMessage): Unit = { - val DeployRequestMessage(_, vehicle_guid, deploy_state, _, _, _) = pkt - val vehicle = player.avatar.vehicle - if (vehicle.contains(vehicle_guid)) { - if (vehicle == player.VehicleSeated) { - continent.GUID(vehicle_guid) match { - case Some(obj: Vehicle) => - if (obj.DeploymentState == DriveState.Deployed) { - obj.Actor ! Deployment.TryDeploymentChange(deploy_state) - } - case _ => () - avatarActor ! AvatarActor.SetVehicle(None) - } - } - } - } + def handleDeployRequest(pkt: DeployRequestMessage): Unit = { /* can not drive vehicle as spectator */ } /* messages */ diff --git a/src/main/scala/net/psforever/actors/session/spectator/WeaponAndProjectileLogic.scala b/src/main/scala/net/psforever/actors/session/spectator/WeaponAndProjectileLogic.scala index 1ebcc1fdd..c6992abb2 100644 --- a/src/main/scala/net/psforever/actors/session/spectator/WeaponAndProjectileLogic.scala +++ b/src/main/scala/net/psforever/actors/session/spectator/WeaponAndProjectileLogic.scala @@ -4,12 +4,10 @@ package net.psforever.actors.session.spectator import akka.actor.ActorContext import net.psforever.actors.session.support.{SessionData, WeaponAndProjectileFunctions, WeaponAndProjectileOperations} import net.psforever.login.WorldSession.{CountGrenades, FindEquipmentStock, FindToolThatUses, RemoveOldEquipmentFromInventory} -import net.psforever.objects.ballistics.Projectile import net.psforever.objects.equipment.ChargeFireModeDefinition -import net.psforever.objects.inventory.Container import net.psforever.objects.serverobject.CommonMessages -import net.psforever.objects.{AmmoBox, BoomerDeployable, BoomerTrigger, GlobalDefinitions, PlanetSideGameObject, Tool} -import net.psforever.packet.game.{AIDamage, AvatarGrenadeStateMessage, ChangeAmmoMessage, ChangeFireModeMessage, ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, HitMessage, InventoryStateMessage, LashMessage, LongRangeProjectileInfoMessage, ProjectileStateMessage, QuantityUpdateMessage, ReloadMessage, SplashHitMessage, UplinkRequest, UplinkRequestType, UplinkResponse, WeaponDelayFireMessage, WeaponDryFireMessage, WeaponFireMessage, WeaponLazeTargetPositionMessage} +import net.psforever.objects.{BoomerDeployable, BoomerTrigger, GlobalDefinitions, Tool} +import net.psforever.packet.game.{AIDamage, AvatarGrenadeStateMessage, ChangeAmmoMessage, ChangeFireModeMessage, ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, HitMessage, LashMessage, LongRangeProjectileInfoMessage, ProjectileStateMessage, QuantityUpdateMessage, ReloadMessage, SplashHitMessage, UplinkRequest, UplinkRequestType, UplinkResponse, WeaponDelayFireMessage, WeaponDryFireMessage, WeaponFireMessage, WeaponLazeTargetPositionMessage} import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.types.PlanetSideGUID @@ -85,32 +83,7 @@ class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit def handleChangeFireMode(pkt: ChangeFireModeMessage): Unit = { /* intentionally blank */ } def handleProjectileState(pkt: ProjectileStateMessage): Unit = { - val ProjectileStateMessage(projectile_guid, shot_pos, shot_vel, shot_orient, seq, end, target_guid) = pkt - val index = projectile_guid.guid - Projectile.baseUID - ops.projectiles(index) match { - case Some(projectile) if projectile.HasGUID => - val projectileGlobalUID = projectile.GUID - projectile.Position = shot_pos - projectile.Orientation = shot_orient - projectile.Velocity = shot_vel - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.ProjectileState( - player.GUID, - projectileGlobalUID, - shot_pos, - shot_vel, - shot_orient, - seq, - end, - target_guid - ) - ) - case _ if seq == 0 => - /* missing the first packet in the sequence is permissible */ - case _ => - log.warn(s"ProjectileState: constructed projectile ${projectile_guid.guid} can not be found") - } + ops.handleProjectileState(pkt) } def handleLongRangeProjectileState(pkt: LongRangeProjectileInfoMessage): Unit = { /* intentionally blank */ } @@ -148,11 +121,11 @@ class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit RemoveOldEquipmentFromInventory(player)(x.obj) sumReloadValue } else { - ModifyAmmunition(player)(box.AmmoSlot.Box, 3 - tailReloadValue) + ops.modifyAmmunition(player)(box.AmmoSlot.Box, 3 - tailReloadValue) 3 } log.info(s"${player.Name} found $actualReloadValue more $ammoType grenades to throw") - ModifyAmmunition(player)( + ops.modifyAmmunition(player)( tool.AmmoSlot.Box, -actualReloadValue ) //grenade item already in holster (negative because empty) @@ -163,20 +136,6 @@ class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit } } - /** - * Given an object that contains a box of amunition in its `Inventory` at a certain location, - * change the amount of ammunition within that box. - * @param obj the `Container` - * @param box an `AmmoBox` to modify - * @param reloadValue the value to modify the `AmmoBox`; - * subtracted from the current `Capacity` of `Box` - */ - private def ModifyAmmunition(obj: PlanetSideGameObject with Container)(box: AmmoBox, reloadValue: Int): Unit = { - val capacity = box.Capacity - reloadValue - box.Capacity = capacity - sendResponse(InventoryStateMessage(box.GUID, obj.GUID, capacity)) - } - private def fireStateStartPlayerMessages(itemGuid: PlanetSideGUID): Unit = { continent.AvatarEvents ! AvatarServiceMessage( continent.id, diff --git a/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala b/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala index 2536eac69..381b80f84 100644 --- a/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala @@ -5,11 +5,7 @@ import akka.actor.Cancellable import akka.actor.typed.ActorRef import akka.actor.{ActorContext, typed} import net.psforever.actors.session.{AvatarActor, SessionActor} -import net.psforever.actors.session.normal.{NormalMode => SessionNormalMode} -import net.psforever.actors.session.spectator.{SpectatorMode => SessionSpectatorMode} -import net.psforever.actors.session.csr.{CustomerServiceRepresentativeMode => SessionCustomerServiceRepresentativeMode} import net.psforever.actors.zone.ZoneActor -import net.psforever.objects.avatar.ModePermissions import net.psforever.objects.sourcing.PlayerSource import net.psforever.objects.zones.ZoneInfo import net.psforever.packet.game.SetChatFilterMessage @@ -69,7 +65,7 @@ class ChatOperations( */ private val ignoredEmoteCooldown: mutable.LongMap[Long] = mutable.LongMap[Long]() - private[session] var SpectatorMode: PlayerMode = SessionSpectatorMode + private[session] var SpectatorMode: PlayerMode = SpectatorMode import akka.actor.typed.scaladsl.adapter._ private val chatServiceAdapter: ActorRef[ChatService.MessageResponse] = context.self.toTyped[ChatService.MessageResponse] @@ -116,17 +112,6 @@ class ChatOperations( sendResponse(message.copy(contents = f"$speed%.3f")) } - def commandToggleSpectatorMode(contents: String): Unit = { - val currentSpectatorActivation = (if (avatar != null) avatar.permissions else ModePermissions()).canSpectate - contents.toLowerCase() match { - case "on" | "o" | "" if !currentSpectatorActivation => - context.self ! SessionActor.SetMode(SessionSpectatorMode) - case "off" | "of" if currentSpectatorActivation => - context.self ! SessionActor.SetMode(SessionNormalMode) - case _ => () - } - } - def commandRecall(session: Session): Unit = { val player = session.player val errorMessage = session.zoningType match { @@ -1257,18 +1242,6 @@ class ChatOperations( true } - def customCommandModerator(contents: String): Boolean = { - val currentCsrActivation = (if (avatar != null) avatar.permissions else ModePermissions()).canGM - contents.toLowerCase() match { - case "on" | "o" | "" if currentCsrActivation => - context.self ! SessionActor.SetMode(SessionCustomerServiceRepresentativeMode) - case "off" | "of" if currentCsrActivation => - context.self ! SessionActor.SetMode(SessionNormalMode) - case _ => () - } - true - } - def firstParam[T]( session: Session, buffer: Iterable[String], diff --git a/src/main/scala/net/psforever/actors/session/support/GeneralOperations.scala b/src/main/scala/net/psforever/actors/session/support/GeneralOperations.scala index 67bf09d4e..24e68571a 100644 --- a/src/main/scala/net/psforever/actors/session/support/GeneralOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/GeneralOperations.scala @@ -3,7 +3,20 @@ package net.psforever.actors.session.support import akka.actor.{ActorContext, ActorRef, Cancellable, typed} import net.psforever.objects.serverobject.containable.Containable -import net.psforever.objects.sourcing.PlayerSource +import net.psforever.objects.serverobject.doors.Door +import net.psforever.objects.serverobject.interior.Sidedness +import net.psforever.objects.serverobject.mblocker.Locker +import net.psforever.objects.serverobject.resourcesilo.ResourceSilo +import net.psforever.objects.serverobject.structures.Building +import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal +import net.psforever.objects.serverobject.terminals.{MatrixTerminalDefinition, Terminal} +import net.psforever.objects.serverobject.tube.SpawnTube +import net.psforever.objects.serverobject.turret.FacilityTurret +import net.psforever.objects.sourcing.{PlayerSource, VehicleSource} +import net.psforever.objects.vehicles.Utility.InternalTelepad +import net.psforever.objects.zones.blockmap.BlockMapEntity +import net.psforever.services.RemoverActor +import net.psforever.services.local.{LocalAction, LocalServiceMessage} import scala.collection.mutable import scala.concurrent.ExecutionContext.Implicits.global @@ -560,6 +573,66 @@ class GeneralOperations( parent.Find(objectGuid).flatMap { slot => Some((parent, Some(slot))) } } + /** + * A simple object searching algorithm that is limited to containers currently known and accessible by the player. + * If all relatively local containers are checked and the object is not found, + * the player's locker inventory will be checked, and then + * the game environment (items on the ground) will be checked too. + * If the target object is discovered, it is removed from its current location and is completely destroyed. + * @see `RequestDestroyMessage` + * @see `Zone.ItemIs.Where` + * @param objectGuid the target object's globally unique identifier; + * it is not expected that the object will be unregistered, but it is also not gauranteed + * @param obj the target object + * @return `true`, if the target object was discovered and removed; + * `false`, otherwise + */ + def findEquipmentToDelete(objectGuid: PlanetSideGUID, obj: Equipment): Boolean = { + val findFunc + : PlanetSideServerObject with Container => Option[(PlanetSideServerObject with Container, Option[Int])] = + findInLocalContainer(objectGuid) + + findFunc(player) + .orElse(accessedContainer match { + case Some(parent: PlanetSideServerObject) => + findFunc(parent) + case _ => + None + }) + .orElse(sessionLogic.vehicles.findLocalVehicle match { + case Some(parent: PlanetSideServerObject) => + findFunc(parent) + case _ => + None + }) match { + case Some((parent, Some(_))) => + obj.Position = Vector3.Zero + RemoveOldEquipmentFromInventory(parent)(obj) + true + case _ if player.avatar.locker.Inventory.Remove(objectGuid) => + sendResponse(ObjectDeleteMessage(objectGuid, 0)) + true + case _ if continent.EquipmentOnGround.contains(obj) => + obj.Position = Vector3.Zero + continent.Ground ! Zone.Ground.RemoveItem(objectGuid) + continent.AvatarEvents ! AvatarServiceMessage.Ground(RemoverActor.ClearSpecific(List(obj), continent)) + true + case _ => + Zone.EquipmentIs.Where(obj, objectGuid, continent) match { + case None => + true + case Some(Zone.EquipmentIs.Orphaned()) if obj.HasGUID => + TaskWorkflow.execute(GUIDTask.unregisterEquipment(continent.GUID, obj)) + true + case Some(Zone.EquipmentIs.Orphaned()) => + true + case _ => + log.warn(s"RequestDestroy: equipment $obj exists, but ${player.Name} can not reach it to dispose of it") + false + } + } + } + /** * na * @param targetGuid na @@ -767,6 +840,407 @@ class GeneralOperations( ) } + def handleDeployObject( + zone: Zone, + deployableType: DeployedItem.Value, + position: Vector3, + orientation: Vector3, + side: Sidedness, + faction: PlanetSideEmpire.Value, + optionalOwnerBuiltWith: Option[(Player, ConstructionItem)] + ): Unit = { + val deployableEntity: Deployable = Deployables.Make(deployableType)() + deployableEntity.Position = position + deployableEntity.Orientation = orientation + deployableEntity.WhichSide = side + deployableEntity.Faction = faction + val tasking: TaskBundle = deployableEntity match { + case turret: TurretDeployable => + GUIDTask.registerDeployableTurret(zone.GUID, turret) + case _ => + GUIDTask.registerObject(zone.GUID, deployableEntity) + } + val zoneBuildCommand = optionalOwnerBuiltWith + .collect { case (owner, tool) => + deployableEntity.AssignOwnership(owner) + Zone.Deployable.BuildByOwner(deployableEntity, owner, tool) + } + .getOrElse(Zone.Deployable.Build(deployableEntity)) + //execute + TaskWorkflow.execute(CallBackForTask(tasking, zone.Deployables, zoneBuildCommand, context.self)) + } + + def handleUseDoor(door: Door, equipment: Option[Equipment]): Unit = { + equipment match { + case Some(tool: Tool) if tool.Definition == GlobalDefinitions.medicalapplicator => + val distance: Float = math.max( + Config.app.game.doorsCanBeOpenedByMedAppFromThisDistance, + door.Definition.initialOpeningDistance + ) + door.Actor ! CommonMessages.Use(player, Some(distance)) + case _ => + door.Actor ! CommonMessages.Use(player) + } + } + + def handleUseResourceSilo(resourceSilo: ResourceSilo, equipment: Option[Equipment]): Unit = { + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + val vehicleOpt = continent.GUID(player.avatar.vehicle) + (vehicleOpt, equipment) match { + case (Some(vehicle: Vehicle), Some(item)) + if GlobalDefinitions.isBattleFrameVehicle(vehicle.Definition) && + GlobalDefinitions.isBattleFrameNTUSiphon(item.Definition) => + resourceSilo.Actor ! CommonMessages.Use(player, Some(vehicle)) + case (Some(vehicle: Vehicle), _) + if vehicle.Definition == GlobalDefinitions.ant && + vehicle.DeploymentState == DriveState.Deployed && + Vector3.DistanceSquared(resourceSilo.Position.xy, vehicle.Position.xy) < math.pow(resourceSilo.Definition.UseRadius, 2) => + resourceSilo.Actor ! CommonMessages.Use(player, Some(vehicle)) + case _ => () + } + } + + def handleUsePlayer(obj: Player, equipment: Option[Equipment], msg: UseItemMessage): Unit = { + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + if (obj.isBackpack) { + if (equipment.isEmpty) { + log.info(s"${player.Name} is looting the corpse of ${obj.Name}") + sendResponse(msg) + accessContainer(obj) + } + } else if (!msg.unk3 && player.isAlive) { //potential kit use + (continent.GUID(msg.item_used_guid), kitToBeUsed) match { + case (Some(kit: Kit), None) => + kitToBeUsed = Some(msg.item_used_guid) + player.Actor ! CommonMessages.Use(player, Some(kit)) + case (Some(_: Kit), Some(_)) | (None, Some(_)) => + //a kit is already queued to be used; ignore this request + sendResponse(ChatMsg(ChatMessageType.UNK_225, wideContents=false, "", "Please wait ...", None)) + case (Some(item), _) => + log.error(s"UseItem: ${player.Name} looking for Kit to use, but found $item instead") + case (None, None) => + log.warn(s"UseItem: anticipated a Kit ${msg.item_used_guid} for ${player.Name}, but can't find it") } + } else if (msg.object_id == ObjectClass.avatar && msg.unk3) { + equipment match { + case Some(tool: Tool) if tool.Definition == GlobalDefinitions.bank => + obj.Actor ! CommonMessages.Use(player, equipment) + + case Some(tool: Tool) if tool.Definition == GlobalDefinitions.medicalapplicator => + obj.Actor ! CommonMessages.Use(player, equipment) + case _ => () + } + } + } + + def handleUseLocker(locker: Locker, equipment: Option[Equipment], msg: UseItemMessage): Unit = { + equipment match { + case Some(item) => + sendUseGeneralEntityMessage(locker, item) + case None if locker.Faction == player.Faction || locker.HackedBy.nonEmpty => + log.info(s"${player.Name} is accessing a locker") + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + val playerLocker = player.avatar.locker + sendResponse(msg.copy(object_guid = playerLocker.GUID, object_id = 456)) + accessContainer(playerLocker) + case _ => () + } + } + + def handleUseCaptureTerminal(captureTerminal: CaptureTerminal, equipment: Option[Equipment]): Unit = { + equipment match { + case Some(item) => + sendUseGeneralEntityMessage(captureTerminal, item) + case _ if specialItemSlotGuid.nonEmpty => + continent.GUID(specialItemSlotGuid) match { + case Some(llu: CaptureFlag) => + if (llu.Target.GUID == captureTerminal.Owner.GUID) { + continent.LocalEvents ! LocalServiceMessage(continent.id, LocalAction.LluCaptured(llu)) + } else { + log.info( + s"LLU target is not this base. Target GUID: ${llu.Target.GUID} This base: ${captureTerminal.Owner.GUID}" + ) + } + case _ => log.warn("Item in specialItemSlotGuid is not registered with continent or is not a LLU") + } + case _ => () + } + } + + def handleUseFacilityTurret(obj: FacilityTurret, equipment: Option[Equipment], msg: UseItemMessage): Unit = { + equipment.foreach { item => + sendUseGeneralEntityMessage(obj, item) + obj.Actor ! CommonMessages.Use(player, Some((item, msg.unk2.toInt))) //try upgrade path + } + } + + def handleUseVehicle(obj: Vehicle, equipment: Option[Equipment], msg: UseItemMessage): Unit = { + equipment match { + case Some(item) => + sendUseGeneralEntityMessage(obj, item) + case None if player.Faction == obj.Faction => + //access to trunk + if ( + obj.AccessingTrunk.isEmpty && + (!obj.PermissionGroup(AccessPermissionGroup.Trunk.id).contains(VehicleLockState.Locked) || obj.OwnerGuid + .contains(player.GUID)) + ) { + log.info(s"${player.Name} is looking in the ${obj.Definition.Name}'s trunk") + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + obj.AccessingTrunk = player.GUID + accessContainer(obj) + sendResponse(msg) + } + case _ => () + } + } + + def handleUseTerminal(terminal: Terminal, equipment: Option[Equipment], msg: UseItemMessage): Unit = { + equipment match { + case Some(item) => + sendUseGeneralEntityMessage(terminal, item) + case None + if terminal.Owner == Building.NoBuilding || terminal.Faction == player.Faction || + terminal.HackedBy.nonEmpty || terminal.Faction == PlanetSideEmpire.NEUTRAL => + val tdef = terminal.Definition + if (tdef.isInstanceOf[MatrixTerminalDefinition]) { + //TODO matrix spawn point; for now, just blindly bind to show work (and hope nothing breaks) + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + sendResponse( + BindPlayerMessage(BindStatus.Bind, "", display_icon=true, logging=true, SpawnGroup.Sanctuary, 0, 0, terminal.Position) + ) + } else if ( + tdef == GlobalDefinitions.multivehicle_rearm_terminal || tdef == GlobalDefinitions.bfr_rearm_terminal || + tdef == GlobalDefinitions.air_rearm_terminal || tdef == GlobalDefinitions.ground_rearm_terminal + ) { + sessionLogic.vehicles.findLocalVehicle match { + case Some(vehicle) => + log.info( + s"${player.Name} is accessing a ${terminal.Definition.Name} for ${player.Sex.possessive} ${vehicle.Definition.Name}" + ) + sendResponse(msg) + sendResponse(msg.copy(object_guid = vehicle.GUID, object_id = vehicle.Definition.ObjectId)) + case None => + log.error(s"UseItem: Expecting a seated vehicle, ${player.Name} found none") + } + } else if (tdef == GlobalDefinitions.teleportpad_terminal) { + //explicit request + log.info(s"${player.Name} is purchasing a router telepad") + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + terminal.Actor ! Terminal.Request( + player, + ItemTransactionMessage(msg.object_guid, TransactionType.Buy, 0, "router_telepad", 0, PlanetSideGUID(0)) + ) + } else if (tdef == GlobalDefinitions.targeting_laser_dispenser) { + //explicit request + log.info(s"${player.Name} is purchasing a targeting laser") + 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}") + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + sendResponse(msg) + } + case _ => () + } + } + + def handleUseSpawnTube(obj: SpawnTube, equipment: Option[Equipment]): Unit = { + equipment match { + case Some(item) => + sendUseGeneralEntityMessage(obj, item) + case None if player.Faction == obj.Faction => + //deconstruction + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + sessionLogic.actionsToCancel() + sessionLogic.terminals.CancelAllProximityUnits() + sessionLogic.zoning.spawn.startDeconstructing(obj) + case _ => () + } + } + + def handleUseTelepadDeployable( + obj: TelepadDeployable, + equipment: Option[Equipment], + msg: UseItemMessage, + useTelepadFunc: (Vehicle, InternalTelepad, TelepadDeployable, PlanetSideGameObject with TelepadLike, PlanetSideGameObject with TelepadLike) => Unit + ): Unit = { + if (equipment.isEmpty) { + (continent.GUID(obj.Router) match { + case Some(vehicle: Vehicle) => Some((vehicle, vehicle.Utility(UtilityType.internal_router_telepad_deployable))) + case Some(vehicle) => Some(vehicle, None) + case None => None + }) match { + case Some((vehicle: Vehicle, Some(util: Utility.InternalTelepad))) => + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel") + player.WhichSide = vehicle.WhichSide + useTelepadFunc(vehicle, util, obj, obj, util) + case Some((vehicle: Vehicle, None)) => + log.error( + s"telepad@${msg.object_guid.guid} is not linked to a router - ${vehicle.Definition.Name}" + ) + case Some((o, _)) => + log.error( + s"telepad@${msg.object_guid.guid} is linked to wrong kind of object - ${o.Definition.Name}, ${obj.Router}" + ) + obj.Actor ! Deployable.Deconstruct() + case _ => () + } + } + } + + def handleUseInternalTelepad( + obj: InternalTelepad, + msg: UseItemMessage, + useTelepadFunc: (Vehicle, InternalTelepad, TelepadDeployable, PlanetSideGameObject with TelepadLike, PlanetSideGameObject with TelepadLike) => Unit + ): Unit = { + continent.GUID(obj.Telepad) match { + case Some(pad: TelepadDeployable) => + player.WhichSide = pad.WhichSide + useTelepadFunc(obj.Owner.asInstanceOf[Vehicle], obj, pad, obj, pad) + case Some(o) => + log.error( + s"internal telepad@${msg.object_guid.guid} is not linked to a remote telepad - ${o.Definition.Name}@${o.GUID.guid}" + ) + case None => () + } + } + + /** + * A player uses a fully-linked Router teleportation system. + * @param router the Router vehicle + * @param internalTelepad the internal telepad within the Router vehicle + * @param remoteTelepad the remote telepad that is currently associated with this Router + * @param src the origin of the teleportation (where the player starts) + * @param dest the destination of the teleportation (where the player is going) + */ + def useRouterTelepadSystem( + router: Vehicle, + internalTelepad: InternalTelepad, + remoteTelepad: TelepadDeployable, + src: PlanetSideGameObject with TelepadLike, + dest: PlanetSideGameObject with TelepadLike + ): Unit = { + val time = System.currentTimeMillis() + if ( + time - recentTeleportAttempt > 2000L && router.DeploymentState == DriveState.Deployed && + internalTelepad.Active && + remoteTelepad.Active + ) { + val pguid = player.GUID + val sguid = src.GUID + val dguid = dest.GUID + sendResponse(PlayerStateShiftMessage(ShiftState(0, dest.Position, player.Orientation.z))) + useRouterTelepadEffect(pguid, sguid, dguid) + continent.LocalEvents ! LocalServiceMessage( + continent.id, + LocalAction.RouterTelepadTransport(pguid, pguid, sguid, dguid) + ) + val vSource = VehicleSource(router) + val zoneNumber = continent.Number + player.LogActivity(VehicleMountActivity(vSource, PlayerSource(player), zoneNumber)) + player.Position = dest.Position + player.LogActivity(VehicleDismountActivity(vSource, PlayerSource(player), zoneNumber)) + } else { + log.warn(s"UseRouterTelepadSystem: ${player.Name} can not teleport") + } + recentTeleportAttempt = time + } + + /** + * A player uses a fully-linked Router teleportation system. + * @param router the Router vehicle + * @param internalTelepad the internal telepad within the Router vehicle + * @param remoteTelepad the remote telepad that is currently associated with this Router + * @param src the origin of the teleportation (where the player starts) + * @param dest the destination of the teleportation (where the player is going) + */ + def useRouterTelepadSystemSecretly( + router: Vehicle, + internalTelepad: InternalTelepad, + remoteTelepad: TelepadDeployable, + src: PlanetSideGameObject with TelepadLike, + dest: PlanetSideGameObject with TelepadLike + ): Unit = { + val time = System.currentTimeMillis() + if ( + time - recentTeleportAttempt > 2000L && router.DeploymentState == DriveState.Deployed && + internalTelepad.Active && + remoteTelepad.Active + ) { + val pguid = player.GUID + val sguid = src.GUID + val dguid = dest.GUID + sendResponse(PlayerStateShiftMessage(ShiftState(0, dest.Position, player.Orientation.z))) + useRouterTelepadEffect(pguid, sguid, dguid) + player.Position = dest.Position + } else { + log.warn(s"UseRouterTelepadSystem: ${player.Name} can not teleport") + } + recentTeleportAttempt = time + } + + def handleUseCaptureFlag(obj: CaptureFlag): Unit = { + // LLU can normally only be picked up the faction that owns it + specialItemSlotGuid match { + case None if obj.Faction == player.Faction => + specialItemSlotGuid = Some(obj.GUID) + player.Carrying = SpecialCarry.CaptureFlag + continent.LocalEvents ! CaptureFlagManager.PickupFlag(obj, player) + case None => + log.warn(s"${player.Faction} player ${player.toString} tried to pick up a ${obj.Faction} LLU - ${obj.GUID}") + case Some(guid) if guid != obj.GUID => + // Ignore duplicate pickup requests + log.warn( + s"${player.Faction} player ${player.toString} tried to pick up a ${obj.Faction} LLU, but their special slot already contains $guid" + ) + case _ => () + } + } + + def handleUseWarpGate(equipment: Option[Equipment]): Unit = { + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + (continent.GUID(player.VehicleSeated), equipment) match { + case (Some(vehicle: Vehicle), Some(item)) + if GlobalDefinitions.isBattleFrameVehicle(vehicle.Definition) && + GlobalDefinitions.isBattleFrameNTUSiphon(item.Definition) => + vehicle.Actor ! CommonMessages.Use(player, equipment) + case _ => () + } + } + + def handleUseGeneralEntity(obj: PlanetSideServerObject, equipment: Option[Equipment]): Unit = { + equipment.foreach { item => + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + obj.Actor ! CommonMessages.Use(player, Some(item)) + } + } + + def sendUseGeneralEntityMessage(obj: PlanetSideServerObject, equipment: Equipment): Unit = { + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + obj.Actor ! CommonMessages.Use(player, Some(equipment)) + } + + def handleUseDefaultEntity(obj: PlanetSideGameObject, equipment: Option[Equipment]): Unit = { + sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") + equipment match { + case Some(item) + if GlobalDefinitions.isBattleFrameArmorSiphon(item.Definition) || + GlobalDefinitions.isBattleFrameNTUSiphon(item.Definition) => () + case _ => + log.warn(s"UseItem: ${player.Name} does not know how to handle $obj") + } + } + + def commonFacilityShieldCharging(obj: PlanetSideServerObject with BlockMapEntity): Unit = { + obj.Actor ! CommonMessages.ChargeShields( + 15, + Some(continent.blockMap.sector(obj).buildingList.maxBy(_.Definition.SOIRadius)) + ) + } + override protected[session] def actionsToCancel(): Unit = { progressBarValue = None kitToBeUsed = None 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 7fc6a893b..f09a432bc 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionLocalHandlers.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionLocalHandlers.scala @@ -6,9 +6,9 @@ import net.psforever.objects.{Players, TurretDeployable} import net.psforever.objects.ce.Deployable import net.psforever.objects.guid.{GUIDTask, TaskWorkflow} import net.psforever.objects.serverobject.interior.Sidedness -import net.psforever.packet.game.GenericObjectActionMessage +import net.psforever.packet.game.{GenericObjectActionMessage, ObjectDeleteMessage, PlanetsideAttributeMessage, TriggerEffectMessage} import net.psforever.services.local.LocalResponse -import net.psforever.types.PlanetSideGUID +import net.psforever.types.{PlanetSideGUID, Vector3} trait LocalHandlerFunctions extends CommonSessionInterfacingFunctionality { def ops: SessionLocalHandlers @@ -47,4 +47,26 @@ class SessionLocalHandlers( else 400f } + + + + /** + * 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)) + } } 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 c124bd182..bc9f30b12 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionMountHandlers.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionMountHandlers.scala @@ -2,15 +2,21 @@ package net.psforever.actors.session.support import akka.actor.{ActorContext, typed} -import net.psforever.objects.Tool -import net.psforever.objects.vehicles.MountableWeapons -import net.psforever.packet.game.{DismountVehicleCargoMsg, InventoryStateMessage, MountVehicleCargoMsg, MountVehicleMsg} +import net.psforever.objects.serverobject.affinity.FactionAffinity +import net.psforever.objects.{PlanetSideGameObject, Tool, Vehicle} +import net.psforever.objects.vehicles.{CargoBehavior, MountableWeapons} +import net.psforever.objects.vital.InGameHistory +import net.psforever.packet.game.{DismountVehicleCargoMsg, InventoryStateMessage, MountVehicleCargoMsg, MountVehicleMsg, ObjectAttachMessage} +import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} +import net.psforever.types.{BailType, PlanetSideGUID, Vector3} // import net.psforever.actors.session.AvatarActor import net.psforever.objects.Player import net.psforever.objects.serverobject.mount.Mountable import net.psforever.packet.game.DismountVehicleMsg +import scala.concurrent.duration._ + trait MountHandlerFunctions extends CommonSessionInterfacingFunctionality { val ops: SessionMountHandlers @@ -30,6 +36,218 @@ class SessionMountHandlers( val avatarActor: typed.ActorRef[AvatarActor.Command], implicit val context: ActorContext ) extends CommonSessionInterfacingFunctionality { + def handleMountVehicle(pkt: MountVehicleMsg): Unit = { + val MountVehicleMsg(_, mountable_guid, entry_point) = pkt + sessionLogic.validObject(mountable_guid, decorator = "MountVehicle").collect { + case obj: Mountable => + obj.Actor ! Mountable.TryMount(player, entry_point) + case _ => + log.error(s"MountVehicleMsg: object ${mountable_guid.guid} not a mountable thing, ${player.Name}") + } + } + + def handleDismountVehicle(pkt: DismountVehicleMsg): Unit = { + val DismountVehicleMsg(player_guid, bailType, wasKickedByDriver) = pkt + val dError: (String, Player)=> Unit = dismountError(bailType, wasKickedByDriver) + //TODO optimize this later + //common warning for this section + if (player.GUID == player_guid) { + //normally disembarking from a mount + (sessionLogic.zoning.interstellarFerry.orElse(continent.GUID(player.VehicleSeated)) match { + case out @ Some(obj: Vehicle) => + continent.GUID(obj.MountedIn) match { + case Some(_: Vehicle) => None //cargo vehicle + case _ => out //arrangement "may" be permissible + } + case out @ Some(_: Mountable) => + out + case _ => + dError(s"DismountVehicleMsg: player ${player.Name} not considered seated in a mountable entity", player) + None + }) match { + case Some(obj: Mountable) => + obj.PassengerInSeat(player) match { + case Some(seat_num) => + obj.Actor ! Mountable.TryDismount(player, seat_num, bailType) + //short-circuit the temporary channel for transferring between zones, the player is no longer doing that + sessionLogic.zoning.interstellarFerry = None + + case None => + dError(s"DismountVehicleMsg: can not find where player ${player.Name}_guid is seated in mountable ${player.VehicleSeated}", player) + } + case _ => + dError(s"DismountVehicleMsg: can not find mountable entity ${player.VehicleSeated}", player) + } + } else { + //kicking someone else out of a mount; need to own that mount/mountable + val dWarn: (String, Player)=> Unit = dismountWarning(bailType, wasKickedByDriver) + player.avatar.vehicle match { + case Some(obj_guid) => + ( + ( + sessionLogic.validObject(obj_guid, decorator = "DismountVehicle/Vehicle"), + sessionLogic.validObject(player_guid, decorator = "DismountVehicle/Player") + ) match { + case (vehicle @ Some(obj: Vehicle), tplayer) => + if (obj.MountedIn.isEmpty) (vehicle, tplayer) else (None, None) + case (mount @ Some(_: Mountable), tplayer) => + (mount, tplayer) + case _ => + (None, None) + }) match { + case (Some(obj: Mountable), Some(tplayer: Player)) => + obj.PassengerInSeat(tplayer) match { + case Some(seat_num) => + obj.Actor ! Mountable.TryDismount(tplayer, seat_num, bailType) + case None => + dError(s"DismountVehicleMsg: can not find where other player ${tplayer.Name} is seated in mountable $obj_guid", tplayer) + } + case (None, _) => + dWarn(s"DismountVehicleMsg: ${player.Name} can not find his vehicle", player) + case (_, None) => + dWarn(s"DismountVehicleMsg: player $player_guid could not be found to kick, ${player.Name}", player) + case _ => + dWarn(s"DismountVehicleMsg: object is either not a Mountable or not a Player", player) + } + case None => + dWarn(s"DismountVehicleMsg: ${player.Name} does not own a vehicle", player) + } + } + } + + def handleMountVehicleCargo(pkt: MountVehicleCargoMsg): Unit = { + val MountVehicleCargoMsg(_, cargo_guid, carrier_guid, _) = pkt + (continent.GUID(cargo_guid), continent.GUID(carrier_guid)) match { + case (Some(cargo: Vehicle), Some(carrier: Vehicle)) => + carrier.CargoHolds.find({ case (_, hold) => !hold.isOccupied }) match { + case Some((mountPoint, _)) => + cargo.Actor ! CargoBehavior.StartCargoMounting(carrier_guid, mountPoint) + case _ => + log.warn( + s"MountVehicleCargoMsg: ${player.Name} trying to load cargo into a ${carrier.Definition.Name} which oes not have a cargo hold" + ) + } + case (None, _) | (Some(_), None) => + log.warn( + s"MountVehicleCargoMsg: ${player.Name} lost a vehicle while working with cargo - either $carrier_guid or $cargo_guid" + ) + case _ => () + } + } + + def handleDismountVehicleCargo(pkt: DismountVehicleCargoMsg): Unit = { + val DismountVehicleCargoMsg(_, cargo_guid, bailed, _, kicked) = pkt + continent.GUID(cargo_guid) match { + case Some(cargo: Vehicle) => + cargo.Actor ! CargoBehavior.StartCargoDismounting(bailed || kicked) + case _ => () + } + } + + private def dismountWarning( + bailAs: BailType.Value, + kickedByDriver: Boolean + ) + ( + note: String, + player: Player + ): Unit = { + log.warn(note) + player.VehicleSeated = None + sendResponse(DismountVehicleMsg(player.GUID, bailAs, kickedByDriver)) + } + + private def dismountError( + bailAs: BailType.Value, + kickedByDriver: Boolean + ) + ( + note: String, + player: Player + ): Unit = { + log.error(s"$note; some vehicle might not know that ${player.Name} is no longer sitting in it") + player.VehicleSeated = None + sendResponse(DismountVehicleMsg(player.GUID, bailAs, kickedByDriver)) + } + + /** + * Common activities/procedure when a player mounts a valid object. + * @param tplayer the player + * @param obj the mountable object + * @param seatNum the mount into which the player is mounting + */ + def MountingAction(tplayer: Player, obj: PlanetSideGameObject with FactionAffinity with InGameHistory, seatNum: Int): Unit = { + val playerGuid: PlanetSideGUID = tplayer.GUID + val objGuid: PlanetSideGUID = obj.GUID + sessionLogic.actionsToCancel() + avatarActor ! AvatarActor.DeactivateActiveImplants + avatarActor ! AvatarActor.SuspendStaminaRegeneration(3.seconds) + sendResponse(ObjectAttachMessage(objGuid, playerGuid, seatNum)) + continent.VehicleEvents ! VehicleServiceMessage( + continent.id, + VehicleAction.MountVehicle(playerGuid, objGuid, seatNum) + ) + } + + /** + * Common activities/procedure when a player dismounts a valid mountable object. + * @param tplayer the player + * @param obj the mountable object + * @param seatNum the mount out of which which the player is disembarking + */ + def DismountVehicleAction(tplayer: Player, obj: PlanetSideGameObject with FactionAffinity with InGameHistory, seatNum: Int): Unit = { + DismountAction(tplayer, obj, seatNum) + //until vehicles maintain synchronized momentum without a driver + obj match { + case v: Vehicle + if seatNum == 0 && Vector3.MagnitudeSquared(v.Velocity.getOrElse(Vector3.Zero)) > 0f => + sessionLogic.vehicles.serverVehicleControlVelocity.collect { _ => + sessionLogic.vehicles.ServerVehicleOverrideStop(v) + } + v.Velocity = Vector3.Zero + continent.VehicleEvents ! VehicleServiceMessage( + continent.id, + VehicleAction.VehicleState( + tplayer.GUID, + v.GUID, + unk1 = 0, + v.Position, + v.Orientation, + vel = None, + v.Flying, + unk3 = 0, + unk4 = 0, + wheel_direction = 15, + unk5 = false, + unk6 = v.Cloaked + ) + ) + case _ => () + } + } + + /** + * Common activities/procedure when a player dismounts a valid mountable object. + * @param tplayer the player + * @param obj the mountable object + * @param seatNum the mount out of which which the player is disembarking + */ + def DismountAction(tplayer: Player, obj: PlanetSideGameObject with FactionAffinity with InGameHistory, seatNum: Int): Unit = { + val playerGuid: PlanetSideGUID = tplayer.GUID + tplayer.ContributionFrom(obj) + sessionLogic.keepAliveFunc = sessionLogic.zoning.NormalKeepAlive + val bailType = if (tplayer.BailProtection) { + BailType.Bailed + } else { + BailType.Normal + } + sendResponse(DismountVehicleMsg(playerGuid, bailType, wasKickedByDriver = false)) + continent.VehicleEvents ! VehicleServiceMessage( + continent.id, + VehicleAction.DismountVehicle(playerGuid, bailType, unk2 = false) + ) + } + /** * 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) 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 1ccb6bf3f..1aaea60c8 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionSquadHandlers.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionSquadHandlers.scala @@ -15,6 +15,8 @@ import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, Vector3} trait SquadHandlerFunctions extends CommonSessionInterfacingFunctionality { val ops: SessionSquadHandlers + protected var waypointCooldown: Long = 0L + def handleSquadDefinitionAction(pkt: SquadDefinitionActionMessage): Unit def handleSquadMemberRequest(pkt: SquadMembershipRequest): Unit 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 347139e6a..bb8770a48 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionTerminalHandlers.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionTerminalHandlers.scala @@ -2,8 +2,12 @@ package net.psforever.actors.session.support import akka.actor.{ActorContext, typed} -import net.psforever.objects.guid.GUIDTask -import net.psforever.packet.game.FavoritesRequest +import net.psforever.objects.guid.{GUIDTask, TaskWorkflow} +import net.psforever.objects.inventory.InventoryItem +import net.psforever.objects.sourcing.AmenitySource +import net.psforever.objects.vital.TerminalUsedActivity +import net.psforever.packet.game.{FavoritesAction, FavoritesRequest, ItemTransactionResultMessage, UnuseItemMessage} +import net.psforever.types.{TransactionType, Vector3} import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future @@ -39,6 +43,99 @@ class SessionTerminalHandlers( private[session] var lastTerminalOrderFulfillment: Boolean = true private[session] var usingMedicalTerminal: Option[PlanetSideGUID] = None + def handleItemTransaction(pkt: ItemTransactionMessage): Unit = { + val ItemTransactionMessage(terminalGuid, transactionType, _, itemName, _, _) = pkt + continent.GUID(terminalGuid) match { + case Some(term: Terminal) if lastTerminalOrderFulfillment => + val msg: String = if (itemName.nonEmpty) s" of $itemName" else "" + log.info(s"${player.Name} is submitting an order - a $transactionType from a ${term.Definition.Name}$msg") + lastTerminalOrderFulfillment = false + term.Actor ! Terminal.Request(player, pkt) + case Some(_: Terminal) => + log.warn(s"Please Wait until your previous order has been fulfilled, ${player.Name}") + case Some(obj) => + log.error(s"ItemTransaction: ${obj.Definition.Name} is not a terminal, ${player.Name}") + case _ => + log.error(s"ItemTransaction: entity with guid=${terminalGuid.guid} does not exist, ${player.Name}") + } + } + + def handleProximityTerminalUse(pkt: ProximityTerminalUseMessage): Unit = { + val ProximityTerminalUseMessage(_, objectGuid, _) = pkt + continent.GUID(objectGuid) match { + case Some(obj: Terminal with ProximityUnit) => + performProximityTerminalUse(obj) + case Some(obj) => + log.warn(s"ProximityTerminalUse: ${obj.Definition.Name} guid=${objectGuid.guid} is not ready to implement proximity effects") + case None => + log.error(s"ProximityTerminalUse: ${player.Name} can not find an object with guid ${objectGuid.guid}") + } + } + + def handleFavoritesRequest(pkt: FavoritesRequest): Unit = { + val FavoritesRequest(_, loadoutType, action, line, label) = pkt + action match { + case FavoritesAction.Save => + avatarActor ! AvatarActor.SaveLoadout(player, loadoutType, label, line) + case FavoritesAction.Delete => + avatarActor ! AvatarActor.DeleteLoadout(player, loadoutType, line) + case FavoritesAction.Unknown => + log.warn(s"FavoritesRequest: ${player.Name} requested an unknown favorites action") + } + } + + def buyVehicle( + terminalGuid: PlanetSideGUID, + transactionType: TransactionType.Value, + vehicle: Vehicle, + weapons: List[InventoryItem], + trunk: List[InventoryItem] + ): Unit = { + continent.map.terminalToSpawnPad + .find { case (termid, _) => termid == terminalGuid.guid } + .map { case (a: Int, b: Int) => (continent.GUID(a), continent.GUID(b)) } + .collect { case (Some(term: Terminal), Some(pad: VehicleSpawnPad)) => + avatarActor ! AvatarActor.UpdatePurchaseTime(vehicle.Definition) + vehicle.Faction = player.Faction + vehicle.Position = pad.Position + vehicle.Orientation = pad.Orientation + Vector3.z(pad.Definition.VehicleCreationZOrientOffset) + //default loadout, weapons + val vWeapons = vehicle.Weapons + weapons.foreach { entry => + vWeapons.get(entry.start) match { + case Some(slot) => + entry.obj.Faction = player.Faction + slot.Equipment = None + slot.Equipment = entry.obj + case None => + log.warn( + s"BuyVehicle: ${player.Name} tries to apply default loadout to $vehicle on spawn, but can not find a mounted weapon for ${entry.start}" + ) + } + } + //default loadout, trunk + val vTrunk = vehicle.Trunk + vTrunk.Clear() + trunk.foreach { entry => + entry.obj.Faction = player.Faction + vTrunk.InsertQuickly(entry.start, entry.obj) + } + TaskWorkflow.execute(registerVehicleFromSpawnPad(vehicle, pad, term)) + sendResponse(ItemTransactionResultMessage(terminalGuid, TransactionType.Buy, success = true)) + if (GlobalDefinitions.isBattleFrameVehicle(vehicle.Definition)) { + sendResponse(UnuseItemMessage(player.GUID, terminalGuid)) + } + player.LogActivity(TerminalUsedActivity(AmenitySource(term), transactionType)) + } + .orElse { + log.error( + s"${player.Name} wanted to spawn a vehicle, but there was no spawn pad associated with terminal ${terminalGuid.guid} to accept it" + ) + sendResponse(ItemTransactionResultMessage(terminalGuid, TransactionType.Buy, success = false)) + None + } + } + /** * Construct tasking that adds a completed and registered vehicle into the scene. * The major difference between `RegisterVehicle` and `RegisterVehicleFromSpawnPad` is the assumption that this vehicle lacks an internal `Actor`. @@ -92,7 +189,7 @@ class SessionTerminalHandlers( * na * @param terminal na */ - def HandleProximityTerminalUse(terminal: Terminal with ProximityUnit): Unit = { + def performProximityTerminalUse(terminal: Terminal with ProximityUnit): Unit = { val term_guid = terminal.GUID val targets = FindProximityUnitTargetsInScope(terminal) val currentTargets = terminal.Targets 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 f23c61d38..a9f4dc2e8 100644 --- a/src/main/scala/net/psforever/actors/session/support/VehicleOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/VehicleOperations.scala @@ -39,6 +39,18 @@ class VehicleOperations( ) extends CommonSessionInterfacingFunctionality { private[session] var serverVehicleControlVelocity: Option[Int] = None + /** + * Get the current `Vehicle` object that the player is riding/driving. + * The vehicle must be found solely through use of `player.VehicleSeated`. + * @return the vehicle + */ + def findLocalVehicle: Option[Vehicle] = { + continent.GUID(player.VehicleSeated) match { + case Some(obj: Vehicle) => Some(obj) + case _ => None + } + } + /** * If the player is mounted in some entity, find that entity and get the mount index number at which the player is sat. * The priority of object confirmation is `direct` then `occupant.VehicleSeated`. 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 55753dff9..e5b1db25f 100644 --- a/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala @@ -2,14 +2,34 @@ package net.psforever.actors.session.support import akka.actor.{ActorContext, typed} -import net.psforever.objects.definition.SpecialExoSuitDefinition -import net.psforever.objects.zones.Zoning +import net.psforever.login.WorldSession.{CountAmmunition, FindAmmoBoxThatUses, FindEquipmentStock, PutEquipmentInInventoryOrDrop, PutNewEquipmentInInventoryOrDrop} +import net.psforever.objects.ballistics.ProjectileQuality +import net.psforever.objects.definition.{ProjectileDefinition, SpecialExoSuitDefinition} +import net.psforever.objects.entity.SimpleWorldEntity +import net.psforever.objects.equipment.{ChargeFireModeDefinition, Equipment, FireModeSwitch} +import net.psforever.objects.guid.{GUIDTask, TaskBundle, TaskWorkflow} +import net.psforever.objects.serverobject.PlanetSideServerObject +import net.psforever.objects.serverobject.affinity.FactionAffinity +import net.psforever.objects.serverobject.doors.InteriorDoorPassage +import net.psforever.objects.serverobject.interior.Sidedness +import net.psforever.objects.serverobject.structures.Amenity +import net.psforever.objects.zones.{Zone, ZoneProjectile, Zoning} import net.psforever.objects.serverobject.turret.VanuSentry +import net.psforever.objects.serverobject.turret.auto.{AutomatedTurret, AutomatedTurretBehavior} +import net.psforever.objects.sourcing.{PlayerSource, SourceEntry} +import net.psforever.objects.vital.Vitality +import net.psforever.objects.vital.base.DamageResolution +import net.psforever.objects.vital.etc.OicwLilBuddyReason +import net.psforever.objects.vital.interaction.DamageInteraction +import net.psforever.objects.vital.projectile.ProjectileReason import net.psforever.objects.zones.exp.ToDatabase -import net.psforever.types.ChatMessageType +import net.psforever.types.{ChatMessageType, Vector3} +import net.psforever.util.Config import scala.collection.mutable +import scala.concurrent.Future import scala.concurrent.duration._ +import scala.concurrent.ExecutionContext.Implicits.global // import net.psforever.actors.session.AvatarActor import net.psforever.objects.avatar.scoring.EquipmentStat @@ -78,7 +98,114 @@ class WeaponAndProjectileOperations( Array.fill[Option[Projectile]](Projectile.rangeUID - Projectile.baseUID)(None) } - def HandleWeaponFireAccountability( + def handleWeaponFireOperations(pkt: WeaponFireMessage): Unit = { + val WeaponFireMessage( + _, + weaponGUID, + projectileGUID, + shotOrigin, + _, + _, + _, + _/*max_distance,*/, + _, + _/*projectile_type,*/, + thrown_projectile_vel + ) = pkt + val shotVelocity = thrown_projectile_vel.flatten + handleWeaponFireAccountability(weaponGUID, projectileGUID) match { + case (Some(obj), Some(tool)) => + val projectileIndex = projectileGUID.guid - Projectile.baseUID + val projectilePlace = projectiles(projectileIndex) + if ( + projectilePlace match { + case Some(projectile) => + !projectile.isResolved && System.currentTimeMillis() - projectile.fire_time < projectile.profile.Lifespan.toLong + case None => + false + } + ) { + log.debug( + s"WeaponFireMessage: overwriting unresolved projectile ${projectileGUID.guid}, known to ${player.Name}" + ) + } + val (angle, attribution, acceptableDistanceToOwner) = obj match { + case p: Player => + ( + SimpleWorldEntity.validateOrientationEntry( + p.Orientation + Vector3.z(p.FacingYawUpper) + ), + tool.Definition.ObjectId, + 10f + (if (p.Velocity.nonEmpty) { + 5f + } else { + 0f + }) + ) + case v: Vehicle if v.Definition.CanFly => + (tool.Orientation, obj.Definition.ObjectId, 1000f) //TODO this is too simplistic to find proper angle + case _: Vehicle => + (tool.Orientation, obj.Definition.ObjectId, 225f) //TODO this is too simplistic to find proper angle + case _ => + (obj.Orientation, obj.Definition.ObjectId, 300f) + } + val distanceToOwner = Vector3.DistanceSquared(shotOrigin, player.Position) + if (distanceToOwner <= acceptableDistanceToOwner) { + val projectile_info = tool.Projectile + val wguid = weaponGUID.guid + val mountedIn = (continent.turretToWeapon + .find { case (guid, _) => guid == wguid } match { + case Some((_, turretGuid)) => Some(( + turretGuid, + continent.GUID(turretGuid).collect { case o: PlanetSideGameObject with FactionAffinity => SourceEntry(o) } + )) + case _ => None + }) match { + case Some((guid, Some(entity))) => Some((guid, entity)) + case _ => None + } + val projectile = new Projectile( + projectile_info, + tool.Definition, + tool.FireMode, + mountedIn, + PlayerSource(player), + attribution, + shotOrigin, + angle, + shotVelocity + ) + val initialQuality = tool.FireMode match { + case mode: ChargeFireModeDefinition => + ProjectileQuality.Modified( + { + val timeInterval = projectile.fire_time - shootingStart.getOrElse(tool.GUID, System.currentTimeMillis()) + timeInterval.toFloat / mode.Time.toFloat + } + ) + case _ => + ProjectileQuality.Normal + } + val qualityprojectile = projectile.quality(initialQuality) + qualityprojectile.WhichSide = player.WhichSide + projectiles(projectileIndex) = Some(qualityprojectile) + if (projectile_info.ExistsOnRemoteClients) { + log.trace( + s"WeaponFireMessage: ${player.Name}'s ${projectile_info.Name} is a remote projectile" + ) + continent.Projectile ! ZoneProjectile.Add(player.GUID, qualityprojectile) + } + } else { + log.warn( + s"WeaponFireMessage: ${player.Name}'s ${tool.Definition.Name} projectile is too far from owner position at time of discharge ($distanceToOwner > $acceptableDistanceToOwner); suspect" + ) + } + + case _ => () + } + } + + def handleWeaponFireAccountability( weaponGUID: PlanetSideGUID, projectileGUID: PlanetSideGUID ): (Option[PlanetSideGameObject with Container], Option[Tool]) = { @@ -105,7 +232,7 @@ class WeaponAndProjectileOperations( case tool: Tool if tool.GUID == weaponGUID => if (tool.Magazine <= 0) { //safety: enforce ammunition depletion prefire -= weaponGUID - EmptyMagazine(weaponGUID, tool) + emptyMagazine(weaponGUID, tool) projectiles(projectileGUID.guid - Projectile.baseUID) = None (None, None) } else if (!player.isAlive) { //proper internal accounting, but no projectile @@ -137,6 +264,471 @@ class WeaponAndProjectileOperations( } } + def handleWeaponDryFire(pkt: WeaponDryFireMessage): Unit = { + val WeaponDryFireMessage(weapon_guid) = pkt + val (containerOpt, tools) = FindContainedWeapon + tools + .find { _.GUID == weapon_guid } + .orElse { continent.GUID(weapon_guid) } + .collect { + case _: Equipment if containerOpt.exists(_.isInstanceOf[Player]) => + continent.AvatarEvents ! AvatarServiceMessage( + continent.id, + AvatarAction.WeaponDryFire(player.GUID, weapon_guid) + ) + case _: Equipment => + continent.VehicleEvents ! VehicleServiceMessage( + continent.id, + VehicleAction.WeaponDryFire(player.GUID, weapon_guid) + ) + } + .orElse { + log.warn( + s"WeaponDryFire: ${player.Name}'s weapon ${weapon_guid.guid} is either not a weapon or does not exist" + ) + None + } + } + + def handleChangeAmmo(pkt: ChangeAmmoMessage): Unit = { + val ChangeAmmoMessage(item_guid, _) = pkt + 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 { + equipment foreach { + case obj: ConstructionItem => + if (Deployables.performConstructionItemAmmoChange(player.avatar.certifications, obj, obj.AmmoTypeIndex)) { + log.info( + s"${player.Name} switched ${player.Sex.possessive} ${obj.Definition.Name} to construct ${obj.AmmoType} (option #${obj.FireModeIndex})" + ) + sendResponse(ChangeAmmoMessage(obj.GUID, obj.AmmoTypeIndex)) + } + case tool: Tool => + thing match { + case Some(player: Player) => + performToolAmmoChange(tool, player, modifyAmmunition(player)) + case Some(mountable: PlanetSideServerObject with Container) => + performToolAmmoChange(tool, mountable, modifyAmmunitionInMountable(mountable)) + case _ => + log.warn(s"ChangeAmmo: the ${thing.get.Definition.Name} in ${player.Name}'s is not the correct type") + } + case obj => + log.warn(s"ChangeAmmo: the ${obj.Definition.Name} in ${player.Name}'s hands does not contain ammunition") + } + } + } + + def handleChangeFireMode(pkt: ChangeFireModeMessage): Unit = { + val ChangeFireModeMessage(item_guid, _/*fire_mode*/) = pkt + sessionLogic.findEquipment(item_guid) match { + case Some(obj: PlanetSideGameObject with FireModeSwitch[_]) => + val originalModeIndex = obj.FireModeIndex + if (obj match { + case citem: ConstructionItem => + val modeChanged = Deployables.performConstructionItemFireModeChange( + player.avatar.certifications, + citem, + originalModeIndex + ) + modeChanged + case _ => + obj.NextFireMode + obj.FireModeIndex != originalModeIndex + }) { + val modeIndex = obj.FireModeIndex + obj match { + case citem: ConstructionItem => + log.info(s"${player.Name} switched ${player.Sex.possessive} ${obj.Definition.Name} to construct ${citem.AmmoType} (mode #$modeIndex)") + case _ => + log.info(s"${player.Name} changed ${player.Sex.possessive} her ${obj.Definition.Name}'s fire mode to #$modeIndex") + } + sendResponse(ChangeFireModeMessage(item_guid, modeIndex)) + continent.AvatarEvents ! AvatarServiceMessage( + continent.id, + AvatarAction.ChangeFireMode(player.GUID, item_guid, modeIndex) + ) + } + case Some(_) => + log.warn(s"ChangeFireMode: the object that was found for $item_guid does not possess fire modes") + case None => + log.warn(s"ChangeFireMode: can not find $item_guid") + } + } + + def handleProjectileState(pkt: ProjectileStateMessage): Unit = { + val ProjectileStateMessage(projectile_guid, shot_pos, shot_vel, shot_orient, seq, end, target_guid) = pkt + val index = projectile_guid.guid - Projectile.baseUID + projectiles(index) match { + case Some(projectile) if projectile.HasGUID => + val projectileGlobalUID = projectile.GUID + projectile.Position = shot_pos + projectile.Orientation = shot_orient + projectile.Velocity = shot_vel + continent.AvatarEvents ! AvatarServiceMessage( + continent.id, + AvatarAction.ProjectileState( + player.GUID, + projectileGlobalUID, + shot_pos, + shot_vel, + shot_orient, + seq, + end, + target_guid + ) + ) + case _ if seq == 0 => + /* missing the first packet in the sequence is permissible */ + case _ => + log.warn(s"ProjectileState: constructed projectile ${projectile_guid.guid} can not be found") + } + } + + def composeDirectDamageInformation(pkt: HitMessage): List[(PlanetSideGameObject with FactionAffinity with Vitality, Projectile, Vector3, Vector3)] = { + val HitMessage( + _, + projectile_guid, + _, + hit_info, + _, + _, + _ + ) = pkt + //find defined projectile + FindProjectileEntry(projectile_guid) + .collect { + case projectile => + hit_info match { + case Some(hitInfo) => + val hitPos = hitInfo.hit_pos + sessionLogic.validObject(hitInfo.hitobject_guid, decorator = "Hit/hitInfo") match { + case _ if projectile.profile == GlobalDefinitions.flail_projectile => + val radius = projectile.profile.DamageRadius * projectile.profile.DamageRadius + Zone + .findAllTargets(continent, player, hitPos, projectile.profile) + .filter(target => Vector3.DistanceSquared(target.Position, hitPos) <= radius) + .map(target => (target, projectile, hitPos, target.Position)) + + case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) => + List((target, projectile, hitInfo.shot_origin, hitPos)) + + case None => + Nil + + case _ => + Nil + } + case None => + Nil + } + } + .getOrElse { + log.warn(s"ResolveProjectile: expected projectile, but ${projectile_guid.guid} not found") + Nil + } + } + + def composeSplashDamageInformation(pkt: SplashHitMessage): List[(PlanetSideGameObject with FactionAffinity with Vitality, Projectile, Vector3, Vector3)] = { + val SplashHitMessage( + _, + projectile_guid, + explosion_pos, + direct_victim_uid, + _, + projectile_vel, + _, + targets + ) = pkt + FindProjectileEntry(projectile_guid) + .collect { + case projectile => + projectile.Position = explosion_pos + projectile.Velocity = projectile_vel + //direct_victim_uid + val direct = sessionLogic + .validObject(direct_victim_uid, decorator = "SplashHit/direct_victim") + .collect { + case target: PlanetSideGameObject with FactionAffinity with Vitality => + val targetPosition = target.Position + List((target, projectile, targetPosition, targetPosition)) + } + .getOrElse(Nil) + //other victims + val others = targets + .flatMap(elem => sessionLogic.validObject(elem.uid, decorator = "SplashHit/other_victims")) + .collect { + case target: PlanetSideGameObject with FactionAffinity with Vitality => + (target, projectile, explosion_pos, target.Position) + } + direct ++ others + } + .getOrElse { + log.warn(s"ResolveProjectile: expected projectile, but ${projectile_guid.guid} not found") + Nil + } + } + + def composeLashDamageInformation(pkt: LashMessage): List[(PlanetSideGameObject with FactionAffinity with Vitality, Projectile, Vector3, Vector3)] = { + val LashMessage(_, _, victim_guid, projectile_guid, hit_pos, _) = pkt + FindProjectileEntry(projectile_guid) + .flatMap { + projectile => + sessionLogic + .validObject(victim_guid, decorator = "LashHit/victim_guid") + .collect { + case target: PlanetSideGameObject with FactionAffinity with Vitality => + List((target, projectile, hit_pos, target.Position)) + } + .orElse(None) + } + .getOrElse(Nil) + } + + def composeAIDamageInformation(pkt: AIDamage): List[(PlanetSideGameObject with FactionAffinity with Vitality, Projectile, Vector3, Vector3)] = { + val AIDamage(targetGuid, attackerGuid, projectileTypeId, _, _) = pkt + (continent.GUID(player.VehicleSeated) match { + case Some(tobj: PlanetSideServerObject with FactionAffinity with Vitality with OwnableByPlayer) + if tobj.GUID == targetGuid && + tobj.OwnerGuid.contains(player.GUID) => + //deployable turrets + Some(tobj) + case Some(tobj: PlanetSideServerObject with FactionAffinity with Vitality with Mountable) + if tobj.GUID == targetGuid && + tobj.Seats.values.flatMap(_.occupants.map(_.GUID)).toSeq.contains(player.GUID) => + //facility turrets, etc. + Some(tobj) + case _ + if player.GUID == targetGuid => + //player avatars + Some(player) + case _ => + None + }).collect { + case target: AutomatedTurret.Target => + sessionLogic.validObject(attackerGuid, decorator = "AIDamage/AutomatedTurret") + .collect { + case turret: AutomatedTurret if turret.Target.isEmpty => + Nil + case turret: AutomatedTurret => + prepareAIProjectile(target, CompileAutomatedTurretDamageData(turret, projectileTypeId)) + } + .getOrElse(Nil) + } + .orElse { + //occasionally, something that is not technically a turret's natural target may be attacked + continent.GUID(targetGuid) //AIDamage/Attacker + .collect { + case target: PlanetSideServerObject with FactionAffinity with Vitality => + sessionLogic.validObject(attackerGuid, decorator = "AIDamage/Attacker") + .collect { + case turret: AutomatedTurret if turret.Target.nonEmpty => + //the turret must be shooting at something (else) first + prepareAIProjectile(target, CompileAutomatedTurretDamageData(turret, projectileTypeId)) + } + } + .flatten + } + .getOrElse(Nil) + } + + def confirmAIDamageTarget( + pkt: AIDamage, + list: List[PlanetSideGameObject with FactionAffinity with Vitality] + ): Boolean = { + val AIDamage(_, attackerGuid, _, _, _) = pkt + sessionLogic + .validObject(attackerGuid, decorator = "AIDamage/AutomatedTurret") + .collect { + case turret: AutomatedTurret => + list.collect { + case target: AutomatedTurret.Target => + turret.Actor ! AutomatedTurretBehavior.ConfirmShot(target) + } + turret.Target.isEmpty + } + .getOrElse(false) + } + + /** + * Take a projectile that was introduced into the game world and + * determine if it generates a secondary damage projectile or + * an method of damage causation that requires additional management. + * @param projectile the projectile + * @param pguid the client-local projectile identifier + * @param hitPos the game world position where the projectile is being recorded + * @return a for all affected targets, a combination of projectiles, projectile location, and the target's location; + * nothing if no targets were affected + */ + def resolveDamageProxy( + projectile: Projectile, + pguid: PlanetSideGUID, + hitPos: Vector3 + ): List[(PlanetSideGameObject with FactionAffinity with Vitality, Projectile, Vector3, Vector3)] = { + GlobalDefinitions.getDamageProxy(projectile, hitPos) match { + case Nil => + Nil + case list if list.isEmpty => + Nil + case list => + setupDamageProxyLittleBuddy(list, hitPos) + WeaponAndProjectileOperations.updateProjectileSidednessAfterHit(continent, projectile, hitPos) + val projectileSide = projectile.WhichSide + list.flatMap { proxy => + if (proxy.profile.ExistsOnRemoteClients) { + proxy.Position = hitPos + proxy.WhichSide = projectileSide + continent.Projectile ! ZoneProjectile.Add(player.GUID, proxy) + Nil + } else if (proxy.tool_def == GlobalDefinitions.maelstrom) { + //server-side maelstrom grenade target selection + val radius = proxy.profile.LashRadius * proxy.profile.LashRadius + val targets = Zone.findAllTargets(continent, hitPos, proxy.profile.LashRadius, { _.livePlayerList }) + .filter { target => + Vector3.DistanceSquared(target.Position, hitPos) <= radius + } + //chainlash is separated from the actual damage application for convenience + continent.AvatarEvents ! AvatarServiceMessage( + continent.id, + AvatarAction.SendResponse( + PlanetSideGUID(0), + ChainLashMessage( + hitPos, + projectile.profile.ObjectId, + targets.map { _.GUID } + ) + ) + ) + targets.map { target => + (target, proxy, hitPos, target.Position) + } + } else { + Nil + } + } + } + } + + private def setupDamageProxyLittleBuddy(listOfProjectiles: List[Projectile], detonationPosition: Vector3): Boolean = { + val listOfLittleBuddies: List[Projectile] = listOfProjectiles.filter { _.tool_def == GlobalDefinitions.oicw } + val size: Int = listOfLittleBuddies.size + if (size > 0) { + val desiredDownwardsProjectiles: Int = 2 + val firstHalf: Int = math.min(size, desiredDownwardsProjectiles) //number that fly straight down + val secondHalf: Int = math.max(size - firstHalf, 0) //number that are flared out + val z: Float = player.Orientation.z //player's standing direction + val north: Vector3 = Vector3(0,1,0) //map North + val speed: Float = 144f //speed (packet discovered) + val dist: Float = 25 //distance (client defined) + val downwardsAngle: Float = -85f + val flaredAngle: Float = -70f + //angle of separation for downwards, degrees from vertical for flared out + val (smallStep, smallAngle): (Float, Float) = if (firstHalf > 1) { + (360f / firstHalf, downwardsAngle) + } else { + (0f, 0f) + } + val (largeStep, largeAngle): (Float, Float) = if (secondHalf > 1) { + (360f / secondHalf, flaredAngle) + } else { + (0f, 0f) + } + val smallRotOffset: Float = z + 90f + val largeRotOffset: Float = z + math.random().toFloat * 45f + val verticalCorrection = Vector3.z(dist - dist * math.sin(math.toRadians(90 - smallAngle + largeAngle)).toFloat) + //downwards projectiles + var i: Int = 0 + listOfLittleBuddies.take(firstHalf).foreach { proxy => + val facing = (smallRotOffset + smallStep * i.toFloat) % 360 + val dir = north.Rx(smallAngle).Rz(facing) + proxy.Position = detonationPosition + dir.xy + verticalCorrection + proxy.Velocity = dir * speed + proxy.Orientation = Vector3(0, (360f + smallAngle) % 360, facing) + i += 1 + } + //flared out projectiles + i = 0 + listOfLittleBuddies.drop(firstHalf).foreach { proxy => + val facing = (largeRotOffset + largeStep * i.toFloat) % 360 + val dir = north.Rx(largeAngle).Rz(facing) + proxy.Position = detonationPosition + dir + proxy.Velocity = dir * speed + proxy.Orientation = Vector3(0, (360f + largeAngle) % 360, facing) + i += 1 + } + true + } else { + false + } + } + + def performLittleBuddyExplosion(listOfProjectiles: List[Projectile]): Boolean = { + val listOfLittleBuddies: List[Projectile] = listOfProjectiles.filter { _.tool_def == GlobalDefinitions.oicw } + val size: Int = listOfLittleBuddies.size + if (size > 0) { + val desiredDownwardsProjectiles: Int = 2 + val firstHalf: Int = math.min(size, desiredDownwardsProjectiles) //number that fly straight down + val speed: Float = 144f //speed (packet discovered) + val dist: Float = 25 //distance (client defined) + //downwards projectiles + var i: Int = 0 + listOfLittleBuddies.take(firstHalf).foreach { proxy => + val dir = proxy.Velocity.map(_ / speed).getOrElse(Vector3.Zero) + queueLittleBuddyDamage(proxy, dir, dist) + i += 1 + } + //flared out projectiles + i = 0 + listOfLittleBuddies.drop(firstHalf).foreach { proxy => + val dir = proxy.Velocity.map(_ / speed).getOrElse(Vector3.Zero) + queueLittleBuddyDamage(proxy, dir, dist) + i += 1 + } + true + } else { + false + } + } + + private def queueLittleBuddyDamage(proxy: Projectile, orientation: Vector3, distance: Float): Unit = { + //explosion + val obj = new DummyExplodingEntity(proxy, proxy.owner.Faction) + obj.Position = obj.Position + orientation * distance + val explosionFunc: ()=>Unit = WeaponAndProjectileOperations.detonateLittleBuddy(continent, obj, proxy, proxy.owner) + context.system.scheduler.scheduleOnce(500.milliseconds) { explosionFunc() } + } + + /** + * Find a projectile with the given globally unique identifier and mark it as a resolved shot. + * A `Resolved` shot has either encountered an obstacle or is being cleaned up for not finding an obstacle. + * @param projectile projectile + * @param resolution resolution status to promote the projectile + * @return package that contains information about the damage + */ + def resolveProjectileInteraction( + target: PlanetSideGameObject with FactionAffinity with Vitality, + projectile: Projectile, + resolution: DamageResolution.Value, + hitPosition: Vector3 + ): Option[DamageInteraction] = { + if (projectile.isMiss) { + None + } else { + val outProjectile = ProjectileQuality.modifiers(projectile, resolution, target, hitPosition, Some(player)) + if (projectile.tool_def.Size == EquipmentSize.Melee && outProjectile.quality == ProjectileQuality.Modified(25)) { + avatarActor ! AvatarActor.ConsumeStamina(10) + } + val resolvedProjectile = DamageInteraction( + SourceEntry(target), + ProjectileReason(resolution, outProjectile, target.DamageModel), + hitPosition + ) + addShotsToMap(shotsLanded, resolvedProjectile.cause.attribution, shots = 1) + sessionLogic.handleDealingDamage(target, resolvedProjectile) + Some(resolvedProjectile) + } + } + def FindEnabledWeaponsToHandleWeaponFireAccountability( o: Option[PlanetSideGameObject with Container], tools: Set[Tool] @@ -182,7 +774,7 @@ class WeaponAndProjectileOperations( * @param weapon_guid the weapon (GUID) * @param tool the weapon (object) */ - def EmptyMagazine(weapon_guid: PlanetSideGUID, tool: Tool): Unit = { + def emptyMagazine(weapon_guid: PlanetSideGUID, tool: Tool): Unit = { tool.Magazine = 0 sendResponse(InventoryStateMessage(tool.AmmoSlot.Box.GUID, weapon_guid, 0)) sendResponse(ChangeFireStateMessage_Stop(weapon_guid)) @@ -274,22 +866,30 @@ class WeaponAndProjectileOperations( */ def FindWeapon: Set[Tool] = FindContainedWeapon._2 + def allowFireStateChangeStart(tool: Tool, itemGuid: PlanetSideGUID): Boolean = { + tool.FireMode.RoundsPerShot == 0 || tool.Magazine > 0 || prefire.contains(itemGuid) + } + def fireStateStopPlayerMessages(itemGuid: PlanetSideGUID): Unit = { - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.ChangeFireState_Stop(player.GUID, itemGuid) - ) + if (!player.spectator) { + continent.AvatarEvents ! AvatarServiceMessage( + continent.id, + AvatarAction.ChangeFireState_Stop(player.GUID, itemGuid) + ) + } } def fireStateStopMountedMessages(itemGuid: PlanetSideGUID): Unit = { - sessionLogic.findContainedEquipment()._1.collect { - case turret: FacilityTurret if continent.map.cavern => - turret.Actor ! VanuSentry.ChangeFireStop + if (!player.spectator) { + sessionLogic.findContainedEquipment()._1.collect { + case turret: FacilityTurret if continent.map.cavern => + turret.Actor ! VanuSentry.ChangeFireStop + } + continent.VehicleEvents ! VehicleServiceMessage( + continent.id, + VehicleAction.ChangeFireState_Stop(player.GUID, itemGuid) + ) } - continent.VehicleEvents ! VehicleServiceMessage( - continent.id, - VehicleAction.ChangeFireState_Stop(player.GUID, itemGuid) - ) } private def addShotsFired(weaponId: Int, shots: Int): Unit = { @@ -326,6 +926,308 @@ class WeaponAndProjectileOperations( ToDatabase.reportToolDischarge(avatarId, EquipmentStat(weaponId, fired, landed, 0, 0)) } + def handleReloadProcedure( + itemGuid: PlanetSideGUID, + obj: PlanetSideGameObject with Container, + tools: Set[Tool], + unk1: Int, + deleteFunc: Equipment => Future[Any], + modifyFunc: (AmmoBox, Int) => Unit, + messageFunc: PlanetSideGUID => Unit + ): Unit = { + tools + .filter { _.GUID == itemGuid } + .foreach { tool => + val currentMagazine : Int = tool.Magazine + val magazineSize : Int = tool.MaxMagazine + val reloadValue : Int = magazineSize - currentMagazine + if (magazineSize > 0 && reloadValue > 0) { + FindEquipmentStock(obj, FindAmmoBoxThatUses(tool.AmmoType), reloadValue, CountAmmunition).reverse match { + case Nil => () + case x :: xs => + xs.foreach { item => deleteFunc(item.obj) } + val box = x.obj.asInstanceOf[AmmoBox] + val tailReloadValue : Int = if (xs.isEmpty) { + 0 + } + else { + xs.map(_.obj.asInstanceOf[AmmoBox].Capacity).sum + } + val sumReloadValue : Int = box.Capacity + tailReloadValue + val actualReloadValue = if (sumReloadValue <= reloadValue) { + deleteFunc(box) + sumReloadValue + } + else { + modifyFunc(box, reloadValue - tailReloadValue) + reloadValue + } + val finalReloadValue = actualReloadValue + currentMagazine + log.info( + s"${player.Name} successfully reloaded $reloadValue ${tool.AmmoType} into ${tool.Definition.Name}" + ) + tool.Magazine = finalReloadValue + sendResponse(ReloadMessage(itemGuid, finalReloadValue, unk1)) + messageFunc(itemGuid) + } + } else { + //the weapon can not reload due to full magazine; the UI for the magazine is obvious bugged, so fix it + sendResponse(QuantityUpdateMessage(tool.AmmoSlot.Box.GUID, magazineSize)) + } + } + } + + /** + * na + * @param tool na + * @param obj na + */ + private def performToolAmmoChange( + tool: Tool, + obj: PlanetSideServerObject with Container, + modifyFunc: (AmmoBox, Int) => Unit + ): Unit = { + val originalAmmoType = tool.AmmoType + do { + val requestedAmmoType = tool.NextAmmoType + val fullMagazine = tool.MaxMagazine + if (requestedAmmoType != tool.AmmoSlot.Box.AmmoType) { + FindEquipmentStock(obj, FindAmmoBoxThatUses(requestedAmmoType), fullMagazine, CountAmmunition).reverse match { + case Nil => () + case x :: xs => + val stowNewFunc: Equipment => TaskBundle = PutNewEquipmentInInventoryOrDrop(obj) + val stowFunc: Equipment => Future[Any] = PutEquipmentInInventoryOrDrop(obj) + + xs.foreach(item => { + obj.Inventory -= item.start + sendResponse(ObjectDeleteMessage(item.obj.GUID, 0)) + TaskWorkflow.execute(GUIDTask.unregisterObject(continent.GUID, item.obj)) + }) + + //box will be the replacement ammo; give it the discovered magazine and load it into the weapon + val box = x.obj.asInstanceOf[AmmoBox] + //previousBox is the current magazine in tool; it will be removed from the weapon + val previousBox = tool.AmmoSlot.Box + val originalBoxCapacity = box.Capacity + val tailReloadValue: Int = if (xs.isEmpty) { + 0 + } else { + xs.map(_.obj.asInstanceOf[AmmoBox].Capacity).sum + } + val sumReloadValue: Int = originalBoxCapacity + tailReloadValue + val ammoSlotIndex = tool.FireMode.AmmoSlotIndex + val box_guid = box.GUID + val tool_guid = tool.GUID + obj.Inventory -= x.start //remove replacement ammo from inventory + tool.AmmoSlots(ammoSlotIndex).Box = box //put replacement ammo in tool + sendResponse(ObjectDetachMessage(tool_guid, previousBox.GUID, Vector3.Zero, 0f)) + sendResponse(ObjectDetachMessage(obj.GUID, box_guid, Vector3.Zero, 0f)) + sendResponse(ObjectAttachMessage(tool_guid, box_guid, ammoSlotIndex)) + + //announce swapped ammunition box in weapon + val previous_box_guid = previousBox.GUID + val boxDef = box.Definition + sendResponse(ChangeAmmoMessage(tool_guid, box.Capacity)) + continent.AvatarEvents ! AvatarServiceMessage( + continent.id, + AvatarAction.ChangeAmmo( + player.GUID, + tool_guid, + ammoSlotIndex, + previous_box_guid, + boxDef.ObjectId, + box.GUID, + boxDef.Packet.ConstructorData(box).get + ) + ) + + //handle inventory contents + box.Capacity = if (sumReloadValue <= fullMagazine) { + sumReloadValue + } else { + val splitReloadAmmo: Int = sumReloadValue - fullMagazine + log.trace( + s"PerformToolAmmoChange: ${player.Name} takes ${originalBoxCapacity - splitReloadAmmo} from a box of $originalBoxCapacity $requestedAmmoType ammo" + ) + val boxForInventory = AmmoBox(box.Definition, splitReloadAmmo) + TaskWorkflow.execute(stowNewFunc(boxForInventory)) + fullMagazine + } + sendResponse( + InventoryStateMessage(box.GUID, tool.GUID, box.Capacity) + ) //should work for both players and vehicles + log.info(s"${player.Name} loads ${box.Capacity} $requestedAmmoType into the ${tool.Definition.Name}") + if (previousBox.Capacity > 0) { + //divide capacity across other existing and not full boxes of that ammo type + var capacity = previousBox.Capacity + val iter = obj.Inventory.Items + .filter(entry => { + entry.obj match { + case item: AmmoBox => + item.AmmoType == originalAmmoType && item.FullCapacity != item.Capacity + case _ => + false + } + }) + .sortBy(_.start) + .iterator + while (capacity > 0 && iter.hasNext) { + val entry = iter.next() + val item: AmmoBox = entry.obj.asInstanceOf[AmmoBox] + val ammoAllocated = math.min(item.FullCapacity - item.Capacity, capacity) + log.info(s"${player.Name} put $ammoAllocated back into a box of ${item.Capacity} $originalAmmoType") + capacity -= ammoAllocated + modifyFunc(item, -ammoAllocated) + } + previousBox.Capacity = capacity + } + + if (previousBox.Capacity > 0) { + //split previousBox into AmmoBox objects of appropriate max capacity, e.g., 100 9mm -> 2 x 50 9mm + obj.Inventory.Fit(previousBox) match { + case Some(_) => + stowFunc(previousBox) + case None => + sessionLogic.general.normalItemDrop(player, continent)(previousBox) + } + AmmoBox.Split(previousBox) match { + case Nil | List(_) => () //done (the former case is technically not possible) + case _ :: toUpdate => + modifyFunc(previousBox, 0) //update to changed capacity value + toUpdate.foreach(box => { TaskWorkflow.execute(stowNewFunc(box)) }) + } + } else { + TaskWorkflow.execute(GUIDTask.unregisterObject(continent.GUID, previousBox)) + } + } + } + } while (tool.AmmoType != originalAmmoType && tool.AmmoType != tool.AmmoSlot.Box.AmmoType) + } + + /** + * Given an object that contains a box of amunition in its `Inventory` at a certain location, + * change the amount of ammunition within that box. + * @param obj the `Container` + * @param box an `AmmoBox` to modify + * @param reloadValue the value to modify the `AmmoBox`; + * subtracted from the current `Capacity` of `Box` + */ + def modifyAmmunition(obj: PlanetSideGameObject with Container)(box: AmmoBox, reloadValue: Int): Unit = { + val capacity = box.Capacity - reloadValue + box.Capacity = capacity + sendResponse(InventoryStateMessage(box.GUID, obj.GUID, capacity)) + } + + /** + * Given a vehicle that contains a box of ammunition in its `Trunk` at a certain location, + * change the amount of ammunition within that box. + * @param obj the `Container` + * @param box an `AmmoBox` to modify + * @param reloadValue the value to modify the `AmmoBox`; + * subtracted from the current `Capacity` of `Box` + */ + def modifyAmmunitionInMountable(obj: PlanetSideServerObject with Container)(box: AmmoBox, reloadValue: Int): Unit = { + modifyAmmunition(obj)(box, reloadValue) + obj.Find(box).collect { index => + continent.VehicleEvents ! VehicleServiceMessage( + s"${obj.Actor}", + VehicleAction.InventoryState( + player.GUID, + box, + obj.GUID, + index, + box.Definition.Packet.DetailedConstructorData(box).get + ) + ) + } + } + + def checkForHitPositionDiscrepancy( + projectile_guid: PlanetSideGUID, + hitPos: Vector3, + target: PlanetSideGameObject with Vitality + ): Unit = { + val hitPositionDiscrepancy = Vector3.DistanceSquared(hitPos, target.Position) + if (hitPositionDiscrepancy > Config.app.antiCheat.hitPositionDiscrepancyThreshold) { + // If the target position on the server does not match the position where the projectile landed within reason there may be foul play + log.warn( + s"${player.Name}'s shot #${projectile_guid.guid} has hit discrepancy with target. Target: ${target.Position}, Reported: $hitPos, Distance: $hitPositionDiscrepancy / ${math.sqrt(hitPositionDiscrepancy).toFloat}; suspect" + ) + } + } + + private def CompileAutomatedTurretDamageData( + turret: AutomatedTurret, + projectileTypeId: Long + ): Option[(AutomatedTurret, Tool, SourceEntry, ProjectileDefinition)] = { + turret match { + case tOwner: OwnableByPlayer => + CompileAutomatedTurretDamageData( + turret, + CompileAutomatedTurretOwnableBlame(tOwner), + projectileTypeId + ) + case tAmenity: Amenity => + CompileAutomatedTurretDamageData( + turret, + CompileAutomatedTurretAmenityBlame(tAmenity), + projectileTypeId + ) + case _ => + None + } + } + + private def CompileAutomatedTurretOwnableBlame(turret: AutomatedTurret with OwnableByPlayer): SourceEntry = { + Deployables.AssignBlameTo(continent, turret.OwnerName, SourceEntry(turret)) + } + + private def CompileAutomatedTurretAmenityBlame(turret: AutomatedTurret with Amenity): SourceEntry = { + turret + .Seats + .values + .flatMap(_.occupant) + .collectFirst(SourceEntry(_)) + .getOrElse(SourceEntry(turret.Owner)) + } + + private def CompileAutomatedTurretDamageData( + turret: AutomatedTurret, + blame: SourceEntry, + projectileTypeId: Long + ): Option[(AutomatedTurret, Tool, SourceEntry, ProjectileDefinition)] = { + turret.Weapons + .values + .flatMap { _.Equipment } + .collect { + case weapon: Tool => (turret, weapon, blame, weapon.Projectile) + } + .find { case (_, _, _, p) => p.ObjectId == projectileTypeId } + } + + private def prepareAIProjectile( + target: PlanetSideServerObject with FactionAffinity with Vitality, + results: Option[(AutomatedTurret, Tool, SourceEntry, ProjectileDefinition)] + ): List[(PlanetSideGameObject with FactionAffinity with Vitality, Projectile, Vector3, Vector3)] = { + val hitPos = target.Position + Vector3.z(value = 1f) + results.collect { + case (obj, tool, owner, projectileInfo) => + val angle = Vector3.Unit(target.Position - obj.Position) + val projectile = new Projectile( + projectileInfo, + tool.Definition, + tool.FireMode, + None, + owner, + obj.Definition.ObjectId, + obj.Position + Vector3.z(value = 1f), + angle, + Some(angle * projectileInfo.FinalVelocity) + ) + (target, projectile, hitPos, target.Position) + }.toList + } + override protected[session] def actionsToCancel(): Unit = { shootingStart.clear() shootingStop.clear() @@ -352,3 +1254,132 @@ class WeaponAndProjectileOperations( } } } + +object WeaponAndProjectileOperations { + def updateProjectileSidednessAfterHit(zone: Zone, projectile: Projectile, hitPosition: Vector3): Unit = { + val origin = projectile.Position + val distance = Vector3.Magnitude(hitPosition - origin) + zone.blockMap + .sector(hitPosition, distance) + .environmentList + .collect { case o: InteriorDoorPassage => + val door = o.door + val intersectTest = quickLineSphereIntersectionPoints( + origin, + hitPosition, + door.Position, + door.Definition.UseRadius + 0.1f + ) + (door, intersectTest) + } + .collect { case (door, intersectionTest) if intersectionTest.nonEmpty => + (door, Vector3.Magnitude(hitPosition - door.Position), intersectionTest) + } + .minByOption { case (_, dist, _) => dist } + .foreach { case (door, _, intersects) => + val strictly = if (Vector3.DotProduct(Vector3.Unit(hitPosition - door.Position), door.Outwards) > 0f) { + Sidedness.OutsideOf + } else { + Sidedness.InsideOf + } + projectile.WhichSide = if (intersects.size == 1) { + Sidedness.InBetweenSides(door, strictly) + } else { + strictly + } + } + } + + /** + * Does a line segment line intersect with a sphere?
+ * This most likely belongs in `Geometry` or `GeometryForm` or somehow in association with the `\objects\geometry\` package. + * @param start first point of the line segment + * @param end second point of the line segment + * @param center center of the sphere + * @param radius radius of the sphere + * @return list of all points of intersection, if any + * @see `Vector3.DistanceSquared` + * @see `Vector3.MagnitudeSquared` + */ + def quickLineSphereIntersectionPoints( + start: Vector3, + end: Vector3, + center: Vector3, + radius: Float + ): Iterable[Vector3] = { + /* + Algorithm adapted from code found on https://paulbourke.net/geometry/circlesphere/index.html#linesphere, + because I kept messing up proper substitution of the line formula and the circle formula into the quadratic equation. + */ + val Vector3(cx, cy, cz) = center + val Vector3(sx, sy, sz) = start + val vector = end - start + //speed our way through a quadratic equation + val (a, b) = { + val Vector3(dx, dy, dz) = vector + ( + dx * dx + dy * dy + dz * dz, + 2f * (dx * (sx - cx) + dy * (sy - cy) + dz * (sz - cz)) + ) + } + val c = Vector3.MagnitudeSquared(center) + Vector3.MagnitudeSquared(start) - 2f * (cx * sx + cy * sy + cz * sz) - radius * radius + val result = b * b - 4 * a * c + if (result < 0f) { + //negative, no intersection + Seq() + } else if (result < 0.00001f) { + //zero-ish, one intersection point + Seq(start - vector * (b / (2f * a))) + } else { + //positive, two intersection points + val sqrt = math.sqrt(result).toFloat + val endStart = vector / (2f * a) + Seq(start + endStart * (sqrt - b), start + endStart * (b + sqrt) * -1f) + }.filter(p => Vector3.DistanceSquared(start, p) <= a) + } + + /** + * Preparation for explosion damage that utilizes the Scorpion's little buddy sub-projectiles. + * The main difference from "normal" server-side explosion + * is that the owner of the projectile must be clarified explicitly. + * @see `Zone::serverSideDamage` + * @param zone where the explosion is taking place + * (`source` contains the coordinate location) + * @param source a game object that represents the source of the explosion + * @param owner who or what to accredit damage from the explosion to; + * clarifies a normal `SourceEntry(source)` accreditation + */ + def detonateLittleBuddy( + zone: Zone, + source: PlanetSideGameObject with FactionAffinity with Vitality, + proxy: Projectile, + owner: SourceEntry + )(): Unit = { + Zone.serverSideDamage(zone, source, littleBuddyExplosionDamage(owner, proxy.id, source.Position)) + } + + /** + * Preparation for explosion damage that utilizes the Scorpion's little buddy sub-projectiles. + * The main difference from "normal" server-side explosion + * is that the owner of the projectile must be clarified explicitly. + * The sub-projectiles will be the product of a normal projectile rather than a standard game object + * so a custom `source` entity must wrap around it and fulfill the requirements of the field. + * @see `Zone::explosionDamage` + * @param owner who or what to accredit damage from the explosion to + * @param explosionPosition where the explosion will be positioned in the game world + * @param source a game object that represents the source of the explosion + * @param target a game object that is affected by the explosion + * @return a `DamageInteraction` object + */ + private def littleBuddyExplosionDamage( + owner: SourceEntry, + projectileId: Long, + explosionPosition: Vector3 + ) + ( + source: PlanetSideGameObject with FactionAffinity with Vitality, + target: PlanetSideGameObject with FactionAffinity with Vitality + ): DamageInteraction = { + DamageInteraction(SourceEntry(target), OicwLilBuddyReason(owner, projectileId, target.DamageModel), explosionPosition) + } +} 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 9d56fc5f3..46ea275fc 100644 --- a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala @@ -6,7 +6,6 @@ import akka.actor.typed.scaladsl.adapter._ import akka.actor.{ActorContext, ActorRef, Cancellable, typed} import akka.pattern.ask import akka.util.Timeout -import net.psforever.actors.session.spectator.SpectatorMode import net.psforever.login.WorldSession import net.psforever.objects.avatar.{BattleRank, DeployableToolbox} import net.psforever.objects.avatar.scoring.{CampaignStatistics, ScoreCard, SessionStatistics} @@ -193,6 +192,7 @@ class ZoningOperations( /** a flag that forces the current zone to reload itself during a zoning operation */ private[session] var zoneReload: Boolean = false private[session] val spawn: SpawnOperations = new SpawnOperations() + private[session] var maintainInitialGmState: Boolean = false private var loadConfZone: Boolean = false private var instantActionFallbackDestination: Option[Zoning.InstantAction.Located] = None @@ -609,6 +609,7 @@ class ZoningOperations( def handleZoneResponse(foundZone: Zone): Unit = { log.trace(s"ZoneResponse: zone ${foundZone.id} will now load for ${player.Name}") loadConfZone = true + maintainInitialGmState = true val oldZone = session.zone session = session.copy(zone = foundZone) sessionLogic.persist() diff --git a/src/main/scala/net/psforever/objects/Player.scala b/src/main/scala/net/psforever/objects/Player.scala index 24c277b3a..1ac54eba1 100644 --- a/src/main/scala/net/psforever/objects/Player.scala +++ b/src/main/scala/net/psforever/objects/Player.scala @@ -81,6 +81,7 @@ class Player(var avatar: Avatar) Continent = "home2" //the zone id var spectator: Boolean = false + var bops: Boolean = false var silenced: Boolean = false var death_by: Int = 0 var lastShotSeq_time: Int = -1 diff --git a/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala b/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala index 31f5f3992..28aafe07c 100644 --- a/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala +++ b/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala @@ -69,7 +69,7 @@ object AvatarConverter { obj.avatar.basic, CommonFieldData( obj.Faction, - bops = obj.spectator, + bops = obj.bops, alt_model_flag, v1 = false, None, diff --git a/src/main/scala/net/psforever/objects/zones/blockmap/Sector.scala b/src/main/scala/net/psforever/objects/zones/blockmap/Sector.scala index 531d64502..c78cb1b1e 100644 --- a/src/main/scala/net/psforever/objects/zones/blockmap/Sector.scala +++ b/src/main/scala/net/psforever/objects/zones/blockmap/Sector.scala @@ -185,6 +185,10 @@ class Sector(val longitude: Int, val latitude: Int, val span: Int) */ def addTo(o: BlockMapEntity): Boolean = { o match { + case p: Player if p.spectator => + livePlayers.removeFrom(p) + corpses.removeFrom(p) + false case p: Player if p.isBackpack => //when adding to the "corpse" list, first attempt to remove from the "player" list livePlayers.removeFrom(p) diff --git a/src/main/scala/net/psforever/packet/game/objectcreate/DetailedCharacterData.scala b/src/main/scala/net/psforever/packet/game/objectcreate/DetailedCharacterData.scala index 27dda936a..b68edb8d6 100644 --- a/src/main/scala/net/psforever/packet/game/objectcreate/DetailedCharacterData.scala +++ b/src/main/scala/net/psforever/packet/game/objectcreate/DetailedCharacterData.scala @@ -204,6 +204,7 @@ final case class DetailedCharacterB( */ final case class DetailedCharacterData(a: DetailedCharacterA, b: DetailedCharacterB)(pad_length: Option[Int]) extends ConstructorData { + val padLength: Option[Int] = pad_length override def bitsize: Long = a.bitsize + b.bitsize }