From 32194e88fb609fa1ac407741cc7d24b8ad490c17 Mon Sep 17 00:00:00 2001 From: Resaec Date: Mon, 14 Oct 2024 19:51:47 +0200 Subject: [PATCH 01/57] Fix up some chat commands --- .../psforever/actors/session/normal/ChatLogic.scala | 2 +- .../actors/session/support/ChatOperations.scala | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) 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 e701c0a55..98caca158 100644 --- a/src/main/scala/net/psforever/actors/session/normal/ChatLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/ChatLogic.scala @@ -234,7 +234,7 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext ) ) } - result + true // do not accidentally send mistyped commands to chat } else { false // not a handled command } 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 02efbc7e4..bb1585918 100644 --- a/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala @@ -603,8 +603,9 @@ class ChatOperations( } def commandAddCertification(session: Session, message: ChatMsg, contents: String): Unit = { - val certs = cliTokenization(contents).map(name => Certification.values.find(_.name == name)) - val result = if (certs.nonEmpty) { + val tokens = cliTokenization(contents) + val certs = tokens.map(name => Certification.values.find(_.name == name)) + val result = if (tokens.nonEmpty) { if (certs.contains(None)) { s"@AckErrorCertifications" } else { @@ -623,7 +624,7 @@ class ChatOperations( } def commandKick(session: Session, message: ChatMsg, contents: String): Unit = { - val inputs = cliTokenization(contents) + val inputs = cliTokenizationCaseSensitive(contents) inputs.headOption match { case Some(input) => val determination: Player => Boolean = input.toLongOption match { @@ -1269,7 +1270,11 @@ class ChatOperations( } def cliTokenization(str: String): List[String] = { - str.replaceAll("\\s+", " ").toLowerCase.trim.split("\\s").toList + str.replaceAll("\\s+", " ").toLowerCase.trim.split("\\s").toList.filter(!_.equals("")) + } + + def cliTokenizationCaseSensitive(str: String): List[String] = { + str.replaceAll("\\s+", " ").trim.split("\\s").toList.filter(!_.equals("")) } def commandIncomingSend(message: ChatMsg): Unit = { From 96d6c3737cb1c52fbcc76ca5f6bf90dc7ce69893 Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Mon, 4 Nov 2024 13:10:03 -0500 Subject: [PATCH 02/57] added a death plane under hunhau and and under byblos --- .../scala/net/psforever/objects/zones/MapInfo.scala | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/main/scala/net/psforever/objects/zones/MapInfo.scala b/src/main/scala/net/psforever/objects/zones/MapInfo.scala index f08dfac16..3875a2287 100644 --- a/src/main/scala/net/psforever/objects/zones/MapInfo.scala +++ b/src/main/scala/net/psforever/objects/zones/MapInfo.scala @@ -375,7 +375,8 @@ case object MapInfo extends StringEnum[MapInfo] { Pool(EnvironmentAttribute.Water, 192.32812f, 1966.1562f, 1252.7344f, 1889.8047f, 1148.5312f), //top, northern pool Pool(EnvironmentAttribute.Water, 191.65625f, 1869.1484f, 1195.6406f, 1743.8125f, 1050.7344f), //middle, northern pool Pool(EnvironmentAttribute.Water, 183.98438f, 914.33594f, 1369.5f, 626.03906f, 666.3047f), //upper southern pools - Pool(EnvironmentAttribute.Water, 182.96875f, 580.7578f, 913.52344f, 520.4531f, 843.97656f) //lowest southern pool + Pool(EnvironmentAttribute.Water, 182.96875f, 580.7578f, 913.52344f, 520.4531f, 843.97656f), //lowest southern pool + SeaLevel(EnvironmentAttribute.Death, 10) ) ) @@ -394,7 +395,10 @@ case object MapInfo extends StringEnum[MapInfo] { checksum = 3797992164L, scale = MapScale.Dim2048, hotSpotSpan = 80, - environment = List(Pool(EnvironmentAttribute.Death, DeepSurface(51.414f, 2048, 2048, 0, 0))) + environment = List( + Pool(EnvironmentAttribute.Death, DeepSurface(51.414f, 2048, 2048, 0, 0)), + SeaLevel(EnvironmentAttribute.Death, 10) + ) ) case object Ugd05 @@ -451,7 +455,7 @@ case object MapInfo extends StringEnum[MapInfo] { Pool(EnvironmentAttribute.Water, 3.5f, 2867f, 4096f, 1227f, 2900f), //east Pool(EnvironmentAttribute.Water, 3.5f, 1227f, 4096f, 0f, 2000f), //southeast Pool(EnvironmentAttribute.Water, 3.5f, 1128f, 2000f, 0f, 0f), //southwest - Pool(EnvironmentAttribute.Death, 0.5f, 2867f, 2900f, 1128f, 1228f) //central, kill + Pool(EnvironmentAttribute.Death, 0.5f, 2867f, 2900f, 1128f, 1228f), //central, kill ) ++ MapEnvironment.dim4096MapEdgeKillPlanes ) @@ -708,7 +712,7 @@ object MapEnvironment { p.Name, AvatarAction.SendResponseTargeted( Service.defaultPlayerGUID, - ChatMsg(ChatMessageType.CMT_QUIT, false, "", warning, None) + ChatMsg(ChatMessageType.CMT_QUIT, warning) ) ) case _ => ; From 58238df1fd2657d11322f97637e96ca68965e96b Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Mon, 2 Dec 2024 13:26:37 -0500 Subject: [PATCH 03/57] Customer Service (#1247) * initial workings for a csr/gm player mode * weird line removal after merge * too many changes and refactors, but the code compiles properly; need to push these to the repo and sort out issues with it all later; hopefully, nothing has changed except for the csr stuff * extensive changes to mode logic for refactoring and functionality improvements, especially for accommodating csr spectator mode * csr's may now see players who are spectating, including csr's who are spectating, including a wide variety of their behavior such as location, weapon management, etc.; removes zoning cancel messages as a required log from certain use item actions * corrected various issues identified in the comments of PR#1247 * restored killed->autorevive functionality; removed last traces of old door code from general operations * knife stab corrected; spitfire turret corrected; blockmap interactivity using zone interaction allowance flag; visibility during resurgence code for csr; removing messages and experience gain for interactions with csr * pruning imports and correcting the default spectator setting * separated router/telepad activity from other vehicular activity, previously all considered a form of mounting/dismounting, and restored support experience rewards * can not activate or deactive spectator of gm when dead; zoning while spectator removes spectator and defaults to sanctuary * field turrets are neutral-neutral when constructed by a csr; custom bang command 'setempire' will adjust the faction affiliation of a variety of game objects * fixed facility occupation recognition * when transitioning between player modes, do not parse new messages about transitioning between player modes, until fully transitioned between player modes --- src/main/resources/application.conf | 10 +- .../actors/session/AvatarActor.scala | 11 +- .../actors/session/SessionActor.scala | 2 +- .../session/csr/AvatarHandlerLogic.scala | 577 +++++++ .../actors/session/csr/ChatLogic.scala | 419 ++++++ .../CustomerServiceRepresentativeMode.scala | 204 +++ .../actors/session/csr/GeneralLogic.scala | 781 ++++++++++ .../session/csr/MountHandlerLogic.scala | 330 ++++ ...eAsCustomerServiceRepresentativeMode.scala | 92 ++ .../session/csr/SquadHandlerLogic.scala | 362 +++++ .../session/csr/TerminalHandlerLogic.scala | 116 ++ .../actors/session/csr/VehicleLogic.scala | 397 +++++ .../csr/WeaponAndProjectileLogic.scala | 208 +++ .../session/normal/AvatarHandlerLogic.scala | 106 +- .../actors/session/normal/ChatLogic.scala | 195 +-- .../actors/session/normal/GeneralLogic.scala | 727 +-------- .../session/normal/LocalHandlerLogic.scala | 32 +- .../session/normal/MountHandlerLogic.scala | 295 +--- .../actors/session/normal/NormalMode.scala | 5 +- .../session/normal/SquadHandlerLogic.scala | 2 - .../session/normal/TerminalHandlerLogic.scala | 94 +- .../actors/session/normal/VehicleLogic.scala | 3 +- .../normal/WeaponAndProjectileLogic.scala | 1337 ++--------------- .../spectator/AvatarHandlerLogic.scala | 13 +- .../actors/session/spectator/ChatLogic.scala | 56 +- .../spectator/GalaxyHandlerLogic.scala | 85 -- .../session/spectator/GeneralLogic.scala | 213 +-- .../session/spectator/LocalHandlerLogic.scala | 257 ---- .../session/spectator/MountHandlerLogic.scala | 188 +-- .../session/spectator/SpectatorMode.scala | 13 +- .../spectator/VehicleHandlerLogic.scala | 147 +- .../session/spectator/VehicleLogic.scala | 227 +-- .../spectator/WeaponAndProjectileLogic.scala | 134 +- .../session/support/ChatOperations.scala | 70 +- .../session/support/GeneralOperations.scala | 792 +++++++++- .../support/SessionAvatarHandlers.scala | 43 + .../actors/session/support/SessionData.scala | 3 +- .../support/SessionLocalHandlers.scala | 30 +- .../support/SessionMountHandlers.scala | 224 ++- .../support/SessionSquadHandlers.scala | 2 + .../support/SessionTerminalHandlers.scala | 103 +- .../session/support/VehicleOperations.scala | 12 + .../WeaponAndProjectileOperations.scala | 1247 ++++++++++++++- .../session/support/ZoningOperations.scala | 67 +- .../net/psforever/login/WorldSession.scala | 15 + .../scala/net/psforever/objects/Player.scala | 6 +- .../scala/net/psforever/objects/Players.scala | 41 +- .../objects/avatar/PlayerControl.scala | 104 +- .../objects/ballistics/Projectile.scala | 30 +- .../converter/AvatarConverter.scala | 12 +- .../global/GlobalDefinitionsProjectile.scala | 8 +- .../objects/vital/InGameHistory.scala | 9 +- .../vital/damage/DamageCalculations.scala | 4 +- .../objects/zones/blockmap/Sector.scala | 6 +- .../objects/zones/exp/KillContributions.scala | 173 ++- .../psforever/objects/zones/exp/Support.scala | 14 +- .../net/psforever/packet/game/ChatMsg.scala | 29 +- .../objectcreate/DetailedCharacterData.scala | 1 + .../services/avatar/AvatarService.scala | 4 +- .../avatar/AvatarServiceMessage.scala | 3 +- .../avatar/AvatarServiceResponse.scala | 3 +- .../psforever/services/chat/ChatChannel.scala | 2 + .../scala/objects/PlayerControlTest.scala | 109 +- 63 files changed, 6844 insertions(+), 3960 deletions(-) create mode 100644 src/main/scala/net/psforever/actors/session/csr/AvatarHandlerLogic.scala create mode 100644 src/main/scala/net/psforever/actors/session/csr/ChatLogic.scala create mode 100644 src/main/scala/net/psforever/actors/session/csr/CustomerServiceRepresentativeMode.scala create mode 100644 src/main/scala/net/psforever/actors/session/csr/GeneralLogic.scala create mode 100644 src/main/scala/net/psforever/actors/session/csr/MountHandlerLogic.scala create mode 100644 src/main/scala/net/psforever/actors/session/csr/SpectateAsCustomerServiceRepresentativeMode.scala create mode 100644 src/main/scala/net/psforever/actors/session/csr/SquadHandlerLogic.scala create mode 100644 src/main/scala/net/psforever/actors/session/csr/TerminalHandlerLogic.scala create mode 100644 src/main/scala/net/psforever/actors/session/csr/VehicleLogic.scala create mode 100644 src/main/scala/net/psforever/actors/session/csr/WeaponAndProjectileLogic.scala delete mode 100644 src/main/scala/net/psforever/actors/session/spectator/GalaxyHandlerLogic.scala delete mode 100644 src/main/scala/net/psforever/actors/session/spectator/LocalHandlerLogic.scala diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf index 9c603e2a0..806a23ff1 100644 --- a/src/main/resources/application.conf +++ b/src/main/resources/application.conf @@ -27,7 +27,7 @@ world { # How the server is displayed in the server browser. # One of: released beta development - server-type = released + server-type = development } # Admin API configuration @@ -408,8 +408,14 @@ game { base = 25 } { - name = "router" + name = "router-driver" base = 15 + shots-multiplier = 1.0 + } + { + name = "telepad-use" + base = 20 + shots-multiplier = 1.0 } { name = "hotdrop" diff --git a/src/main/scala/net/psforever/actors/session/AvatarActor.scala b/src/main/scala/net/psforever/actors/session/AvatarActor.scala index 8a243b6dc..1dd805e3e 100644 --- a/src/main/scala/net/psforever/actors/session/AvatarActor.scala +++ b/src/main/scala/net/psforever/actors/session/AvatarActor.scala @@ -1131,6 +1131,11 @@ class AvatarActor( var supportExperienceTimer: Cancellable = Default.Cancellable var experienceDebt: Long = 0L + private def setSession(newSession: Session): Unit = { + session = Some(newSession) + _avatar = Option(newSession.avatar) + } + def avatar: Avatar = _avatar.get def avatar_=(avatar: Avatar): Unit = { @@ -1156,7 +1161,7 @@ class AvatarActor( postLoginBehaviour() case SetSession(newSession) => - session = Some(newSession) + setSession(newSession) postLoginBehaviour() case other => @@ -1176,7 +1181,7 @@ class AvatarActor( Behaviors .receiveMessage[Command] { case SetSession(newSession) => - session = Some(newSession) + setSession(newSession) Behaviors.same case SetLookingForSquad(lfs) => @@ -1329,7 +1334,7 @@ class AvatarActor( Behaviors .receiveMessagePartial[Command] { case SetSession(newSession) => - session = Some(newSession) + setSession(newSession) Behaviors.same case ReplaceAvatar(newAvatar) => diff --git a/src/main/scala/net/psforever/actors/session/SessionActor.scala b/src/main/scala/net/psforever/actors/session/SessionActor.scala index d24b0cee3..899d32651 100644 --- a/src/main/scala/net/psforever/actors/session/SessionActor.scala +++ b/src/main/scala/net/psforever/actors/session/SessionActor.scala @@ -160,7 +160,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con if (mode != newMode) { logic.switchFrom(data.session) mode = newMode - logic = mode.setup(data) + logic = newMode.setup(data) } logic.switchTo(data.session) } diff --git a/src/main/scala/net/psforever/actors/session/csr/AvatarHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/csr/AvatarHandlerLogic.scala new file mode 100644 index 000000000..b7294aca0 --- /dev/null +++ b/src/main/scala/net/psforever/actors/session/csr/AvatarHandlerLogic.scala @@ -0,0 +1,577 @@ +// Copyright (c) 2024 PSForever +package net.psforever.actors.session.csr + +import akka.actor.{ActorContext, typed} +import net.psforever.actors.session.SessionActor +import net.psforever.actors.session.normal.NormalMode +import net.psforever.actors.session.support.AvatarHandlerFunctions +import net.psforever.login.WorldSession.PutLoadoutEquipmentInInventory +import net.psforever.objects.PlanetSideGameObject +import net.psforever.objects.inventory.Container +import net.psforever.objects.serverobject.containable.ContainableBehavior +import net.psforever.objects.serverobject.mount.Mountable +import net.psforever.packet.game.{AvatarImplantMessage, CreateShortcutMessage, ImplantAction} +import net.psforever.services.avatar.AvatarServiceResponse +import net.psforever.types.ImplantType + +// +import net.psforever.actors.session.AvatarActor +import net.psforever.actors.session.support.{SessionAvatarHandlers, SessionData} +import net.psforever.login.WorldSession.{DropEquipmentFromInventory, DropLeftovers, HoldNewEquipmentUp} +import net.psforever.objects.{GlobalDefinitions, Player, Tool, Vehicle} +import net.psforever.objects.guid.{GUIDTask, TaskWorkflow} +import net.psforever.objects.inventory.InventoryItem +import net.psforever.objects.serverobject.terminals.{ProximityUnit, Terminal} +import net.psforever.objects.zones.Zoning +import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent +import net.psforever.packet.game.{ArmorChangedMessage, ChangeAmmoMessage, ChangeFireModeMessage, ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, ChatMsg, DestroyMessage, DrowningTarget, GenericActionMessage, GenericObjectActionMessage, HitHint, ItemTransactionResultMessage, ObjectCreateDetailedMessage, ObjectCreateMessage, ObjectDeleteMessage, ObjectHeldMessage, OxygenStateMessage, PlanetsideAttributeMessage, PlayerStateMessage, ProjectileStateMessage, ReloadMessage, SetEmpireMessage, UseItemMessage, WeaponDryFireMessage} +import net.psforever.services.avatar.AvatarResponse +import net.psforever.services.Service +import net.psforever.types.{ChatMessageType, PlanetSideGUID, TransactionType, Vector3} +import net.psforever.util.Config + +object AvatarHandlerLogic { + def apply(ops: SessionAvatarHandlers): AvatarHandlerLogic = { + new AvatarHandlerLogic(ops, ops.context) + } +} + +class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: ActorContext) extends AvatarHandlerFunctions { + def sessionLogic: SessionData = ops.sessionLogic + + private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor + + /** + * na + * @param toChannel na + * @param guid na + * @param reply na + */ + def handle(toChannel: String, guid: PlanetSideGUID, reply: AvatarResponse.Response): Unit = { + val resolvedPlayerGuid = if (player != null && player.HasGUID) { + player.GUID + } else { + Service.defaultPlayerGUID + } + val isNotSameTarget = resolvedPlayerGuid != guid + val isSameTarget = !isNotSameTarget + reply match { + /* special messages */ + case AvatarResponse.TeardownConnection() if player.spectator => + context.self ! SessionActor.SetMode(CustomerServiceRepresentativeMode) + context.self.forward(AvatarServiceResponse(toChannel, guid, reply)) + + case AvatarResponse.TeardownConnection() => + context.self ! SessionActor.SetMode(NormalMode) + context.self.forward(AvatarServiceResponse(toChannel, guid, reply)) + + /* really common messages (very frequently, every life) */ + case pstate @ AvatarResponse.PlayerState( + pos, + vel, + yaw, + pitch, + yawUpper, + _, + isCrouching, + isJumping, + jumpThrust, + isCloaking, + isNotRendered, + canSeeReallyFar + ) if isNotSameTarget => + val pstateToSave = pstate.copy(timestamp = 0) + val (lastMsg, lastTime, lastPosition, wasVisible, wasShooting) = ops.lastSeenStreamMessage.get(guid.guid) match { + case Some(SessionAvatarHandlers.LastUpstream(Some(msg), visible, shooting, time)) => (Some(msg), time, msg.pos, visible, shooting) + case _ => (None, 0L, Vector3.Zero, false, None) + } + val drawConfig = Config.app.game.playerDraw //m + val maxRange = drawConfig.rangeMax * drawConfig.rangeMax //sq.m + val ourPosition = player.Position //xyz + val currentDistance = Vector3.DistanceSquared(ourPosition, pos) //sq.m + val inDrawableRange = currentDistance <= maxRange + val now = System.currentTimeMillis() //ms + if ( + sessionLogic.zoning.zoningStatus != Zoning.Status.Deconstructing && + !isNotRendered && inDrawableRange + ) { + //conditions where visibility is assured + val durationSince = now - lastTime //ms + lazy val previouslyInDrawableRange = Vector3.DistanceSquared(ourPosition, lastPosition) <= maxRange + lazy val targetDelay = { + val populationOver = math.max( + 0, + sessionLogic.localSector.livePlayerList.size - drawConfig.populationThreshold + ) + val distanceAdjustment = math.pow(populationOver / drawConfig.populationStep * drawConfig.rangeStep, 2) //sq.m + val adjustedDistance = currentDistance + distanceAdjustment //sq.m + drawConfig.ranges.lastIndexWhere { dist => adjustedDistance > dist * dist } match { + case -1 => 1 + case index => drawConfig.delays(index) + } + } //ms + if (!wasVisible || + !previouslyInDrawableRange || + durationSince > drawConfig.delayMax || + (!lastMsg.contains(pstateToSave) && + (canSeeReallyFar || + currentDistance < drawConfig.rangeMin * drawConfig.rangeMin || + sessionLogic.general.canSeeReallyFar || + durationSince > targetDelay + ) + ) + ) { + //must draw + sendResponse( + PlayerStateMessage( + guid, + pos, + vel, + yaw, + pitch, + yawUpper, + timestamp = 0, //is this okay? + isCrouching, + isJumping, + jumpThrust, + isCloaking + ) + ) + ops.lastSeenStreamMessage.put(guid.guid, SessionAvatarHandlers.LastUpstream(Some(pstateToSave), visible=true, wasShooting, now)) + } else { + //is visible, but skip reinforcement + ops.lastSeenStreamMessage.put(guid.guid, SessionAvatarHandlers.LastUpstream(Some(pstateToSave), visible=true, wasShooting, lastTime)) + } + } else { + //conditions where the target is not currently visible + if (wasVisible) { + //the target was JUST PREVIOUSLY visible; one last draw to move target beyond a renderable distance + val lat = (1 + ops.hidingPlayerRandomizer.nextInt(continent.map.scale.height.toInt)).toFloat + sendResponse( + PlayerStateMessage( + guid, + Vector3(1f, lat, 1f), + vel=None, + facingYaw=0f, + facingPitch=0f, + facingYawUpper=0f, + timestamp=0, //is this okay? + is_cloaked = isCloaking + ) + ) + ops.lastSeenStreamMessage.put(guid.guid, SessionAvatarHandlers.LastUpstream(Some(pstateToSave), visible=false, wasShooting, now)) + } else { + //skip drawing altogether + ops.lastSeenStreamMessage.put(guid.guid, SessionAvatarHandlers.LastUpstream(Some(pstateToSave), visible=false, wasShooting, lastTime)) + } + } + + case AvatarResponse.AvatarImplant(ImplantAction.Add, implant_slot, value) + if value == ImplantType.SecondWind.value => + sendResponse(AvatarImplantMessage(resolvedPlayerGuid, ImplantAction.Add, implant_slot, 7)) + //second wind does not normally load its icon into the shortcut hotbar + avatar + .shortcuts + .zipWithIndex + .find { case (s, _) => s.isEmpty} + .foreach { case (_, index) => + sendResponse(CreateShortcutMessage(resolvedPlayerGuid, index + 1, Some(ImplantType.SecondWind.shortcut))) + } + + case AvatarResponse.AvatarImplant(ImplantAction.Remove, implant_slot, value) + if value == ImplantType.SecondWind.value => + sendResponse(AvatarImplantMessage(resolvedPlayerGuid, ImplantAction.Remove, implant_slot, value)) + //second wind does not normally unload its icon from the shortcut hotbar + val shortcut = { + val imp = ImplantType.SecondWind.shortcut + net.psforever.objects.avatar.Shortcut(imp.code, imp.tile) //case class + } + avatar + .shortcuts + .zipWithIndex + .find { case (s, _) => s.contains(shortcut) } + .foreach { case (_, index) => + sendResponse(CreateShortcutMessage(resolvedPlayerGuid, index + 1, None)) + } + + case AvatarResponse.AvatarImplant(action, implant_slot, value) => + sendResponse(AvatarImplantMessage(resolvedPlayerGuid, action, implant_slot, value)) + + case AvatarResponse.ObjectHeld(slot, _) + if isSameTarget && player.VisibleSlots.contains(slot) => + sendResponse(ObjectHeldMessage(guid, slot, unk1=true)) + //Stop using proximity terminals if player unholsters a weapon + continent.GUID(sessionLogic.terminals.usingMedicalTerminal).collect { + case term: Terminal with ProximityUnit => sessionLogic.terminals.StopUsingProximityUnit(term) + } + if (sessionLogic.zoning.zoningStatus == Zoning.Status.Deconstructing) { + sessionLogic.zoning.spawn.stopDeconstructing() + } + + case AvatarResponse.ObjectHeld(slot, _) + if isSameTarget && slot > -1 => + sendResponse(ObjectHeldMessage(guid, slot, unk1=true)) + + case AvatarResponse.ObjectHeld(_, _) + if isSameTarget => () + + case AvatarResponse.ObjectHeld(_, previousSlot) => + sendResponse(ObjectHeldMessage(guid, previousSlot, unk1=false)) + + case AvatarResponse.ChangeFireState_Start(weaponGuid) + if isNotSameTarget && ops.lastSeenStreamMessage.get(guid.guid).exists { _.visible } => + sendResponse(ChangeFireStateMessage_Start(weaponGuid)) + val entry = ops.lastSeenStreamMessage(guid.guid) + ops.lastSeenStreamMessage.put(guid.guid, entry.copy(shooting = Some(weaponGuid))) + + case AvatarResponse.ChangeFireState_Start(weaponGuid) + if isNotSameTarget => + sendResponse(ChangeFireStateMessage_Start(weaponGuid)) + + case AvatarResponse.ChangeFireState_Stop(weaponGuid) + if isNotSameTarget && ops.lastSeenStreamMessage.get(guid.guid).exists { msg => msg.visible || msg.shooting.nonEmpty } => + sendResponse(ChangeFireStateMessage_Stop(weaponGuid)) + val entry = ops.lastSeenStreamMessage(guid.guid) + ops.lastSeenStreamMessage.put(guid.guid, entry.copy(shooting = None)) + + case AvatarResponse.ChangeFireState_Stop(weaponGuid) + if isNotSameTarget => + sendResponse(ChangeFireStateMessage_Stop(weaponGuid)) + + case AvatarResponse.LoadPlayer(pkt) if isNotSameTarget => + sendResponse(pkt) + + case AvatarResponse.EquipmentInHand(pkt) if isNotSameTarget => + sendResponse(pkt) + + case AvatarResponse.PlanetsideAttribute(attributeType, attributeValue) if isNotSameTarget => + sendResponse(PlanetsideAttributeMessage(guid, attributeType, attributeValue)) + + case AvatarResponse.PlanetsideAttributeToAll(attributeType, attributeValue) => + sendResponse(PlanetsideAttributeMessage(guid, attributeType, attributeValue)) + + case AvatarResponse.PlanetsideAttributeSelf(attributeType, attributeValue) if isSameTarget => + sendResponse(PlanetsideAttributeMessage(guid, attributeType, attributeValue)) + + case AvatarResponse.GenericObjectAction(objectGuid, actionCode) if isNotSameTarget => + sendResponse(GenericObjectActionMessage(objectGuid, actionCode)) + + case AvatarResponse.HitHint(sourceGuid) if player.isAlive => + sendResponse(HitHint(sourceGuid, guid)) + sessionLogic.zoning.CancelZoningProcess() + + case AvatarResponse.Destroy(victim, killer, weapon, pos) => + // guid = victim // killer = killer + sendResponse(DestroyMessage(victim, killer, weapon, pos)) + + case AvatarResponse.DestroyDisplay(killer, victim, method, unk) => + sendResponse(ops.destroyDisplayMessage(killer, victim, method, unk)) + + case AvatarResponse.TerminalOrderResult(terminalGuid, action, result) + if result && (action == TransactionType.Buy || action == TransactionType.Loadout) => + sendResponse(ItemTransactionResultMessage(terminalGuid, action, result)) + sessionLogic.terminals.lastTerminalOrderFulfillment = true + AvatarActor.savePlayerData(player) + + case AvatarResponse.TerminalOrderResult(terminalGuid, action, result) => + sendResponse(ItemTransactionResultMessage(terminalGuid, action, result)) + sessionLogic.terminals.lastTerminalOrderFulfillment = true + + case AvatarResponse.ChangeExosuit( + target, + armor, + exosuit, + subtype, + _, + maxhand, + oldHolsters, + holsters, + oldInventory, + inventory, + drop, + delete + ) if resolvedPlayerGuid == target => + sendResponse(ArmorChangedMessage(target, exosuit, subtype)) + sendResponse(PlanetsideAttributeMessage(target, attribute_type=4, armor)) + //happening to this player + //cleanup + sendResponse(ObjectHeldMessage(target, Player.HandsDownSlot, unk1=false)) + (oldHolsters ++ oldInventory ++ delete).foreach { + case (_, dguid) => sendResponse(ObjectDeleteMessage(dguid, unk1=0)) + } + //functionally delete + delete.foreach { case (obj, _) => TaskWorkflow.execute(GUIDTask.unregisterEquipment(continent.GUID, obj)) } + //redraw + if (maxhand) { + TaskWorkflow.execute(HoldNewEquipmentUp(player)( + Tool(GlobalDefinitions.MAXArms(subtype, player.Faction)), + 0 + )) + } + //draw free hand + player.FreeHand.Equipment.foreach { obj => + val definition = obj.Definition + sendResponse( + ObjectCreateDetailedMessage( + definition.ObjectId, + obj.GUID, + ObjectCreateMessageParent(target, Player.FreeHandSlot), + definition.Packet.DetailedConstructorData(obj).get + ) + ) + } + //draw holsters and inventory + (holsters ++ inventory).foreach { + case InventoryItem(obj, index) => + val definition = obj.Definition + sendResponse( + ObjectCreateDetailedMessage( + definition.ObjectId, + obj.GUID, + ObjectCreateMessageParent(target, index), + definition.Packet.DetailedConstructorData(obj).get + ) + ) + } + DropLeftovers(player)(drop) + + case AvatarResponse.ChangeExosuit(target, armor, exosuit, subtype, slot, _, oldHolsters, holsters, _, _, drop, delete) => + sendResponse(ArmorChangedMessage(target, exosuit, subtype)) + sendResponse(PlanetsideAttributeMessage(target, attribute_type=4, armor)) + //happening to some other player + sendResponse(ObjectHeldMessage(target, slot, unk1 = false)) + //cleanup + val dropPred = ContainableBehavior.DropPredicate(player) + val deleteFromDrop = drop.filterNot(dropPred) + (oldHolsters ++ delete ++ deleteFromDrop.map(f =>(f.obj, f.GUID))) + .distinctBy(_._2) + .foreach { case (_, guid) => sendResponse(ObjectDeleteMessage(guid, unk1=0)) } + //draw holsters + holsters.foreach { + case InventoryItem(obj, index) => + val definition = obj.Definition + sendResponse( + ObjectCreateMessage( + definition.ObjectId, + obj.GUID, + ObjectCreateMessageParent(target, index), + definition.Packet.ConstructorData(obj).get + ) + ) + } + + case AvatarResponse.ChangeLoadout( + target, + armor, + exosuit, + subtype, + _, + maxhand, + oldHolsters, + holsters, + oldInventory, + inventory, + drops + ) if resolvedPlayerGuid == target => + sendResponse(ArmorChangedMessage(target, exosuit, subtype)) + sendResponse(PlanetsideAttributeMessage(target, attribute_type=4, armor)) + //happening to this player + sendResponse(ObjectHeldMessage(target, Player.HandsDownSlot, unk1=true)) + //cleanup + (oldHolsters ++ oldInventory).foreach { + case (obj, objGuid) => + sendResponse(ObjectDeleteMessage(objGuid, unk1=0)) + TaskWorkflow.execute(GUIDTask.unregisterEquipment(continent.GUID, obj)) + } + drops.foreach(item => sendResponse(ObjectDeleteMessage(item.obj.GUID, unk1=0))) + //redraw + if (maxhand) { + sendResponse(PlanetsideAttributeMessage(target, attribute_type=7, player.Capacitor.toLong)) + TaskWorkflow.execute(HoldNewEquipmentUp(player)( + Tool(GlobalDefinitions.MAXArms(subtype, player.Faction)), + slot = 0 + )) + } + (holsters ++ inventory).foreach { case InventoryItem(item, slot) => + TaskWorkflow.execute(PutLoadoutEquipmentInInventory(player)(item, slot)) + } + DropLeftovers(player)(drops) + + case AvatarResponse.ChangeLoadout(target, armor, exosuit, subtype, slot, _, oldHolsters, _, _, _, _) => + //redraw handled by callbacks + sendResponse(ArmorChangedMessage(target, exosuit, subtype)) + sendResponse(PlanetsideAttributeMessage(target, attribute_type=4, armor)) + //happening to some other player + sendResponse(ObjectHeldMessage(target, slot, unk1=false)) + //cleanup + oldHolsters.foreach { case (_, guid) => sendResponse(ObjectDeleteMessage(guid, unk1=0)) } + + case AvatarResponse.UseKit(kguid, kObjId) => + sendResponse( + UseItemMessage( + resolvedPlayerGuid, + kguid, + resolvedPlayerGuid, + unk2 = 4294967295L, + unk3 = false, + unk4 = Vector3.Zero, + unk5 = Vector3.Zero, + unk6 = 126, + unk7 = 0, //sequence time? + unk8 = 137, + kObjId + ) + ) + sendResponse(ObjectDeleteMessage(kguid, unk1=0)) + + case AvatarResponse.KitNotUsed(_, "") => + sessionLogic.general.kitToBeUsed = None + + case AvatarResponse.KitNotUsed(_, msg) => + sessionLogic.general.kitToBeUsed = None + sendResponse(ChatMsg(ChatMessageType.UNK_225, msg)) + + case AvatarResponse.SendResponse(msg) => + sendResponse(msg) + + case AvatarResponse.SendResponseTargeted(targetGuid, msg) if resolvedPlayerGuid == targetGuid => + sendResponse(msg) + + /* common messages (maybe once every respawn) */ + case AvatarResponse.Reload(itemGuid) + if isNotSameTarget && ops.lastSeenStreamMessage.get(guid.guid).exists { _.visible } => + sendResponse(ReloadMessage(itemGuid, ammo_clip=1, unk1=0)) + + case AvatarResponse.Killed(_, mount) => + //pure logic + sessionLogic.shooting.shotsWhileDead = 0 + sessionLogic.zoning.CancelZoningProcess() + sessionLogic.keepAliveFunc = sessionLogic.zoning.NormalKeepAlive + sessionLogic.zoning.zoningStatus = Zoning.Status.None + continent.GUID(mount).collect { + case obj: Vehicle if obj.Destroyed => + ops.killedWhileMounted(obj, resolvedPlayerGuid) + sessionLogic.vehicles.ConditionalDriverVehicleControl(obj) + sessionLogic.general.unaccessContainer(obj) + + case obj: Vehicle => + ops.killedWhileMounted(obj, resolvedPlayerGuid) + sessionLogic.vehicles.ConditionalDriverVehicleControl(obj) + + case obj: PlanetSideGameObject with Mountable with Container if obj.Destroyed => + ops.killedWhileMounted(obj, resolvedPlayerGuid) + sessionLogic.general.unaccessContainer(obj) + + case obj: PlanetSideGameObject with Mountable with Container => + ops.killedWhileMounted(obj, resolvedPlayerGuid) + + case obj: PlanetSideGameObject with Mountable => + ops.killedWhileMounted(obj, resolvedPlayerGuid) + } + //player state changes + sessionLogic.general.dropSpecialSlotItem() + sessionLogic.general.toggleMaxSpecialState(enable = false) + player.FreeHand.Equipment.foreach(DropEquipmentFromInventory(player)(_)) + AvatarActor.updateToolDischargeFor(avatar) + AvatarActor.savePlayerLocation(player) + ops.revive(player.GUID) + avatarActor ! AvatarActor.InitializeImplants + //render + CustomerServiceRepresentativeMode.renderPlayer(sessionLogic, continent, player) + + case AvatarResponse.Release(tplayer) if isNotSameTarget => + sessionLogic.zoning.spawn.DepictPlayerAsCorpse(tplayer) + + case AvatarResponse.Revive(revivalTargetGuid) + if resolvedPlayerGuid == revivalTargetGuid => + ops.revive(revivalTargetGuid) + + /* uncommon messages (utility, or once in a while) */ + case AvatarResponse.ChangeAmmo(weapon_guid, weapon_slot, previous_guid, ammo_id, ammo_guid, ammo_data) + if isNotSameTarget && ops.lastSeenStreamMessage.get(guid.guid).exists { _.visible } => + ops.changeAmmoProcedures(weapon_guid, previous_guid, ammo_id, ammo_guid, weapon_slot, ammo_data) + sendResponse(ChangeAmmoMessage(weapon_guid, 1)) + + case AvatarResponse.ChangeAmmo(weapon_guid, weapon_slot, previous_guid, ammo_id, ammo_guid, ammo_data) + if isNotSameTarget => + ops.changeAmmoProcedures(weapon_guid, previous_guid, ammo_id, ammo_guid, weapon_slot, ammo_data) + + case AvatarResponse.ChangeFireMode(itemGuid, mode) if isNotSameTarget => + sendResponse(ChangeFireModeMessage(itemGuid, mode)) + + case AvatarResponse.ConcealPlayer() => + sendResponse(GenericObjectActionMessage(guid, code=9)) + + case AvatarResponse.EnvironmentalDamage(_, _, _) => + //TODO damage marker? + sessionLogic.zoning.CancelZoningProcess() + + case AvatarResponse.DropItem(pkt) if isNotSameTarget => + sendResponse(pkt) + + case AvatarResponse.ObjectDelete(itemGuid, unk) if isNotSameTarget => + sendResponse(ObjectDeleteMessage(itemGuid, unk)) + + /* rare messages */ + case AvatarResponse.SetEmpire(objectGuid, faction) if isNotSameTarget => + sendResponse(SetEmpireMessage(objectGuid, faction)) + + case AvatarResponse.DropSpecialItem() => + sessionLogic.general.dropSpecialSlotItem() + + case AvatarResponse.OxygenState(player, vehicle) => + sendResponse(OxygenStateMessage( + DrowningTarget(player.guid, player.progress, player.state), + vehicle.flatMap { vinfo => Some(DrowningTarget(vinfo.guid, vinfo.progress, vinfo.state)) } + )) + + case AvatarResponse.LoadProjectile(pkt) if isNotSameTarget => + sendResponse(pkt) + + case AvatarResponse.ProjectileState(projectileGuid, shotPos, shotVel, shotOrient, seq, end, targetGuid) if isNotSameTarget => + sendResponse(ProjectileStateMessage(projectileGuid, shotPos, shotVel, shotOrient, seq, end, targetGuid)) + + case AvatarResponse.ProjectileExplodes(projectileGuid, projectile) => + sendResponse( + ProjectileStateMessage( + projectileGuid, + projectile.Position, + shot_vel = Vector3.Zero, + projectile.Orientation, + sequence_num=0, + end=true, + hit_target_guid=PlanetSideGUID(0) + ) + ) + sendResponse(ObjectDeleteMessage(projectileGuid, unk1=2)) + + case AvatarResponse.ProjectileAutoLockAwareness(mode) => + sendResponse(GenericActionMessage(mode)) + + case AvatarResponse.PutDownFDU(target) if isNotSameTarget => + sendResponse(GenericObjectActionMessage(target, code=53)) + + case AvatarResponse.StowEquipment(target, slot, item) if isNotSameTarget => + val definition = item.Definition + sendResponse( + ObjectCreateDetailedMessage( + definition.ObjectId, + item.GUID, + ObjectCreateMessageParent(target, slot), + definition.Packet.DetailedConstructorData(item).get + ) + ) + + case AvatarResponse.WeaponDryFire(weaponGuid) + if isNotSameTarget && ops.lastSeenStreamMessage.get(guid.guid).exists { _.visible } => + continent.GUID(weaponGuid).collect { + case tool: Tool if tool.Magazine == 0 => + // check that the magazine is still empty before sending WeaponDryFireMessage + // if it has been reloaded since then, other clients will not see it firing + sendResponse(WeaponDryFireMessage(weaponGuid)) + } + + case _ => () + } + } +} diff --git a/src/main/scala/net/psforever/actors/session/csr/ChatLogic.scala b/src/main/scala/net/psforever/actors/session/csr/ChatLogic.scala new file mode 100644 index 000000000..e7a5a9c1e --- /dev/null +++ b/src/main/scala/net/psforever/actors/session/csr/ChatLogic.scala @@ -0,0 +1,419 @@ +// Copyright (c) 2024 PSForever +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.{GlobalDefinitions, PlanetSideGameObject, Session, TurretDeployable} +import net.psforever.objects.ce.{Deployable, DeployableCategory} +import net.psforever.objects.serverobject.affinity.FactionAffinity +import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} +import net.psforever.objects.serverobject.hackable.Hackable +import net.psforever.objects.serverobject.structures.Building +import net.psforever.objects.zones.Zone +import net.psforever.packet.game.{ChatMsg, SetChatFilterMessage} +import net.psforever.services.Service +import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} +import net.psforever.services.chat.{ChatChannel, DefaultChannel, SpectatorChannel} +import net.psforever.types.ChatMessageType.{CMT_TOGGLESPECTATORMODE, CMT_TOGGLE_GM} +import net.psforever.types.{ChatMessageType, PlanetSideEmpire} + +import scala.util.Success + +object ChatLogic { + def apply(ops: ChatOperations): ChatLogic = { + new ChatLogic(ops, ops.context) + } +} + +class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) extends ChatFunctions { + ops.transitoryCommandEntered match { + case Some(CMT_TOGGLESPECTATORMODE) => + //we are transitioning down from csr spectator mode to normal mode, continue to block transitory messages + () + case _ => + //correct player mode + ops.transitoryCommandEntered = None + } + + def sessionLogic: SessionData = ops.sessionLogic + + ops.CurrentSpectatorMode = SpectateAsCustomerServiceRepresentativeMode + + private var comms: ChatChannel = DefaultChannel + private var seeSpectatorsIn: Option[Zone] = None + + def handleChatMsg(message: ChatMsg): Unit = { + import net.psforever.types.ChatMessageType._ + val isAlive = if (player != null) player.isAlive else false + (message.messageType, message.recipient.trim, message.contents.trim) match { + /** Messages starting with ! are custom chat commands */ + case (_, _, contents) if contents.startsWith("!") && + customCommandMessages(message, session) => () + + case (CMT_FLY, recipient, contents) => + ops.commandFly(contents, recipient) + + case (CMT_ANONYMOUS, _, _) => () + + case (CMT_TOGGLE_GM, _, contents) => + customCommandModerator(contents) + + case (CMT_CULLWATERMARK, _, contents) => + ops.commandWatermark(contents) + + case (CMT_SPEED, _, contents) => + ops.commandSpeed(message, contents) + + case (CMT_TOGGLESPECTATORMODE, _, contents) if isAlive => + commandToggleSpectatorMode(contents) + + case (CMT_RECALL, _, _) => + ops.commandRecall(session) + + case (CMT_INSTANTACTION, _, _) => + ops.commandInstantAction(session) + + case (CMT_QUIT, _, _) => + ops.commandQuit(session) + + case (CMT_SUICIDE, _, _) => + ops.commandSuicide(session) + + case (CMT_DESTROY, _, contents) if contents.matches("\\d+") => + ops.commandDestroy(session, message, contents) + + case (CMT_SETBASERESOURCES, _, contents) => + ops.commandSetBaseResources(session, contents) + + case (CMT_ZONELOCK, _, contents) => + ops.commandZoneLock(contents) + + case (U_CMT_ZONEROTATE, _, _) => + ops.commandZoneRotate() + + case (CMT_CAPTUREBASE, _, contents) => + ops.commandCaptureBase(session, message, contents) + + case (CMT_GMBROADCAST | CMT_GMBROADCAST_NC | CMT_GMBROADCAST_VS | CMT_GMBROADCAST_TR, _, _) => + ops.commandSendToRecipient(session, message, comms) + + case (CMT_GMTELL, _, _) => + ops.commandSend(session, message, comms) + + case (CMT_GMBROADCASTPOPUP, _, _) => + ops.commandSendToRecipient(session, message, comms) + + case (CMT_OPEN, _, _) if !player.silenced => + ops.commandSendToRecipient(session, message, comms) + + case (CMT_VOICE, _, contents) => + ops.commandVoice(session, message, contents, comms) + + case (CMT_TELL, _, _) if !player.silenced => + ops.commandTellOrIgnore(session, message, comms) + + case (CMT_BROADCAST, _, _) if !player.silenced => + ops.commandSendToRecipient(session, message, comms) + + case (CMT_PLATOON, _, _) if !player.silenced => + ops.commandSendToRecipient(session, message, comms) + + case (CMT_COMMAND, _, _) => + ops.commandSendToRecipient(session, message, comms) + + case (CMT_NOTE, _, _) => + ops.commandSend(session, message, comms) + + case (CMT_SILENCE, _, _) => + ops.commandSend(session, message, comms) + + case (CMT_SQUAD, _, _) => + ops.commandSquad(session, message, comms) //todo SquadChannel, but what is the guid + + case (CMT_WHO | CMT_WHO_CSR | CMT_WHO_CR | CMT_WHO_PLATOONLEADERS | CMT_WHO_SQUADLEADERS | CMT_WHO_TEAMS, _, _) => + ops.commandWho(session) + + case (CMT_ZONE, _, contents) => + ops.commandZone(message, contents) + + case (CMT_WARP, _, contents) => + ops.commandWarp(session, message, contents) + + case (CMT_SETBATTLERANK, _, contents) => + ops.commandSetBattleRank(session, message, contents) + + case (CMT_SETCOMMANDRANK, _, contents) => + ops.commandSetCommandRank(session, message, contents) + + case (CMT_ADDBATTLEEXPERIENCE, _, contents) => + ops.commandAddBattleExperience(message, contents) + + case (CMT_ADDCOMMANDEXPERIENCE, _, contents) => + ops.commandAddCommandExperience(message, contents) + + case (CMT_TOGGLE_HAT, _, contents) => + ops.commandToggleHat(session, message, contents) + + case (CMT_HIDE_HELMET | CMT_TOGGLE_SHADES | CMT_TOGGLE_EARPIECE, _, contents) => + ops.commandToggleCosmetics(session, message, contents) + + case (CMT_ADDCERTIFICATION, _, contents) => + ops.commandAddCertification(session, message, contents) + + case (CMT_KICK, _, contents) => + ops.commandKick(session, message, contents) + + case _ => + log.warn(s"Unhandled chat message $message") + } + } + + def handleChatFilter(pkt: SetChatFilterMessage): Unit = { + val SetChatFilterMessage(_, _, _) = pkt + } + + def handleIncomingMessage(message: ChatMsg, fromSession: Session): Unit = { + import ChatMessageType._ + message.messageType match { + case CMT_BROADCAST | CMT_SQUAD | CMT_PLATOON | CMT_COMMAND | CMT_NOTE => + ops.commandIncomingSendAllIfOnline(session, message) + + case CMT_OPEN => + ops.commandIncomingSendToLocalIfOnline(session, fromSession, message) + + case CMT_TELL | U_CMT_TELLFROM | + CMT_GMOPEN | CMT_GMBROADCAST | CMT_GMBROADCAST_NC | CMT_GMBROADCAST_TR | CMT_GMBROADCAST_VS | + CMT_GMBROADCASTPOPUP | CMT_GMTELL | U_CMT_GMTELLFROM | UNK_45 | UNK_71 | UNK_227 | UNK_229 => + ops.commandIncomingSend(message) + + case CMT_VOICE => + ops.commandIncomingVoice(session, fromSession, message) + + case CMT_SILENCE => + ops.commandIncomingSilence(session, message) + + case _ => + log.warn(s"Unexpected messageType $message") + } + } + + private def customCommandMessages( + message: ChatMsg, + session: Session + ): Boolean = { + val contents = message.contents + if (contents.startsWith("!")) { + val (command, params) = ops.cliTokenization(contents.drop(1)) match { + case a :: b => (a, b) + case _ => ("", Seq("")) + } + command match { + case "loc" => ops.customCommandLoc(session, message) + case "suicide" => ops.customCommandSuicide(session) + case "grenade" => ops.customCommandGrenade(session, log) + case "macro" => ops.customCommandMacro(session, params) + case "progress" => ops.customCommandProgress(session, params) + case "whitetext" => ops.customCommandWhitetext(session, params) + case "list" => ops.customCommandList(session, params, message) + case "ntu" => ops.customCommandNtu(session, params) + case "zonerotate" => ops.customCommandZonerotate(params) + case "nearby" => ops.customCommandNearby(session) + case "togglespectators" => customCommandToggleSpectators(params) + case "showspectators" => customCommandShowSpectators() + case "hidespectators" => customCommandHideSpectators() + case "sayspectator" => customCommandSpeakAsSpectator(params, message) + case "setempire" => customCommandSetEmpire(params) + case _ => + // command was not handled + sendResponse( + ChatMsg( + ChatMessageType.CMT_GMOPEN, // CMT_GMTELL + message.wideContents, + "Server", + s"Unknown command !$command", + message.note + ) + ) + false + } + } else { + false + } + } + + private def commandToggleSpectatorMode(contents: String): Unit = { + contents.toLowerCase() match { + case "on" | "o" | "" if !player.spectator => + context.self ! SessionActor.SetMode(SpectateAsCustomerServiceRepresentativeMode) + case "off" | "of" if player.spectator => + context.self ! SessionActor.SetMode(CustomerServiceRepresentativeMode) + case _ => () + } + } + + private def customCommandModerator(contents: String): Boolean = { + if (sessionLogic.zoning.maintainInitialGmState) { + sessionLogic.zoning.maintainInitialGmState = false + true + } else { + ops.transitoryCommandEntered + .collect { + case CMT_TOGGLE_GM => true + case CMT_TOGGLESPECTATORMODE => false + } + .getOrElse { + contents.toLowerCase() match { + case "off" | "of" if player.spectator => + ops.transitoryCommandEntered = Some(CMT_TOGGLESPECTATORMODE) + context.self ! SessionActor.SetMode(CustomerServiceRepresentativeMode) + context.self ! SessionActor.SetMode(NormalMode) + true + case "off" | "of" => + ops.transitoryCommandEntered = Some(CMT_TOGGLE_GM) + context.self ! SessionActor.SetMode(NormalMode) + true + case _ => + false + } + } + } + } + + private def customCommandToggleSpectators(contents: Seq[String]): Boolean = { + contents + .headOption + .map(_.toLowerCase()) + .collect { + case "on" | "o" | "" if !seeSpectatorsIn.contains(continent) => + customCommandShowSpectators() + case "off" | "of" if seeSpectatorsIn.contains(continent) => + customCommandHideSpectators() + case _ => () + } + true + } + + private def customCommandShowSpectators(): Boolean = { + val channel = player.Name + val events = continent.AvatarEvents + seeSpectatorsIn = Some(continent) + events ! Service.Join(s"spectator") + continent + .AllPlayers + .filter(_.spectator) + .foreach { spectator => + val guid = spectator.GUID + val definition = spectator.Definition + events ! AvatarServiceMessage( + channel, + AvatarAction.LoadPlayer(guid, definition.ObjectId, guid, definition.Packet.ConstructorData(spectator).get, None) + ) + } + true + } + + private def customCommandHideSpectators(): Boolean = { + val channel = player.Name + val events = continent.AvatarEvents + seeSpectatorsIn = None + events ! Service.Leave(Some("spectator")) + continent + .AllPlayers + .filter(_.spectator) + .foreach { spectator => + val guid = spectator.GUID + events ! AvatarServiceMessage( + channel, + AvatarAction.ObjectDelete(guid, guid) + ) + } + true + } + + private def customCommandSpeakAsSpectator(params: Seq[String], message: ChatMsg): Boolean = { + comms = SpectatorChannel + handleChatMsg(message.copy(contents = params.mkString(" "))) + comms = DefaultChannel + true + } + + private def customCommandSetEmpire(params: Seq[String]): Boolean = { + var postUsage: Boolean = false + val (entityOpt, foundFaction) = (params.headOption, params.lift(1)) match { + case (Some(guid), Some(faction)) if guid.toIntOption.nonEmpty => + try { + (continent.GUID(guid.toInt), PlanetSideEmpire.apply(faction)) + } catch { + case _: Exception => + (None, PlanetSideEmpire.NEUTRAL) + } + case (Some(guid), None) if guid.toIntOption.nonEmpty => + (continent.GUID(guid.toInt), player.Faction) + case _ => + postUsage = true + (None, PlanetSideEmpire.NEUTRAL) + } + entityOpt + .collect { + case f: FactionAffinity if f.Faction != foundFaction && foundFaction != PlanetSideEmpire.NEUTRAL => f + } + .collect { + case o: TurretDeployable + if o.Definition.DeployCategory == DeployableCategory.FieldTurrets => + //remove prior turret and construct new one + import scala.concurrent.ExecutionContext.Implicits.global + import scala.concurrent.duration._ + o.Actor ! Deployable.Deconstruct(Some(2.seconds)) + sessionLogic.general.handleDeployObject( + continent, + GlobalDefinitions.PortableMannedTurret(foundFaction).Item, + o.Position, + o.Orientation, + o.WhichSide, + foundFaction + ).onComplete { + case Success(obj2) => sendResponse(ChatMsg(ChatMessageType.UNK_227, s"${obj2.GUID.guid}")) + case _ => () + } + true + case o: Deployable => + o.Faction = foundFaction + continent.AvatarEvents ! AvatarServiceMessage( + continent.id, + AvatarAction.SetEmpire(Service.defaultPlayerGUID, o.GUID, foundFaction) + ) + true + case o: Building => + ops.commandCaptureBaseProcessResults(Some(Seq(o)), Some(foundFaction), Some(1)) + true + case o: PlanetSideServerObject with Hackable => + o.Actor ! CommonMessages.Hack(player, o) + true + case o: PlanetSideGameObject with FactionAffinity => + o.Faction = foundFaction + continent.AvatarEvents ! AvatarServiceMessage( + continent.id, + AvatarAction.SetEmpire(Service.defaultPlayerGUID, o.GUID, foundFaction) + ) + true + } + .getOrElse { + if (postUsage) { + sendResponse(ChatMsg(ChatMessageType.UNK_227, "!setempire guid [faction]")) + } else if (entityOpt.nonEmpty) { + sendResponse(ChatMsg(ChatMessageType.UNK_227, "set empire entity not supported")) + } else { + sendResponse(ChatMsg(ChatMessageType.UNK_227, "set empire entity not found")) + } + true + } + } + + override def stop(): Unit = { + super.stop() + seeSpectatorsIn.foreach(_ => customCommandHideSpectators()) + seeSpectatorsIn = None + } +} diff --git a/src/main/scala/net/psforever/actors/session/csr/CustomerServiceRepresentativeMode.scala b/src/main/scala/net/psforever/actors/session/csr/CustomerServiceRepresentativeMode.scala new file mode 100644 index 000000000..a6b17186b --- /dev/null +++ b/src/main/scala/net/psforever/actors/session/csr/CustomerServiceRepresentativeMode.scala @@ -0,0 +1,204 @@ +// Copyright (c) 2024 PSForever +package net.psforever.actors.session.csr + +import net.psforever.actors.session.support.{ChatFunctions, GalaxyHandlerFunctions, GeneralFunctions, LocalHandlerFunctions, ModeLogic, MountHandlerFunctions, PlayerMode, SessionData, SquadHandlerFunctions, TerminalHandlerFunctions, VehicleFunctions, VehicleHandlerFunctions, WeaponAndProjectileFunctions} +import net.psforever.objects.{Deployables, PlanetSideGameObject, Player, Session, Vehicle} +import net.psforever.objects.avatar.Certification +import net.psforever.objects.serverobject.ServerObject +import net.psforever.objects.serverobject.mount.Mountable +import net.psforever.objects.vital.Vitality +import net.psforever.objects.zones.Zone +import net.psforever.objects.zones.blockmap.BlockMapEntity +import net.psforever.packet.game.{ChatMsg, ObjectCreateDetailedMessage, PlanetsideAttributeMessage} +import net.psforever.packet.game.objectcreate.RibbonBars +import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} +import net.psforever.services.chat.{CustomerServiceChannel, SpectatorChannel} +import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} +import net.psforever.types.{ChatMessageType, MeritCommendation, PlanetSideGUID} + +class CustomerServiceRepresentativeMode(data: SessionData) extends ModeLogic { + val avatarResponse: AvatarHandlerLogic = AvatarHandlerLogic(data.avatarResponse) + val chat: ChatFunctions = ChatLogic(data.chat) + val galaxy: GalaxyHandlerFunctions = net.psforever.actors.session.normal.GalaxyHandlerLogic(data.galaxyResponseHandlers) + val general: GeneralFunctions = GeneralLogic(data.general) + 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 = net.psforever.actors.session.normal.VehicleHandlerLogic(data.vehicleResponseOperations) + + private var oldRibbons: RibbonBars = RibbonBars() + private var oldCertifications : Set[Certification] = Set() + + override def switchTo(session: Session): Unit = { + val player = session.player + val avatar = session.avatar + val continent = session.zone + // + data.zoning.displayZoningMessageWhenCancelled = false + if (oldCertifications.isEmpty) { + oldCertifications = avatar.certifications + oldRibbons = avatar.decoration.ribbonBars + val newAvatar = avatar.copy( + certifications = Certification.values.toSet, + decoration = avatar.decoration.copy(ribbonBars = RibbonBars( + MeritCommendation.CSAppreciation, + MeritCommendation.Loser, + MeritCommendation.Loser, + MeritCommendation.CSAppreciation + )) + ) + player.avatar = newAvatar + data.session = session.copy(avatar = newAvatar, player = player) + Deployables.InitializeDeployableQuantities(newAvatar) + } + requireDismount(continent, player) + data.keepAlivePersistenceFunc = keepAlivePersistanceCSR + // + CustomerServiceRepresentativeMode.renderPlayer(data, continent, player) + player.allowInteraction = false + if (player.silenced) { + data.chat.commandIncomingSilence(session, ChatMsg(ChatMessageType.CMT_SILENCE, "player 0")) + } + data.chat.JoinChannel(SpectatorChannel) + data.chat.JoinChannel(CustomerServiceChannel) + data.sendResponse(ChatMsg(ChatMessageType.UNK_225, "CSR MODE ON")) + } + + override def switchFrom(session: Session): Unit = { + val player = session.player + val avatar = session.avatar + val continent = session.zone + // + data.zoning.displayZoningMessageWhenCancelled = true + val newAvatar = avatar.copy( + certifications = oldCertifications, + decoration = avatar.decoration.copy(ribbonBars = oldRibbons) + ) + oldCertifications = Set() + oldRibbons = RibbonBars() + player.avatar = newAvatar + data.session = session.copy(avatar = newAvatar, player = player) + Deployables.InitializeDeployableQuantities(newAvatar) + // + requireDismount(continent, player) + data.keepAlivePersistenceFunc = data.keepAlivePersistence + // + CustomerServiceRepresentativeMode.renderPlayer(data, continent, player) + player.allowInteraction = true + data.chat.LeaveChannel(SpectatorChannel) + data.chat.LeaveChannel(CustomerServiceChannel) + data.sendResponse(ChatMsg(ChatMessageType.UNK_225, "CSR MODE OFF")) + } + + private def requireDismount(zone: Zone, player: Player): Unit = { + data.vehicles.GetMountableAndSeat(None, player, zone) match { + case (Some(obj: Vehicle), Some(seatNum)) if seatNum == 0 => + data.vehicles.ServerVehicleOverrideStop(obj) + obj.Actor ! ServerObject.AttributeMsg(10, 3) //faction-accessible driver seat + obj.Actor ! Mountable.TryDismount(player, seatNum) + player.VehicleSeated = None + case (Some(obj), Some(seatNum)) => + obj.Actor ! Mountable.TryDismount(player, seatNum) + player.VehicleSeated = None + case _ => + player.VehicleSeated = None + } + } + + private def keepAlivePersistanceCSR(): Unit = { + val player = data.player + data.keepAlivePersistence() + topOffHealthOfPlayer(player) + player.allowInteraction = false + topOffHealthOfPlayer(player) + data.continent.GUID(data.player.VehicleSeated) + .collect { + case obj: PlanetSideGameObject with Vitality with BlockMapEntity => + topOffHealth(obj) + data.updateBlockMap(obj, obj.Position) + obj + } + .getOrElse { + data.updateBlockMap(player, player.Position) + } + } + + private def topOffHealth(obj: PlanetSideGameObject with Vitality): Unit = { + obj match { + case p: Player => topOffHealthOfPlayer(p) + case v: Vehicle => topOffHealthOfVehicle(v) + case o: PlanetSideGameObject with Vitality => topOffHealthOfGeneric(o) + case _ => () + } + } + + private def topOffHealthOfPlayer(player: Player): 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) + data.sendResponse(PlanetsideAttributeMessage(player.GUID, 0, maxHealthOfPlayer)) + data.continent.AvatarEvents ! AvatarServiceMessage(data.zoning.zoneChannel, AvatarAction.PlanetsideAttribute(player.GUID, 0, maxHealthOfPlayer)) + } + } + + private def topOffHealthOfVehicle(vehicle: Vehicle): Unit = { + topOffHealthOfGeneric(vehicle) + //vehicle shields below half, full shields + val maxShieldsOfVehicle = vehicle.MaxShields.toLong + val shieldsUi = vehicle.Definition.shieldUiAttribute + if (vehicle.Shields < maxShieldsOfVehicle) { + val guid = vehicle.GUID + vehicle.Shields = maxShieldsOfVehicle.toInt + data.sendResponse(PlanetsideAttributeMessage(guid, shieldsUi, maxShieldsOfVehicle)) + data.continent.VehicleEvents ! VehicleServiceMessage( + data.continent.id, + VehicleAction.PlanetsideAttribute(PlanetSideGUID(0), guid, shieldsUi, maxShieldsOfVehicle) + ) + } + } + + private def topOffHealthOfGeneric(obj: PlanetSideGameObject with Vitality): Unit = { + //below half health, full heal + val guid = obj.GUID + val maxHealthOf = obj.MaxHealth.toLong + if (obj.Health < maxHealthOf) { + obj.Health = maxHealthOf.toInt + data.sendResponse(PlanetsideAttributeMessage(guid, 0, maxHealthOf)) + data.continent.VehicleEvents ! VehicleServiceMessage( + data.continent.id, + VehicleAction.PlanetsideAttribute(PlanetSideGUID(0), guid, 0, maxHealthOf) + ) + } + } +} + +case object CustomerServiceRepresentativeMode extends PlayerMode { + def setup(data: SessionData): ModeLogic = { + new CustomerServiceRepresentativeMode(data) + } + + private[csr] def renderPlayer(data: SessionData, zone: Zone, player: Player): Unit = { + val pguid = player.GUID + val definition = player.Definition + val objectClass = definition.ObjectId + val packet = definition.Packet + data.sendResponse(ObjectCreateDetailedMessage( + objectClass, + pguid, + packet.DetailedConstructorData(player).get + )) + data.zoning.spawn.HandleSetCurrentAvatar(player) + zone.AvatarEvents ! AvatarServiceMessage(zone.id, AvatarAction.LoadPlayer( + pguid, + objectClass, + pguid, + packet.ConstructorData(player).get, + None + )) + } +} diff --git a/src/main/scala/net/psforever/actors/session/csr/GeneralLogic.scala b/src/main/scala/net/psforever/actors/session/csr/GeneralLogic.scala new file mode 100644 index 000000000..68f59b434 --- /dev/null +++ b/src/main/scala/net/psforever/actors/session/csr/GeneralLogic.scala @@ -0,0 +1,781 @@ +// 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.{GeneralFunctions, GeneralOperations, SessionData} +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 +import net.psforever.objects.definition.{BasicDefinition, KitDefinition, SpecialExoSuitDefinition} +import net.psforever.objects.entity.WorldEntity +import net.psforever.objects.equipment.Equipment +import net.psforever.objects.inventory.Container +import net.psforever.objects.serverobject.{CommonMessages, ServerObject} +import net.psforever.objects.serverobject.containable.Containable +import net.psforever.objects.serverobject.doors.Door +import net.psforever.objects.serverobject.generator.Generator +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.WarpGate +import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal +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.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.{ActionCancelMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, BugReportMessage, ChangeFireModeMessage, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestMessage, ChatMsg, CollisionIs, ConnectToWorldRequestMessage, CreateShortcutMessage, DeadState, DeployObjectMessage, DisplayedAwardMessage, DropItemMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FriendsRequest, GenericAction, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, InvalidTerrainMessage, LootItemMessage, MoveItemMessage, ObjectDetectedMessage, ObjectHeldMessage, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, RequestDestroyMessage, TargetingImplantRequest, TerrainCondition, TradeMessage, UnuseItemMessage, UseItemMessage, VoiceHostInfo, VoiceHostRequest, ZipLineMessage} +import net.psforever.services.RemoverActor +import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} +import net.psforever.types.{CapacitorStateType, ChatMessageType, Cosmetic, ExoSuitType, PlanetSideEmpire, PlanetSideGUID, Vector3} + +import scala.util.Success + +object GeneralLogic { + def apply(ops: GeneralOperations): GeneralLogic = { + new GeneralLogic(ops, ops.context) + } +} + +class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContext) extends GeneralFunctions { + def sessionLogic: SessionData = ops.sessionLogic + + private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor + + def handleConnectToWorldRequest(pkt: ConnectToWorldRequestMessage): Unit = { /* intentionally blank */ } + + def handleCharacterCreateRequest(pkt: CharacterCreateRequestMessage): Unit = { /* intentionally blank */ } + + def handleCharacterRequest(pkt: CharacterRequestMessage): Unit = { /* intentionally blank */ } + + def handlePlayerStateUpstream(pkt: PlayerStateMessageUpstream): Unit = { + val PlayerStateMessageUpstream( + avatarGuid, + pos, + vel, + yaw, + pitch, + yawUpper, + seqTime, + _, + isCrouching, + isJumping, + jumpThrust, + isCloaking, + _, + _ + ) = pkt + sessionLogic.persist() + sessionLogic.turnCounterFunc(avatarGuid) + sessionLogic.updateBlockMap(player, pos) + //below half health, full heal + val maxHealth = player.MaxHealth.toLong + if (player.Health < maxHealth) { + 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) { + avatarActor ! AvatarActor.RestoreStamina(maxStamina) + sendResponse(PlanetsideAttributeMessage(player.GUID, 2, maxStamina.toLong)) + } + //below half armor, full armor + val maxArmor = player.MaxArmor.toLong + if (player.Armor < maxArmor) { + player.Armor = maxArmor.toInt + sendResponse(PlanetsideAttributeMessage(avatarGuid, 4, maxArmor)) + continent.AvatarEvents ! AvatarServiceMessage(continent.id, AvatarAction.PlanetsideAttribute(avatarGuid, 4, maxArmor)) + } + //expected + val isMoving = WorldEntity.isMoving(vel) + val isMovingPlus = isMoving || isJumping || jumpThrust + if (isMovingPlus) { + if (sessionLogic.zoning.zoningStatus == Zoning.Status.Deconstructing) { + sessionLogic.zoning.spawn.stopDeconstructing() + } else if (sessionLogic.zoning.zoningStatus != Zoning.Status.None) { + sessionLogic.zoning.CancelZoningProcess() + } + } + ops.fallHeightTracker(pos.z) +// if (isCrouching && !player.Crouching) { +// //dev stuff goes here +// } + player.Position = pos + player.Velocity = vel + player.Orientation = Vector3(player.Orientation.x, pitch, yaw) + player.FacingYawUpper = yawUpper + player.Crouching = isCrouching + player.Jumping = isJumping + if (isCloaking && !player.Cloaked) { + sessionLogic.zoning.CancelZoningProcess() + } + player.Cloaked = player.ExoSuit == ExoSuitType.Infiltration && isCloaking + maxCapacitorTick(jumpThrust) + if (isMovingPlus && sessionLogic.terminals.usingMedicalTerminal.isDefined) { + continent.GUID(sessionLogic.terminals.usingMedicalTerminal) match { + case Some(term: Terminal with ProximityUnit) => + sessionLogic.terminals.StopUsingProximityUnit(term) + case _ => () + } + } + 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(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. + sendResponse(UnuseItemMessage(guid, guid)) + ops.unaccessContainer(container) + case _ => () + } + val channel = if (!player.spectator) { + sessionLogic.updateBlockMap(player, pos) + continent.id + } else { + "spectator" + } + 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( + channel, + AvatarAction.PlayerState( + avatarGuid, + player.Position, + player.Velocity, + yaw, + pitch, + yawUpper, + seqTime, + isCrouching, + isJumping, + jumpThrust, + isCloaking, + isNotVisible, + eagleEye + ) + ) + sessionLogic.squad.updateSquad() + player.allowInteraction = false + } + + def handleVoiceHostRequest(pkt: VoiceHostRequest): Unit = { + ops.noVoicedChat(pkt) + } + + def handleVoiceHostInfo(pkt: VoiceHostInfo): Unit = { + ops.noVoicedChat(pkt) + } + + def handleEmote(pkt: EmoteMsg): Unit = { + val EmoteMsg(avatarGuid, emote) = pkt + sendResponse(EmoteMsg(avatarGuid, emote)) + } + + def handleDropItem(pkt: DropItemMessage): Unit = { + ops.handleDropItem(pkt) match { + case GeneralOperations.ItemDropState.Dropped => + sessionLogic.zoning.CancelZoningProcess() + case _ => () + } + } + + def handlePickupItem(pkt: PickupItemMessage): Unit = { + ops.handlePickupItem(pkt) match { + case GeneralOperations.ItemPickupState.PickedUp => + sessionLogic.zoning.CancelZoningProcess() + case _ => () + } + } + + def handleObjectHeld(pkt: ObjectHeldMessage): Unit = { + val ObjectHeldMessage(_, heldHolsters, _) = pkt + player.Actor ! PlayerControl.ObjectHeld(heldHolsters) + } + + def handleAvatarJump(pkt: AvatarJumpMessage): Unit = { /* no stamina loss */ } + + def handleZipLine(pkt: ZipLineMessage): Unit = { + ops.handleZipLine(pkt) match { + case GeneralOperations.ZiplineBehavior.Teleporter | GeneralOperations.ZiplineBehavior.Zipline => + sessionLogic.zoning.CancelZoningProcess() + case _ => + () + } + } + + def handleRequestDestroy(pkt: RequestDestroyMessage): Unit = { + val RequestDestroyMessage(objectGuid) = pkt + //make sure this is the correct response for all cases + sessionLogic.validObject(objectGuid, decorator = "RequestDestroy") match { + case Some(vehicle: Vehicle) => + vehicle.Actor ! Vehicle.Deconstruct() + + case Some(obj: Projectile) => + if (!obj.isResolved) { + obj.Miss() + } + continent.Projectile ! ZoneProjectile.Remove(objectGuid) + + case Some(obj: BoomerTrigger) => + if (ops.findEquipmentToDelete(objectGuid, obj)) { + continent.GUID(obj.Companion) match { + case Some(boomer: BoomerDeployable) => + boomer.Trigger = None + boomer.Actor ! Deployable.Deconstruct() + case Some(thing) => + log.warn(s"RequestDestroy: BoomerTrigger object connected to wrong object - $thing") + case None => () + } + } + + case Some(obj: Deployable) => + obj.Actor ! Deployable.Deconstruct() + + case Some(obj: Equipment) => + ops.findEquipmentToDelete(objectGuid, obj) + + 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 => () + } + } + + def handleMoveItem(pkt: MoveItemMessage): Unit = { + ops.handleMoveItem(pkt) + } + + def handleLootItem(pkt: LootItemMessage): Unit = { + ops.handleLootItem(pkt) + } + + def handleAvatarImplant(pkt: AvatarImplantMessage): Unit = { + ops.handleAvatarImplant(pkt) match { + case GeneralOperations.ImplantActivationBehavior.Activate | GeneralOperations.ImplantActivationBehavior.Deactivate => + sessionLogic.zoning.CancelZoningProcess() + case GeneralOperations.ImplantActivationBehavior.NotFound => + log.error(s"AvatarImplantMessage: ${player.Name} has an unknown implant in ${pkt.implantSlot}") + case _ => () + } + } + + def handleUseItem(pkt: UseItemMessage): Unit = { + val equipment = ops.findContainedEquipment(pkt.item_used_guid) match { + case (o @ Some(_), a) if a.exists(_.isInstanceOf[Tool]) => + sessionLogic.shooting.FindEnabledWeaponsToHandleWeaponFireAccountability(o, a.collect { case w: Tool => w })._2.headOption + case (Some(_), a) => + a.headOption + case _ => + None + } + sessionLogic.validObject(pkt.object_guid, decorator = "UseItem") match { + case Some(door: Door) => + handleUseDoor(door, equipment) + case Some(resourceSilo: ResourceSilo) => + ops.handleUseResourceSilo(resourceSilo, equipment) + case Some(panel: IFFLock) => + ops.handleUseGeneralEntity(panel, equipment) + case Some(obj: Player) => + ops.handleUsePlayer(obj, equipment, pkt) + case Some(locker: Locker) => + ops.handleUseLocker(locker, equipment, pkt) + case Some(gen: Generator) => + ops.handleUseGeneralEntity(gen, equipment) + case Some(mech: ImplantTerminalMech) => + ops.handleUseGeneralEntity(mech, equipment) + case Some(captureTerminal: CaptureTerminal) => + ops.handleUseCaptureTerminal(captureTerminal, equipment) + case Some(obj: FacilityTurret) => + ops.handleUseFacilityTurret(obj, equipment, pkt) + case Some(obj: Vehicle) => + ops.handleUseVehicle(obj, equipment, pkt) + case Some(terminal: Terminal) => + ops.handleUseTerminal(terminal, equipment, pkt) + case Some(obj: SpawnTube) => + ops.handleUseSpawnTube(obj, equipment) + case Some(obj: SensorDeployable) => + ops.handleUseGeneralEntity(obj, equipment) + case Some(obj: TurretDeployable) => + ops.handleUseGeneralEntity(obj, equipment) + case Some(obj: TrapDeployable) => + ops.handleUseGeneralEntity(obj, equipment) + case Some(obj: ShieldGeneratorDeployable) => + 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) => + ops.handleUseTelepadDeployable(obj, equipment, pkt, ops.useRouterTelepadSystem) + case Some(obj: Utility.InternalTelepad) => + ops.handleUseInternalTelepad(obj, pkt, ops.useRouterTelepadSystem) + case Some(obj: CaptureFlag) => + ops.handleUseCaptureFlag(obj) + case Some(_: WarpGate) => + ops.handleUseWarpGate(equipment) + case Some(obj) => + ops.handleUseDefaultEntity(obj, equipment) + case None => () + } + } + + def handleUnuseItem(pkt: UnuseItemMessage): Unit = { + val UnuseItemMessage(_, objectGuid) = pkt + sessionLogic.validObject(objectGuid, decorator = "UnuseItem") match { + case Some(obj: Player) => + ops.unaccessContainer(obj) + sessionLogic.zoning.spawn.TryDisposeOfLootedCorpse(obj) + case Some(obj: Container) => + // Make sure we don't unload the contents of the vehicle the player is seated in + // An example scenario of this would be closing the trunk contents when rearming at a landing pad + if (player.VehicleSeated.isEmpty || player.VehicleSeated.get != obj.GUID) { + ops.unaccessContainer(obj) + } + case _ => () + } + } + + def handleDeployObject(pkt: DeployObjectMessage): Unit = { + if (!player.spectator) { + import scala.concurrent.ExecutionContext.Implicits.global + val DeployObjectMessage(guid, _, pos, orient, _) = pkt + player.Holsters().find(slot => slot.Equipment.nonEmpty && slot.Equipment.get.GUID == guid).flatMap { slot => slot.Equipment } match { + case Some(obj: ConstructionItem) => + sessionLogic.zoning.CancelZoningProcess() + val result = ops.handleDeployObject(continent, obj.AmmoType, pos, orient, player.WhichSide, PlanetSideEmpire.NEUTRAL) + result.onComplete { + case Success(obj) => sendResponse(ChatMsg(ChatMessageType.UNK_227, s"${obj.GUID.guid}")) + case _ => () + } + case Some(obj) => + log.warn(s"DeployObject: what is $obj, ${player.Name}? It's not a construction tool!") + case None => + log.error(s"DeployObject: nothing, ${player.Name}? It's not a construction tool!") + } + } + } + + def handlePlanetsideAttribute(pkt: PlanetsideAttributeMessage): Unit = { + val PlanetsideAttributeMessage(objectGuid, attributeType, attributeValue) = pkt + sessionLogic.validObject(objectGuid, decorator = "PlanetsideAttribute") match { + case Some(vehicle: Vehicle) => + vehicle.Actor ! ServerObject.AttributeMsg(attributeType, attributeValue) + // Cosmetics options + case Some(_: Player) if attributeType == 106 => + avatarActor ! AvatarActor.SetCosmetics(Cosmetic.valuesFromAttributeValue(attributeValue)) + case Some(obj) => + log.trace(s"PlanetsideAttribute: ${player.Name} does not know how to apply unknown attributes behavior $attributeType to ${obj.Definition.Name}") + case _ => () + } + } + + def handleGenericObjectAction(pkt: GenericObjectActionMessage): Unit = { + val GenericObjectActionMessage(objectGuid, code) = pkt + sessionLogic.validObject(objectGuid, decorator = "GenericObjectAction") match { + case Some(vehicle: Vehicle) + if vehicle.OwnerName.contains(player.Name) => + vehicle.Actor ! ServerObject.GenericObjectAction(objectGuid, code, Some(player.GUID)) + + case Some(tool: Tool) => + if (code == 35 && + (tool.Definition == GlobalDefinitions.maelstrom || tool.Definition.Name.startsWith("aphelion_laser")) + ) { + //maelstrom primary fire mode discharge (no target) + //aphelion_laser discharge (no target) + sessionLogic.shooting.handleWeaponFireAccountability(objectGuid, PlanetSideGUID(Projectile.baseUID)) + } else { + sessionLogic.validObject(player.VehicleSeated, decorator = "GenericObjectAction/Vehicle") collect { + case vehicle: Vehicle + if vehicle.OwnerName.contains(player.Name) => + vehicle.Actor ! ServerObject.GenericObjectAction(objectGuid, code, Some(tool)) + } + } + case _ => + log.info(s"${player.Name} - $pkt") + } + } + + def handleGenericObjectActionAtPosition(pkt: GenericObjectActionAtPositionMessage): Unit = { + val GenericObjectActionAtPositionMessage(objectGuid, _, _) = pkt + sessionLogic.validObject(objectGuid, decorator = "GenericObjectActionAtPosition") match { + case Some(tool: Tool) if GlobalDefinitions.isBattleFrameNTUSiphon(tool.Definition) => + sessionLogic.shooting.FindContainedWeapon match { + case (Some(vehicle: Vehicle), weps) if weps.exists(_.GUID == objectGuid) => + vehicle.Actor ! SpecialEmp.Burst() + case _ => () + } + case _ => + log.info(s"${player.Name} - $pkt") + } + } + + def handleGenericObjectState(pkt: GenericObjectStateMsg): Unit = { + val GenericObjectStateMsg(_, _) = pkt + log.info(s"${player.Name} - $pkt") + } + + def handleGenericAction(pkt: GenericActionMessage): Unit = { + val GenericActionMessage(action) = pkt + if (player != null) { + val (toolOpt, definition) = player.Slot(0).Equipment match { + case Some(tool: Tool) => + (Some(tool), tool.Definition) + case _ => + (None, GlobalDefinitions.bullet_9mm) + } + action match { + case GenericAction.DropSpecialItem => + ops.dropSpecialSlotItem() + case GenericAction.MaxAnchorsExtend_RCV => + player.UsingSpecial = SpecialExoSuitDefinition.Mode.Anchored + continent.AvatarEvents ! AvatarServiceMessage( + continent.id, + AvatarAction.PlanetsideAttribute(player.GUID, 19, 1) + ) + definition match { + case GlobalDefinitions.trhev_dualcycler | GlobalDefinitions.trhev_burster => + val tool = toolOpt.get + tool.ToFireMode = 1 + sendResponse(ChangeFireModeMessage(tool.GUID, 1)) + case GlobalDefinitions.trhev_pounder => + val tool = toolOpt.get + val convertFireModeIndex = if (tool.FireModeIndex == 0) { 1 } + else { 4 } + tool.ToFireMode = convertFireModeIndex + sendResponse(ChangeFireModeMessage(tool.GUID, convertFireModeIndex)) + case _ => + log.warn(s"GenericObject: ${player.Name} is a MAX with an unexpected attachment - ${definition.Name}") + } + case GenericAction.MaxAnchorsRelease_RCV => + player.UsingSpecial = SpecialExoSuitDefinition.Mode.Normal + continent.AvatarEvents ! AvatarServiceMessage( + continent.id, + AvatarAction.PlanetsideAttribute(player.GUID, 19, 0) + ) + definition match { + case GlobalDefinitions.trhev_dualcycler | GlobalDefinitions.trhev_burster => + val tool = toolOpt.get + tool.ToFireMode = 0 + sendResponse(ChangeFireModeMessage(tool.GUID, 0)) + case GlobalDefinitions.trhev_pounder => + val tool = toolOpt.get + val convertFireModeIndex = if (tool.FireModeIndex == 1) { 0 } else { 3 } + tool.ToFireMode = convertFireModeIndex + sendResponse(ChangeFireModeMessage(tool.GUID, convertFireModeIndex)) + case _ => + log.warn(s"GenericObject: $player is MAX with an unexpected attachment - ${definition.Name}") + } + case GenericAction.MaxSpecialEffect_RCV => + if (player.ExoSuit == ExoSuitType.MAX) { + ops.toggleMaxSpecialState(enable = true) + } else { + log.warn(s"GenericActionMessage: ${player.Name} can't handle MAX special effect") + } + case GenericAction.StopMaxSpecialEffect_RCV => + if (player.ExoSuit == ExoSuitType.MAX) { + player.Faction match { + case PlanetSideEmpire.NC => + ops.toggleMaxSpecialState(enable = false) + case _ => + log.warn(s"GenericActionMessage: ${player.Name} tried to cancel an uncancellable MAX special ability") + } + } else { + log.warn(s"GenericActionMessage: ${player.Name} can't stop MAX special effect") + } + case GenericAction.AwayFromKeyboard_RCV => + AvatarActor.savePlayerLocation(player) + player.AwayFromKeyboard = true + case GenericAction.BackInGame_RCV => + player.AwayFromKeyboard = false + case GenericAction.LookingForSquad_RCV => //Looking For Squad ON + if (!avatar.lookingForSquad) { + avatarActor ! AvatarActor.SetLookingForSquad(false) + } + case GenericAction.NotLookingForSquad_RCV => //Looking For Squad OFF + if (avatar.lookingForSquad) { + avatarActor ! AvatarActor.SetLookingForSquad(false) + } + case _ => + log.warn(s"GenericActionMessage: ${player.Name} can't handle $action") + } + } + } + + + + def handleGenericCollision(pkt: GenericCollisionMsg): Unit = { + player.BailProtection = false + 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 + } + } + (ctype, sessionLogic.validObject(p, decorator = "GenericCollision/Primary")) match { + case (CollisionIs.OfInfantry, Some(user: Player)) + if user == player => () + case (CollisionIs.OfGroundVehicle, Some(v: Vehicle)) + if v.Seats(0).occupant.contains(player) => + v.BailProtection = false + 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") + case _ => () + } + } + + def handleAvatarFirstTimeEvent(pkt: AvatarFirstTimeEventMessage): Unit = { + val AvatarFirstTimeEventMessage(_, _, _, eventName) = pkt + avatarActor ! AvatarActor.AddFirstTimeEvent(eventName) + } + + def handleBugReport(pkt: PlanetSideGamePacket): Unit = { + val BugReportMessage( + _/*versionMajor*/, + _/*versionMinor*/, + _/*versionDate*/, + _/*bugType*/, + _/*repeatable*/, + _/*location*/, + _/*zone*/, + _/*pos*/, + _/*summary*/, + _/*desc*/ + ) = pkt + log.warn(s"${player.Name} filed a bug report - it might be something important") + log.debug(s"$pkt") + } + + def handleFacilityBenefitShieldChargeRequest(pkt: FacilityBenefitShieldChargeRequestMessage): Unit = { + val FacilityBenefitShieldChargeRequestMessage(_) = pkt + val vehicleGuid = player.VehicleSeated + continent + .GUID(vehicleGuid) + .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 => ops.commonFacilityShieldCharging(obj) + case obj: TurretDeployable => ops.commonFacilityShieldCharging(obj) + case _ if vehicleGuid.nonEmpty => () + case _ => () + } + } + + def handleBattleplan(pkt: BattleplanMessage): Unit = { + /* can not draw battleplan */ + //todo csr exclusive battleplan channel + } + + def handleBindPlayer(pkt: BindPlayerMessage): Unit = { + val BindPlayerMessage(_, _, _, _, _, _, _, _) = pkt + } + + def handleCreateShortcut(pkt: CreateShortcutMessage): Unit = { + val CreateShortcutMessage(_, slot, shortcutOpt) = pkt + shortcutOpt match { + case Some(shortcut) => + avatarActor ! AvatarActor.AddShortcut(slot - 1, shortcut) + case None => + avatarActor ! AvatarActor.RemoveShortcut(slot - 1) + } + } + + def handleChangeShortcutBank(pkt: ChangeShortcutBankMessage): Unit = { + val ChangeShortcutBankMessage(_, _) = pkt + } + + def handleFriendRequest(pkt: FriendsRequest): Unit = { + val FriendsRequest(action, name) = pkt + 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 handleActionCancel(pkt: ActionCancelMessage): Unit = { + val ActionCancelMessage(_, _, _) = pkt + ops.progressBarUpdate.cancel() + ops.progressBarValue = None + } + + def handleTrade(pkt: TradeMessage): Unit = { + val TradeMessage(trade) = pkt + log.trace(s"${player.Name} wants to trade for some reason - $trade") + } + + def handleDisplayedAward(pkt: DisplayedAwardMessage): Unit = { /* intentionally blank */ } + + def handleObjectDetected(pkt: ObjectDetectedMessage): Unit = { + ops.handleObjectDetected(pkt) + } + + def handleTargetingImplantRequest(pkt: TargetingImplantRequest): Unit = { + ops.handleTargetingImplantRequest(pkt) + } + + def handleHitHint(pkt: HitHint): Unit = { + val HitHint(_, _) = pkt + } + + /* messages */ + + def handleRenewCharSavedTimer(): Unit = { /* */ } + + def handleRenewCharSavedTimerMsg(): Unit = { /* */ } + + def handleSetAvatar(avatar: Avatar): Unit = { + session = session.copy(avatar = avatar) + if (session.player != null) { + session.player.avatar = avatar + } + LivePlayerList.Update(avatar.id, avatar) + } + + def handleReceiveAccountData(account: Account): Unit = { /* no need */ } + + def handleUseCooldownRenew: BasicDefinition => Unit = { + case _: KitDefinition => ops.kitToBeUsed = None + case _ => () + } + + def handleAvatarResponse(avatar: Avatar): Unit = { /* no need */ } + + def handleSetSpeed(speed: Float): Unit = { + session = session.copy(speed = speed) + } + + def handleSetFlying(flying: Boolean): Unit = { + session = session.copy(flying = flying) + } + + def handleSetSpectator(spectator: Boolean): Unit = { + session.player.spectator = spectator + } + + def handleKick(player: Player, time: Option[Long]): Unit = { + ops.administrativeKick(player, time) + } + + def handleSilenced(isSilenced: Boolean): Unit = { /* can not be silenced */ } + + def handleItemPutInSlot(msg: Containable.ItemPutInSlot): Unit = { + log.debug(s"ItemPutInSlot: $msg") + } + + def handleCanNotPutItemInSlot(msg: Containable.CanNotPutItemInSlot): Unit = { + log.debug(s"CanNotPutItemInSlot: $msg") + } + + def handleReceiveDefaultMessage(default: Any, sender: ActorRef): Unit = { + log.warn(s"Invalid packet class received: $default from $sender") + } + + /* supporting functions */ + + def handleUseDoor(door: Door, equipment: Option[Equipment]): Unit = { + if (!player.spectator) { + //opens for everyone + equipment match { + case Some(tool: Tool) if tool.Definition == GlobalDefinitions.medicalapplicator => + door.Actor ! CommonMessages.Use(player, Some(Float.MaxValue)) + case _ => + door.Actor ! CommonMessages.Use(player) + } + } + } + + private def maxCapacitorTick(jumpThrust: Boolean): Unit = { + if (player.ExoSuit == ExoSuitType.MAX) { + val activate = (jumpThrust || player.isOverdrived || player.isShielded) && player.Capacitor > 0 + player.CapacitorState match { + case CapacitorStateType.Discharging => maxCapacitorTickDischarging(activate) + case CapacitorStateType.Charging => maxCapacitorTickCharging(activate) + case _ => maxCapacitorTickIdle(activate) + } + } 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) { + player.CapacitorState = CapacitorStateType.Charging + } + } + + 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 == 0) { + if (player.Faction == PlanetSideEmpire.TR) { + ops.toggleMaxSpecialState(enable = false) + } + player.Capacitor = player.ExoSuitDef.MaxCapacitor.toFloat + sendResponse(PlanetsideAttributeMessage(player.GUID, 7, player.Capacitor.toInt)) + player.CapacitorState = CapacitorStateType.Idle + } else if (player.Capacitor < player.ExoSuitDef.MaxCapacitor) { + player.CapacitorState = CapacitorStateType.Charging + } else { + player.CapacitorState = CapacitorStateType.Idle + } + } + + private def maxCapacitorTickCharging(activate: Boolean): Unit = { + val maxCapacitor = player.ExoSuitDef.MaxCapacitor + if (activate) { + player.CapacitorState = CapacitorStateType.Discharging + //maxCapacitorTickDischarging(activate) + } else if (player.Capacitor < player.ExoSuitDef.MaxCapacitor) { + player.Capacitor = maxCapacitor.toFloat + sendResponse(PlanetsideAttributeMessage(player.GUID, 7, maxCapacitor)) + } else { + player.CapacitorState = CapacitorStateType.Idle + } + } +} diff --git a/src/main/scala/net/psforever/actors/session/csr/MountHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/csr/MountHandlerLogic.scala new file mode 100644 index 000000000..5ca48d683 --- /dev/null +++ b/src/main/scala/net/psforever/actors/session/csr/MountHandlerLogic.scala @@ -0,0 +1,330 @@ +// Copyright (c) 2024 PSForever +package net.psforever.actors.session.csr + +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.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 +import net.psforever.objects.vital.InGameHistory +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} + +object MountHandlerLogic { + def apply(ops: SessionMountHandlers): MountHandlerLogic = { + new MountHandlerLogic(ops, ops.context) + } +} + +class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: ActorContext) extends MountHandlerFunctions { + def sessionLogic: SessionData = ops.sessionLogic + + /* packets */ + + def handleMountVehicle(pkt: MountVehicleMsg): Unit = { + //can only mount vehicle when not in csr spectator mode + if (!player.spectator) { + ops.handleMountVehicle(pkt) + } + } + + def handleDismountVehicle(pkt: DismountVehicleMsg): Unit = { + //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 = { + //can't do this if we're not in vehicle, so also not csr spectator + ops.handleMountVehicleCargo(pkt) + } + + def handleDismountVehicleCargo(pkt: DismountVehicleCargoMsg): Unit = { + //can't do this if we're not in vehicle, so also not csr spectator + ops.handleDismountVehicleCargo(pkt.copy(bailed = true)) + } + + /* response handlers */ + + /** + * na + * + * @param tplayer na + * @param reply na + */ + def handle(tplayer: Player, reply: Mountable.Exchange): Unit = { + reply match { + case Mountable.CanMount(obj: ImplantTerminalMech, seatNumber, _) => + sessionLogic.zoning.CancelZoningProcess() + sessionLogic.terminals.CancelAllProximityUnits() + ops.MountingAction(tplayer, obj, seatNumber) + sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistenceFunc + + case Mountable.CanMount(obj: Vehicle, seatNumber, _) + if obj.Definition == GlobalDefinitions.orbital_shuttle => + sessionLogic.zoning.CancelZoningProcess() + sessionLogic.terminals.CancelAllProximityUnits() + ops.MountingAction(tplayer, obj, seatNumber) + sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistenceFunc + + case Mountable.CanMount(obj: Vehicle, seatNumber, _) + if obj.Definition == GlobalDefinitions.ant => + sessionLogic.zoning.CancelZoningProcess() + val obj_guid: PlanetSideGUID = obj.GUID + sessionLogic.terminals.CancelAllProximityUnits() + sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health)) + sendResponse(PlanetsideAttributeMessage(obj_guid, obj.Definition.shieldUiAttribute, obj.Shields)) + sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=45, obj.NtuCapacitorScaled)) + sendResponse(GenericObjectActionMessage(obj_guid, code=11)) + sessionLogic.general.accessContainer(obj) + tplayer.Actor ! ResetAllEnvironmentInteractions + ops.MountingAction(tplayer, obj, seatNumber) + + case Mountable.CanMount(obj: Vehicle, seatNumber, _) + if obj.Definition == GlobalDefinitions.quadstealth => + sessionLogic.zoning.CancelZoningProcess() + val obj_guid: PlanetSideGUID = obj.GUID + sessionLogic.terminals.CancelAllProximityUnits() + sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health)) + sendResponse(PlanetsideAttributeMessage(obj_guid, obj.Definition.shieldUiAttribute, obj.Shields)) + //exclusive to the wraith, cloak state matches the cloak state of the driver + //phantasm doesn't uncloak if the driver is uncloaked and no other vehicle cloaks + obj.Cloaked = tplayer.Cloaked + sendResponse(GenericObjectActionMessage(obj_guid, code=11)) + sessionLogic.general.accessContainer(obj) + tplayer.Actor ! ResetAllEnvironmentInteractions + ops.MountingAction(tplayer, obj, seatNumber) + + case Mountable.CanMount(obj: Vehicle, seatNumber, _) + if seatNumber == 0 && obj.Definition.MaxCapacitor > 0 => + sessionLogic.zoning.CancelZoningProcess() + val obj_guid: PlanetSideGUID = obj.GUID + sessionLogic.terminals.CancelAllProximityUnits() + sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health)) + sendResponse(PlanetsideAttributeMessage(obj_guid, obj.Definition.shieldUiAttribute, obj.Shields)) + sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=113, obj.Capacitor)) + sendResponse(GenericObjectActionMessage(obj_guid, code=11)) + sessionLogic.general.accessContainer(obj) + ops.updateWeaponAtSeatPosition(obj, seatNumber) + tplayer.Actor ! ResetAllEnvironmentInteractions + ops.MountingAction(tplayer, obj, seatNumber) + + case Mountable.CanMount(obj: Vehicle, seatNumber, _) + if seatNumber == 0 => + sessionLogic.zoning.CancelZoningProcess() + val obj_guid: PlanetSideGUID = obj.GUID + sessionLogic.terminals.CancelAllProximityUnits() + sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health)) + sendResponse(PlanetsideAttributeMessage(obj_guid, obj.Definition.shieldUiAttribute, obj.Shields)) + sendResponse(GenericObjectActionMessage(obj_guid, code=11)) + sessionLogic.general.accessContainer(obj) + ops.updateWeaponAtSeatPosition(obj, seatNumber) + tplayer.Actor ! ResetAllEnvironmentInteractions + ops.MountingAction(tplayer, obj, seatNumber) + + case Mountable.CanMount(obj: Vehicle, seatNumber, _) + if obj.Definition.MaxCapacitor > 0 => + sessionLogic.zoning.CancelZoningProcess() + val obj_guid: PlanetSideGUID = obj.GUID + sessionLogic.terminals.CancelAllProximityUnits() + sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health)) + sendResponse(PlanetsideAttributeMessage(obj_guid, obj.Definition.shieldUiAttribute, obj.Shields)) + sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=113, obj.Capacitor)) + sessionLogic.general.accessContainer(obj) + ops.updateWeaponAtSeatPosition(obj, seatNumber) + sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistenceFunc + tplayer.Actor ! ResetAllEnvironmentInteractions + ops.MountingAction(tplayer, obj, seatNumber) + + case Mountable.CanMount(obj: Vehicle, seatNumber, _) => + sessionLogic.zoning.CancelZoningProcess() + val obj_guid: PlanetSideGUID = obj.GUID + sessionLogic.terminals.CancelAllProximityUnits() + sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health)) + sendResponse(PlanetsideAttributeMessage(obj_guid, obj.Definition.shieldUiAttribute, obj.Shields)) + sessionLogic.general.accessContainer(obj) + ops.updateWeaponAtSeatPosition(obj, seatNumber) + sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistenceFunc + tplayer.Actor ! ResetAllEnvironmentInteractions + ops.MountingAction(tplayer, obj, seatNumber) + + case Mountable.CanMount(obj: FacilityTurret, seatNumber, _) + if obj.Definition == GlobalDefinitions.vanu_sentry_turret => + 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) + ops.MountingAction(tplayer, obj, seatNumber) + + case Mountable.CanMount(obj: FacilityTurret, seatNumber, _) + if !obj.isUpgrading || System.currentTimeMillis() - GenericHackables.getTurretUpgradeTime >= 1500L => + obj.setMiddleOfUpgrade(false) + sessionLogic.zoning.CancelZoningProcess() + sendResponse(PlanetsideAttributeMessage(obj.GUID, attribute_type=0, obj.Health)) + ops.updateWeaponAtSeatPosition(obj, seatNumber) + ops.MountingAction(tplayer, obj, seatNumber) + + case Mountable.CanMount(obj: FacilityTurret, _, _) => + 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.CancelZoningProcess() + sendResponse(PlanetsideAttributeMessage(obj.GUID, attribute_type=0, obj.Health)) + ops.updateWeaponAtSeatPosition(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, _) => + 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 + val sguid = obj.GUID + val (pos, zang) = Vehicles.dismountShuttle(obj, mountPoint) + tplayer.Position = pos + sendResponse(DelayedPathMountMsg(pguid, sguid, u1=60, u2=true)) + continent.LocalEvents ! LocalServiceMessage( + continent.id, + LocalAction.SendResponse(ObjectDetachMessage(sguid, pguid, pos, roll=0, pitch=0, zang)) + ) + sessionLogic.keepAliveFunc = sessionLogic.zoning.NormalKeepAlive + + case Mountable.CanDismount(obj: Vehicle, seatNum, _) + if obj.Definition == GlobalDefinitions.orbital_shuttle => + //get ready for orbital drop + val pguid = player.GUID + val events = continent.VehicleEvents + 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( + player.Name, + VehicleAction.SendResponse(Service.defaultPlayerGUID, PlayerStasisMessage(pguid)) //the stasis message + ) + //when the player dismounts, they will be positioned where the shuttle was when it disappeared in the sky + //the player will fall to the ground and is perfectly vulnerable in this state + //additionally, our player must exist in the current zone + //having no in-game avatar target will throw us out of the map screen when deploying and cause softlock + events ! VehicleServiceMessage( + player.Name, + VehicleAction.SendResponse( + Service.defaultPlayerGUID, + PlayerStateShiftMessage(ShiftState(unk=0, obj.Position, obj.Orientation.z, vel=None)) //cower in the shuttle bay + ) + ) + events ! VehicleServiceMessage( + continent.id, + VehicleAction.SendResponse(pguid, GenericObjectActionMessage(pguid, code=9)) //conceal the player + ) + sessionLogic.keepAliveFunc = sessionLogic.zoning.NormalKeepAlive + + case Mountable.CanDismount(obj: Vehicle, seatNum, _) + if obj.Definition == GlobalDefinitions.droppod => + sessionLogic.general.unaccessContainer(obj) + 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 => + ops.DismountVehicleAction(tplayer, obj, seatNum) + + case Mountable.CanDismount(obj: Vehicle, seat_num, _) => + continent.VehicleEvents ! VehicleServiceMessage( + continent.id, + VehicleAction.KickPassenger(tplayer.GUID, seat_num, unk2=true, obj.GUID) + ) + + case Mountable.CanDismount(obj: PlanetSideGameObject with PlanetSideGameObject with Mountable with FactionAffinity with InGameHistory, seatNum, _) => + 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}") + + case Mountable.CanNotMount(obj: Vehicle, seatNumber) => + log.warn(s"MountVehicleMsg: ${tplayer.Name} attempted to mount $obj's seat $seatNumber, but was not allowed") + obj.GetSeatFromMountPoint(seatNumber).collect { + case seatNum if obj.SeatPermissionGroup(seatNum).contains(AccessPermissionGroup.Driver) => + sendResponse( + ChatMsg(ChatMessageType.CMT_OPEN, wideContents=false, recipient="", "You are not the driver of this vehicle.", note=None) + ) + } + + case Mountable.CanNotMount(obj: Mountable, seatNumber) => + log.warn(s"MountVehicleMsg: ${tplayer.Name} attempted to mount $obj's seat $seatNumber, but was not allowed") + + case Mountable.CanNotDismount(obj: 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 */ +} diff --git a/src/main/scala/net/psforever/actors/session/csr/SpectateAsCustomerServiceRepresentativeMode.scala b/src/main/scala/net/psforever/actors/session/csr/SpectateAsCustomerServiceRepresentativeMode.scala new file mode 100644 index 000000000..592fbedd5 --- /dev/null +++ b/src/main/scala/net/psforever/actors/session/csr/SpectateAsCustomerServiceRepresentativeMode.scala @@ -0,0 +1,92 @@ +// Copyright (c) 2024 PSForever +package net.psforever.actors.session.csr + +import net.psforever.actors.session.support.{AvatarHandlerFunctions, ChatFunctions, GalaxyHandlerFunctions, GeneralFunctions, LocalHandlerFunctions, MountHandlerFunctions, SquadHandlerFunctions, TerminalHandlerFunctions, VehicleFunctions, VehicleHandlerFunctions, WeaponAndProjectileFunctions} +import net.psforever.objects.serverobject.ServerObject +import net.psforever.objects.serverobject.mount.Mountable +import net.psforever.objects.{Player, Session, Vehicle} +import net.psforever.objects.zones.Zone +import net.psforever.packet.PlanetSidePacket +import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} +import net.psforever.services.chat.SpectatorChannel +import net.psforever.services.teamwork.{SquadAction, SquadServiceMessage} +import net.psforever.types.{ChatMessageType, SquadRequestType} +// +import net.psforever.actors.session.support.{ModeLogic, PlayerMode, SessionData} +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 = net.psforever.actors.session.normal.GalaxyHandlerLogic(data.galaxyResponseHandlers) + val general: GeneralFunctions = GeneralLogic(data.general) + 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 = net.psforever.actors.session.normal.VehicleHandlerLogic(data.vehicleResponseOperations) + + override def switchTo(session: Session): Unit = { + val player = session.player + val continent = session.zone + val pguid = player.GUID + val sendResponse: PlanetSidePacket => Unit = data.sendResponse + // + data.squadService ! SquadServiceMessage( + player, + continent, + SquadAction.Membership(SquadRequestType.Leave, player.CharId, Some(player.CharId), player.Name, None) + ) + if (requireDismount(data, continent, player)) { + CustomerServiceRepresentativeMode.renderPlayer(data, continent, player) + } + // + player.spectator = true + data.chat.JoinChannel(SpectatorChannel) + continent.AvatarEvents ! AvatarServiceMessage(continent.id, AvatarAction.ObjectDelete(pguid, pguid)) + sendResponse(ChatMsg(ChatMessageType.CMT_TOGGLESPECTATORMODE, "on")) + 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 + // + player.spectator = false + data.chat.LeaveChannel(SpectatorChannel) + continent.AvatarEvents ! AvatarServiceMessage( + continent.id, + AvatarAction.LoadPlayer(pguid, avatarId, pguid, player.Definition.Packet.ConstructorData(player).get, None) + ) + sendResponse(ChatMsg(ChatMessageType.CMT_TOGGLESPECTATORMODE, "off")) + } + + private def requireDismount(data: SessionData, zone: Zone, player: Player): Boolean = { + data.vehicles.GetMountableAndSeat(None, player, zone) match { + case (Some(obj: Vehicle), Some(seatNum)) if seatNum == 0 => + data.vehicles.ServerVehicleOverrideStop(obj) + obj.Actor ! ServerObject.AttributeMsg(10, 3) //faction-accessible driver seat + obj.Actor ! Mountable.TryDismount(player, seatNum) + player.VehicleSeated = None + true + case (Some(obj), Some(seatNum)) => + obj.Actor ! Mountable.TryDismount(player, seatNum) + player.VehicleSeated = None + true + case _ => + player.VehicleSeated = None + false + } + } +} + +case object SpectateAsCustomerServiceRepresentativeMode extends PlayerMode { + def setup(data: SessionData): ModeLogic = { + new SpectatorCSRModeLogic(data) + } +} diff --git a/src/main/scala/net/psforever/actors/session/csr/SquadHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/csr/SquadHandlerLogic.scala new file mode 100644 index 000000000..f6be56232 --- /dev/null +++ b/src/main/scala/net/psforever/actors/session/csr/SquadHandlerLogic.scala @@ -0,0 +1,362 @@ +// Copyright (c) 2024 PSForever +package net.psforever.actors.session.csr + +import akka.actor.{ActorContext, ActorRef, typed} +import net.psforever.actors.session.support.SessionSquadHandlers.SquadUIElement +import net.psforever.actors.session.AvatarActor +import net.psforever.actors.session.support.{SessionData, SessionSquadHandlers, SquadHandlerFunctions} +import net.psforever.objects.{Default, LivePlayerList} +import net.psforever.objects.avatar.Avatar +import net.psforever.packet.game.{CharacterKnowledgeInfo, CharacterKnowledgeMessage, ChatMsg, MemberEvent, PlanetsideAttributeMessage, ReplicationStreamMessage, SquadAction, SquadDefinitionActionMessage, SquadDetailDefinitionUpdateMessage, SquadListing, SquadMemberEvent, SquadMembershipRequest, SquadMembershipResponse, SquadState, SquadStateInfo, SquadWaypointEvent, SquadWaypointRequest, WaypointEvent, WaypointEventAction} +import net.psforever.services.chat.SquadChannel +import net.psforever.services.teamwork.{SquadResponse, SquadServiceMessage, SquadAction => SquadServiceAction} +import net.psforever.types.{ChatMessageType, PlanetSideGUID, SquadListDecoration, SquadResponseType, WaypointSubtype} + +object SquadHandlerLogic { + def apply(ops: SessionSquadHandlers): SquadHandlerLogic = { + new SquadHandlerLogic(ops, ops.context) + } +} + +class SquadHandlerLogic(val ops: SessionSquadHandlers, implicit val context: ActorContext) extends SquadHandlerFunctions { + def sessionLogic: SessionData = ops.sessionLogic + + private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor + + private val squadService: ActorRef = ops.squadService + + /* packet */ + + def handleSquadDefinitionAction(pkt: SquadDefinitionActionMessage): Unit = { + if (!player.spectator) { + val SquadDefinitionActionMessage(u1, u2, action) = pkt + squadService ! SquadServiceMessage(player, continent, SquadServiceAction.Definition(u1, u2, action)) + } + } + + def handleSquadMemberRequest(pkt: SquadMembershipRequest): Unit = { + 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 = { + 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)) + } + } + } + + /* response handlers */ + + def handle(response: SquadResponse.Response, excluded: Iterable[Long]): Unit = { + if (!excluded.exists(_ == avatar.id)) { + response match { + case SquadResponse.ListSquadFavorite(line, task) => + sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), line, SquadAction.ListSquadFavorite(task))) + + case SquadResponse.InitList(infos) => + sendResponse(ReplicationStreamMessage(infos)) + + case SquadResponse.UpdateList(infos) if infos.nonEmpty => + sendResponse( + ReplicationStreamMessage( + 6, + None, + infos.map { + case (index, squadInfo) => + SquadListing(index, squadInfo) + }.toVector + ) + ) + + case SquadResponse.RemoveFromList(infos) if infos.nonEmpty => + sendResponse( + ReplicationStreamMessage( + 1, + None, + infos.map { index => + SquadListing(index, None) + }.toVector + ) + ) + + case SquadResponse.SquadDecoration(guid, squad) => + val decoration = if ( + ops.squadUI.nonEmpty || + squad.Size == squad.Capacity || + { + val offer = avatar.certifications + !squad.Membership.exists { _.isAvailable(offer) } + } + ) { + SquadListDecoration.NotAvailable + } else { + SquadListDecoration.Available + } + sendResponse(SquadDefinitionActionMessage(guid, 0, SquadAction.SquadListDecorator(decoration))) + + case SquadResponse.Detail(guid, detail) => + sendResponse(SquadDetailDefinitionUpdateMessage(guid, detail)) + + case SquadResponse.IdentifyAsSquadLeader(squad_guid) => + sendResponse(SquadDefinitionActionMessage(squad_guid, 0, SquadAction.IdentifyAsSquadLeader())) + + case SquadResponse.SetListSquad(squad_guid) => + sendResponse(SquadDefinitionActionMessage(squad_guid, 0, SquadAction.SetListSquad())) + + case SquadResponse.Membership(request_type, unk1, unk2, charId, opt_char_id, player_name, unk5, unk6) => + val name = request_type match { + case SquadResponseType.Invite if unk5 => + //the name of the player indicated by unk3 is needed + LivePlayerList.WorldPopulation({ case (_, a: Avatar) => charId == a.id }).headOption match { + case Some(player) => + player.name + case None => + player_name + } + case _ => + player_name + } + sendResponse(SquadMembershipResponse(request_type, unk1, unk2, charId, opt_char_id, name, unk5, unk6)) + + case SquadResponse.WantsSquadPosition(_, name) => + sendResponse( + ChatMsg( + ChatMessageType.CMT_SQUAD, + wideContents=true, + name, + s"\\#6 would like to join your squad. (respond with \\#3/accept\\#6 or \\#3/reject\\#6)", + None + ) + ) + + case SquadResponse.Join(squad, positionsToUpdate, _, ref) => + val avatarId = avatar.id + val membershipPositions = (positionsToUpdate map squad.Membership.zipWithIndex) + .filter { case (mem, index) => + mem.CharId > 0 && positionsToUpdate.contains(index) + } + membershipPositions.find { case (mem, _) => mem.CharId == avatarId } match { + case Some((ourMember, ourIndex)) => + //we are joining the squad + //load each member's entry (our own too) + ops.squad_supplement_id = squad.GUID.guid + 1 + membershipPositions.foreach { + case (member, index) => + sendResponse( + SquadMemberEvent.Add( + ops.squad_supplement_id, + member.CharId, + index, + member.Name, + member.ZoneId, + outfit_id = 0 + ) + ) + ops.squadUI(member.CharId) = + SquadUIElement(member.Name, outfit=0L, index, member.ZoneId, member.Health, member.Armor, member.Position) + } + //repeat our entry + sendResponse( + SquadMemberEvent.Add( + ops.squad_supplement_id, + ourMember.CharId, + ourIndex, + ourMember.Name, + ourMember.ZoneId, + outfit_id = 0 + ) + ) + //turn lfs off + if (avatar.lookingForSquad) { + avatarActor ! AvatarActor.SetLookingForSquad(false) + } + val playerGuid = player.GUID + val factionChannel = s"${player.Faction}" + //squad colors + ops.GiveSquadColorsToMembers() + ops.GiveSquadColorsForOthers(playerGuid, factionChannel, ops.squad_supplement_id) + //associate with member position in squad + sendResponse(PlanetsideAttributeMessage(playerGuid, 32, ourIndex)) + //a finalization? what does this do? + sendResponse(SquadDefinitionActionMessage(squad.GUID, 0, SquadAction.Unknown(18))) + squadService ! SquadServiceMessage(player, continent, SquadServiceAction.ReloadDecoration()) + ops.updateSquadRef = ref + ops.updateSquad = ops.PeriodicUpdatesWhenEnrolledInSquad + sessionLogic.chat.JoinChannel(SquadChannel(squad.GUID)) + case _ => + //other player is joining our squad + //load each member's entry + ops.GiveSquadColorsToMembers( + membershipPositions.map { + case (member, index) => + val charId = member.CharId + sendResponse( + SquadMemberEvent.Add(ops.squad_supplement_id, charId, index, member.Name, member.ZoneId, outfit_id = 0) + ) + ops.squadUI(charId) = + SquadUIElement(member.Name, outfit=0L, index, member.ZoneId, member.Health, member.Armor, member.Position) + charId + } + ) + } + //send an initial dummy update for map icon(s) + sendResponse( + SquadState( + PlanetSideGUID(ops.squad_supplement_id), + membershipPositions.map { case (member, _) => + SquadStateInfo(member.CharId, member.Health, member.Armor, member.Position) + } + ) + ) + + case SquadResponse.Leave(squad, positionsToUpdate) => + positionsToUpdate.find({ case (member, _) => member == avatar.id }) match { + case Some((ourMember, ourIndex)) => + //we are leaving the squad + //remove each member's entry (our own too) + ops.updateSquadRef = Default.Actor + positionsToUpdate.foreach { + case (member, index) => + sendResponse(SquadMemberEvent.Remove(ops.squad_supplement_id, member, index)) + ops.squadUI.remove(member) + } + //uninitialize + val playerGuid = player.GUID + sendResponse(SquadMemberEvent.Remove(ops.squad_supplement_id, ourMember, ourIndex)) //repeat of our entry + ops.GiveSquadColorsToSelf(value = 0) + sendResponse(PlanetsideAttributeMessage(playerGuid, 32, 0)) //disassociate with member position in squad? + sendResponse(PlanetsideAttributeMessage(playerGuid, 34, 4294967295L)) //unknown, perhaps unrelated? + avatarActor ! AvatarActor.SetLookingForSquad(false) + //a finalization? what does this do? + sendResponse(SquadDefinitionActionMessage(PlanetSideGUID(0), 0, SquadAction.Unknown(18))) + ops.squad_supplement_id = 0 + ops.squadUpdateCounter = 0 + ops.updateSquad = ops.NoSquadUpdates + sessionLogic.chat.LeaveChannel(SquadChannel(squad.GUID)) + case _ => + //remove each member's entry + ops.GiveSquadColorsToMembers( + positionsToUpdate.map { + case (member, index) => + sendResponse(SquadMemberEvent.Remove(ops.squad_supplement_id, member, index)) + ops.squadUI.remove(member) + member + }, + value = 0 + ) + } + + case SquadResponse.AssignMember(squad, from_index, to_index) => + //we've already swapped position internally; now we swap the cards + ops.SwapSquadUIElements(squad, from_index, to_index) + + case SquadResponse.PromoteMember(squad, promotedPlayer, from_index) => + if (promotedPlayer != player.CharId) { + //demoted from leader; no longer lfsm + if (player.avatar.lookingForSquad) { + avatarActor ! AvatarActor.SetLookingForSquad(false) + } + } + sendResponse(SquadMemberEvent(MemberEvent.Promote, squad.GUID.guid, promotedPlayer, position = 0)) + //the players have already been swapped in the backend object + ops.PromoteSquadUIElements(squad, from_index) + + case SquadResponse.UpdateMembers(_, positions) => + val pairedEntries = positions.collect { + case entry if ops.squadUI.contains(entry.char_id) => + (entry, ops.squadUI(entry.char_id)) + } + //prune entries + val updatedEntries = pairedEntries + .collect({ + case (entry, element) if entry.zone_number != element.zone => + //zone gets updated for these entries + sendResponse( + SquadMemberEvent.UpdateZone(ops.squad_supplement_id, entry.char_id, element.index, entry.zone_number) + ) + ops.squadUI(entry.char_id) = + SquadUIElement(element.name, element.outfit, element.index, entry.zone_number, entry.health, entry.armor, entry.pos) + entry + case (entry, element) + if entry.health != element.health || entry.armor != element.armor || entry.pos != element.position => + //other elements that need to be updated + ops.squadUI(entry.char_id) = + SquadUIElement(element.name, element.outfit, element.index, entry.zone_number, entry.health, entry.armor, entry.pos) + entry + }) + .filterNot(_.char_id == avatar.id) //we want to update our backend, but not our frontend + if (updatedEntries.nonEmpty) { + sendResponse( + SquadState( + PlanetSideGUID(ops.squad_supplement_id), + updatedEntries.map { entry => + SquadStateInfo(entry.char_id, entry.health, entry.armor, entry.pos) + } + ) + ) + } + + case SquadResponse.CharacterKnowledge(charId, name, certs, u1, u2, zone) => + sendResponse(CharacterKnowledgeMessage(charId, Some(CharacterKnowledgeInfo(name, certs, u1, u2, zone)))) + + case SquadResponse.SquadSearchResults(_/*results*/) => + //TODO positive squad search results message? + // if(results.nonEmpty) { + // results.foreach { guid => + // sendResponse(SquadDefinitionActionMessage( + // guid, + // 0, + // SquadAction.SquadListDecorator(SquadListDecoration.SearchResult)) + // ) + // } + // } else { + // sendResponse(SquadDefinitionActionMessage(player.GUID, 0, SquadAction.NoSquadSearchResults())) + // } + // sendResponse(SquadDefinitionActionMessage(player.GUID, 0, SquadAction.CancelSquadSearch())) + + case SquadResponse.InitWaypoints(char_id, waypoints) => + waypoints.foreach { + case (waypoint_type, info, unk) => + sendResponse( + SquadWaypointEvent.Add( + ops.squad_supplement_id, + char_id, + waypoint_type, + WaypointEvent(info.zone_number, info.pos, unk) + ) + ) + } + + case SquadResponse.WaypointEvent(WaypointEventAction.Add, char_id, waypoint_type, _, Some(info), unk) => + sendResponse( + SquadWaypointEvent.Add( + ops.squad_supplement_id, + char_id, + waypoint_type, + WaypointEvent(info.zone_number, info.pos, unk) + ) + ) + + case SquadResponse.WaypointEvent(WaypointEventAction.Remove, char_id, waypoint_type, _, _, _) => + sendResponse(SquadWaypointEvent.Remove(ops.squad_supplement_id, char_id, waypoint_type)) + + case _ => () + } + } + } +} diff --git a/src/main/scala/net/psforever/actors/session/csr/TerminalHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/csr/TerminalHandlerLogic.scala new file mode 100644 index 000000000..1527398d2 --- /dev/null +++ b/src/main/scala/net/psforever/actors/session/csr/TerminalHandlerLogic.scala @@ -0,0 +1,116 @@ +// Copyright (c) 2024 PSForever +package net.psforever.actors.session.csr + +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.{Player, Vehicle} +import net.psforever.objects.guid.TaskWorkflow +import net.psforever.objects.serverobject.terminals.{OrderTerminalDefinition, Terminal} +import net.psforever.packet.game.{FavoritesRequest, ItemTransactionMessage, ItemTransactionResultMessage, ProximityTerminalUseMessage} + +object TerminalHandlerLogic { + def apply(ops: SessionTerminalHandlers): TerminalHandlerLogic = { + new TerminalHandlerLogic(ops, ops.context) + } +} + +class TerminalHandlerLogic(val ops: SessionTerminalHandlers, implicit val context: ActorContext) extends TerminalHandlerFunctions { + def sessionLogic: SessionData = ops.sessionLogic + + private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor + + def handleItemTransaction(pkt: ItemTransactionMessage): Unit = { + if (player.spectator) { + val ItemTransactionMessage(terminal_guid, _, _, _, _, _) = pkt + sessionLogic.zoning.CancelZoningProcess() + continent + .GUID(terminal_guid) + .collect { case t: Terminal => t.Definition } + .collect { case t: OrderTerminalDefinition => t } + .map(t => t.Request(player, pkt)) + .collect { + case order: Terminal.BuyVehicle => + //do not handle transaction + order + } + .orElse { + ops.handleItemTransaction(pkt) + None + } + } else { + ops.handleItemTransaction(pkt) + } + } + + def handleProximityTerminalUse(pkt: ProximityTerminalUseMessage): Unit = { + ops.handleProximityTerminalUse(pkt) + } + + def handleFavoritesRequest(pkt: FavoritesRequest): Unit = { + sessionLogic.zoning.CancelZoningProcess() + ops.handleFavoritesRequest(pkt) + } + + /** + * na + * @param tplayer na + * @param msg na + * @param order na + */ + def handle(tplayer: Player, msg: ItemTransactionMessage, order: Terminal.Exchange): Unit = { + order match { + case Terminal.BuyEquipment(item) => + TaskWorkflow.execute(BuyNewEquipmentPutInInventory( + continent.GUID(tplayer.VehicleSeated) match { + case Some(v: Vehicle) => v + case _ => player + }, + tplayer, + msg.terminal_guid + )(item)) + + case Terminal.SellEquipment() => + SellEquipmentFromInventory(tplayer, tplayer, msg.terminal_guid)(Player.FreeHandSlot) + + case Terminal.LearnCertification(cert) => + avatarActor ! AvatarActor.LearnCertification(msg.terminal_guid, cert) + ops.lastTerminalOrderFulfillment = true + + case Terminal.SellCertification(cert) => + avatarActor ! AvatarActor.SellCertification(msg.terminal_guid, cert) + ops.lastTerminalOrderFulfillment = true + + case Terminal.LearnImplant(implant) => + avatarActor ! AvatarActor.LearnImplant(msg.terminal_guid, implant) + ops.lastTerminalOrderFulfillment = true + + case Terminal.SellImplant(implant) => + avatarActor ! AvatarActor.SellImplant(msg.terminal_guid, implant) + ops.lastTerminalOrderFulfillment = true + + case Terminal.BuyVehicle(vehicle, weapons, trunk) => + ops.buyVehicle(msg.terminal_guid, msg.transaction_type, vehicle, weapons, trunk) + ops.lastTerminalOrderFulfillment = true + + case Terminal.NoDeal() if msg != null => + val transaction = msg.transaction_type + log.warn(s"NoDeal: ${tplayer.Name} made a request but the terminal rejected the ${transaction.toString} order") + sendResponse(ItemTransactionResultMessage(msg.terminal_guid, transaction, success = false)) + ops.lastTerminalOrderFulfillment = true + + case _ => + val terminal = msg.terminal_guid.guid + continent.GUID(terminal) match { + case Some(term: Terminal) => + log.warn(s"NoDeal?: ${tplayer.Name} made a request but the ${term.Definition.Name}#$terminal rejected the missing order") + case Some(_) => + log.warn(s"NoDeal?: ${tplayer.Name} made a request to a non-terminal entity#$terminal") + case None => + log.warn(s"NoDeal?: ${tplayer.Name} made a request to a missing entity#$terminal") + } + ops.lastTerminalOrderFulfillment = true + } + } +} diff --git a/src/main/scala/net/psforever/actors/session/csr/VehicleLogic.scala b/src/main/scala/net/psforever/actors/session/csr/VehicleLogic.scala new file mode 100644 index 000000000..4307dce9f --- /dev/null +++ b/src/main/scala/net/psforever/actors/session/csr/VehicleLogic.scala @@ -0,0 +1,397 @@ +// Copyright (c) 2024 PSForever +package net.psforever.actors.session.csr + +import akka.actor.{ActorContext, typed} +import net.psforever.actors.session.AvatarActor +import net.psforever.actors.session.support.{SessionData, VehicleFunctions, VehicleOperations} +import net.psforever.objects.serverobject.PlanetSideServerObject +import net.psforever.objects.{PlanetSideGameObject, Player, Vehicle, Vehicles} +import net.psforever.objects.serverobject.deploy.Deployment +import net.psforever.objects.serverobject.mount.Mountable +import net.psforever.objects.vehicles.control.BfrFlight +import net.psforever.objects.vital.Vitality +import net.psforever.objects.zones.Zone +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, PlanetSideGUID, Vector3} + +object VehicleLogic { + def apply(ops: VehicleOperations): VehicleLogic = { + new VehicleLogic(ops, ops.context) + } +} + +class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContext) extends VehicleFunctions { + def sessionLogic: SessionData = ops.sessionLogic + + private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor + + /* packets */ + + def handleVehicleState(pkt: VehicleStateMessage): Unit = { + val VehicleStateMessage( + vehicle_guid, + unk1, + pos, + ang, + vel, + is_flying, + unk6, + unk7, + wheels, + is_decelerating, + is_cloaked + ) = pkt + 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) + } + topOffHealthOfPlayer() + topOffHealth(obj) + 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() + player.allowInteraction = false + obj.zoneInteractions() + case (None, _) => + //log.error(s"VehicleState: no vehicle $vehicle_guid found in zone") + //TODO placing a "not driving" warning here may trigger as we are disembarking the vehicle + case (_, Some(index)) => + log.error( + s"VehicleState: ${player.Name} should not be dispatching this kind of packet from vehicle ${vehicle_guid.guid} when not the driver (actually, seat $index)" + ) + case _ => () + } + if (player.death_by == -1) { + sessionLogic.kickedByAdministration() + } + } + + def handleFrameVehicleState(pkt: FrameVehicleStateMessage): Unit = { + val FrameVehicleStateMessage( + vehicle_guid, + unk1, + pos, + ang, + vel, + unk2, + unk3, + unk4, + is_crouched, + is_airborne, + ascending_flight, + flight_time, + unk9, + unkA + ) = pkt + ops.GetVehicleAndSeat() match { + case (Some(obj), Some(0)) => + //we're driving the vehicle + sessionLogic.persist() + sessionLogic.turnCounterFunc(player.GUID) + topOffHealthOfPlayer() + topOffHealth(obj) + 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 + } + player.allowInteraction = false + obj.zoneInteractions() + } else { + obj.Velocity = None + obj.Flying = None + } + continent.VehicleEvents ! VehicleServiceMessage( + continent.id, + VehicleAction.FrameVehicleState( + player.GUID, + vehicle_guid, + unk1, + position, + angle, + velocity, + unk2, + unk3, + unk4, + is_crouched, + is_airborne, + ascending_flight, + flight_time, + unk9, + unkA + ) + ) + sessionLogic.squad.updateSquad() + case (None, _) => + //log.error(s"VehicleState: no vehicle $vehicle_guid found in zone") + //TODO placing a "not driving" warning here may trigger as we are disembarking the vehicle + case (_, Some(index)) => + log.error( + s"VehicleState: ${player.Name} should not be dispatching this kind of packet from vehicle ${vehicle_guid.guid} when not the driver (actually, seat $index)" + ) + case _ => () + } + if (player.death_by == -1) { + sessionLogic.kickedByAdministration() + } + } + + def handleChildObjectState(pkt: ChildObjectStateMessage): Unit = { + val ChildObjectStateMessage(object_guid, pitch, yaw) = pkt + val (o, tools) = sessionLogic.shooting.FindContainedWeapon + //is COSM our primary upstream packet? + (o match { + case Some(mount: Mountable) => (o, mount.PassengerInSeat(player)) + case _ => (None, None) + }) match { + case (None, None) | (_, None) | (Some(_: Vehicle), Some(0)) => + () + case (Some(obj: PlanetSideGameObject with Vitality), _) => + sessionLogic.persist() + sessionLogic.turnCounterFunc(player.GUID) + topOffHealthOfPlayer() + topOffHealth(obj) + case _ => + sessionLogic.persist() + sessionLogic.turnCounterFunc(player.GUID) + } + //the majority of the following check retrieves information to determine if we are in control of the child + tools.find { _.GUID == object_guid } match { + case None => + //todo: old warning; this state is problematic, but can trigger in otherwise valid instances + //log.warn( + // s"ChildObjectState: ${player.Name} is using a different controllable agent than entity ${object_guid.guid}" + //) + case Some(_) => + //TODO set tool orientation? + player.Orientation = Vector3(0f, pitch, yaw) + continent.VehicleEvents ! VehicleServiceMessage( + continent.id, + VehicleAction.ChildObjectState(player.GUID, object_guid, pitch, yaw) + ) + } + //TODO status condition of "playing getting out of vehicle to allow for late packets without warning + if (player.death_by == -1) { + sessionLogic.kickedByAdministration() + } + } + + def handleVehicleSubState(pkt: VehicleSubStateMessage): Unit = { + val VehicleSubStateMessage(vehicle_guid, _, pos, ang, vel, unk1, _) = pkt + sessionLogic.validObject(vehicle_guid, decorator = "VehicleSubState") + .collect { + case obj: Vehicle => + import net.psforever.login.WorldSession.boolToInt + obj.Position = pos + obj.Orientation = ang + obj.Velocity = vel + sessionLogic.updateBlockMap(obj, pos) + obj.zoneInteractions() + continent.VehicleEvents ! VehicleServiceMessage( + continent.id, + VehicleAction.VehicleState( + player.GUID, + vehicle_guid, + unk1, + pos, + ang, + obj.Velocity, + obj.Flying, + 0, + 0, + 15, + unk5 = false, + obj.Cloaked + ) + ) + } + } + + def handleDeployRequest(pkt: DeployRequestMessage): Unit = { + val DeployRequestMessage(_, vehicle_guid, deploy_state, _, _, _) = pkt + continent.GUID(vehicle_guid) + .collect { + case obj: Vehicle => + 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") + obj + } + .orElse { + log.error(s"DeployRequest: ${player.Name} can not find entity $vehicle_guid") + avatarActor ! AvatarActor.SetVehicle(None) //todo is this safe + None + } + } + + /* messages */ + + def handleCanDeploy(obj: Deployment.DeploymentObject, state: DriveState.Value): Unit = { + if (!Deployment.CheckForDeployState(state)) { + CanNotChangeDeployment(obj, state, "incorrect deploy state") + } + } + + def handleCanUndeploy(obj: Deployment.DeploymentObject, state: DriveState.Value): Unit = { + if (!Deployment.CheckForUndeployState(state)) { + CanNotChangeDeployment(obj, state, "incorrect undeploy state") + } + } + + def handleCanNotChangeDeployment(obj: Deployment.DeploymentObject, state: DriveState.Value, reason: String): Unit = { + CanNotChangeDeployment(obj, state, reason) + } + + /* support functions */ + + /** + * Common reporting behavior when a `Deployment` object fails to properly transition between states. + * @param obj the game object that could not + * @param state the `DriveState` that could not be promoted + * @param reason a string explaining why the state can not or will not change + */ + private def CanNotChangeDeployment( + obj: PlanetSideServerObject with Deployment, + state: DriveState.Value, + reason: String + ): Unit = { + 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) + ) + } + } + + private def topOffHealth(obj: PlanetSideGameObject with Vitality): Unit = { + obj match { + case _: Player => topOffHealthOfPlayer() + case v: Vehicle => topOffHealthOfVehicle(v) + case o: PlanetSideGameObject with Vitality => topOffHealthOfGeneric(o) + case _ => () + } + } + + 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(sessionLogic.zoning.zoneChannel, AvatarAction.PlanetsideAttribute(player.GUID, 0, maxHealthOfPlayer)) + } + } + + private def topOffHealthOfVehicle(vehicle: Vehicle): Unit = { + topOffHealthOfPlayer() + topOffHealthOfGeneric(vehicle) + //vehicle shields below half, full shields + val maxShieldsOfVehicle = vehicle.MaxShields.toLong + val shieldsUi = vehicle.Definition.shieldUiAttribute + if (vehicle.Shields < maxShieldsOfVehicle) { + val guid = vehicle.GUID + vehicle.Shields = maxShieldsOfVehicle.toInt + sendResponse(PlanetsideAttributeMessage(guid, shieldsUi, maxShieldsOfVehicle)) + continent.VehicleEvents ! VehicleServiceMessage( + continent.id, + VehicleAction.PlanetsideAttribute(PlanetSideGUID(0), guid, shieldsUi, maxShieldsOfVehicle) + ) + } + } + + private def topOffHealthOfGeneric(obj: PlanetSideGameObject with Vitality): Unit = { + topOffHealthOfPlayer() + //vehicle below half health, full heal + val guid = obj.GUID + val maxHealthOf = obj.MaxHealth.toLong + if (obj.Health < maxHealthOf) { + obj.Health = maxHealthOf.toInt + sendResponse(PlanetsideAttributeMessage(guid, 0, maxHealthOf)) + continent.VehicleEvents ! VehicleServiceMessage( + continent.id, + VehicleAction.PlanetsideAttribute(PlanetSideGUID(0), guid, 0, maxHealthOf) + ) + } + } +} diff --git a/src/main/scala/net/psforever/actors/session/csr/WeaponAndProjectileLogic.scala b/src/main/scala/net/psforever/actors/session/csr/WeaponAndProjectileLogic.scala new file mode 100644 index 000000000..3b2d780d2 --- /dev/null +++ b/src/main/scala/net/psforever/actors/session/csr/WeaponAndProjectileLogic.scala @@ -0,0 +1,208 @@ +// Copyright (c) 2024 PSForever +package net.psforever.actors.session.csr + +import akka.actor.ActorContext +import net.psforever.actors.session.support.{SessionData, WeaponAndProjectileFunctions, WeaponAndProjectileOperations} +import net.psforever.objects.inventory.Container +import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} +import net.psforever.objects.{BoomerDeployable, BoomerTrigger, Player, SpecialEmp, Tool, Vehicle} +import net.psforever.objects.vital.base.{DamageResolution, DamageType} +import net.psforever.objects.zones.{Zone, ZoneProjectile} +import net.psforever.packet.game.{AIDamage, AvatarGrenadeStateMessage, ChangeAmmoMessage, ChangeFireModeMessage, ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, HitMessage, LashMessage, LongRangeProjectileInfoMessage, ProjectileStateMessage, ReloadMessage, SplashHitMessage, UplinkRequest, WeaponDelayFireMessage, WeaponDryFireMessage, WeaponFireMessage, WeaponLazeTargetPositionMessage} +import net.psforever.types.Vector3 + +object WeaponAndProjectileLogic { + def apply(ops: WeaponAndProjectileOperations): WeaponAndProjectileLogic = { + new WeaponAndProjectileLogic(ops, ops.context) + } +} + +class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit val context: ActorContext) extends WeaponAndProjectileFunctions { + def sessionLogic: SessionData = ops.sessionLogic + + //private val avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor + + /* packets */ + + def handleWeaponFire(pkt: WeaponFireMessage): Unit = { + ops.handleWeaponFireOperations(pkt) + } + + def handleWeaponDelayFire(pkt: WeaponDelayFireMessage): Unit = { + val WeaponDelayFireMessage(_, _) = pkt + } + + def handleWeaponDryFire(pkt: WeaponDryFireMessage): Unit = { + ops.handleWeaponDryFire(pkt) + } + + def handleWeaponLazeTargetPosition(pkt: WeaponLazeTargetPositionMessage): Unit = { /* laze is handled elsewhere */ } + + def handleUplinkRequest(packet: UplinkRequest): Unit = { /* CUD not implemented yet */ } + + def handleAvatarGrenadeState(pkt: AvatarGrenadeStateMessage): Unit = { /* grenades are handled elsewhere */ } + + def handleChangeFireStateStart(pkt: ChangeFireStateMessage_Start): Unit = { + val ChangeFireStateMessage_Start(item_guid) = pkt + if (ops.shooting.isEmpty) { + sessionLogic.findEquipment(item_guid) match { + case Some(tool: Tool) if player.VehicleSeated.isEmpty => + ops.fireStateStartWhenPlayer(tool, item_guid) + case Some(tool: Tool) => + ops.fireStateStartWhenMounted(tool, item_guid) + case Some(_) if player.VehicleSeated.isEmpty => + ops.fireStateStartSetup(item_guid) + ops.fireStateStartPlayerMessages(item_guid) + case Some(_) => + ops.fireStateStartSetup(item_guid) + ops.fireStateStartMountedMessages(item_guid) + case None => + log.warn(s"ChangeFireState_Start: can not find $item_guid") + } + } + } + + def handleChangeFireStateStop(pkt: ChangeFireStateMessage_Stop): Unit = { + val ChangeFireStateMessage_Stop(item_guid) = pkt + val now = System.currentTimeMillis() + ops.prefire -= item_guid + ops.shootingStop += item_guid -> now + ops.shooting -= item_guid + sessionLogic.findEquipment(item_guid) match { + case Some(tool: Tool) if player.VehicleSeated.isEmpty => + ops.fireStateStopWhenPlayer(tool, item_guid) + case Some(tool: Tool) => + ops.fireStateStopWhenMounted(tool, item_guid) + case Some(trigger: BoomerTrigger) => + ops.fireStateStopPlayerMessages(item_guid) + continent.GUID(trigger.Companion).collect { + case boomer: BoomerDeployable => + boomer.Actor ! CommonMessages.Use(player, Some(trigger)) + } + case Some(_) if player.VehicleSeated.isEmpty => + ops.fireStateStopPlayerMessages(item_guid) + case Some(_) => + ops.fireStateStopMountedMessages(item_guid) + case _ => + log.warn(s"ChangeFireState_Stop: can not find $item_guid") + } + sessionLogic.general.progressBarUpdate.cancel() + sessionLogic.general.progressBarValue = None + } + + def handleReload(pkt: ReloadMessage): Unit = { + val ReloadMessage(item_guid, _, unk1) = pkt + ops.FindContainedWeapon match { + case (Some(obj: Player), tools) => + ops.handleReloadWhenPlayer(item_guid, obj, tools, unk1) + case (Some(obj: PlanetSideServerObject with Container), tools) => + ops.handleReloadWhenMountable(item_guid, obj, tools, unk1) + case (_, _) => + log.warn(s"ReloadMessage: either can not find $item_guid or the object found was not a Tool") + } + } + + def handleChangeAmmo(pkt: ChangeAmmoMessage): Unit = { + ops.handleChangeAmmo(pkt) + } + + def handleChangeFireMode(pkt: ChangeFireModeMessage): Unit = { + ops.handleChangeFireMode(pkt) + } + + def handleProjectileState(pkt: ProjectileStateMessage): Unit = { + ops.handleProjectileState(pkt) + } + + def handleLongRangeProjectileState(pkt: LongRangeProjectileInfoMessage): Unit = { + val LongRangeProjectileInfoMessage(guid, _, _) = pkt + ops.FindContainedWeapon match { + case (Some(_: Vehicle), weapons) + if weapons.exists(_.GUID == guid) => () //now what? + case _ => () + } + } + + def handleDirectHit(pkt: HitMessage): Unit = { + val list = ops.composeDirectDamageInformation(pkt) + if (!player.spectator) { + list.foreach { + case (target, projectile, _, _) => + ops.resolveProjectileInteraction(target, projectile, DamageResolution.Hit, target.Position) + } + //... + if (list.isEmpty) { + ops.handleProxyDamage(pkt.projectile_guid, pkt.hit_info.map(_.hit_pos).getOrElse(Vector3.Zero)) + } + } + } + + def handleSplashHit(pkt: SplashHitMessage): Unit = { + val list = ops.composeSplashDamageInformation(pkt) + if (!player.spectator) { + 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, _, _, _) => + ops.resolveProjectileInteraction(target, projectile, resolution1, target.Position) + } + others.foreach { + case (target, _, _, _) => + ops.resolveProjectileInteraction(target, projectile, resolution2, target.Position) + } + //... + 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) { + continent.Projectile ! ZoneProjectile.Remove(projectileGuid) + } + } + //... + ops.handleProxyDamage(pkt.projectile_uid, pkt.projectile_pos) + } + } + + def handleLashHit(pkt: LashMessage): Unit = { + 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 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) + } + } + } +} diff --git a/src/main/scala/net/psforever/actors/session/normal/AvatarHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/normal/AvatarHandlerLogic.scala index 890cd27be..96ef7e81c 100644 --- a/src/main/scala/net/psforever/actors/session/normal/AvatarHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/AvatarHandlerLogic.scala @@ -3,8 +3,15 @@ package net.psforever.actors.session.normal import akka.actor.{ActorContext, typed} import net.psforever.actors.session.support.AvatarHandlerFunctions +import net.psforever.actors.zone.ZoneActor +import net.psforever.objects.inventory.Container +import net.psforever.objects.{Default, PlanetSideGameObject} import net.psforever.objects.serverobject.containable.ContainableBehavior +import net.psforever.objects.serverobject.mount.Mountable +import net.psforever.objects.sourcing.PlayerSource +import net.psforever.objects.vital.interaction.Adversarial import net.psforever.packet.game.{AvatarImplantMessage, CreateShortcutMessage, ImplantAction} +import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.types.ImplantType import scala.concurrent.duration._ @@ -21,7 +28,7 @@ import net.psforever.objects.vital.etc.ExplodingEntityReason import net.psforever.objects.zones.Zoning import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent import net.psforever.packet.game.{ArmorChangedMessage, AvatarDeadStateMessage, ChangeAmmoMessage, ChangeFireModeMessage, ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, ChatMsg, DeadState, DestroyMessage, DrowningTarget, GenericActionMessage, GenericObjectActionMessage, HitHint, ItemTransactionResultMessage, ObjectCreateDetailedMessage, ObjectCreateMessage, ObjectDeleteMessage, ObjectHeldMessage, OxygenStateMessage, PlanetsideAttributeMessage, PlayerStateMessage, ProjectileStateMessage, ReloadMessage, SetEmpireMessage, UseItemMessage, WeaponDryFireMessage} -import net.psforever.services.avatar.{AvatarAction, AvatarResponse, AvatarServiceMessage} +import net.psforever.services.avatar.AvatarResponse import net.psforever.services.Service import net.psforever.types.{ChatMessageType, PlanetSideGUID, TransactionType, Vector3} import net.psforever.util.Config @@ -311,10 +318,14 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A //redraw if (maxhand) { sendResponse(PlanetsideAttributeMessage(target, attribute_type=7, player.Capacitor.toLong)) - TaskWorkflow.execute(HoldNewEquipmentUp(player)( - Tool(GlobalDefinitions.MAXArms(subtype, player.Faction)), - 0 - )) + val maxArmDefinition = GlobalDefinitions.MAXArms(subtype, player.Faction) + TaskWorkflow.execute(HoldNewEquipmentUp(player)(Tool(maxArmDefinition), slot = 0)) + player.avatar.purchaseCooldown(maxArmDefinition) + .collect(a => a) + .getOrElse { + avatarActor ! AvatarActor.UpdatePurchaseTime(maxArmDefinition) + None + } } //draw free hand player.FreeHand.Equipment.foreach { obj => @@ -342,6 +353,8 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A ) } DropLeftovers(player)(drop) + //deactivate non-passive implants + avatarActor ! AvatarActor.DeactivateActiveImplants case AvatarResponse.ChangeExosuit(target, armor, exosuit, subtype, slot, _, oldHolsters, holsters, _, _, drop, delete) => sendResponse(ArmorChangedMessage(target, exosuit, subtype)) @@ -394,14 +407,19 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A drops.foreach(item => sendResponse(ObjectDeleteMessage(item.obj.GUID, unk1=0))) //redraw if (maxhand) { + val maxArmWeapon = GlobalDefinitions.MAXArms(subtype, player.Faction) sendResponse(PlanetsideAttributeMessage(target, attribute_type=7, player.Capacitor.toLong)) - TaskWorkflow.execute(HoldNewEquipmentUp(player)( - Tool(GlobalDefinitions.MAXArms(subtype, player.Faction)), - slot = 0 - )) + TaskWorkflow.execute(HoldNewEquipmentUp(player)(Tool(maxArmWeapon), slot = 0)) + player.avatar.purchaseCooldown(maxArmWeapon) + if (!oldHolsters.exists { case (e, _) => e.Definition == maxArmWeapon } && + player.avatar.purchaseCooldown(maxArmWeapon).isEmpty) { + avatarActor ! AvatarActor.UpdatePurchaseTime(maxArmWeapon) //switching for first time causes cooldown + } } sessionLogic.general.applyPurchaseTimersBeforePackingLoadout(player, player, holsters ++ inventory) DropLeftovers(player)(drops) + //deactivate non-passive implants + avatarActor ! AvatarActor.DeactivateActiveImplants case AvatarResponse.ChangeLoadout(target, armor, exosuit, subtype, slot, _, oldHolsters, _, _, _, _) => //redraw handled by callbacks @@ -466,9 +484,33 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A if isNotSameTarget && ops.lastSeenStreamMessage.get(guid.guid).exists { _.visible } => sendResponse(ReloadMessage(itemGuid, ammo_clip=1, unk1=0)) - case AvatarResponse.Killed(mount) => + case AvatarResponse.Killed(cause, mount) => //log and chat messages - val cause = player.LastDamage.flatMap { damage => + //destroy display + val zoneChannel = continent.id + val events = continent.AvatarEvents + val pentry = PlayerSource(player) + cause + .adversarial + .collect { case out @ Adversarial(attacker, _, _) if attacker != PlayerSource.Nobody => out } + .orElse { + player.LastDamage.collect { + case attack if System.currentTimeMillis() - attack.interaction.hitTime < (10 seconds).toMillis => + attack + .adversarial + .collect { case out @ Adversarial(attacker, _, _) if attacker != PlayerSource.Nobody => out } + }.flatten + } match { + case Some(adversarial) => + events ! AvatarServiceMessage( + zoneChannel, + AvatarAction.DestroyDisplay(adversarial.attacker, pentry, adversarial.implement) + ) + case _ => + events ! AvatarServiceMessage(zoneChannel, AvatarAction.DestroyDisplay(pentry, pentry, 0)) + } + //events chat and log + val excuse = player.LastDamage.flatMap { damage => val interaction = damage.interaction val reason = interaction.cause val adversarial = interaction.adversarial.map { _.attacker } @@ -480,15 +522,17 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A } adversarial.map {_.Name }.orElse { Some(s"a ${reason.getClass.getSimpleName}") } }.getOrElse { s"an unfortunate circumstance (probably ${player.Sex.pronounObject} own fault)" } - log.info(s"${player.Name} has died, killed by $cause") + log.info(s"${player.Name} has died, killed by $excuse") if (sessionLogic.shooting.shotsWhileDead > 0) { log.warn( s"SHOTS_WHILE_DEAD: client of ${avatar.name} fired ${sessionLogic.shooting.shotsWhileDead} rounds while character was dead on server" ) sessionLogic.shooting.shotsWhileDead = 0 } + //TODO other methods of death? sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason(msg = "cancel") sessionLogic.general.renewCharSavedTimer(fixedLen = 1800L, varLen = 0L) + continent.actor ! ZoneActor.RewardThisDeath(player) //player state changes AvatarActor.updateToolDischargeFor(avatar) @@ -500,9 +544,18 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A sessionLogic.keepAliveFunc = sessionLogic.zoning.NormalKeepAlive sessionLogic.zoning.zoningStatus = Zoning.Status.None sessionLogic.zoning.spawn.deadState = DeadState.Dead - continent.GUID(mount).collect { case obj: Vehicle => - sessionLogic.vehicles.ConditionalDriverVehicleControl(obj) - sessionLogic.general.unaccessContainer(obj) + continent.GUID(mount).collect { + case obj: Vehicle => + killedWhileMounted(obj, resolvedPlayerGuid) + sessionLogic.vehicles.ConditionalDriverVehicleControl(obj) + sessionLogic.general.unaccessContainer(obj) + + case obj: PlanetSideGameObject with Mountable with Container => + killedWhileMounted(obj, resolvedPlayerGuid) + sessionLogic.general.unaccessContainer(obj) + + case obj: PlanetSideGameObject with Mountable => + killedWhileMounted(obj, resolvedPlayerGuid) } sessionLogic.actionsToCancel() sessionLogic.terminals.CancelAllProximityUnits() @@ -510,7 +563,10 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A sessionLogic.zoning.spawn.shiftPosition = Some(player.Position) //respawn + val respawnTimer = 300000 //milliseconds + sendResponse(AvatarDeadStateMessage(DeadState.Dead, respawnTimer, respawnTimer, player.Position, player.Faction, unk5=true)) sessionLogic.zoning.spawn.reviveTimer.cancel() + sessionLogic.zoning.spawn.reviveTimer = Default.Cancellable if (player.death_by == 0) { sessionLogic.zoning.spawn.randomRespawn(300.seconds) } else { @@ -523,16 +579,7 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A case AvatarResponse.Revive(revivalTargetGuid) if resolvedPlayerGuid == revivalTargetGuid => log.info(s"No time for rest, ${player.Name}. Back on your feet!") - sessionLogic.zoning.spawn.reviveTimer.cancel() - sessionLogic.zoning.spawn.deadState = DeadState.Alive - player.Revive - val health = player.Health - sendResponse(PlanetsideAttributeMessage(revivalTargetGuid, attribute_type=0, health)) - sendResponse(AvatarDeadStateMessage(DeadState.Alive, timer_max=0, timer=0, player.Position, player.Faction, unk5=true)) - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.PlanetsideAttributeToAll(revivalTargetGuid, attribute_type=0, health) - ) + ops.revive(revivalTargetGuid) /* uncommon messages (utility, or once in a while) */ case AvatarResponse.ChangeAmmo(weapon_guid, weapon_slot, previous_guid, ammo_id, ammo_guid, ammo_data) @@ -622,4 +669,13 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A case _ => () } } + + def killedWhileMounted(obj: PlanetSideGameObject with Mountable, playerGuid: PlanetSideGUID): Unit = { + val events = continent.AvatarEvents + ops.killedWhileMounted(obj, playerGuid) + //make player invisible on client + events ! AvatarServiceMessage(player.Name, AvatarAction.PlanetsideAttributeToAll(playerGuid, 29, 1)) + //only the dead player should "see" their own body, so that the death camera has something to focus on + events ! AvatarServiceMessage(continent.id, AvatarAction.ObjectDelete(playerGuid, playerGuid)) + } } 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 e701c0a55..35c09b7f5 100644 --- a/src/main/scala/net/psforever/actors/session/normal/ChatLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/ChatLogic.scala @@ -2,12 +2,14 @@ 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.packet.game.{ChatMsg, ServerType, SetChatFilterMessage} import net.psforever.services.chat.DefaultChannel import net.psforever.types.ChatMessageType +import net.psforever.types.ChatMessageType.{CMT_TOGGLESPECTATORMODE, CMT_TOGGLE_GM} import net.psforever.util.Config object ChatLogic { @@ -19,34 +21,27 @@ object ChatLogic { class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) extends ChatFunctions { def sessionLogic: SessionData = ops.sessionLogic + ops.CurrentSpectatorMode = SpectatorMode + ops.transitoryCommandEntered = None + def handleChatMsg(message: ChatMsg): Unit = { import net.psforever.types.ChatMessageType._ - val isAlive = if (player != null) player.isAlive else false - val perms = if (avatar != null) avatar.permissions else ModePermissions() - val gmCommandAllowed = (session.account.gm && perms.canGM) || - Config.app.development.unprivilegedGmCommands.contains(message.messageType) + lazy val isAlive = avatar != null && player != null && player.isAlive (message.messageType, message.recipient.trim, message.contents.trim) match { /** Messages starting with ! are custom chat commands */ case (_, _, contents) if contents.startsWith("!") && customCommandMessages(message, session) => () - case (CMT_FLY, recipient, contents) if gmCommandAllowed => - ops.commandFly(contents, recipient) + case (CMT_ANONYMOUS, _, _) => () - case (CMT_ANONYMOUS, _, _) => - // ? - - case (CMT_TOGGLE_GM, _, _) => - // ? + case (CMT_TOGGLE_GM, _, contents) if isAlive => + customCommandModerator(contents) case (CMT_CULLWATERMARK, _, contents) => ops.commandWatermark(contents) - case (CMT_SPEED, _, contents) if gmCommandAllowed => - ops.commandSpeed(message, contents) - - case (CMT_TOGGLESPECTATORMODE, _, contents) if isAlive && (gmCommandAllowed || perms.canSpectate) => - ops.commandToggleSpectatorMode(session, contents) + case (CMT_TOGGLESPECTATORMODE, _, contents) if isAlive => + commandToggleSpectatorMode(contents) case (CMT_RECALL, _, _) => ops.commandRecall(session) @@ -63,28 +58,6 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext case (CMT_DESTROY, _, contents) if contents.matches("\\d+") => ops.commandDestroy(session, message, contents) - case (CMT_SETBASERESOURCES, _, contents) if gmCommandAllowed => - ops.commandSetBaseResources(session, contents) - - case (CMT_ZONELOCK, _, contents) if gmCommandAllowed => - ops.commandZoneLock(contents) - - case (U_CMT_ZONEROTATE, _, _) if gmCommandAllowed => - ops.commandZoneRotate() - - case (CMT_CAPTUREBASE, _, contents) if gmCommandAllowed => - ops.commandCaptureBase(session, message, contents) - - case (CMT_GMBROADCAST | CMT_GMBROADCAST_NC | CMT_GMBROADCAST_VS | CMT_GMBROADCAST_TR, _, _) - if gmCommandAllowed => - ops.commandSendToRecipient(session, message, DefaultChannel) - - case (CMT_GMTELL, _, _) if gmCommandAllowed => - ops.commandSend(session, message, DefaultChannel) - - case (CMT_GMBROADCASTPOPUP, _, _) if gmCommandAllowed => - ops.commandSendToRecipient(session, message, DefaultChannel) - case (CMT_OPEN, _, _) if !player.silenced => ops.commandSendToRecipient(session, message, DefaultChannel) @@ -100,54 +73,21 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext case (CMT_PLATOON, _, _) if !player.silenced => ops.commandSendToRecipient(session, message, DefaultChannel) - case (CMT_COMMAND, _, _) if gmCommandAllowed => - ops.commandSendToRecipient(session, message, DefaultChannel) - case (CMT_NOTE, _, _) => ops.commandSend(session, message, DefaultChannel) - case (CMT_SILENCE, _, _) if gmCommandAllowed => - ops.commandSend(session, message, DefaultChannel) - case (CMT_SQUAD, _, _) => ops.commandSquad(session, message, DefaultChannel) //todo SquadChannel, but what is the guid case (CMT_WHO | CMT_WHO_CSR | CMT_WHO_CR | CMT_WHO_PLATOONLEADERS | CMT_WHO_SQUADLEADERS | CMT_WHO_TEAMS, _, _) => ops.commandWho(session) - case (CMT_ZONE, _, contents) if gmCommandAllowed => - ops.commandZone(message, contents) - - case (CMT_WARP, _, contents) if gmCommandAllowed => - ops.commandWarp(session, message, contents) - - case (CMT_SETBATTLERANK, _, contents) if gmCommandAllowed => - ops.commandSetBattleRank(session, message, contents) - - case (CMT_SETCOMMANDRANK, _, contents) if gmCommandAllowed => - ops.commandSetCommandRank(session, message, contents) - - case (CMT_ADDBATTLEEXPERIENCE, _, contents) if gmCommandAllowed => - ops.commandAddBattleExperience(message, contents) - - case (CMT_ADDCOMMANDEXPERIENCE, _, contents) if gmCommandAllowed => - ops.commandAddCommandExperience(message, contents) - case (CMT_TOGGLE_HAT, _, contents) => ops.commandToggleHat(session, message, contents) case (CMT_HIDE_HELMET | CMT_TOGGLE_SHADES | CMT_TOGGLE_EARPIECE, _, contents) => ops.commandToggleCosmetics(session, message, contents) - case (CMT_ADDCERTIFICATION, _, contents) if gmCommandAllowed => - ops.commandAddCertification(session, message, contents) - - case (CMT_KICK, _, contents) if gmCommandAllowed => - ops.commandKick(session, message, contents) - - case (CMT_REPORTUSER, _, contents) => - ops.commandReportUser(session, message, contents) - case _ => sendResponse(ChatMsg(ChatMessageType.UNK_227, "@no_permission")) } @@ -192,51 +132,76 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext case a :: b => (a, b) case _ => ("", Seq("")) } - val perms = if (avatar != null) avatar.permissions else ModePermissions() - val gmBangCommandAllowed = (session.account.gm && perms.canGM) || - Config.app.development.unprivilegedGmBangCommands.contains(command) - //try gm commands - val tryGmCommandResult = if (gmBangCommandAllowed) { - command match { - case "whitetext" => Some(ops.customCommandWhitetext(session, params)) - case "list" => Some(ops.customCommandList(session, params, message)) - case "ntu" => Some(ops.customCommandNtu(session, params)) - case "zonerotate" => Some(ops.customCommandZonerotate(params)) - case "nearby" => Some(ops.customCommandNearby(session)) - case _ => None - } - } else { - None - } - //try commands for all players if not caught as a gm command - val result = tryGmCommandResult match { - case None => - command match { - case "loc" => ops.customCommandLoc(session, message) - case "suicide" => ops.customCommandSuicide(session) - case "grenade" => ops.customCommandGrenade(session, log) - case "macro" => ops.customCommandMacro(session, params) - case "progress" => ops.customCommandProgress(session, params) - case _ => false - } - case Some(out) => - out - } - if (!result) { - // command was not handled - sendResponse( - ChatMsg( - ChatMessageType.CMT_GMOPEN, // CMT_GMTELL - message.wideContents, - "Server", - s"Unknown command !$command", - message.note + command match { + case "loc" => ops.customCommandLoc(session, message) + case "suicide" => ops.customCommandSuicide(session) + case "grenade" => ops.customCommandGrenade(session, log) + case "macro" => ops.customCommandMacro(session, params) + case "progress" => ops.customCommandProgress(session, params) + case _ => + // command was not handled + sendResponse( + ChatMsg( + ChatMessageType.CMT_GMOPEN, // CMT_GMTELL + message.wideContents, + "Server", + s"Unknown command !$command", + message.note + ) ) - ) + true } - result } else { - false // not a handled command + false + } + } + + def commandToggleSpectatorMode(contents: String): Boolean = { + ops.transitoryCommandEntered + .collect { + case CMT_TOGGLESPECTATORMODE => true + case CMT_TOGGLE_GM => false + } + .getOrElse { + val currentSpectatorActivation = + avatar.permissions.canSpectate || + avatar.permissions.canGM || + Config.app.world.serverType == ServerType.Development + contents.toLowerCase() match { + case "on" | "o" | "" if currentSpectatorActivation && !player.spectator => + ops.transitoryCommandEntered = Some(CMT_TOGGLESPECTATORMODE) + context.self ! SessionActor.SetMode(ops.CurrentSpectatorMode) + true + case _ => + false + } + } + } + + def customCommandModerator(contents: String): Boolean = { + if (sessionLogic.zoning.maintainInitialGmState) { + sessionLogic.zoning.maintainInitialGmState = false + true + } else { + ops.transitoryCommandEntered + .collect { + case CMT_TOGGLE_GM => true + case CMT_TOGGLESPECTATORMODE => false + } + .getOrElse { + val currentCsrActivation = + avatar.permissions.canGM || + Config.app.world.serverType == ServerType.Development + contents.toLowerCase() match { + case "on" | "o" | "" if currentCsrActivation => + import net.psforever.actors.session.csr.CustomerServiceRepresentativeMode + ops.transitoryCommandEntered = Some(CMT_TOGGLE_GM) + context.self ! SessionActor.SetMode(CustomerServiceRepresentativeMode) + true + case _ => + false + } + } } } } 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 ab40f6222..3a84a4324 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,15 @@ 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.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 +22,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, InvalidTerrainMessage, LootItemMessage, MoveItemMessage, ObjectDetectedMessage, ObjectHeldMessage, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, RequestDestroyMessage, TargetingImplantRequest, 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._ @@ -209,34 +202,25 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex } def handleDropItem(pkt: DropItemMessage): Unit = { - val DropItemMessage(itemGuid) = pkt - (sessionLogic.validObject(itemGuid, decorator = "DropItem"), player.FreeHand.Equipment) match { - case (Some(anItem: Equipment), Some(heldItem)) - if (anItem eq heldItem) && continent.GUID(player.VehicleSeated).nonEmpty => + ops.handleDropItem(pkt) match { + case GeneralOperations.ItemDropState.Dropped => sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") - RemoveOldEquipmentFromInventory(player)(heldItem) - case (Some(anItem: Equipment), Some(heldItem)) - if anItem eq heldItem => - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") - DropEquipmentFromInventory(player)(heldItem) - case (Some(anItem: Equipment), _) + case GeneralOperations.ItemDropState.NotDropped if continent.GUID(player.VehicleSeated).isEmpty => - //suppress the warning message if in a vehicle - log.warn(s"DropItem: ${player.Name} wanted to drop a ${anItem.Definition.Name}, but it wasn't at hand") - case (Some(obj), _) => - log.warn(s"DropItem: ${player.Name} wanted to drop a ${obj.Definition.Name}, but it was not equipment") + log.warn(s"DropItem: ${player.Name} wanted to drop an item, but it wasn't at hand") + case GeneralOperations.ItemDropState.NotDropped => + log.warn(s"DropItem: ${player.Name} wanted to drop an item, but it was not equipment") case _ => () } } def handlePickupItem(pkt: PickupItemMessage): Unit = { - val PickupItemMessage(itemGuid, _, _, _) = pkt - sessionLogic.validObject(itemGuid, decorator = "PickupItem").collect { - case item: Equipment if player.Fit(item).nonEmpty => + ops.handlePickupItem(pkt) match { + case GeneralOperations.ItemPickupState.PickedUp => sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") - PickUpEquipmentFromGround(player)(item) - case _: Equipment => + case GeneralOperations.ItemPickupState.Dropped => sendResponse(ActionResultMessage.Fail(16)) //error code? + case _ => () } } @@ -252,33 +236,17 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex } def handleZipLine(pkt: ZipLineMessage): Unit = { - val ZipLineMessage(playerGuid, forwards, action, pathId, pos) = pkt - continent.zipLinePaths.find(x => x.PathId == pathId) match { - case Some(path) if path.IsTeleporter => + ops.handleZipLine(pkt) match { + case GeneralOperations.ZiplineBehavior.Teleporter => sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel") - val endPoint = path.ZipLinePoints.last - sendResponse(ZipLineMessage(PlanetSideGUID(0), forwards, 0, pathId, pos)) - //todo: send to zone to show teleport animation to all clients - sendResponse(PlayerStateShiftMessage(ShiftState(0, endPoint, (player.Orientation.z + player.FacingYawUpper) % 360f, None))) - case Some(_) => + case GeneralOperations.ZiplineBehavior.Zipline => sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_motion") - action match { - case 0 => - //travel along the zipline in the direction specified - sendResponse(ZipLineMessage(playerGuid, forwards, action, pathId, pos)) - case 1 => - //disembark from zipline at destination - sendResponse(ZipLineMessage(playerGuid, forwards, action, 0, pos)) - case 2 => - //get off by force - sendResponse(ZipLineMessage(playerGuid, forwards, action, 0, pos)) - case _ => - log.warn( - s"${player.Name} tried to do something with a zipline but can't handle it. forwards: $forwards action: $action pathId: $pathId zone: ${continent.Number} / ${continent.id}" - ) - } - case _ => - log.warn(s"${player.Name} couldn't find a zipline path $pathId in zone ${continent.id}") + case GeneralOperations.ZiplineBehavior.Unsupported => + log.warn( + s"${player.Name} tried to do something with a zipline but can't handle it. action: ${pkt.action}, pathId: ${pkt.path_id}, zone: ${continent.id}" + ) + case GeneralOperations.ZiplineBehavior.NotFound => + log.warn(s"${player.Name} couldn't find a zipline path ${pkt.path_id} in zone ${continent.id}") } } @@ -287,13 +255,11 @@ 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) => - /* line 1a: player is admin (and overrules other access requirements) */ - /* line 1b: vehicle and player (as the owner) acknowledge each other */ - /* line 1c: vehicle is the same faction as player, is ownable, and either the owner is absent or the vehicle is destroyed */ + /* line 1a: vehicle and player (as the owner) acknowledge each other */ + /* line 1b: vehicle is the same faction as player, is ownable, and either the owner is absent or the vehicle is destroyed */ /* line 2: vehicle is not mounted in anything or, if it is, its seats are empty */ if ( - (session.account.gm || - (player.avatar.vehicle.contains(objectGuid) && vehicle.OwnerGuid.contains(player.GUID)) || + ((avatar.vehicle.contains(objectGuid) && vehicle.OwnerGuid.contains(player.GUID)) || (player.Faction == vehicle.Faction && (vehicle.Definition.CanBeOwned.nonEmpty && (vehicle.OwnerGuid.isEmpty || continent.GUID(vehicle.OwnerGuid.get).isEmpty) || vehicle.Destroyed))) && @@ -312,7 +278,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 @@ -324,14 +290,14 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex } case Some(obj: Deployable) => - if (session.account.gm || obj.OwnerGuid.isEmpty || obj.OwnerGuid.contains(player.GUID) || obj.Destroyed) { + if (obj.OwnerGuid.isEmpty || obj.OwnerGuid.contains(player.GUID) || obj.Destroyed) { obj.Actor ! Deployable.Deconstruct() } else { log.warn(s"RequestDestroy: ${player.Name} must own the deployable in order to deconstruct it") } 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}") @@ -341,99 +307,20 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex } def handleMoveItem(pkt: MoveItemMessage): Unit = { - val MoveItemMessage(itemGuid, sourceGuid, destinationGuid, dest, _) = pkt - ( - continent.GUID(sourceGuid), - continent.GUID(destinationGuid), - sessionLogic.validObject(itemGuid, decorator = "MoveItem") - ) match { - case ( - Some(source: PlanetSideServerObject with Container), - Some(destination: PlanetSideServerObject with Container), - Some(item: Equipment) - ) => - ContainableMoveItem(player.Name, source, destination, item, destination.SlotMapResolution(dest)) - case (None, _, _) => - log.error( - s"MoveItem: ${player.Name} wanted to move $itemGuid from $sourceGuid, but could not find source object" - ) - case (_, None, _) => - log.error( - s"MoveItem: ${player.Name} wanted to move $itemGuid to $destinationGuid, but could not find destination object" - ) - case (_, _, None) => () - case _ => - log.error( - s"MoveItem: ${player.Name} wanted to move $itemGuid from $sourceGuid to $destinationGuid, but multiple problems were encountered" - ) - } + ops.handleMoveItem(pkt) } def handleLootItem(pkt: LootItemMessage): Unit = { - val LootItemMessage(itemGuid, targetGuid) = pkt - (sessionLogic.validObject(itemGuid, decorator = "LootItem"), continent.GUID(targetGuid)) match { - case (Some(item: Equipment), Some(destination: PlanetSideServerObject with Container)) => - //figure out the source - ( - { - val findFunc: PlanetSideServerObject with Container => Option[ - (PlanetSideServerObject with Container, Option[Int]) - ] = ops.findInLocalContainer(itemGuid) - findFunc(player.avatar.locker) - .orElse(findFunc(player)) - .orElse(ops.accessedContainer match { - case Some(parent: PlanetSideServerObject) => - findFunc(parent) - case _ => - None - }) - }, - destination.Fit(item) - ) match { - case (Some((source, Some(_))), Some(dest)) => - ContainableMoveItem(player.Name, source, destination, item, dest) - case (None, _) => - log.error(s"LootItem: ${player.Name} can not find where $item is put currently") - case (_, None) => - log.error(s"LootItem: ${player.Name} can not find anywhere to put $item in $destination") - case _ => - log.error( - s"LootItem: ${player.Name}wanted to move $itemGuid to $targetGuid, but multiple problems were encountered" - ) - } - case (Some(obj), _) => - log.error(s"LootItem: item $obj is (probably) not lootable to ${player.Name}") - case (None, _) => () - case (_, None) => - log.error(s"LootItem: ${player.Name} can not find where to put $itemGuid") - } + ops.handleLootItem(pkt) } def handleAvatarImplant(pkt: AvatarImplantMessage): Unit = { - val AvatarImplantMessage(_, action, slot, status) = pkt - if (action == ImplantAction.Activation) { - if (sessionLogic.zoning.zoningStatus == Zoning.Status.Deconstructing) { - //do not activate; play deactivation sound instead - sessionLogic.zoning.spawn.stopDeconstructing() - avatar.implants(slot).collect { - case implant if implant.active => - avatarActor ! AvatarActor.DeactivateImplant(implant.definition.implantType) - case implant => - sendResponse(PlanetsideAttributeMessage(player.GUID, 28, implant.definition.implantType.value * 2)) - } - } else { + ops.handleAvatarImplant(pkt) match { + case GeneralOperations.ImplantActivationBehavior.Activate | GeneralOperations.ImplantActivationBehavior.Deactivate => sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_implant") - avatar.implants(slot) match { - case Some(implant) => - if (status == 1) { - avatarActor ! AvatarActor.ActivateImplant(implant.definition.implantType) - } else { - avatarActor ! AvatarActor.DeactivateImplant(implant.definition.implantType) - } - case _ => - log.error(s"AvatarImplantMessage: ${player.Name} has an unknown implant in $slot") - } - } + case GeneralOperations.ImplantActivationBehavior.NotFound => + log.error(s"AvatarImplantMessage: ${player.Name} has an unknown implant in ${pkt.implantSlot}") + case _ => () } } @@ -448,47 +335,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 => () } } @@ -519,19 +406,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, player, obj) case Some(obj) => log.warn(s"DeployObject: what is $obj, ${player.Name}? It's not a construction tool!") case None => @@ -568,7 +443,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 @@ -748,7 +623,8 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex } val curr = System.currentTimeMillis() (target1, t, target2) match { - case (None, _, _) => () + case (None, _, _) => + () case (Some(us: PlanetSideServerObject with Vitality with FactionAffinity), PlanetSideGUID(0), _) => if (updateCollisionHistoryForTarget(us, curr)) { @@ -820,10 +696,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}" @@ -845,13 +719,7 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex } def handleCreateShortcut(pkt: CreateShortcutMessage): Unit = { - val CreateShortcutMessage(_, slot, shortcutOpt) = pkt - shortcutOpt match { - case Some(shortcut) => - avatarActor ! AvatarActor.AddShortcut(slot - 1, shortcut) - case None => - avatarActor ! AvatarActor.RemoveShortcut(slot - 1) - } + ops.handleCreateShortcut(pkt) } def handleChangeShortcutBank(pkt: ChangeShortcutBankMessage): Unit = { @@ -901,39 +769,11 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex } def handleObjectDetected(pkt: ObjectDetectedMessage): Unit = { - val ObjectDetectedMessage(_, _, _, targets) = pkt - sessionLogic.shooting.FindWeapon.foreach { - case weapon if weapon.Projectile.AutoLock => - //projectile with auto-lock instigates a warning on the target - val detectedTargets = sessionLogic.shooting.FindDetectedProjectileTargets(targets) - val mode = 7 + (if (weapon.Projectile == GlobalDefinitions.wasp_rocket_projectile) 1 else 0) - detectedTargets.foreach { target => - continent.AvatarEvents ! AvatarServiceMessage(target, AvatarAction.ProjectileAutoLockAwareness(mode)) - } - case _ => () - } + ops.handleObjectDetected(pkt) } def handleTargetingImplantRequest(pkt: TargetingImplantRequest): Unit = { - val TargetingImplantRequest(list) = pkt - val targetInfo: List[TargetInfo] = list.flatMap { x => - continent.GUID(x.target_guid) match { - case Some(player: Player) => - val health = player.Health.toFloat / player.MaxHealth - val armor = if (player.MaxArmor > 0) { - player.Armor.toFloat / player.MaxArmor - } else { - 0 - } - Some(TargetInfo(player.GUID, health, armor)) - case _ => - log.warn( - s"TargetingImplantRequest: the info that ${player.Name} requested for target ${x.target_guid} is not for a player" - ) - None - } - } - sendResponse(TargetingInfoMessage(targetInfo)) + ops.handleTargetingImplantRequest(pkt) } def handleHitHint(pkt: HitHint): Unit = { @@ -988,13 +828,10 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex session = session.copy(flying = flying) } - def handleSetSpectator(spectator: Boolean): Unit = { - session.player.spectator = spectator - } + def handleSetSpectator(spectator: Boolean): Unit = { /* normal players can not flag spectate */ } def handleKick(player: Player, time: Option[Long]): Unit = { - ops.administrativeKick(player) - sessionLogic.accountPersistence ! AccountPersistenceService.Kick(player.Name, time) + ops.administrativeKick(player, time) } def handleSilenced(isSilenced: Boolean): Unit = { @@ -1015,413 +852,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 @@ -1541,11 +971,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..2a6249d62 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) - sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistence + ops.MountingAction(tplayer, obj, seatNumber) + sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistenceFunc 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) - sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistence + ops.MountingAction(tplayer, obj, seatNumber) + sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistenceFunc 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)) @@ -245,71 +148,71 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=113, obj.Capacitor)) sessionLogic.general.accessContainer(obj) ops.updateWeaponAtSeatPosition(obj, seatNumber) - sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistence + sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistenceFunc 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)) sendResponse(PlanetsideAttributeMessage(obj_guid, obj.Definition.shieldUiAttribute, obj.Shields)) sessionLogic.general.accessContainer(obj) ops.updateWeaponAtSeatPosition(obj, seatNumber) - sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistence + sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistenceFunc 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..f42e82c3d 100644 --- a/src/main/scala/net/psforever/actors/session/normal/TerminalHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/TerminalHandlerLogic.scala @@ -5,14 +5,11 @@ import akka.actor.{ActorContext, typed} import net.psforever.actors.session.AvatarActor import net.psforever.actors.session.support.{SessionData, SessionTerminalHandlers, TerminalHandlerFunctions} import net.psforever.login.WorldSession.{BuyNewEquipmentPutInInventory, SellEquipmentFromInventory} -import net.psforever.objects.{GlobalDefinitions, Player, Vehicle} +import net.psforever.objects.{Player, Vehicle} import net.psforever.objects.guid.TaskWorkflow -import net.psforever.objects.serverobject.pad.VehicleSpawnPad -import net.psforever.objects.serverobject.terminals.{ProximityUnit, Terminal} -import net.psforever.objects.sourcing.AmenitySource -import net.psforever.objects.vital.TerminalUsedActivity -import net.psforever.packet.game.{FavoritesAction, FavoritesRequest, ItemTransactionMessage, ItemTransactionResultMessage, ProximityTerminalUseMessage, UnuseItemMessage} -import net.psforever.types.{TransactionType, Vector3} +import net.psforever.objects.serverobject.terminals.Terminal +import net.psforever.packet.game.{FavoritesRequest, ItemTransactionMessage, ItemTransactionResultMessage, ProximityTerminalUseMessage} +import net.psforever.types.TransactionType object TerminalHandlerLogic { def apply(ops: SessionTerminalHandlers): TerminalHandlerLogic = { @@ -26,46 +23,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,48 +85,10 @@ 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) + .collect { + case _: Vehicle => + avatarActor ! AvatarActor.UpdatePurchaseTime(vehicle.Definition) } ops.lastTerminalOrderFulfillment = true diff --git a/src/main/scala/net/psforever/actors/session/normal/VehicleLogic.scala b/src/main/scala/net/psforever/actors/session/normal/VehicleLogic.scala index 5b8ab62d1..5ed83c9b1 100644 --- a/src/main/scala/net/psforever/actors/session/normal/VehicleLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/VehicleLogic.scala @@ -214,7 +214,8 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex case Some(mount: Mountable) => (o, mount.PassengerInSeat(player)) case _ => (None, None) }) match { - case (None, None) | (_, None) | (Some(_: Vehicle), Some(0)) => () + case (None, None) | (_, None) | (Some(_: Vehicle), Some(0)) => + () case _ => sessionLogic.persist() sessionLogic.turnCounterFunc(player.GUID) 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..0d33f5aba 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,31 @@ // 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.{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.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.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.{BoomerDeployable, BoomerTrigger, Player, SpecialEmp, Tool, Vehicle} 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.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._ +import net.psforever.packet.game.{AIDamage, AvatarGrenadeStateMessage, ChangeAmmoMessage, ChangeFireModeMessage, ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, HitMessage, LashMessage, LongRangeProjectileInfoMessage, ProjectileStateMessage, ReloadMessage, SplashHitMessage, UplinkRequest, WeaponDelayFireMessage, WeaponDryFireMessage, WeaponFireMessage, WeaponLazeTargetPositionMessage} +import net.psforever.types.Vector3 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 +34,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 +54,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") } @@ -218,15 +64,15 @@ class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit if (ops.shooting.isEmpty) { sessionLogic.findEquipment(item_guid) match { case Some(tool: Tool) if player.VehicleSeated.isEmpty => - fireStateStartWhenPlayer(tool, item_guid) + ops.fireStateStartWhenPlayer(tool, item_guid) case Some(tool: Tool) => - fireStateStartWhenMounted(tool, item_guid) + ops.fireStateStartWhenMounted(tool, item_guid) case Some(_) if player.VehicleSeated.isEmpty => - fireStateStartSetup(item_guid) - fireStateStartPlayerMessages(item_guid) + ops.fireStateStartSetup(item_guid) + ops.fireStateStartPlayerMessages(item_guid) case Some(_) => - fireStateStartSetup(item_guid) - fireStateStartMountedMessages(item_guid) + ops.fireStateStartSetup(item_guid) + ops.fireStateStartMountedMessages(item_guid) case None => log.warn(s"ChangeFireState_Start: can not find $item_guid") } @@ -241,9 +87,9 @@ class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit ops.shooting -= item_guid sessionLogic.findEquipment(item_guid) match { case Some(tool: Tool) if player.VehicleSeated.isEmpty => - fireStateStopWhenPlayer(tool, item_guid) + ops.fireStateStopWhenPlayer(tool, item_guid) case Some(tool: Tool) => - fireStateStopWhenMounted(tool, item_guid) + ops.fireStateStopWhenMounted(tool, item_guid) case Some(trigger: BoomerTrigger) => ops.fireStateStopPlayerMessages(item_guid) continent.GUID(trigger.Companion).collect { @@ -265,1115 +111,122 @@ class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit val ReloadMessage(item_guid, _, unk1) = pkt ops.FindContainedWeapon match { case (Some(obj: Player), tools) => - handleReloadWhenPlayer(item_guid, obj, tools, unk1) + ops.handleReloadWhenPlayer(item_guid, obj, tools, unk1) case (Some(obj: PlanetSideServerObject with Container), tools) => - handleReloadWhenMountable(item_guid, obj, tools, unk1) + ops.handleReloadWhenMountable(item_guid, obj, tools, unk1) case (_, _) => log.warn(s"ReloadMessage: either can not find $item_guid or the object found was not a Tool") } } 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 = { val LongRangeProjectileInfoMessage(guid, _, _) = pkt ops.FindContainedWeapon match { case (Some(_: Vehicle), weapons) - if weapons.exists { _.GUID == guid } => () //now what? + if weapons.exists(_.GUID == guid) => () //now what? case _ => () } } 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) { + ops.handleProxyDamage(pkt.projectile_guid, pkt.hit_info.map(_.hit_pos).getOrElse(Vector3.Zero)).foreach { + case (target, proxy, hitPos, _) => + 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) - } - //direct_victim_uid - sessionLogic.validObject(direct_victim_uid, decorator = "SplashHit/direct_victim") match { - case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) => - CheckForHitPositionDiscrepancy(projectile_guid, explosion_pos, target) - ResolveProjectileInteraction(projectile, resolution1, target, target.Position).collect { resprojectile => - addShotsLanded(resprojectile.cause.attribution, shots = 1) - sessionLogic.handleDealingDamage(target, resprojectile) - } - case _ => () - } - //other victims - targets.foreach(elem => { - sessionLogic.validObject(elem.uid, decorator = "SplashHit/other_victims") match { - case Some(target: PlanetSideGameObject with FactionAffinity with Vitality) => - CheckForHitPositionDiscrepancy(projectile_guid, explosion_pos, target) - ResolveProjectileInteraction(projectile, resolution2, target, explosion_pos).collect { resprojectile => - addShotsLanded(resprojectile.cause.attribution, shots = 1) - sessionLogic.handleDealingDamage(target, resprojectile) - } - case _ => () - } - }) - //... - HandleDamageProxy(projectile, projectile_guid, explosion_pos) - if ( - projectile.profile.HasJammedEffectDuration || - projectile.profile.JammerProjectile || - projectile.profile.SympatheticExplosion - ) { - //can also substitute 'projectile.profile' for 'SpecialEmp.emp' - Zone.serverSideDamage( - continent, - player, - SpecialEmp.emp, - SpecialEmp.createEmpInteraction(SpecialEmp.emp, explosion_pos), - SpecialEmp.prepareDistanceCheck(player, explosion_pos, player.Faction), - SpecialEmp.findAllBoomers(profile.DamageRadius) - ) - } - if (profile.ExistsOnRemoteClients && projectile.HasGUID) { - //cleanup - continent.Projectile ! ZoneProjectile.Remove(projectile.GUID) - } - case None => () + 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(projectileGuid, hitPos, target) + ops.resolveProjectileInteraction(target, projectile, resolution1, hitPos) + } + others.foreach { + case (target, _, hitPos, _) => + ops.checkForHitPositionDiscrepancy(projectileGuid, hitPos, target) + ops.resolveProjectileInteraction(target, projectile, resolution2, hitPos) + } + //... + 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) + } + } + //... + ops.handleProxyDamage(pkt.projectile_uid, pkt.projectile_pos).foreach { + case (target, proxy, hitPos, _) => + ops.checkForHitPositionDiscrepancy(proxy.GUID, hitPos, target) } } 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(pkt.attacker_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 - */ - private def FireCycleCleanup(tool: Tool): Unit = { - //TODO replaced by more appropriate functionality in the future - val tdef = tool.Definition - if (GlobalDefinitions.isGrenade(tdef)) { - val ammoType = tool.AmmoType - FindEquipmentStock(player, FindToolThatUses(ammoType), 3, CountGrenades).reverse match { //do not search sidearm holsters - case Nil => - log.info(s"${player.Name} has no more $ammoType grenades to throw") - RemoveOldEquipmentFromInventory(player)(tool) - - case x :: xs => //this is similar to ReloadMessage - val box = x.obj.asInstanceOf[Tool] - val tailReloadValue: Int = if (xs.isEmpty) { 0 } - else { xs.map(_.obj.asInstanceOf[Tool].Magazine).sum } - val sumReloadValue: Int = box.Magazine + tailReloadValue - val actualReloadValue = if (sumReloadValue <= 3) { - RemoveOldEquipmentFromInventory(player)(x.obj) - sumReloadValue - } else { - ModifyAmmunition(player)(box.AmmoSlot.Box, 3 - tailReloadValue) - 3 - } - log.info(s"${player.Name} found $actualReloadValue more $ammoType grenades to throw") - ModifyAmmunition(player)( - tool.AmmoSlot.Box, - -actualReloadValue - ) //grenade item already in holster (negative because empty) - xs.foreach(item => { RemoveOldEquipmentFromInventory(player)(item.obj) }) - } - } else if (tdef == GlobalDefinitions.phoenix) { - RemoveOldEquipmentFromInventory(player)(tool) - } - } - - /** - * Given an object that contains a box of amunition in its `Inventory` at a certain location, - * change the amount of ammunition within that box. - * @param obj the `Container` - * @param box an `AmmoBox` to modify - * @param reloadValue the value to modify the `AmmoBox`; - * subtracted from the current `Capacity` of `Box` - */ - private def ModifyAmmunition(obj: PlanetSideGameObject with Container)(box: AmmoBox, reloadValue: Int): Unit = { - val capacity = box.Capacity - reloadValue - box.Capacity = capacity - sendResponse(InventoryStateMessage(box.GUID, obj.GUID, capacity)) - } - - /** - * 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 - */ - private def fireStateStartSetup(itemGuid: PlanetSideGUID): Unit = { - ops.prefire -= itemGuid - ops.shooting += itemGuid - ops.shootingStart += itemGuid -> System.currentTimeMillis() - } - - private def fireStateStartChargeMode(tool: Tool): Unit = { - //charge ammunition drain - tool.FireMode match { - case mode: ChargeFireModeDefinition => - sessionLogic.general.progressBarValue = Some(0f) - sessionLogic.general.progressBarUpdate = context.system.scheduler.scheduleOnce( - (mode.Time + mode.DrainInterval) milliseconds, - context.self, - CommonMessages.ProgressEvent(1f, () => {}, Tools.ChargeFireMode(player, tool), mode.DrainInterval) - ) - case _ => () - } - } - - private def fireStateStartPlayerMessages(itemGuid: PlanetSideGUID): Unit = { - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.ChangeFireState_Start(player.GUID, itemGuid) - ) - } - - private def fireStateStartMountedMessages(itemGuid: PlanetSideGUID): Unit = { - sessionLogic.findContainedEquipment()._1.collect { - case turret: FacilityTurret if continent.map.cavern => - turret.Actor ! VanuSentry.ChangeFireStart - } - continent.VehicleEvents ! VehicleServiceMessage( - continent.id, - VehicleAction.ChangeFireState_Start(player.GUID, itemGuid) - ) - } - - 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) - } - - private def fireStateStartWhenPlayer(tool: Tool, itemGuid: PlanetSideGUID): Unit = { - if (allowFireStateChangeStart(tool, itemGuid)) { - fireStateStartSetup(itemGuid) - //special case - suppress the decimator's alternate fire mode, by projectile - if (tool.Projectile != GlobalDefinitions.phoenix_missile_guided_projectile) { - fireStateStartPlayerMessages(itemGuid) - } - fireStateStartChargeMode(tool) - } else { - enforceEmptyMagazine(tool, itemGuid) - } - } - - private def fireStateStartWhenMounted(tool: Tool, itemGuid: PlanetSideGUID): Unit = { - if (allowFireStateChangeStart(tool, itemGuid)) { - fireStateStartSetup(itemGuid) - fireStateStartMountedMessages(itemGuid) - fireStateStartChargeMode(tool) - } else { - enforceEmptyMagazine(tool, itemGuid) - } - } - - /* - used by ChangeFireStateMessage_Stop handling - */ - private def fireStateStopUpdateChargeAndCleanup(tool: Tool): Unit = { - tool.FireMode match { - case _: ChargeFireModeDefinition => - sendResponse(QuantityUpdateMessage(tool.AmmoSlot.Box.GUID, tool.Magazine)) - case _ => () - } - if (tool.Magazine == 0) { - FireCycleCleanup(tool) - } - } - - private def fireStateStopWhenPlayer(tool: Tool, itemGuid: PlanetSideGUID): Unit = { - //the decimator does not send a ChangeFireState_Start on the last shot; heaven knows why - //suppress the decimator's alternate fire mode, however - if ( - tool.Definition == GlobalDefinitions.phoenix && - tool.Projectile != GlobalDefinitions.phoenix_missile_guided_projectile - ) { - fireStateStartPlayerMessages(itemGuid) - } - fireStateStopUpdateChargeAndCleanup(tool) - ops.fireStateStopPlayerMessages(itemGuid) - } - - private def fireStateStopWhenMounted(tool: Tool, itemGuid: PlanetSideGUID): Unit = { - fireStateStopUpdateChargeAndCleanup(tool) - ops.fireStateStopMountedMessages(itemGuid) - } - - /* - used by ReloadMessage handling - */ - private def reloadPlayerMessages(itemGuid: PlanetSideGUID): Unit = { - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.Reload(player.GUID, itemGuid) - ) - } - - private def reloadVehicleMessages(itemGuid: PlanetSideGUID): Unit = { - continent.VehicleEvents ! VehicleServiceMessage( - continent.id, - VehicleAction.Reload(player.GUID, itemGuid) - ) - } - - 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( - itemGuid, - obj, - tools, - unk1, - RemoveOldEquipmentFromInventory(obj)(_), - ModifyAmmunition(obj)(_, _), - reloadPlayerMessages - ) - } - - private def handleReloadWhenMountable( - itemGuid: PlanetSideGUID, - obj: PlanetSideServerObject with Container, - tools: Set[Tool], - unk1: Int - ): Unit = { - handleReloadProcedure( - itemGuid, - obj, - tools, - unk1, - RemoveOldEquipmentFromInventory(obj)(_), - 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..e96e6cee6 100644 --- a/src/main/scala/net/psforever/actors/session/spectator/AvatarHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/spectator/AvatarHandlerLogic.scala @@ -221,7 +221,7 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A case AvatarResponse.HitHint(sourceGuid) if player.isAlive => sendResponse(HitHint(sourceGuid, guid)) - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_dmg") + sessionLogic.zoning.CancelZoningProcess() case AvatarResponse.Destroy(victim, killer, weapon, pos) => // guid = victim // killer = killer @@ -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)) @@ -430,7 +426,7 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A if isNotSameTarget && ops.lastSeenStreamMessage.get(guid.guid).exists { _.visible } => sendResponse(ReloadMessage(itemGuid, ammo_clip=1, unk1=0)) - case AvatarResponse.Killed(mount) => + case AvatarResponse.Killed(_, mount) => //log and chat messages val cause = player.LastDamage.flatMap { damage => val interaction = damage.interaction @@ -451,8 +447,7 @@ 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) + sessionLogic.zoning.CancelZoningProcess() //player state changes AvatarActor.updateToolDischargeFor(avatar) @@ -515,7 +510,7 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A case AvatarResponse.EnvironmentalDamage(_, _, _) => //TODO damage marker? - sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_dmg") + sessionLogic.zoning.CancelZoningProcess() case AvatarResponse.DropItem(pkt) if isNotSameTarget => sendResponse(pkt) 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..a75ea4dcc 100644 --- a/src/main/scala/net/psforever/actors/session/spectator/ChatLogic.scala +++ b/src/main/scala/net/psforever/actors/session/spectator/ChatLogic.scala @@ -9,6 +9,8 @@ import net.psforever.objects.Session import net.psforever.packet.game.{ChatMsg, SetChatFilterMessage} import net.psforever.services.chat.SpectatorChannel import net.psforever.types.ChatMessageType +import net.psforever.types.ChatMessageType.{CMT_TOGGLESPECTATORMODE, CMT_TOGGLE_GM} +import net.psforever.zones.Zones import scala.collection.Seq @@ -19,6 +21,8 @@ object ChatLogic { } class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) extends ChatFunctions { + ops.transitoryCommandEntered = None + def sessionLogic: SessionData = ops.sessionLogic def handleChatMsg(message: ChatMsg): Unit = { @@ -31,11 +35,10 @@ 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, _, _) => () + sessionLogic.zoning.maintainInitialGmState = false case (CMT_CULLWATERMARK, _, contents) => ops.commandWatermark(contents) @@ -56,19 +59,19 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext commandToggleSpectatorMode(contents = "off") case (CMT_OPEN, _, _) => - ops.commandSendToRecipient(session, message, SpectatorChannel) + ops.commandSendToRecipient(session, spectatorColoredMessage(message), SpectatorChannel) case (CMT_VOICE, _, contents) => ops.commandVoice(session, message, contents, SpectatorChannel) case (CMT_TELL, _, _) => - ops.commandTellOrIgnore(session, message, SpectatorChannel) + ops.commandTellOrIgnore(session, spectatorColoredMessage(message), SpectatorChannel) case (CMT_BROADCAST, _, _) => - ops.commandSendToRecipient(session, message, SpectatorChannel) + ops.commandSendToRecipient(session, spectatorColoredMessage(message), SpectatorChannel) case (CMT_PLATOON, _, _) => - ops.commandSendToRecipient(session, message, SpectatorChannel) + ops.commandSendToRecipient(session, spectatorColoredMessage(message), SpectatorChannel) case (CMT_GMTELL, _, _) => ops.commandSend(session, message, SpectatorChannel) @@ -79,8 +82,9 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext case (CMT_WHO | CMT_WHO_CSR | CMT_WHO_CR | CMT_WHO_PLATOONLEADERS | CMT_WHO_SQUADLEADERS | CMT_WHO_TEAMS, _, _) => ops.commandWho(session) - case (CMT_ZONE, _, contents) => - ops.commandZone(message, contents) + case (CMT_ZONE, _, _) => + commandToggleSpectatorMode(contents = "off") + ops.commandZone(message, Zones.sanctuaryZoneId(player.Faction)) case (CMT_WARP, _, contents) => ops.commandWarp(session, message, contents) @@ -115,6 +119,16 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext } } + private def spectatorColoredMessage(message: ChatMsg): ChatMsg = { + if (message.contents.nonEmpty) { + val colorlessText = message.contents.replaceAll("//#\\d", "").trim + val colorCodedText = s"/#5$colorlessText/#0" + message.copy(recipient = s"", contents = colorCodedText) + } else { + message + } + } + private def customCommandMessages( message: ChatMsg, session: Session @@ -136,11 +150,21 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext } } - private def commandToggleSpectatorMode(contents: String): Unit = { - contents.toLowerCase() match { - case "off" | "of" => - context.self ! SessionActor.SetMode(NormalMode) - case _ => () - } + private def commandToggleSpectatorMode(contents: String): Boolean = { + ops.transitoryCommandEntered + .collect { + case CMT_TOGGLESPECTATORMODE => true + case CMT_TOGGLE_GM => false + } + .getOrElse { + contents.toLowerCase() match { + case "off" | "of" => + ops.transitoryCommandEntered = Some(CMT_TOGGLESPECTATORMODE) + context.self ! SessionActor.SetMode(NormalMode) + true + case _ => + false + } + } } } 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..869369fc7 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.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, RequestDestroyMessage, TargetingImplantRequest, 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, Vector3} object GeneralLogic { def apply(ops: GeneralOperations): GeneralLogic = { @@ -52,7 +47,7 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex yaw, pitch, yawUpper, - _/*seqTime*/, + seqTime, _, isCrouching, isJumping, @@ -63,6 +58,7 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex )= pkt sessionLogic.persist() sessionLogic.turnCounterFunc(avatarGuid) + sessionLogic.updateBlockMap(player, pos) ops.fallHeightTracker(pos.z) // if (isCrouching && !player.Crouching) { // //dev stuff goes here @@ -74,6 +70,24 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex player.Crouching = isCrouching player.Jumping = isJumping player.Cloaked = player.ExoSuit == ExoSuitType.Infiltration && isCloaking + continent.AvatarEvents ! AvatarServiceMessage( + "spectator", + AvatarAction.PlayerState( + avatarGuid, + player.Position, + player.Velocity, + yaw, + pitch, + yawUpper, + seqTime, + isCrouching, + isJumping, + jump_thrust = false, + is_cloaked = isCloaking, + spectator = false, + weaponInHand = false + ) + ) if (player.death_by == -1) { sessionLogic.kickedByAdministration() } @@ -106,31 +120,15 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex def handleAvatarJump(pkt: AvatarJumpMessage): Unit = { /* intentionally blank */ } def handleZipLine(pkt: ZipLineMessage): Unit = { - val ZipLineMessage(playerGuid, forwards, action, pathId, pos) = pkt - continent.zipLinePaths.find(x => x.PathId == pathId) match { - case Some(path) if path.IsTeleporter => - val endPoint = path.ZipLinePoints.last - sendResponse(ZipLineMessage(PlanetSideGUID(0), forwards, 0, pathId, pos)) - //todo: send to zone to show teleport animation to all clients - sendResponse(PlayerStateShiftMessage(ShiftState(0, endPoint, (player.Orientation.z + player.FacingYawUpper) % 360f, None))) - case Some(_) => - action match { - case 0 => - //travel along the zipline in the direction specified - sendResponse(ZipLineMessage(playerGuid, forwards, action, pathId, pos)) - case 1 => - //disembark from zipline at destination! - sendResponse(ZipLineMessage(playerGuid, forwards, action, 0, pos)) - case 2 => - //get off by force - sendResponse(ZipLineMessage(playerGuid, forwards, action, 0, pos)) - case _ => - log.warn( - s"${player.Name} tried to do something with a zipline but can't handle it. forwards: $forwards action: $action pathId: $pathId zone: ${continent.Number} / ${continent.id}" - ) - } - case _ => - log.warn(s"${player.Name} couldn't find a zipline path $pathId in zone ${continent.id}") + ops.handleZipLine(pkt) match { + case GeneralOperations.ZiplineBehavior.Teleporter | GeneralOperations.ZiplineBehavior.Zipline => + sessionLogic.zoning.CancelZoningProcess() + case GeneralOperations.ZiplineBehavior.Unsupported => + log.warn( + s"${player.Name} tried to do something with a zipline but can't handle it. action: ${pkt.action}, pathId: ${pkt.path_id}, zone: ${continent.id}" + ) + case GeneralOperations.ZiplineBehavior.NotFound => + log.warn(s"${player.Name} couldn't find a zipline path ${pkt.path_id} in zone ${continent.id}") } } @@ -168,11 +166,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 +265,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) @@ -369,34 +362,14 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex def handleTrade(pkt: TradeMessage): Unit = { /* intentionally blank */ } - def handleDisplayedAward(pkt: DisplayedAwardMessage): Unit = { - val DisplayedAwardMessage(_, ribbon, bar) = pkt - log.trace(s"${player.Name} changed the $bar displayed award ribbon to $ribbon") - avatarActor ! AvatarActor.SetRibbon(ribbon, bar) + def handleDisplayedAward(pkt: DisplayedAwardMessage): Unit = { /* intentionally blank */ } + + def handleObjectDetected(pkt: ObjectDetectedMessage): Unit = { + ops.handleObjectDetected(pkt) } - def handleObjectDetected(pkt: ObjectDetectedMessage): Unit = { /* intentionally blank */ } - def handleTargetingImplantRequest(pkt: TargetingImplantRequest): Unit = { - val TargetingImplantRequest(list) = pkt - val targetInfo: List[TargetInfo] = list.flatMap { x => - continent.GUID(x.target_guid) match { - case Some(player: Player) => - val health = player.Health.toFloat / player.MaxHealth - val armor = if (player.MaxArmor > 0) { - player.Armor.toFloat / player.MaxArmor - } else { - 0 - } - Some(TargetInfo(player.GUID, health, armor)) - case _ => - log.warn( - s"TargetingImplantRequest: the info that ${player.Name} requested for target ${x.target_guid} is not for a player" - ) - None - } - } - sendResponse(TargetingInfoMessage(targetInfo)) + ops.handleTargetingImplantRequest(pkt) } def handleHitHint(pkt: HitHint): Unit = { /* intentionally blank */ } @@ -444,8 +417,7 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex } def handleKick(player: Player, time: Option[Long]): Unit = { - administrativeKick(player) - sessionLogic.accountPersistence ! AccountPersistenceService.Kick(player.Name, time) + ops.administrativeKick(player, None) } def handleSilenced(isSilenced: Boolean): Unit = { @@ -460,107 +432,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 - sessionLogic.accountPersistence ! AccountPersistenceService.Kick(tplayer.Name) - } - private def customImplantOff(slot: Int, implant: Implant): Unit = { customImplants = customImplants.updated(slot, implant.copy(active = false)) sendResponse(AvatarImplantMessage(player.GUID, ImplantAction.Activation, slot, 0)) 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..483f13a31 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) @@ -119,6 +119,7 @@ class SpectatorModeLogic(data: SessionData) extends ModeLogic { } // player.spectator = true + player.allowInteraction = false data.chat.JoinChannel(SpectatorChannel) val newPlayer = SpectatorModeLogic.spectatorCharacter(player) newPlayer.LogActivity(player.History.headOption) @@ -152,6 +153,8 @@ class SpectatorModeLogic(data: SessionData) extends ModeLogic { val pguid = player.GUID val sendResponse: PlanetSidePacket => Unit = data.sendResponse // + player.spectator = false + player.allowInteraction = true data.general.stop() player.avatar.shortcuts.slice(1, 4) .zipWithIndex @@ -159,12 +162,11 @@ 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")) zoning.zoneReload = true - zoning.spawn.randomRespawn(0.seconds) //to sanctuary + zoning.spawn.randomRespawn(10.milliseconds) //to sanctuary } } @@ -178,7 +180,7 @@ object SpectatorModeLogic { private def spectatorCharacter(player: Player): Player = { val avatar = player.avatar val newAvatar = avatar.copy( - basic = avatar.basic.copy(name = "spectator"), + basic = avatar.basic, bep = BattleRank.BR18.experience, cep = CommandRank.CR5.experience, certifications = Set(), @@ -212,6 +214,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..ac930161c 100644 --- a/src/main/scala/net/psforever/actors/session/spectator/VehicleHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/spectator/VehicleHandlerLogic.scala @@ -1,19 +1,15 @@ // Copyright (c) 2024 PSForever package net.psforever.actors.session.spectator -import akka.actor.{ActorContext, ActorRef, typed} -import net.psforever.actors.session.AvatarActor +import akka.actor.ActorContext 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.{Tool, Vehicle, Vehicles} +import net.psforever.objects.equipment.Equipment 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.packet.game.{ChangeAmmoMessage, ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, ChildObjectStateMessage, DeadState, DeployRequestMessage, DismountVehicleMsg, FrameVehicleStateMessage, GenericObjectActionMessage, HitHint, InventoryStateMessage, ObjectAttachMessage, ObjectCreateDetailedMessage, ObjectCreateMessage, ObjectDeleteMessage, ObjectDetachMessage, PlanetsideAttributeMessage, ReloadMessage, ServerVehicleOverrideMsg, VehicleStateMessage, WeaponDryFireMessage} import net.psforever.services.vehicle.{VehicleResponse, VehicleServiceResponse} -import net.psforever.types.{BailType, ChatMessageType, PlanetSideGUID, Vector3} +import net.psforever.types.{BailType, PlanetSideGUID, Vector3} object VehicleHandlerLogic { def apply(ops: SessionVehicleHandlers): VehicleHandlerLogic = { @@ -24,9 +20,9 @@ object VehicleHandlerLogic { 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 avatarActor: typed.ActorRef[AvatarActor.Command] = ops.avatarActor - private val galaxyService: ActorRef = ops.galaxyService + //private val galaxyService: ActorRef = ops.galaxyService /** * na @@ -151,9 +147,6 @@ class VehicleHandlerLogic(val ops: SessionVehicleHandlers, implicit val context: 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 @@ -169,14 +162,11 @@ class VehicleHandlerLogic(val ops: SessionVehicleHandlers, implicit val context: //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 { + 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" + case _ => () } - 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 @@ -194,11 +184,6 @@ class VehicleHandlerLogic(val ops: SessionVehicleHandlers, implicit val context: 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)) @@ -211,10 +196,6 @@ class VehicleHandlerLogic(val ops: SessionVehicleHandlers, implicit val context: 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)) @@ -226,13 +207,6 @@ class VehicleHandlerLogic(val ops: SessionVehicleHandlers, implicit val context: 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) @@ -263,100 +237,9 @@ class VehicleHandlerLogic(val ops: SessionVehicleHandlers, implicit val context: 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 => @@ -384,16 +267,4 @@ class VehicleHandlerLogic(val ops: SessionVehicleHandlers, implicit val context: 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/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..89a95dac1 100644 --- a/src/main/scala/net/psforever/actors/session/spectator/WeaponAndProjectileLogic.scala +++ b/src/main/scala/net/psforever/actors/session/spectator/WeaponAndProjectileLogic.scala @@ -3,15 +3,9 @@ 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.services.avatar.{AvatarAction, AvatarServiceMessage} -import net.psforever.types.PlanetSideGUID +import net.psforever.objects.{BoomerDeployable, BoomerTrigger, Tool} +import net.psforever.packet.game.{AIDamage, AvatarGrenadeStateMessage, ChangeAmmoMessage, ChangeFireModeMessage, ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, HitMessage, LashMessage, LongRangeProjectileInfoMessage, ProjectileStateMessage, ReloadMessage, SplashHitMessage, UplinkRequest, UplinkRequestType, UplinkResponse, WeaponDelayFireMessage, WeaponDryFireMessage, WeaponFireMessage, WeaponLazeTargetPositionMessage} object WeaponAndProjectileLogic { def apply(ops: WeaponAndProjectileOperations): WeaponAndProjectileLogic = { @@ -59,9 +53,9 @@ class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit ops.shooting -= item_guid sessionLogic.findEquipment(item_guid) match { case Some(tool: Tool) if player.VehicleSeated.isEmpty => - fireStateStopWhenPlayer(tool, item_guid) + ops.fireStateStopWhenPlayer(tool, item_guid) case Some(tool: Tool) => - fireStateStopWhenMounted(tool, item_guid) + ops.fireStateStopWhenMounted(tool, item_guid) case Some(trigger: BoomerTrigger) => ops.fireStateStopPlayerMessages(item_guid) continent.GUID(trigger.Companion).collect { @@ -85,32 +79,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 */ } @@ -122,97 +91,4 @@ class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit def handleLashHit(pkt: LashMessage): Unit = { /* intentionally blank */ } def handleAIDamage(pkt: AIDamage): Unit = { /* intentionally blank */ } - - /* support code */ - - /** - * After a weapon has finished shooting, determine if it needs to be sorted in a special way. - * @param tool a weapon - */ - private def FireCycleCleanup(tool: Tool): Unit = { - //TODO replaced by more appropriate functionality in the future - val tdef = tool.Definition - if (GlobalDefinitions.isGrenade(tdef)) { - val ammoType = tool.AmmoType - FindEquipmentStock(player, FindToolThatUses(ammoType), 3, CountGrenades).reverse match { //do not search sidearm holsters - case Nil => - log.info(s"${player.Name} has no more $ammoType grenades to throw") - RemoveOldEquipmentFromInventory(player)(tool) - - case x :: xs => //this is similar to ReloadMessage - val box = x.obj.asInstanceOf[Tool] - val tailReloadValue: Int = if (xs.isEmpty) { 0 } - else { xs.map(_.obj.asInstanceOf[Tool].Magazine).sum } - val sumReloadValue: Int = box.Magazine + tailReloadValue - val actualReloadValue = if (sumReloadValue <= 3) { - RemoveOldEquipmentFromInventory(player)(x.obj) - sumReloadValue - } else { - ModifyAmmunition(player)(box.AmmoSlot.Box, 3 - tailReloadValue) - 3 - } - log.info(s"${player.Name} found $actualReloadValue more $ammoType grenades to throw") - ModifyAmmunition(player)( - tool.AmmoSlot.Box, - -actualReloadValue - ) //grenade item already in holster (negative because empty) - xs.foreach(item => { RemoveOldEquipmentFromInventory(player)(item.obj) }) - } - } else if (tdef == GlobalDefinitions.phoenix) { - RemoveOldEquipmentFromInventory(player)(tool) - } - } - - /** - * Given an object that contains a box of amunition in its `Inventory` at a certain location, - * change the amount of ammunition within that box. - * @param obj the `Container` - * @param box an `AmmoBox` to modify - * @param reloadValue the value to modify the `AmmoBox`; - * subtracted from the current `Capacity` of `Box` - */ - private def ModifyAmmunition(obj: PlanetSideGameObject with Container)(box: AmmoBox, reloadValue: Int): Unit = { - val capacity = box.Capacity - reloadValue - box.Capacity = capacity - sendResponse(InventoryStateMessage(box.GUID, obj.GUID, capacity)) - } - - private def fireStateStartPlayerMessages(itemGuid: PlanetSideGUID): Unit = { - continent.AvatarEvents ! AvatarServiceMessage( - continent.id, - AvatarAction.ChangeFireState_Start(player.GUID, itemGuid) - ) - } - - /* - used by ChangeFireStateMessage_Stop handling - */ - private def fireStateStopUpdateChargeAndCleanup(tool: Tool): Unit = { - tool.FireMode match { - case _: ChargeFireModeDefinition => - sendResponse(QuantityUpdateMessage(tool.AmmoSlot.Box.GUID, tool.Magazine)) - case _ => () - } - if (tool.Magazine == 0) { - FireCycleCleanup(tool) - } - } - - private def fireStateStopWhenPlayer(tool: Tool, itemGuid: PlanetSideGUID): Unit = { - //the decimator does not send a ChangeFireState_Start on the last shot; heaven knows why - //suppress the decimator's alternate fire mode, however - if ( - tool.Definition == GlobalDefinitions.phoenix && - tool.Projectile != GlobalDefinitions.phoenix_missile_guided_projectile - ) { - fireStateStartPlayerMessages(itemGuid) - } - fireStateStopUpdateChargeAndCleanup(tool) - ops.fireStateStopPlayerMessages(itemGuid) - } - - private def fireStateStopWhenMounted(tool: Tool, itemGuid: PlanetSideGUID): Unit = { - fireStateStopUpdateChargeAndCleanup(tool) - ops.fireStateStopMountedMessages(itemGuid) - } } 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 02efbc7e4..0e72be694 100644 --- a/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala @@ -4,9 +4,8 @@ package net.psforever.actors.session.support import akka.actor.Cancellable import akka.actor.typed.ActorRef import akka.actor.{ActorContext, typed} +import net.psforever.actors.session.spectator.SpectatorMode 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.zone.ZoneActor import net.psforever.objects.sourcing.PlayerSource import net.psforever.objects.zones.ZoneInfo @@ -59,6 +58,7 @@ class ChatOperations( ) extends CommonSessionInterfacingFunctionality { private var channels: List[ChatChannel] = List() private var silenceTimer: Cancellable = Default.Cancellable + private[session] var transitoryCommandEntered: Option[ChatMessageType] = None /** * when another player is listed as one of our ignored players, * and that other player sends an emote, @@ -67,6 +67,8 @@ class ChatOperations( */ private val ignoredEmoteCooldown: mutable.LongMap[Long] = mutable.LongMap[Long]() + private[session] var CurrentSpectatorMode: PlayerMode = SpectatorMode + import akka.actor.typed.scaladsl.adapter._ private val chatServiceAdapter: ActorRef[ChatService.MessageResponse] = context.self.toTyped[ChatService.MessageResponse] @@ -112,17 +114,6 @@ class ChatOperations( sendResponse(message.copy(contents = f"$speed%.3f")) } - def commandToggleSpectatorMode(session: Session, contents: String): Unit = { - val currentSpectatorActivation = session.player.spectator - 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 { @@ -337,6 +328,28 @@ class ChatOperations( } } //evaluate results + if (!commandCaptureBaseProcessResults(resolvedFacilities, resolvedFaction, resolvedTimer)) { + if (usageMessage) { + sendResponse( + message.copy(messageType = UNK_229, contents = "@CMT_CAPTUREBASE_usage") + ) + } else { + val msg = if (facilityError == 1) { "can not contextually determine building target" } + else if (facilityError == 2) { s"\'${foundFacilitiesTag.get}\' is not a valid building name" } + else if (factionError) { s"\'${foundFactionTag.get}\' is not a valid faction designation" } + else if (timerError) { s"\'${foundTimerTag.get}\' is not a valid timer value" } + else { "malformed params; check usage" } + sendResponse(ChatMsg(UNK_229, wideContents=true, "", s"\\#FF4040ERROR - $msg", None)) + } + } + } + + def commandCaptureBaseProcessResults( + resolvedFacilities: Option[Seq[Building]], + resolvedFaction: Option[PlanetSideEmpire.Value], + resolvedTimer: Option[Int] + ): Boolean = { + //evaluate results (resolvedFacilities, resolvedFaction, resolvedTimer) match { case (Some(buildings), Some(faction), Some(_)) => buildings.foreach { building => @@ -360,19 +373,9 @@ class ChatOperations( //push for map updates again zoneActor ! ZoneActor.ZoneMapUpdate() } + true case _ => - if (usageMessage) { - sendResponse( - message.copy(messageType = UNK_229, contents = "@CMT_CAPTUREBASE_usage") - ) - } else { - val msg = if (facilityError == 1) { "can not contextually determine building target" } - else if (facilityError == 2) { s"\'${foundFacilitiesTag.get}\' is not a valid building name" } - else if (factionError) { s"\'${foundFactionTag.get}\' is not a valid faction designation" } - else if (timerError) { s"\'${foundTimerTag.get}\' is not a valid timer value" } - else { "malformed params; check usage" } - sendResponse(ChatMsg(UNK_229, wideContents=true, "", s"\\#FF4040ERROR - $msg", None)) - } + false } } @@ -997,10 +1000,17 @@ class ChatOperations( private def captureBaseCurrSoi( session: Session ): Iterable[Building] = { - val charId = session.player.CharId - session.zone.Buildings.values.filter { building => - building.PlayersInSOI.exists(_.CharId == charId) - } + val player = session.player + val positionxy = player.Position.xy + session + .zone + .blockMap + .sector(player) + .buildingList + .filter { building => + val radius = building.Definition.SOIRadius + Vector3.DistanceSquared(building.Position.xy, positionxy) < radius * radius + } } private def captureBaseParamFaction( @@ -1226,7 +1236,7 @@ class ChatOperations( params: Seq[String] ): Boolean = { val ourRank = BattleRank.withExperience(session.avatar.bep).value - if (!session.account.gm && + if (!avatar.permissions.canGM && (ourRank <= Config.app.game.promotion.broadcastBattleRank || ourRank > Config.app.game.promotion.resetBattleRank && ourRank < Config.app.game.promotion.maxBattleRank + 1)) { setBattleRank(session, params, AvatarActor.Progress) 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..181a1e746 100644 --- a/src/main/scala/net/psforever/actors/session/support/GeneralOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/GeneralOperations.scala @@ -3,11 +3,26 @@ 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.{DeployableSource, 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 +import scala.concurrent.{Future, Promise} import scala.concurrent.duration._ +import scala.util.Success // import net.psforever.actors.session.{AvatarActor, SessionActor} import net.psforever.login.WorldSession._ @@ -170,6 +185,217 @@ class GeneralOperations( private[session] var progressBarUpdate: Cancellable = Default.Cancellable private var charSavedTimer: Cancellable = Default.Cancellable + def handleDropItem(pkt: DropItemMessage): GeneralOperations.ItemDropState.Behavior = { + val DropItemMessage(itemGuid) = pkt + (sessionLogic.validObject(itemGuid, decorator = "DropItem"), player.FreeHand.Equipment) match { + case (Some(anItem: Equipment), Some(heldItem)) + if (anItem eq heldItem) && continent.GUID(player.VehicleSeated).nonEmpty => + RemoveOldEquipmentFromInventory(player)(heldItem) + GeneralOperations.ItemDropState.Dropped + case (Some(anItem: Equipment), Some(heldItem)) + if anItem eq heldItem => + DropEquipmentFromInventory(player)(heldItem) + GeneralOperations.ItemDropState.Dropped + case (Some(_), _) => + GeneralOperations.ItemDropState.NotDropped + case _ => + GeneralOperations.ItemDropState.NotFound + } + } + + def handlePickupItem(pkt: PickupItemMessage): GeneralOperations.ItemPickupState.Behavior = { + val PickupItemMessage(itemGuid, _, _, _) = pkt + sessionLogic.validObject(itemGuid, decorator = "PickupItem") match { + case Some(item: Equipment) + if player.Fit(item).nonEmpty => + PickUpEquipmentFromGround(player)(item) + GeneralOperations.ItemPickupState.PickedUp + case Some(_: Equipment) => + GeneralOperations.ItemPickupState.Dropped + case _ => + GeneralOperations.ItemPickupState.NotFound + } + } + + def handleZipLine(pkt: ZipLineMessage): GeneralOperations.ZiplineBehavior.Behavior = { + val ZipLineMessage(playerGuid, forwards, action, pathId, pos) = pkt + continent.zipLinePaths.find(x => x.PathId == pathId) match { + case Some(path) if path.IsTeleporter => + val endPoint = path.ZipLinePoints.last + sendResponse(ZipLineMessage(PlanetSideGUID(0), forwards, 0, pathId, pos)) + //todo: send to zone to show teleport animation to all clients + sendResponse(PlayerStateShiftMessage(ShiftState(0, endPoint, (player.Orientation.z + player.FacingYawUpper) % 360f, None))) + GeneralOperations.ZiplineBehavior.Teleporter + case Some(_) => + //todo: send to zone to show zipline animation to all clients + action match { + case 0 => + //travel along the zipline in the direction specified + sendResponse(ZipLineMessage(playerGuid, forwards, action, pathId, pos)) + GeneralOperations.ZiplineBehavior.Zipline + case 1 => + //disembark from zipline at destination + sendResponse(ZipLineMessage(playerGuid, forwards, action, 0, pos)) + GeneralOperations.ZiplineBehavior.Zipline + case 2 => + //get off by force + sendResponse(ZipLineMessage(playerGuid, forwards, action, 0, pos)) + GeneralOperations.ZiplineBehavior.Zipline + case _ => + GeneralOperations.ZiplineBehavior.Unsupported + } + case _ => + GeneralOperations.ZiplineBehavior.NotFound + } + } + + def handleMoveItem(pkt: MoveItemMessage): Unit = { + val MoveItemMessage(itemGuid, sourceGuid, destinationGuid, dest, _) = pkt + ( + continent.GUID(sourceGuid), + continent.GUID(destinationGuid), + sessionLogic.validObject(itemGuid, decorator = "MoveItem") + ) match { + case ( + Some(source: PlanetSideServerObject with Container), + Some(destination: PlanetSideServerObject with Container), + Some(item: Equipment) + ) => + ContainableMoveItem(player.Name, source, destination, item, destination.SlotMapResolution(dest)) + case (None, _, _) => + log.error( + s"MoveItem: ${player.Name} wanted to move $itemGuid from $sourceGuid, but could not find source object" + ) + case (_, None, _) => + log.error( + s"MoveItem: ${player.Name} wanted to move $itemGuid to $destinationGuid, but could not find destination object" + ) + case (_, _, None) => () + case _ => + log.error( + s"MoveItem: ${player.Name} wanted to move $itemGuid from $sourceGuid to $destinationGuid, but multiple problems were encountered" + ) + } + } + + def handleLootItem(pkt: LootItemMessage): Unit = { + val LootItemMessage(itemGuid, targetGuid) = pkt + (sessionLogic.validObject(itemGuid, decorator = "LootItem"), continent.GUID(targetGuid)) match { + case (Some(item: Equipment), Some(destination: PlanetSideServerObject with Container)) => + //figure out the source + ( + { + val findFunc: PlanetSideServerObject with Container => Option[ + (PlanetSideServerObject with Container, Option[Int]) + ] = findInLocalContainer(itemGuid) + findFunc(player.avatar.locker) + .orElse(findFunc(player)) + .orElse(accessedContainer match { + case Some(parent: PlanetSideServerObject) => + findFunc(parent) + case _ => + None + }) + }, + destination.Fit(item) + ) match { + case (Some((source, Some(_))), Some(dest)) => + ContainableMoveItem(player.Name, source, destination, item, dest) + case (None, _) => + log.error(s"LootItem: ${player.Name} can not find where $item is put currently") + case (_, None) => + log.error(s"LootItem: ${player.Name} can not find anywhere to put $item in $destination") + case _ => + log.error( + s"LootItem: ${player.Name}wanted to move $itemGuid to $targetGuid, but multiple problems were encountered" + ) + } + case (Some(obj), _) => + log.error(s"LootItem: item $obj is (probably) not lootable to ${player.Name}") + case (None, _) => () + case (_, None) => + log.error(s"LootItem: ${player.Name} can not find where to put $itemGuid") + } + } + + def handleAvatarImplant(pkt: AvatarImplantMessage): GeneralOperations.ImplantActivationBehavior.Behavior = { + val AvatarImplantMessage(_, action, slot, status) = pkt + if (action == ImplantAction.Activation) { + if (sessionLogic.zoning.zoningStatus == Zoning.Status.Deconstructing) { + //do not activate; play deactivation sound instead + sessionLogic.zoning.spawn.stopDeconstructing() + avatar.implants(slot).collect { + case implant if implant.active => + avatarActor ! AvatarActor.DeactivateImplant(implant.definition.implantType) + case implant => + sendResponse(PlanetsideAttributeMessage(player.GUID, 28, implant.definition.implantType.value * 2)) + } + GeneralOperations.ImplantActivationBehavior.Failed + } else { + avatar.implants(slot) match { + case Some(implant) => + if (status == 1) { + avatarActor ! AvatarActor.ActivateImplant(implant.definition.implantType) + GeneralOperations.ImplantActivationBehavior.Activate + } else { + avatarActor ! AvatarActor.DeactivateImplant(implant.definition.implantType) + GeneralOperations.ImplantActivationBehavior.Deactivate + } + case _ => + GeneralOperations.ImplantActivationBehavior.NotFound + } + } + } else { + GeneralOperations.ImplantActivationBehavior.Failed + } + } + + def handleCreateShortcut(pkt: CreateShortcutMessage): Unit = { + val CreateShortcutMessage(_, slot, shortcutOpt) = pkt + shortcutOpt match { + case Some(shortcut) => + avatarActor ! AvatarActor.AddShortcut(slot - 1, shortcut) + case None => + avatarActor ! AvatarActor.RemoveShortcut(slot - 1) + } + } + + def handleObjectDetected(pkt: ObjectDetectedMessage): Unit = { + val ObjectDetectedMessage(_, _, _, targets) = pkt + sessionLogic.shooting.FindWeapon.foreach { + case weapon if weapon.Projectile.AutoLock => + //projectile with auto-lock instigates a warning on the target + val detectedTargets = sessionLogic.shooting.FindDetectedProjectileTargets(targets) + val mode = 7 + (if (weapon.Projectile == GlobalDefinitions.wasp_rocket_projectile) 1 else 0) + detectedTargets.foreach { target => + continent.AvatarEvents ! AvatarServiceMessage(target, AvatarAction.ProjectileAutoLockAwareness(mode)) + } + case _ => () + } + } + + def handleTargetingImplantRequest(pkt: TargetingImplantRequest): Unit = { + val TargetingImplantRequest(list) = pkt + val targetInfo: List[TargetInfo] = list.flatMap { x => + continent.GUID(x.target_guid) match { + case Some(player: Player) => + val health = player.Health.toFloat / player.MaxHealth + val armor = if (player.MaxArmor > 0) { + player.Armor.toFloat / player.MaxArmor + } else { + 0 + } + Some(TargetInfo(player.GUID, health, armor)) + case _ => + log.warn( + s"TargetingImplantRequest: the info that ${player.Name} requested for target ${x.target_guid} is not for a player" + ) + None + } + } + sendResponse(TargetingInfoMessage(targetInfo)) + } + /** * Enforce constraints on bulk purchases as determined by a given player's previous purchase times and hard acquisition delays. * Intended to assist in sanitizing loadout information from the perspective of the player, or target owner. @@ -560,6 +786,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 @@ -692,10 +978,10 @@ class GeneralOperations( ) } - def administrativeKick(tplayer: Player): Unit = { + def administrativeKick(tplayer: Player, time: Option[Long]): Unit = { log.warn(s"${tplayer.Name} has been kicked by ${player.Name}") tplayer.death_by = -1 - sessionLogic.accountPersistence ! AccountPersistenceService.Kick(tplayer.Name) + sessionLogic.accountPersistence ! AccountPersistenceService.Kick(tplayer.Name, time) //get out of that vehicle sessionLogic.vehicles.GetMountableAndSeat(None, tplayer, continent) match { case (Some(obj), Some(seatNum)) => @@ -767,6 +1053,460 @@ class GeneralOperations( ) } + def handleDeployObject( + zone: Zone, + deployableType: DeployedItem.Value, + position: Vector3, + orientation: Vector3, + side: Sidedness, + faction: PlanetSideEmpire.Value, + owner: Player, + builtWith: ConstructionItem + ): Future[Deployable] = { + val (deployableEntity, tasking) = commonHandleDeployObjectSetup(zone, deployableType, position, orientation, side, faction) + deployableEntity.AssignOwnership(owner) + val promisedDeployable: Promise[Deployable] = Promise() + //execute + val result = TaskWorkflow.execute(CallBackForTask( + tasking, + zone.Deployables, + Zone.Deployable.BuildByOwner(deployableEntity, owner, builtWith), + context.self + )) + result.onComplete { + case Success(_) => promisedDeployable.success(deployableEntity) + case _ => () + } + promisedDeployable.future + } + + def handleDeployObject( + zone: Zone, + deployableType: DeployedItem.Value, + position: Vector3, + orientation: Vector3, + side: Sidedness, + faction: PlanetSideEmpire.Value + ): Future[Deployable] = { + val (deployableEntity, tasking) = commonHandleDeployObjectSetup(zone, deployableType, position, orientation, side, faction) + val promisedDeployable: Promise[Deployable] = Promise() + //execute + val result = TaskWorkflow.execute(CallBackForTask( + tasking, + zone.Deployables, + Zone.Deployable.Build(deployableEntity), + context.self + )) + result.onComplete { + case Success(_) => + Players.buildCooldownReset(zone, player.Name, deployableEntity.GUID) + deployableEntity.Actor ! Deployable.Deconstruct(Some(20.minutes)) + if (deployableType == DeployedItem.boomer) { + val trigger = new BoomerTrigger + trigger.Companion = deployableEntity.GUID + deployableEntity.asInstanceOf[BoomerDeployable].Trigger = trigger + TaskWorkflow.execute(CallBackForTask( + GUIDTask.registerEquipment(zone.GUID, trigger), + zone.Ground, + Zone.Ground.DropItem(trigger, position + Vector3.z(value = 0.5f), Vector3.z(orientation.z)) + )) + } + promisedDeployable.success(deployableEntity) + case _ => () + } + promisedDeployable.future + } + + 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) + ) + player.Position = dest.Position + player.LogActivity(TelepadUseActivity(VehicleSource(router), DeployableSource(remoteTelepad), PlayerSource(player))) + } 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)) + ) + } + + private def commonHandleDeployObjectSetup( + zone: Zone, + deployableType: DeployedItem.Value, + position: Vector3, + orientation: Vector3, + side: Sidedness, + faction: PlanetSideEmpire.Value + ): (Deployable, TaskBundle) = { + 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) + } + (deployableEntity, tasking) + } + override protected[session] def actionsToCancel(): Unit = { progressBarValue = None kitToBeUsed = None @@ -786,11 +1526,12 @@ class GeneralOperations( } } + case Some(o) if player.isAlive => + unaccessContainer(o) + sendResponse(UnuseItemMessage(player.GUID, o.GUID)) + case Some(o) => unaccessContainer(o) - if (player.isAlive) { - sendResponse(UnuseItemMessage(player.GUID, o.GUID)) - } case None => () } @@ -801,3 +1542,42 @@ class GeneralOperations( charSavedTimer.cancel() } } + +object GeneralOperations { + object UseItem { + sealed trait Behavior + case object Handled extends Behavior + case object HandledPassive extends Behavior + case object Unhandled extends Behavior + } + + object ItemDropState { + sealed trait Behavior + case object Dropped extends Behavior + case object NotDropped extends Behavior + case object NotFound extends Behavior + } + + object ItemPickupState { + sealed trait Behavior + case object PickedUp extends Behavior + case object Dropped extends Behavior + case object NotFound extends Behavior + } + + object ZiplineBehavior { + sealed trait Behavior + case object Teleporter extends Behavior + case object Zipline extends Behavior + case object Unsupported extends Behavior + case object NotFound extends Behavior + } + + object ImplantActivationBehavior { + sealed trait Behavior + case object Activate extends Behavior + case object Deactivate extends Behavior + case object Failed extends Behavior + case object NotFound extends Behavior + } +} diff --git a/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala b/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala index 3e0e7f9d8..995f7666d 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala @@ -2,9 +2,13 @@ package net.psforever.actors.session.support import akka.actor.{ActorContext, typed} +import net.psforever.objects.serverobject.mount.Mountable +import net.psforever.objects.{Default, PlanetSideGameObject} import net.psforever.objects.sourcing.{PlayerSource, SourceEntry} import net.psforever.packet.game.objectcreate.ConstructorData import net.psforever.objects.zones.exp +import net.psforever.services.Service +import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage, AvatarServiceResponse} import scala.collection.mutable // @@ -154,6 +158,45 @@ class SessionAvatarHandlers( victimSeated ) } + + def revive(revivalTargetGuid: PlanetSideGUID): Unit = { + val spawn = sessionLogic.zoning.spawn + spawn.reviveTimer.cancel() + spawn.reviveTimer = Default.Cancellable + spawn.respawnTimer.cancel() + spawn.respawnTimer = Default.Cancellable + player.Revive + val health = player.Health + sendResponse(PlanetsideAttributeMessage(revivalTargetGuid, attribute_type=0, health)) + sendResponse(AvatarDeadStateMessage(DeadState.Alive, timer_max=0, timer=0, player.Position, player.Faction, unk5=true)) + continent.AvatarEvents ! AvatarServiceMessage( + continent.id, + AvatarAction.PlanetsideAttributeToAll(revivalTargetGuid, attribute_type=0, health) + ) + } + + def killedWhileMounted(obj: PlanetSideGameObject with Mountable, playerGuid: PlanetSideGUID): Unit = { + val playerName = player.Name + //boot cadaver from mount on client + context.self ! AvatarServiceResponse( + playerName, + Service.defaultPlayerGUID, + AvatarResponse.SendResponse( + ObjectDetachMessage(obj.GUID, playerGuid, player.Position, Vector3.Zero) + ) + ) + //player no longer seated + obj.PassengerInSeat(player).foreach { seatNumber => + //boot cadaver from mount internally (vehicle perspective) + obj.Seats(seatNumber).unmount(player) + //inform client-specific logic + context.self ! Mountable.MountMessages( + player, + Mountable.CanDismount(obj, seatNumber, 0) + ) + } + player.VehicleSeated = None + } } object SessionAvatarHandlers { diff --git a/src/main/scala/net/psforever/actors/session/support/SessionData.scala b/src/main/scala/net/psforever/actors/session/support/SessionData.scala index 8821f6cd9..68fcf9f0d 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionData.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionData.scala @@ -89,6 +89,7 @@ class SessionData( private[session] var persistFunc: () => Unit = noPersistence private[session] var persist: () => Unit = updatePersistenceOnly private[session] var keepAliveFunc: () => Unit = keepAlivePersistenceInitial + private[session] var keepAlivePersistenceFunc: () => Unit = keepAlivePersistence private[session] var turnCounterFunc: PlanetSideGUID => Unit = SessionData.NoTurnCounterYet private[session] val oldRefsMap: mutable.HashMap[PlanetSideGUID, String] = new mutable.HashMap[PlanetSideGUID, String]() private var contextSafeEntity: PlanetSideGUID = PlanetSideGUID(0) @@ -471,7 +472,7 @@ class SessionData( def keepAlivePersistenceInitial(): Unit = { persist() if (player != null && player.HasGUID) { - keepAliveFunc = keepAlivePersistence + keepAliveFunc = keepAlivePersistenceFunc } } 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..8480a1d55 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 @@ -30,12 +30,12 @@ class SessionLocalHandlers( } def handleTurretDeployableIsDismissed(obj: TurretDeployable): Unit = { - Players.buildCooldownReset(continent, player.Name, obj) + Players.buildCooldownReset(continent, player.Name, obj.GUID) TaskWorkflow.execute(GUIDTask.unregisterDeployableTurret(continent.GUID, obj)) } def handleDeployableIsDismissed(obj: Deployable): Unit = { - Players.buildCooldownReset(continent, player.Name, obj) + Players.buildCooldownReset(continent, player.Name, obj.GUID) TaskWorkflow.execute(GUIDTask.unregisterObject(continent.GUID, obj)) } @@ -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..835ae7f6d 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] + ): Option[Vehicle] = { + 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)) => + 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)) + vehicle + } + .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..5633e76f7 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, CountGrenades, FindAmmoBoxThatUses, FindEquipmentStock, FindToolThatUses, PutEquipmentInInventoryOrDrop, PutNewEquipmentInInventoryOrDrop, RemoveOldEquipmentFromInventory} +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.{CommonMessages, 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,116 @@ 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 + ) + projectile.GUID = projectileGUID + 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" + ) + qualityprojectile.Invalidate() + 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 +234,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 +266,483 @@ 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( + sessionLogic.zoning.zoneChannel, + 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 => + 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.nonEmpty + } + .getOrElse(false) + } + + def handleProxyDamage( + projectileGuid: PlanetSideGUID, + explosionPosition: Vector3 + ): List[(PlanetSideGameObject with FactionAffinity with Vitality, Projectile, Vector3, Vector3)] = { + val proxyList = FindProjectileEntry(projectileGuid) + .map(projectile => resolveDamageProxy(projectile, projectile.GUID, explosionPosition)) + .getOrElse(Nil) + proxyList.collectFirst { + case (_, proxy, _, _) if proxy.profile == GlobalDefinitions.oicw_little_buddy => + performLittleBuddyExplosion(proxyList.map(_._2)) + } + proxyList + } + + /** + * 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 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 + } + } + + private 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 +788,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,9 +880,105 @@ class WeaponAndProjectileOperations( */ def FindWeapon: Set[Tool] = FindContainedWeapon._2 + /* + used by ChangeFireStateMessage_Start handling + */ + def fireStateStartSetup(itemGuid: PlanetSideGUID): Unit = { + prefire -= itemGuid + shooting += itemGuid + shootingStart += itemGuid -> System.currentTimeMillis() + } + + def fireStateStartChargeMode(tool: Tool): Unit = { + //charge ammunition drain + tool.FireMode match { + case mode: ChargeFireModeDefinition => + sessionLogic.general.progressBarValue = Some(0f) + sessionLogic.general.progressBarUpdate = context.system.scheduler.scheduleOnce( + (mode.Time + mode.DrainInterval) milliseconds, + context.self, + CommonMessages.ProgressEvent(1f, () => {}, Tools.ChargeFireMode(player, tool), mode.DrainInterval) + ) + case _ => () + } + } + + def allowFireStateChangeStart(tool: Tool, itemGuid: PlanetSideGUID): Boolean = { + tool.FireMode.RoundsPerShot == 0 || tool.Magazine > 0 || prefire.contains(itemGuid) + } + + 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" + ) + emptyMagazine(itemGuid, tool) + } + + def fireStateStartWhenPlayer(tool: Tool, itemGuid: PlanetSideGUID): Unit = { + if (allowFireStateChangeStart(tool, itemGuid)) { + fireStateStartSetup(itemGuid) + //special case - suppress the decimator's alternate fire mode, by projectile + if (tool.Projectile != GlobalDefinitions.phoenix_missile_guided_projectile) { + fireStateStartPlayerMessages(itemGuid) + } + fireStateStartChargeMode(tool) + } else { + enforceEmptyMagazine(tool, itemGuid) + } + } + + def fireStateStartWhenMounted(tool: Tool, itemGuid: PlanetSideGUID): Unit = { + if (allowFireStateChangeStart(tool, itemGuid)) { + fireStateStartSetup(itemGuid) + fireStateStartMountedMessages(itemGuid) + fireStateStartChargeMode(tool) + } else { + enforceEmptyMagazine(tool, itemGuid) + } + } + + def fireStateStartPlayerMessages(itemGuid: PlanetSideGUID): Unit = { + continent.AvatarEvents ! AvatarServiceMessage( + sessionLogic.zoning.zoneChannel, + AvatarAction.ChangeFireState_Start(player.GUID, itemGuid) + ) + } + + def fireStateStartMountedMessages(itemGuid: PlanetSideGUID): Unit = { + sessionLogic.findContainedEquipment()._1.collect { + case turret: FacilityTurret if continent.map.cavern => + turret.Actor ! VanuSentry.ChangeFireStart + } + continent.VehicleEvents ! VehicleServiceMessage( + continent.id, + VehicleAction.ChangeFireState_Start(player.GUID, itemGuid) + ) + } + + /* + used by ChangeFireStateMessage_Stop handling + */ + def fireStateStopWhenPlayer(tool: Tool, itemGuid: PlanetSideGUID): Unit = { + //the decimator does not send a ChangeFireState_Start on the last shot; heaven knows why + //suppress the decimator's alternate fire mode, however + if ( + tool.Definition == GlobalDefinitions.phoenix && + tool.Projectile != GlobalDefinitions.phoenix_missile_guided_projectile + ) { + fireStateStartPlayerMessages(itemGuid) + } + fireStateStopUpdateChargeAndCleanup(tool) + fireStateStopPlayerMessages(itemGuid) + } + + def fireStateStopWhenMounted(tool: Tool, itemGuid: PlanetSideGUID): Unit = { + fireStateStopUpdateChargeAndCleanup(tool) + fireStateStopMountedMessages(itemGuid) + } + def fireStateStopPlayerMessages(itemGuid: PlanetSideGUID): Unit = { continent.AvatarEvents ! AvatarServiceMessage( - continent.id, + sessionLogic.zoning.zoneChannel, AvatarAction.ChangeFireState_Stop(player.GUID, itemGuid) ) } @@ -292,6 +994,106 @@ class WeaponAndProjectileOperations( ) } + /** + * After a weapon has finished shooting, determine if it needs to be sorted in a special way. + * @param tool a weapon + */ + def fireCycleCleanup(tool: Tool): Unit = { + //TODO replaced by more appropriate functionality in the future + val tdef = tool.Definition + if (GlobalDefinitions.isGrenade(tdef)) { + val ammoType = tool.AmmoType + FindEquipmentStock(player, FindToolThatUses(ammoType), 3, CountGrenades).reverse match { //do not search sidearm holsters + case Nil => + RemoveOldEquipmentFromInventory(player)(tool) + + case x :: xs => //this is similar to ReloadMessage + val box = x.obj.asInstanceOf[Tool] + val tailReloadValue: Int = if (xs.isEmpty) { 0 } + else { xs.map(_.obj.asInstanceOf[Tool].Magazine).sum } + val sumReloadValue: Int = box.Magazine + tailReloadValue + val actualReloadValue = if (sumReloadValue <= 3) { + RemoveOldEquipmentFromInventory(player)(x.obj) + sumReloadValue + } else { + modifyAmmunition(player)(box.AmmoSlot.Box, 3 - tailReloadValue) + 3 + } + modifyAmmunition(player)( + tool.AmmoSlot.Box, + -actualReloadValue + ) //grenade item already in holster (negative because empty) + xs.foreach(item => { RemoveOldEquipmentFromInventory(player)(item.obj) }) + } + } else if (tdef == GlobalDefinitions.phoenix) { + RemoveOldEquipmentFromInventory(player)(tool) + } + } + + def fireStateStopUpdateChargeAndCleanup(tool: Tool): Unit = { + tool.FireMode match { + case _: ChargeFireModeDefinition => + sendResponse(QuantityUpdateMessage(tool.AmmoSlot.Box.GUID, tool.Magazine)) + case _ => () + } + if (tool.Magazine == 0) { + fireCycleCleanup(tool) + } + } + + + + /* + used by ReloadMessage handling + */ + def reloadPlayerMessages(itemGuid: PlanetSideGUID): Unit = { + continent.AvatarEvents ! AvatarServiceMessage( + sessionLogic.zoning.zoneChannel, + AvatarAction.Reload(player.GUID, itemGuid) + ) + } + + def reloadVehicleMessages(itemGuid: PlanetSideGUID): Unit = { + continent.VehicleEvents ! VehicleServiceMessage( + continent.id, + VehicleAction.Reload(player.GUID, itemGuid) + ) + } + + def handleReloadWhenPlayer( + itemGuid: PlanetSideGUID, + obj: Player, + tools: Set[Tool], + unk1: Int + ): Unit = { + handleReloadProcedure( + itemGuid, + obj, + tools, + unk1, + RemoveOldEquipmentFromInventory(obj)(_), + modifyAmmunition(obj)(_, _), + reloadPlayerMessages + ) + } + + def handleReloadWhenMountable( + itemGuid: PlanetSideGUID, + obj: PlanetSideServerObject with Container, + tools: Set[Tool], + unk1: Int + ): Unit = { + handleReloadProcedure( + itemGuid, + obj, + tools, + unk1, + RemoveOldEquipmentFromInventory(obj)(_), + modifyAmmunitionInMountable(obj)(_, _), + reloadVehicleMessages + ) + } + private def addShotsFired(weaponId: Int, shots: Int): Unit = { addShotsToMap(shotsFired, weaponId, shots) } @@ -326,6 +1128,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( + sessionLogic.zoning.zoneChannel, + 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 +1456,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 528f34bf6..5e7501c3e 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} @@ -18,6 +17,7 @@ import net.psforever.objects.serverobject.tube.SpawnTube import net.psforever.objects.serverobject.turret.auto.AutomatedTurret import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource} import net.psforever.objects.vital.{InGameHistory, IncarnationActivity, ReconstructionActivity, SpawningActivity} +import net.psforever.objects.zones.blockmap.BlockMapEntity import net.psforever.packet.game.{CampaignStatistic, ChangeFireStateMessage_Start, HackState7, MailMessage, ObjectDetectedMessage, SessionStatistic, TriggeredSound} import net.psforever.services.chat.DefaultChannel @@ -159,6 +159,30 @@ object ZoningOperations { events ! LocalServiceMessage(target, soundMessage) } } + + def findBuildingsBySoiOccupancy(zone: Zone, obj: PlanetSideGameObject with BlockMapEntity): List[Building] = { + val positionxy = obj.Position.xy + zone + .blockMap + .sector(obj) + .buildingList + .filter { building => + val radius = building.Definition.SOIRadius + Vector3.DistanceSquared(building.Position.xy, positionxy) < radius * radius + } + } + + def findBuildingsBySoiOccupancy(zone: Zone, position: Vector3): List[Building] = { + val positionxy = position.xy + zone + .blockMap + .sector(positionxy, range=5) + .buildingList + .filter { building => + val radius = building.Definition.SOIRadius + Vector3.DistanceSquared(building.Position.xy, positionxy) < radius * radius + } + } } class ZoningOperations( @@ -193,6 +217,9 @@ 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[session] var zoneChannel: String = Zone.Nowhere.id private var loadConfZone: Boolean = false private var instantActionFallbackDestination: Option[Zoning.InstantAction.Located] = None @@ -200,6 +227,7 @@ class ZoningOperations( private var zoningChatMessageType: ChatMessageType = ChatMessageType.CMT_QUIT private var zoningCounter: Int = 0 private var zoningTimer: Cancellable = Default.Cancellable + var displayZoningMessageWhenCancelled: Boolean = true /* packets */ @@ -609,6 +637,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() @@ -864,12 +893,7 @@ class ZoningOperations( val location = if (Zones.sanctuaryZoneNumber(player.Faction) == continent.Number) { Zoning.Time.Sanctuary } else { - val playerPosition = player.Position.xy - continent.Buildings.values - .filter { building => - val radius = building.Definition.SOIRadius - Vector3.DistanceSquared(building.Position.xy, playerPosition) < radius * radius - } match { + ZoningOperations.findBuildingsBySoiOccupancy(continent, player.Position) match { case Nil => Zoning.Time.None case List(building: FactionAffinity) => @@ -903,7 +927,7 @@ class ZoningOperations( * defaults to `None` */ def CancelZoningProcessWithReason(msg: String, msgType: Option[ChatMessageType] = None): Unit = { - if (zoningStatus != Zoning.Status.None) { + if (displayZoningMessageWhenCancelled && zoningStatus != Zoning.Status.None) { sendResponse(ChatMsg(msgType.getOrElse(zoningChatMessageType), wideContents=false, "", msg, None)) } CancelZoningProcess() @@ -2128,6 +2152,7 @@ class ZoningOperations( val map = zone.map val mapName = map.name log.info(s"${tplayer.Name} has spawned into $id") + sessionLogic.zoning.zoneChannel = Players.ZoneChannelIfSpectating(tplayer, zone.id) sessionLogic.oldRefsMap.clear() sessionLogic.persist = UpdatePersistenceAndRefs tplayer.avatar = avatar @@ -2137,6 +2162,7 @@ class ZoningOperations( sendResponse(LoadMapMessage(mapName, id, 40100, 25, weaponsEnabled, map.checksum)) if (isAcceptableNextSpawnPoint) { //important! the LoadMapMessage must be processed by the client before the avatar is created + player.allowInteraction = true setupAvatarFunc() //interimUngunnedVehicle should have been setup by setupAvatarFunc, if it is applicable sessionLogic.turnCounterFunc = interimUngunnedVehicle match { @@ -2174,6 +2200,7 @@ class ZoningOperations( session = session.copy(player = tplayer) if (isAcceptableNextSpawnPoint) { //try this spawn point + player.allowInteraction = true setupAvatarFunc() //interimUngunnedVehicle should have been setup by setupAvatarFunc, if it is applicable sessionLogic.turnCounterFunc = interimUngunnedVehicle match { @@ -2294,7 +2321,7 @@ class ZoningOperations( * @param zone na */ def HandleReleaseAvatar(tplayer: Player, zone: Zone): Unit = { - sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistence + sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistenceFunc tplayer.Release tplayer.VehicleSeated match { case None => @@ -2439,6 +2466,16 @@ class ZoningOperations( log.debug(s"AvatarCreate (vehicle): ${player.Name}'s ${vdef.Name}") AvatarCreateInVehicle(player, vehicle, seat) + case _ if player.spectator => + player.VehicleSeated = None + val definition = player.avatar.definition + val guid = player.GUID + sendResponse(OCM.detailed(player)) + continent.AvatarEvents ! AvatarServiceMessage( + s"spectator", + AvatarAction.LoadPlayer(guid, definition.ObjectId, guid, definition.Packet.ConstructorData(player).get, None) + ) + case _ => player.VehicleSeated = None val definition = player.avatar.definition @@ -2587,8 +2624,6 @@ class ZoningOperations( zones.exp.ToDatabase.reportRespawns(tplayer.CharId, ScoreCard.reviveCount(player.avatar.scorecard.CurrentLife)) val obj = Player.Respawn(tplayer) DefinitionUtil.applyDefaultLoadout(obj) - obj.death_by = tplayer.death_by - obj.silenced = tplayer.silenced obj } @@ -2866,7 +2901,6 @@ class ZoningOperations( ) ) nextSpawnPoint = physSpawnPoint - prevSpawnPoint = physSpawnPoint shiftPosition = Some(pos) shiftOrientation = Some(ori) val toZoneNumber = if (continent.id.equals(zoneId)) { @@ -3012,10 +3046,11 @@ class ZoningOperations( sessionLogic.keepAliveFunc = sessionLogic.vehicles.GetMountableAndSeat(None, player, continent) match { case (Some(v: Vehicle), Some(seatNumber)) if seatNumber > 0 && v.WeaponControlledFromSeat(seatNumber).isEmpty => - sessionLogic.keepAlivePersistence + sessionLogic.keepAlivePersistenceFunc case _ => NormalKeepAlive } + prevSpawnPoint = nextSpawnPoint nextSpawnPoint = None } //if not the condition above, player has started playing normally @@ -3214,10 +3249,10 @@ class ZoningOperations( upstreamMessageCount = 0 if (tplayer.spectator) { if (!setAvatar) { - context.self ! SessionActor.SetMode(SpectatorMode) //should reload spectator status + context.self ! SessionActor.SetMode(sessionLogic.chat.CurrentSpectatorMode) //should reload spectator status } } else if ( - !account.gm && /* gm's are excluded */ + !avatar.permissions.canGM && /* gm's are excluded */ Config.app.game.promotion.active && /* play versus progress system must be active */ BattleRank.withExperience(tplayer.avatar.bep).value <= Config.app.game.promotion.broadcastBattleRank && /* must be below a certain battle rank */ tavatar.scorecard.Lives.isEmpty && /* first life after login */ @@ -3464,7 +3499,7 @@ class ZoningOperations( //sit down sendResponse(ObjectAttachMessage(vguid, pguid, seat)) sessionLogic.general.accessContainer(vehicle) - sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistence + sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistenceFunc case _ => () //we can't find a vehicle? and we're still here? that's bad player.VehicleSeated = None diff --git a/src/main/scala/net/psforever/login/WorldSession.scala b/src/main/scala/net/psforever/login/WorldSession.scala index a83a9df00..e24360396 100644 --- a/src/main/scala/net/psforever/login/WorldSession.scala +++ b/src/main/scala/net/psforever/login/WorldSession.scala @@ -1067,6 +1067,13 @@ object WorldSession { } } + /** + * Perform a task and, upon completion, dispatch a message. + * @param task task to be completed first + * @param sendTo where to send the message + * @param pass message + * @return a `TaskBundle` object + */ def CallBackForTask(task: TaskBundle, sendTo: ActorRef, pass: Any): TaskBundle = { TaskBundle( new StraightforwardTask() { @@ -1085,6 +1092,14 @@ object WorldSession { ) } + /** + * Perform a task and, upon completion, dispatch a message whose origin is specific and different from normal. + * @param task task to be completed first + * @param sendTo where to send the message + * @param pass message + * @param replyTo whom to attribute the message being passed (e.g., `tell`) + * @return a `TaskBundle` object + */ def CallBackForTask(task: TaskBundle, sendTo: ActorRef, pass: Any, replyTo: ActorRef): TaskBundle = { TaskBundle( new StraightforwardTask() { diff --git a/src/main/scala/net/psforever/objects/Player.scala b/src/main/scala/net/psforever/objects/Player.scala index 24c277b3a..8415e3b6e 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 @@ -634,13 +635,16 @@ object Player { player.Inventory.Resize(eSuit.InventoryScale.Width, eSuit.InventoryScale.Height) player.Inventory.Offset = eSuit.InventoryOffset //holsters - (0 until 5).foreach(index => { player.Slot(index).Size = eSuit.Holster(index) }) + (0 until 5).foreach { index => player.Slot(index).Size = eSuit.Holster(index) } } def Respawn(player: Player): Player = { if (player.Release) { val obj = new Player(player.avatar) obj.Continent = player.Continent + obj.death_by = player.death_by + obj.silenced = player.silenced + obj.allowInteraction = player.allowInteraction obj.avatar.scorecard.respawn() obj } else { diff --git a/src/main/scala/net/psforever/objects/Players.scala b/src/main/scala/net/psforever/objects/Players.scala index a69c18dfa..7ec07f3c2 100644 --- a/src/main/scala/net/psforever/objects/Players.scala +++ b/src/main/scala/net/psforever/objects/Players.scala @@ -18,7 +18,7 @@ import net.psforever.objects.vital.projectile.ProjectileReason import net.psforever.objects.vital.{InGameActivity, InGameHistory, RevivingActivity} import net.psforever.objects.zones.Zone import net.psforever.packet.game._ -import net.psforever.types.{ChatMessageType, ExoSuitType, Vector3} +import net.psforever.types.{ChatMessageType, ExoSuitType, PlanetSideGUID, Vector3} import net.psforever.services.Service import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.local.{LocalAction, LocalServiceMessage} @@ -327,7 +327,7 @@ object Players { */ def successfulBuildActivity(zone: Zone, channel: String, obj: Deployable): Unit = { //sent to avatar event bus to preempt additional tool management - buildCooldownReset(zone, channel, obj) + buildCooldownReset(zone, channel, obj.GUID) //sent to local event bus to cooperate with deployable management zone.LocalEvents ! LocalServiceMessage( channel, @@ -339,13 +339,13 @@ object Players { * Common actions related to constructing a new `Deployable` object in the game environment. * @param zone in which zone these messages apply * @param channel to whom to send the messages - * @param obj the `Deployable` object + * @param guid `Deployable` object */ - def buildCooldownReset(zone: Zone, channel: String, obj: Deployable): Unit = { + def buildCooldownReset(zone: Zone, channel: String, guid: PlanetSideGUID): Unit = { //sent to avatar event bus to preempt additional tool management zone.AvatarEvents ! AvatarServiceMessage( channel, - AvatarAction.SendResponse(Service.defaultPlayerGUID, GenericObjectActionMessage(obj.GUID, 21)) + AvatarAction.SendResponse(Service.defaultPlayerGUID, GenericObjectActionMessage(guid, 21)) ) } @@ -488,4 +488,35 @@ object Players { } player.HistoryAndContributions() } + + /** + * Select the player's zone channel. + * If the player is spectating, then that is their channel instead. + * The resulting channel name should never be used for subscribing - only for publishing. + * @param player player in a zone + * @return channel name + */ + def ZoneChannelIfSpectating(player: Player): String = { + if (player.spectator) { + "spectator" + } else { + player.Zone.id + } + } + + /** + * Select the player's zone channel. + * If the player is spectating, then that is their channel instead. + * The resulting channel name should never be used for subscribing - only for publishing. + * @param player player in a zone + * @param zoneid custom zone name to be used as the channel name + * @return channel name + */ + def ZoneChannelIfSpectating(player: Player, zoneid: String): String = { + if (player.spectator) { + "spectator" + } else { + zoneid + } + } } diff --git a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala index f0b36413a..ee5fef561 100644 --- a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala +++ b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala @@ -3,7 +3,6 @@ package net.psforever.objects.avatar import akka.actor.{Actor, ActorRef, Props, typed} import net.psforever.actors.session.AvatarActor -import net.psforever.actors.zone.ZoneActor import net.psforever.login.WorldSession.{DropEquipmentFromInventory, HoldNewEquipmentUp, PutNewEquipmentInInventoryOrDrop, RemoveOldEquipmentFromInventory} import net.psforever.objects._ import net.psforever.objects.ce.Deployable @@ -18,7 +17,6 @@ import net.psforever.objects.serverobject.containable.{Containable, ContainableB import net.psforever.objects.serverobject.damage.Damageable.Target import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} import net.psforever.objects.serverobject.damage.{AggravatedBehavior, Damageable, DamageableEntity} -import net.psforever.objects.serverobject.mount.Mountable import net.psforever.objects.serverobject.terminals.Terminal import net.psforever.objects.vital._ import net.psforever.objects.vital.resolution.ResolutionCalculations.Output @@ -35,7 +33,7 @@ import net.psforever.objects.serverobject.repair.Repairable import net.psforever.objects.sourcing.{AmenitySource, PlayerSource} import net.psforever.objects.vital.collision.CollisionReason import net.psforever.objects.vital.etc.{PainboxReason, SuicideReason} -import net.psforever.objects.vital.interaction.{Adversarial, DamageInteraction, DamageResult} +import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult} import net.psforever.packet.PlanetSideGamePacket import scala.concurrent.duration._ @@ -320,7 +318,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm } else if ((!resistance && before != slot && (player.DrawnSlot = slot) != before) && ItemSwapSlot != before) { val mySlot = if (updateMyHolsterArm) slot else -1 //use as a short-circuit events ! AvatarServiceMessage( - player.Continent, + Players.ZoneChannelIfSpectating(player), AvatarAction.ObjectHeld(player.GUID, mySlot, player.LastDrawnSlot) ) val isHolsters = player.VisibleSlots.contains(slot) @@ -332,11 +330,11 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm if (unholsteredItem.Definition == GlobalDefinitions.remote_electronics_kit) { //rek beam/icon colour must match the player's correct hack level events ! AvatarServiceMessage( - player.Continent, + Players.ZoneChannelIfSpectating(player), AvatarAction.PlanetsideAttribute(unholsteredItem.GUID, 116, player.avatar.hackingSkillLevel()) ) } - case None => ; + case None => () } } else { equipment match { @@ -385,15 +383,13 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm } if (Players.CertificationToUseExoSuit(player, exosuit, subtype)) { if (exosuit == ExoSuitType.MAX) { - val weapon = GlobalDefinitions.MAXArms(subtype, player.Faction) - val cooldown = player.avatar.purchaseCooldown(weapon) + val cooldown = player.avatar.purchaseCooldown(GlobalDefinitions.MAXArms(subtype, player.Faction)) if (originalSubtype == subtype) { - (exosuit, subtype) //same MAX subtype is free + (exosuit, subtype) //same MAX subtype } else if (cooldown.nonEmpty) { - fallbackSuit //different MAX subtype can not have cooldown + fallbackSuit //different MAX subtype } else { - avatarActor ! AvatarActor.UpdatePurchaseTime(weapon) - (exosuit, subtype) //switching for first time causes cooldown + (exosuit, subtype) //switching } } else { (exosuit, subtype) @@ -475,11 +471,9 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm case InventoryItem(citem: ConstructionItem, _) => Deployables.initializeConstructionItem(player.avatar.certifications, citem) } - //deactivate non-passive implants - avatarActor ! AvatarActor.DeactivateActiveImplants val zone = player.Zone zone.AvatarEvents ! AvatarServiceMessage( - zone.id, + Players.ZoneChannelIfSpectating(player), AvatarAction.ChangeLoadout( player.GUID, toArmor, @@ -556,7 +550,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm //don't know where boomer trigger "should" go TaskWorkflow.execute(PutNewEquipmentInInventoryOrDrop(player)(trigger)) } - Players.buildCooldownReset(zone, player.Name, obj) + Players.buildCooldownReset(zone, player.Name, obj.GUID) case _ => () } deployablePair = None @@ -573,7 +567,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm player.Actor ! Player.LoseDeployable(obj) TelepadControl.TelepadError(zone, player.Name, msg = "@Telepad_NoDeploy_RouterLost") } - Players.buildCooldownReset(zone, player.Name, obj) + Players.buildCooldownReset(zone, player.Name, obj.GUID) case _ => () } deployablePair = None @@ -581,7 +575,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm case Zone.Deployable.IsBuilt(obj) => deployablePair match { case Some((deployable, tool)) if deployable eq obj => - Players.buildCooldownReset(player.Zone, player.Name, obj) + Players.buildCooldownReset(player.Zone, player.Name, obj.GUID) player.Find(tool) match { case Some(index) => Players.commonDestroyConstructionItem(player, tool, index) @@ -613,10 +607,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm val weapon = GlobalDefinitions.MAXArms(subtype, player.Faction) player.avatar.purchaseCooldown(weapon) .collect(_ => false) - .getOrElse { - avatarActor ! AvatarActor.UpdatePurchaseTime(weapon) - true - } + .getOrElse(true) } else { true }) @@ -671,10 +662,8 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm //insert afterHolsters.foreach(elem => player.Slot(elem.start).Equipment = elem.obj) afterInventory.foreach(elem => player.Inventory.InsertQuickly(elem.start, elem.obj)) - //deactivate non-passive implants - avatarActor ! AvatarActor.DeactivateActiveImplants player.Zone.AvatarEvents ! AvatarServiceMessage( - player.Zone.id, + Players.ZoneChannelIfSpectating(player), AvatarAction.ChangeExosuit( player.GUID, toArmor, @@ -744,7 +733,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm else { log.warn(s"cannot build a ${obj.Definition.Name}") DropEquipmentFromInventory(player)(tool, Some(obj.Position)) - Players.buildCooldownReset(zone, player.Name, obj) + Players.buildCooldownReset(zone, player.Name, obj.GUID) obj.Position = Vector3.Zero obj.AssignOwnership(None) zone.Deployables ! Zone.Deployable.Dismiss(obj) @@ -755,7 +744,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm obj.AssignOwnership(None) val zone = player.Zone zone.Deployables ! Zone.Deployable.Dismiss(obj) - Players.buildCooldownReset(zone, player.Name, obj) + Players.buildCooldownReset(zone, player.Name, obj.GUID) } } @@ -954,7 +943,6 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm def DestructionAwareness(target: Player, cause: DamageResult): Unit = { val player_guid = target.GUID val pos = target.Position - val respawnTimer = 300000 //milliseconds val zone = target.Zone val events = zone.AvatarEvents val nameChannel = target.Name @@ -980,36 +968,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm damageLog.info(s"${player.Name} killed ${player.Sex.pronounObject}self") } - // This would normally happen async as part of AvatarAction.Killed, but if it doesn't happen before deleting calling AvatarAction.ObjectDelete on the player the LLU will end up invisible to others if carried - // Therefore, queue it up to happen first. - events ! AvatarServiceMessage(nameChannel, AvatarAction.DropSpecialItem()) - - events ! AvatarServiceMessage( - nameChannel, - AvatarAction.Killed(player_guid, target.VehicleSeated) - ) //align client interface fields with state - zone.GUID(target.VehicleSeated) match { - case Some(obj: Mountable) => - //boot cadaver from mount internally (vehicle perspective) - obj.PassengerInSeat(target) match { - case Some(index) => - obj.Seats(index).unmount(target) - case _ => ; - } - //boot cadaver from mount on client - events ! AvatarServiceMessage( - nameChannel, - AvatarAction.SendResponse( - Service.defaultPlayerGUID, - ObjectDetachMessage(obj.GUID, player_guid, target.Position, Vector3.Zero) - ) - ) - //make player invisible on client - events ! AvatarServiceMessage(nameChannel, AvatarAction.PlanetsideAttributeToAll(player_guid, 29, 1)) - //only the dead player should "see" their own body, so that the death camera has something to focus on - events ! AvatarServiceMessage(zoneChannel, AvatarAction.ObjectDelete(player_guid, player_guid)) - case _ => ; - } + events ! AvatarServiceMessage(nameChannel, AvatarAction.Killed(player_guid, cause, target.VehicleSeated)) //align client interface fields with state events ! AvatarServiceMessage(zoneChannel, AvatarAction.PlanetsideAttributeToAll(player_guid, 0, 0)) //health if (target.Capacitor > 0) { target.Capacitor = 0 @@ -1023,35 +982,6 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm DestroyMessage(player_guid, attribute, Service.defaultPlayerGUID, pos) ) //how many players get this message? ) - events ! AvatarServiceMessage( - nameChannel, - AvatarAction.SendResponse( - Service.defaultPlayerGUID, - AvatarDeadStateMessage(DeadState.Dead, respawnTimer, respawnTimer, pos, target.Faction, unk5=true) - ) - ) - //TODO other methods of death? - val pentry = PlayerSource(target) - cause - .adversarial - .collect { case out @ Adversarial(attacker, _, _) if attacker != PlayerSource.Nobody => out } - .orElse { - target.LastDamage.collect { - case attack if System.currentTimeMillis() - attack.interaction.hitTime < (10 seconds).toMillis => - attack - .adversarial - .collect { case out @ Adversarial(attacker, _, _) if attacker != PlayerSource.Nobody => out } - }.flatten - } match { - case Some(adversarial) => - events ! AvatarServiceMessage( - zoneChannel, - AvatarAction.DestroyDisplay(adversarial.attacker, pentry, adversarial.implement) - ) - case _ => - events ! AvatarServiceMessage(zoneChannel, AvatarAction.DestroyDisplay(pentry, pentry, 0)) - } - zone.actor ! ZoneActor.RewardThisDeath(player) } def suicide() : Unit = { diff --git a/src/main/scala/net/psforever/objects/ballistics/Projectile.scala b/src/main/scala/net/psforever/objects/ballistics/Projectile.scala index 4cf27207d..13c53d3a4 100644 --- a/src/main/scala/net/psforever/objects/ballistics/Projectile.scala +++ b/src/main/scala/net/psforever/objects/ballistics/Projectile.scala @@ -81,24 +81,7 @@ final case class Projectile( * @return a new `Projectile` entity */ def quality(value: ProjectileQuality): Projectile = { - val projectile = new Projectile( - profile, - tool_def, - fire_mode, - mounted_in, - owner, - attribute_to, - shot_origin, - shot_angle, - shot_velocity, - value, - id, - fire_time - ) - if(isMiss) projectile.Miss() - else if(isResolved) projectile.Resolve() - projectile.WhichSide = this.WhichSide - projectile + Projectile.copy(original=this, copy(quality = value)) } /** @@ -186,4 +169,15 @@ object Projectile { ): Projectile = { Projectile(profile, tool_def, fire_mode, None, owner, attribute_to, shot_origin, shot_angle, None) } + + def copy(original: Projectile, dirtyCopy: Projectile): Projectile = { + val properCopy = dirtyCopy.copy(fire_time = original.fire_time, id = original.id) + properCopy.GUID = original.GUID + properCopy.WhichSide = original.WhichSide + if (original.isMiss) + properCopy.Miss() + else if (original.isResolved) + properCopy.Resolve() + properCopy + } } 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..1d2f16fe6 100644 --- a/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala +++ b/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala @@ -65,11 +65,17 @@ object AvatarConverter { */ def MakeAppearanceData(obj: Player): Int => CharacterAppearanceData = { val alt_model_flag: Boolean = obj.isBackpack + val avatar = obj.avatar + val tempAvatarInfo = if (obj.spectator) { + avatar.basic.copy(name = s"") + } else { + avatar.basic + } val aa: Int => CharacterAppearanceA = CharacterAppearanceA( - obj.avatar.basic, + tempAvatarInfo, CommonFieldData( obj.Faction, - bops = obj.spectator, + bops = obj.bops, alt_model_flag, v1 = false, None, @@ -106,7 +112,7 @@ object AvatarConverter { unk7 = false, on_zipline = None ) - CharacterAppearanceData(aa, ab, obj.avatar.decoration.ribbonBars) + CharacterAppearanceData(aa, ab, avatar.decoration.ribbonBars) } def MakeCharacterData(obj: Player): (Boolean, Boolean) => CharacterData = { diff --git a/src/main/scala/net/psforever/objects/global/GlobalDefinitionsProjectile.scala b/src/main/scala/net/psforever/objects/global/GlobalDefinitionsProjectile.scala index c16487aea..4771f421b 100644 --- a/src/main/scala/net/psforever/objects/global/GlobalDefinitionsProjectile.scala +++ b/src/main/scala/net/psforever/objects/global/GlobalDefinitionsProjectile.scala @@ -289,7 +289,7 @@ object GlobalDefinitionsProjectile { chainblade_projectile.Damage1 = 0 chainblade_projectile.ProjectileDamageType = DamageType.Direct chainblade_projectile.InitialVelocity = 100 - chainblade_projectile.Lifespan = .02f + chainblade_projectile.Lifespan = .03f //.02f ProjectileDefinition.CalculateDerivedFields(chainblade_projectile) chainblade_projectile.Modifiers = List(MeleeBoosted, MaxDistanceCutoff) @@ -601,7 +601,7 @@ object GlobalDefinitionsProjectile { forceblade_projectile.Damage1 = 0 forceblade_projectile.ProjectileDamageType = DamageType.Direct forceblade_projectile.InitialVelocity = 100 - forceblade_projectile.Lifespan = .02f + forceblade_projectile.Lifespan = .03f //.02f ProjectileDefinition.CalculateDerivedFields(forceblade_projectile) forceblade_projectile.Modifiers = List(MeleeBoosted, MaxDistanceCutoff) @@ -942,7 +942,7 @@ object GlobalDefinitionsProjectile { katana_projectile.Damage1 = 0 katana_projectile.ProjectileDamageType = DamageType.Direct katana_projectile.InitialVelocity = 100 - katana_projectile.Lifespan = .03f + katana_projectile.Lifespan = .04f //.03f ProjectileDefinition.CalculateDerivedFields(katana_projectile) katana_projectileb.Name = "katana_projectileb" @@ -1088,7 +1088,7 @@ object GlobalDefinitionsProjectile { magcutter_projectile.Damage1 = 0 magcutter_projectile.ProjectileDamageType = DamageType.Direct magcutter_projectile.InitialVelocity = 100 - magcutter_projectile.Lifespan = .02f + magcutter_projectile.Lifespan = .03f //.02f ProjectileDefinition.CalculateDerivedFields(magcutter_projectile) magcutter_projectile.Modifiers = List(MeleeBoosted, MaxDistanceCutoff) diff --git a/src/main/scala/net/psforever/objects/vital/InGameHistory.scala b/src/main/scala/net/psforever/objects/vital/InGameHistory.scala index 359cfe2f8..70f8b4193 100644 --- a/src/main/scala/net/psforever/objects/vital/InGameHistory.scala +++ b/src/main/scala/net/psforever/objects/vital/InGameHistory.scala @@ -4,7 +4,7 @@ package net.psforever.objects.vital import net.psforever.objects.PlanetSideGameObject import net.psforever.objects.definition.{EquipmentDefinition, KitDefinition, ToolDefinition} import net.psforever.objects.serverobject.affinity.FactionAffinity -import net.psforever.objects.sourcing.{AmenitySource, PlayerSource, SourceEntry, SourceUniqueness, SourceWithHealthEntry, VehicleSource} +import net.psforever.objects.sourcing.{AmenitySource, DeployableSource, PlayerSource, SourceEntry, SourceUniqueness, SourceWithHealthEntry, VehicleSource} import net.psforever.objects.vital.environment.EnvironmentReason import net.psforever.objects.vital.etc.{ExplodingEntityReason, PainboxReason, SuicideReason} import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult} @@ -82,6 +82,9 @@ final case class ShieldCharge(amount: Int, cause: Option[SourceEntry]) final case class TerminalUsedActivity(terminal: AmenitySource, transaction: TransactionType.Value) extends GeneralActivity +final case class TelepadUseActivity(router: VehicleSource, telepad: DeployableSource, player: PlayerSource) + extends GeneralActivity + sealed trait VehicleMountChange extends GeneralActivity { def vehicle: VehicleSource def zoneNumber: Int @@ -248,7 +251,7 @@ trait InGameHistory { */ def LogActivity(action: Option[InGameActivity]): List[InGameActivity] = { action match { - case Some(act: VehicleDismountActivity) => + case Some(act: VehicleDismountActivity) if act.pairedEvent.isEmpty => history .findLast(_.isInstanceOf[VehicleMountActivity]) .collect { @@ -259,6 +262,8 @@ trait InGameHistory { history = history :+ act None } + case Some(act: VehicleDismountActivity) => + history = history :+ act case Some(act: VehicleCargoDismountActivity) => history .findLast(_.isInstanceOf[VehicleCargoMountActivity]) diff --git a/src/main/scala/net/psforever/objects/vital/damage/DamageCalculations.scala b/src/main/scala/net/psforever/objects/vital/damage/DamageCalculations.scala index c2bceeb6a..bd46892ef 100644 --- a/src/main/scala/net/psforever/objects/vital/damage/DamageCalculations.scala +++ b/src/main/scala/net/psforever/objects/vital/damage/DamageCalculations.scala @@ -3,6 +3,8 @@ package net.psforever.objects.vital.damage import net.psforever.objects.vital.interaction.DamageInteraction +import scala.annotation.unused + /** * A series of methods for extraction of the base damage against a given target type * as well as incorporating damage modifiers from the other aspects of the interaction. @@ -11,7 +13,7 @@ object DamageCalculations { type Selector = DamageProfile => Int //raw damage selectors - def AgainstNothing(profile : DamageProfile) : Int = 0 + def AgainstNothing(@unused profile : DamageProfile) : Int = 0 def AgainstExoSuit(profile : DamageProfile) : Int = profile.Damage0 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..cbaf923cb 100644 --- a/src/main/scala/net/psforever/objects/zones/blockmap/Sector.scala +++ b/src/main/scala/net/psforever/objects/zones/blockmap/Sector.scala @@ -313,7 +313,7 @@ object SectorGroup { new SectorGroup( rangeX, rangeY, - sector.livePlayerList, + sector.livePlayerList.filterNot(p => p.spectator || !p.allowInteraction), sector.corpseList, sector.vehicleList, sector.equipmentOnGroundList, @@ -368,7 +368,7 @@ object SectorGroup { new SectorGroup( rangeX, rangeY, - sector.livePlayerList, + sector.livePlayerList.filterNot(p => p.spectator || !p.allowInteraction), sector.corpseList, sector.vehicleList, sector.equipmentOnGroundList, @@ -382,7 +382,7 @@ object SectorGroup { new SectorGroup( rangeX, rangeY, - sectors.flatMap { _.livePlayerList }.toList.distinct, + sectors.flatMap { _.livePlayerList }.toList.distinct.filterNot(p => p.spectator || !p.allowInteraction), sectors.flatMap { _.corpseList }.toList.distinct, sectors.flatMap { _.vehicleList }.toList.distinct, sectors.flatMap { _.equipmentOnGroundList }.toList.distinct, diff --git a/src/main/scala/net/psforever/objects/zones/exp/KillContributions.scala b/src/main/scala/net/psforever/objects/zones/exp/KillContributions.scala index 410492f90..5a58f8b82 100644 --- a/src/main/scala/net/psforever/objects/zones/exp/KillContributions.scala +++ b/src/main/scala/net/psforever/objects/zones/exp/KillContributions.scala @@ -5,7 +5,7 @@ import akka.actor.ActorRef import net.psforever.objects.GlobalDefinitions import net.psforever.objects.avatar.scoring.{Kill, SupportActivity} import net.psforever.objects.sourcing.{BuildingSource, PlayerSource, SourceEntry, SourceUniqueness, TurretSource, VehicleSource} -import net.psforever.objects.vital.{Contribution, HealFromTerminal, InGameActivity, RepairFromTerminal, RevivingActivity, TerminalUsedActivity, VehicleCargoDismountActivity, VehicleCargoMountActivity, VehicleDismountActivity, VehicleMountActivity} +import net.psforever.objects.vital.{Contribution, HealFromTerminal, InGameActivity, RepairFromTerminal, RevivingActivity, TelepadUseActivity, TerminalUsedActivity, VehicleCargoDismountActivity, VehicleCargoMountActivity, VehicleDismountActivity, VehicleMountActivity} import net.psforever.objects.vital.projectile.ProjectileReason import net.psforever.objects.zones.exp.rec.{CombinedHealthAndArmorContributionProcess, MachineRecoveryExperienceContributionProcess} import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} @@ -55,6 +55,11 @@ object KillContributions { /** cached for empty collection returns; please do not add anything to it */ private val emptyMap: mutable.LongMap[ContributionStats] = mutable.LongMap.empty[ContributionStats] + /** cached for use with telepad deployable activities, from the perspective of the router */ + private val routerKillAssist = RouterKillAssist(GlobalDefinitions.router.ObjectId) + /** cached for use with telepad deployable activities */ + private val routerTelepadKillAssist = RouterKillAssist(GlobalDefinitions.router_telepad_deployable.ObjectId) + /** * Primary landing point for calculating the rewards given for helping one player kill another player. * Rewards in the form of "support experience points" are given @@ -263,6 +268,7 @@ object KillContributions { ): mutable.LongMap[ContributionStats] = { contributeWithRevivalActivity(history, existingParticipants) contributeWithTerminalActivity(history, faction, contributions, excludedTargets, existingParticipants) + contributeWithRouterTelepadActivity(kill, history, faction, contributions, excludedTargets, existingParticipants) contributeWithVehicleTransportActivity(kill, history, faction, contributions, excludedTargets, existingParticipants) contributeWithVehicleCargoTransportActivity(kill, history, faction, contributions, excludedTargets, existingParticipants) contributeWithKillWhileMountedActivity(kill, faction, contributions, excludedTargets, existingParticipants) @@ -371,10 +377,17 @@ object KillContributions { /* collect the dismount activity of all vehicles from which this player is not the owner make certain all dismount activity can be paired with a mounting activity - certain other qualifications of the prior mounting must be met before the support bonus applies + other qualifications of the prior mounting must be met before the support bonus applies */ + val killerOpt = kill.info.adversarial + .map(_.attacker) + .collect { case p: PlayerSource => p } val dismountActivity = history .collect { + /* + the player should not get credit from being the vehicle owner in matters of transportation + there are considerations of time and distance traveled before the kill as well + */ case out: VehicleDismountActivity if !out.vehicle.owner.contains(out.player.unique) && out.pairedEvent.nonEmpty => (out.pairedEvent.get, out) } @@ -382,29 +395,30 @@ object KillContributions { case (in: VehicleMountActivity, out: VehicleDismountActivity) if in.vehicle.unique == out.vehicle.unique && out.vehicle.Faction == out.player.Faction && - (in.vehicle.Definition == GlobalDefinitions.router || { - val inTime = in.time - val outTime = out.time - out.player.progress.kills.exists { death => - val deathTime = death.info.interaction.hitTime - inTime < deathTime && deathTime <= outTime - } - } || { - val sameZone = in.zoneNumber == out.zoneNumber - val distanceTransported = Vector3.DistanceSquared(in.vehicle.Position.xy, out.vehicle.Position.xy) - val distanceMoved = { - val killLocation = kill.info.adversarial - .collect { adversarial => adversarial.attacker.Position.xy } - .getOrElse(Vector3.Zero) - Vector3.DistanceSquared(killLocation, out.player.Position.xy) - } - val timeSpent = out.time - in.time - distanceMoved < 5625f /* 75m */ && - (timeSpent >= 210000L /* 3:30 */ || - (sameZone && (distanceTransported > 160000f /* 400m */ || - distanceTransported > 10000f /* 100m */ && timeSpent >= 60000L /* 1:00m */)) || - (!sameZone && (distanceTransported > 10000f /* 100m */ || timeSpent >= 120000L /* 2:00 */ ))) - }) => + /* + considerations of time and distance transported before the kill + */ + ({ + val inTime = in.time + val outTime = out.time + out.player.progress.kills.exists { death => + val deathTime = death.info.interaction.hitTime + inTime < deathTime && deathTime <= outTime + } + } || { + val sameZone = in.zoneNumber == out.zoneNumber + val distanceTransported = Vector3.DistanceSquared(in.vehicle.Position.xy, out.vehicle.Position.xy) + val distanceMoved = { + val killLocation = killerOpt.map(_.Position.xy).getOrElse(Vector3.Zero) + Vector3.DistanceSquared(killLocation, out.player.Position.xy) + } + val timeSpent = out.time - in.time + distanceMoved < 5625f /* 75m */ && + (timeSpent >= 210000L /* 3:30 */ || + (sameZone && (distanceTransported > 160000f /* 400m */ || + distanceTransported > 10000f /* 100m */ && timeSpent >= 60000L /* 1:00m */)) || + (!sameZone && (distanceTransported > 10000f /* 100m */ || timeSpent >= 120000L /* 2:00 */ ))) + }) => out } //apply @@ -412,25 +426,20 @@ object KillContributions { .groupBy { _.vehicle } .collect { case (mount, dismountsFromVehicle) if mount.owner.nonEmpty => val promotedOwner = PlayerSource(mount.owner.get, mount.Position) - val (equipmentUseContext, equipmentUseEvent) = mount.Definition match { - case v @ GlobalDefinitions.router => - (RouterKillAssist(v.ObjectId), "router") - case v => - (HotDropKillAssist(v.ObjectId, 0), "hotdrop") - } val size = dismountsFromVehicle.size val time = dismountsFromVehicle.maxBy(_.time).time - val weaponStat = Support.calculateSupportExperience( - equipmentUseEvent, - WeaponStats(equipmentUseContext, size, size, time, 1f) - ) - combineStatsInto( - out, - ( - promotedOwner.CharId, - ContributionStats(promotedOwner, Seq(weaponStat), size, size, size, time) - ) - ) + List((HotDropKillAssist(mount.Definition.ObjectId, 0), "hotdrop", promotedOwner)) + .foreach { + case (equipmentUseContext, equipmentUseEvent, eventOwner) => + val weaponStat = Support.calculateSupportExperience( + equipmentUseEvent, + WeaponStats(equipmentUseContext, size, size, time, 1f) + ) + combineStatsInto( + out, + (eventOwner.CharId, ContributionStats(eventOwner, Seq(weaponStat), size, size, size, time)) + ) + } contributions.get(mount.unique).collect { case list => val mountHistory = dismountsFromVehicle @@ -554,6 +563,86 @@ object KillContributions { } } + /** + * Gather and reward use of a telepad deployable in performing a kill. + * There are two ways to account for telepad deployable use, + * i.e., traveling through s telepad deployable or using the internal telepad system of a Router: + * the user that places the telepad deployable unit, + * and the user that owns the Router.
+ * na + * @param kill the in-game event that maintains information about the other player's death + * @param faction empire to target + * @param contributions mapping between external entities + * the target has interacted with in the form of in-game activity + * and history related to the time period in which the interaction ocurred + * @param excludedTargets if a potential target is listed here already, skip processing it + * @param out quantitative record of activity in relation to the other players and their equipment + * @see `combineStatsInto` + * @see `extractContributionsForMachineByTarget` + */ + private def contributeWithRouterTelepadActivity( + kill: Kill, + history: List[InGameActivity], + faction: PlanetSideEmpire.Value, + contributions: Map[SourceUniqueness, List[InGameActivity]], + excludedTargets: mutable.ListBuffer[SourceUniqueness], + out: mutable.LongMap[ContributionStats] + ): Unit = { + /* + collect the use of all router telepads from which this player is not the owner (deployer) of the telepad + */ + val killer = kill.info.adversarial + .map(_.attacker) + .collect { case p: PlayerSource => p } + .getOrElse(PlayerSource.Nobody) + history + .collect { + case event: TelepadUseActivity if !event.player.Name.equals(event.telepad.OwnerName) => + event + } + .groupBy(_.telepad.unique) + .flatMap { + case (_, telepadEvents) => + val size = telepadEvents.size + val time = telepadEvents.maxBy(_.time).time + val firstEvent = telepadEvents.head + val telepadOwner = firstEvent.telepad.owner.asInstanceOf[PlayerSource] + val mount = firstEvent.router + contributions.get(mount.unique).collect { + case list => + val mountHistory = telepadEvents + .flatMap { event => + val eventTime = event.time + val startTime = eventTime - Config.app.game.experience.longContributionTime + limitHistoryToThisLife(list, eventTime, startTime) + } + .distinctBy(_.time) + combineStatsInto( + out, + extractContributionsForMachineByTarget(mount, faction, mountHistory, contributions, excludedTargets, eventOutputType="support-repair") + ) + } + telepadEvents + .flatMap(_.router.owner) + .distinct + .filterNot(owner => owner == killer.unique || owner == telepadOwner.unique) + .map(p => (WeaponStats(routerKillAssist, size, size, time, 1f), "router-driver", PlayerSource(p, Vector3.Zero))) :+ + (WeaponStats(routerTelepadKillAssist, size, size, time, 1f), "telepad-use", telepadOwner) + } + .foreach { + case (equipmentUseContext, equipmentUseEventId, eventOwner) => + val size = equipmentUseContext.amount + val weaponStat = Support.calculateSupportExperience(equipmentUseEventId, equipmentUseContext) + combineStatsInto( + out, + ( + eventOwner.CharId, + ContributionStats(eventOwner, Seq(weaponStat), size, size, size, equipmentUseContext.time) + ) + ) + } + } + /** * Gather and reward specific in-game equipment use activity.
* na diff --git a/src/main/scala/net/psforever/objects/zones/exp/Support.scala b/src/main/scala/net/psforever/objects/zones/exp/Support.scala index 1b0551b50..0233426ca 100644 --- a/src/main/scala/net/psforever/objects/zones/exp/Support.scala +++ b/src/main/scala/net/psforever/objects/zones/exp/Support.scala @@ -676,12 +676,14 @@ object Support { val shots = weaponStat.shots val shotsMax = event.shotsMax val shotsMultiplier = event.shotsMultiplier - if (shotsMultiplier > 0f && shots < event.shotsCutoff) { - val modifiedShotsReward: Float = - shotsMultiplier * math.log(math.min(shotsMax, shots).toDouble + 2d).toFloat - val modifiedAmountReward: Float = - event.amountMultiplier * weaponStat.amount.toFloat - event.base.toFloat + modifiedShotsReward + modifiedAmountReward + if (shots < event.shotsCutoff) { + if (shotsMultiplier > 0f) { + val modifiedShotsReward: Float = shotsMultiplier * math.log(math.min(shotsMax, shots).toDouble + 2d).toFloat + val modifiedAmountReward: Float = event.amountMultiplier * weaponStat.amount.toFloat + event.base.toFloat + modifiedShotsReward + modifiedAmountReward + } else { + event.base.toFloat + } } else { 0f } diff --git a/src/main/scala/net/psforever/packet/game/ChatMsg.scala b/src/main/scala/net/psforever/packet/game/ChatMsg.scala index 313f1ecc1..4c64451c1 100644 --- a/src/main/scala/net/psforever/packet/game/ChatMsg.scala +++ b/src/main/scala/net/psforever/packet/game/ChatMsg.scala @@ -2,11 +2,34 @@ package net.psforever.packet.game import net.psforever.newcodecs._ +import net.psforever.packet.GamePacketOpcode.Type import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} import net.psforever.types.ChatMessageType -import scodec.Codec +import scodec.bits.BitVector +import scodec.{Attempt, Codec} import scodec.codecs._ +/* +For colors, type '/#n' before text, where `n` is one of the following hexadecimal numbers: + 0 white + 1 black + 2 cyan + 3 yellow + 4 green + 5 light blue + 6 brown + 7 violet + 8 magneta + 9 purple + a purple + b yellow green + c blue + d light pink + e light green + f beige +All other options result in white text. +*/ + /** * Instructs client to display and/or process a chat message/command when sent server to client. * Instructs server to route and/or process a chat message/command when sent client to server. @@ -35,8 +58,8 @@ final case class ChatMsg( assert(note.isEmpty, "Note contents found, but message type isnt Note") type Packet = ChatMsg - def opcode = GamePacketOpcode.ChatMsg - def encode = ChatMsg.encode(this) + def opcode: Type = GamePacketOpcode.ChatMsg + def encode: Attempt[BitVector] = ChatMsg.encode(this) } object ChatMsg extends Marshallable[ChatMsg] { 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 } diff --git a/src/main/scala/net/psforever/services/avatar/AvatarService.scala b/src/main/scala/net/psforever/services/avatar/AvatarService.scala index eec4d7044..d7e585f0a 100644 --- a/src/main/scala/net/psforever/services/avatar/AvatarService.scala +++ b/src/main/scala/net/psforever/services/avatar/AvatarService.scala @@ -138,9 +138,9 @@ class AvatarService(zone: Zone) extends Actor { AvatarEvents.publish( AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.HitHint(source_guid)) ) - case AvatarAction.Killed(player_guid, mount_guid) => + case AvatarAction.Killed(player_guid, cause, mount_guid) => AvatarEvents.publish( - AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.Killed(mount_guid)) + AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.Killed(cause, mount_guid)) ) case AvatarAction.LoadPlayer(player_guid, object_id, target_guid, cdata, pdata) => val pkt = pdata match { diff --git a/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala b/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala index 8c3484ddf..ec2959b11 100644 --- a/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala +++ b/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala @@ -8,6 +8,7 @@ import net.psforever.objects.equipment.Equipment import net.psforever.objects.inventory.InventoryItem import net.psforever.objects.serverobject.environment.interaction.common.Watery.OxygenStateTarget import net.psforever.objects.sourcing.SourceEntry +import net.psforever.objects.vital.interaction.DamageResult import net.psforever.objects.zones.Zone import net.psforever.packet.PlanetSideGamePacket import net.psforever.packet.game.ImplantAction @@ -54,7 +55,7 @@ object AvatarAction { final case class GenericObjectAction(player_guid: PlanetSideGUID, object_guid: PlanetSideGUID, action_code: Int) extends Action final case class HitHint(source_guid: PlanetSideGUID, player_guid: PlanetSideGUID) extends Action - final case class Killed(player_guid: PlanetSideGUID, mount_guid: Option[PlanetSideGUID]) extends Action + final case class Killed(player_guid: PlanetSideGUID, cause: DamageResult, mount_guid: Option[PlanetSideGUID]) extends Action final case class LoadPlayer( player_guid: PlanetSideGUID, object_id: Int, diff --git a/src/main/scala/net/psforever/services/avatar/AvatarServiceResponse.scala b/src/main/scala/net/psforever/services/avatar/AvatarServiceResponse.scala index c90505842..897d105b7 100644 --- a/src/main/scala/net/psforever/services/avatar/AvatarServiceResponse.scala +++ b/src/main/scala/net/psforever/services/avatar/AvatarServiceResponse.scala @@ -8,6 +8,7 @@ import net.psforever.objects.equipment.Equipment import net.psforever.objects.inventory.InventoryItem import net.psforever.objects.serverobject.environment.interaction.common.Watery.OxygenStateTarget import net.psforever.objects.sourcing.SourceEntry +import net.psforever.objects.vital.interaction.DamageResult import net.psforever.packet.PlanetSideGamePacket import net.psforever.packet.game.objectcreate.ConstructorData import net.psforever.packet.game.{ImplantAction, ObjectCreateMessage} @@ -46,7 +47,7 @@ object AvatarResponse { final case class EquipmentInHand(pkt: ObjectCreateMessage) extends Response final case class GenericObjectAction(object_guid: PlanetSideGUID, action_code: Int) extends Response final case class HitHint(source_guid: PlanetSideGUID) extends Response - final case class Killed(mount_guid: Option[PlanetSideGUID]) extends Response + final case class Killed(cause: DamageResult, mount_guid: Option[PlanetSideGUID]) extends Response final case class LoadPlayer(pkt: ObjectCreateMessage) extends Response final case class LoadProjectile(pkt: ObjectCreateMessage) extends Response final case class ObjectDelete(item_guid: PlanetSideGUID, unk: Int) extends Response diff --git a/src/main/scala/net/psforever/services/chat/ChatChannel.scala b/src/main/scala/net/psforever/services/chat/ChatChannel.scala index 94515f0a3..3fbea3e64 100644 --- a/src/main/scala/net/psforever/services/chat/ChatChannel.scala +++ b/src/main/scala/net/psforever/services/chat/ChatChannel.scala @@ -10,3 +10,5 @@ case object DefaultChannel extends ChatChannel final case class SquadChannel(guid: PlanetSideGUID) extends ChatChannel case object SpectatorChannel extends ChatChannel + +case object CustomerServiceChannel extends ChatChannel diff --git a/src/test/scala/objects/PlayerControlTest.scala b/src/test/scala/objects/PlayerControlTest.scala index aad7d21ca..3c343b439 100644 --- a/src/test/scala/objects/PlayerControlTest.scala +++ b/src/test/scala/objects/PlayerControlTest.scala @@ -16,7 +16,6 @@ import net.psforever.objects.zones.{Zone, ZoneMap} import net.psforever.objects._ import net.psforever.objects.definition.ProjectileDefinition import net.psforever.objects.serverobject.CommonMessages -import net.psforever.objects.serverobject.environment.interaction.common.Watery.OxygenStateTarget import net.psforever.objects.serverobject.environment.{DeepSquare, EnvironmentAttribute, Pool} import net.psforever.objects.sourcing.{PlayerSource, SourceEntry} import net.psforever.objects.vital.base.DamageResolution @@ -533,7 +532,7 @@ class PlayerControlDeathStandingTest extends ActorTest { assert(player2.isAlive) player2.Actor ! Vitality.Damage(applyDamageTo) - val msg_avatar = avatarProbe.receiveN(8, 500 milliseconds) + val msg_avatar = avatarProbe.receiveN(5, 500 milliseconds) val msg_stamina = probe.receiveOne(500 milliseconds) activityProbe.expectNoMessage(200 milliseconds) assert( @@ -550,31 +549,25 @@ class PlayerControlDeathStandingTest extends ActorTest { ) assert( msg_avatar(1) match { - case AvatarServiceMessage("TestCharacter2", AvatarAction.DropSpecialItem()) => true - case _ => false - } - ) - assert( - msg_avatar(2) match { - case AvatarServiceMessage("TestCharacter2", AvatarAction.Killed(PlanetSideGUID(2), None)) => true + case AvatarServiceMessage("TestCharacter2", AvatarAction.Killed(PlanetSideGUID(2), _, None)) => true case _ => false } ) assert( - msg_avatar(3) match { + msg_avatar(2) match { case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 0, _)) => true case _ => false } ) assert( - msg_avatar(4) match { + msg_avatar(3) match { case AvatarServiceMessage("TestCharacter2", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 7, _)) => true case _ => false } ) assert( - msg_avatar(5) match { + msg_avatar(4) match { case AvatarServiceMessage( "TestCharacter2", AvatarAction.SendResponse(_, DestroyMessage(PlanetSideGUID(2), PlanetSideGUID(1), _, _)) @@ -583,27 +576,6 @@ class PlayerControlDeathStandingTest extends ActorTest { case _ => false } ) - assert( - msg_avatar(6) match { - case AvatarServiceMessage( - "TestCharacter2", - AvatarAction.SendResponse( - _, - AvatarDeadStateMessage(DeadState.Dead, 300000, 300000, Vector3.Zero, PlanetSideEmpire.NC, true) - ) - ) => - true - case _ => false - } - ) - assert( - msg_avatar(7) match { - case AvatarServiceMessage("test", AvatarAction.DestroyDisplay(killer, victim, _, _)) - if killer.Name.equals(player1.Name) && victim.Name.equals(player2.Name) => - true - case _ => false - } - ) assert(player2.Health <= player2.Definition.DamageDestroysAt) assert(player2.Armor == 0) assert(!player2.isAlive) @@ -680,7 +652,7 @@ class PlayerControlDeathStandingTest extends ActorTest { // assert(player2.isAlive) // // player2.Actor ! Vitality.Damage(applyDamageTo) -// val msg_avatar = avatarProbe.receiveN(9, 1500 milliseconds) +// val msg_avatar = avatarProbe.receiveN(3, 1500 milliseconds) // val msg_stamina = probe.receiveOne(500 milliseconds) // activityProbe.expectNoMessage(200 milliseconds) // assert( @@ -691,83 +663,30 @@ class PlayerControlDeathStandingTest extends ActorTest { // ) // assert( // msg_avatar.head match { -// case AvatarServiceMessage("TestCharacter2", AvatarAction.DropSpecialItem()) => true -// case _ => false +// case AvatarServiceMessage( +// "TestCharacter2", +// AvatarAction.Killed(PlanetSideGUID(2), _, Some(PlanetSideGUID(7))) +// ) => +// true +// case _ => false // } // ) // assert( // msg_avatar(1) match { -// case AvatarServiceMessage( -// "TestCharacter2", -// AvatarAction.Killed(PlanetSideGUID(2), Some(PlanetSideGUID(7))) -// ) => -// true -// case _ => false +// case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 0, _)) => true +// case _ => false // } // ) // assert( // msg_avatar(2) match { // case AvatarServiceMessage( // "TestCharacter2", -// AvatarAction.SendResponse(_, ObjectDetachMessage(PlanetSideGUID(7), PlanetSideGUID(2), _, _, _, _)) -// ) => -// true -// case _ => false -// } -// ) -// assert( -// msg_avatar(3) match { -// case AvatarServiceMessage( -// "TestCharacter2", -// AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 29, 1) -// ) => -// true -// case _ => false -// } -// ) -// assert( -// msg_avatar(4) match { -// case AvatarServiceMessage("test", AvatarAction.ObjectDelete(PlanetSideGUID(2), PlanetSideGUID(2), _)) => true -// case _ => false -// } -// ) -// assert( -// msg_avatar(5) match { -// case AvatarServiceMessage("test", AvatarAction.PlanetsideAttributeToAll(PlanetSideGUID(2), 0, _)) => true -// case _ => false -// } -// ) -// assert( -// msg_avatar(6) match { -// case AvatarServiceMessage( -// "TestCharacter2", // AvatarAction.SendResponse(_, DestroyMessage(PlanetSideGUID(2), PlanetSideGUID(1), _, _)) // ) => // true // case _ => false // } // ) -// assert( -// msg_avatar(7) match { -// case AvatarServiceMessage( -// "TestCharacter2", -// AvatarAction.SendResponse( -// _, -// AvatarDeadStateMessage(DeadState.Dead, 300000, 300000, Vector3.Zero, PlanetSideEmpire.NC, true) -// ) -// ) => -// true -// case _ => false -// } -// ) -// assert( -// msg_avatar(8) match { -// case AvatarServiceMessage("test", AvatarAction.DestroyDisplay(killer, victim, _, _)) -// if killer.Name.equals(player1.Name) && victim.Name.equals(player2.Name) => -// true -// case _ => false -// } -// ) // assert(player2.Health <= player2.Definition.DamageDestroysAt) // assert(!player2.isAlive) // } From 1430d3b5849bcb42c25ef7780be920c386f05c9f Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Sun, 22 Dec 2024 15:13:32 -0500 Subject: [PATCH 04/57] variable hack timer --- src/main/resources/application.conf | 3 +++ .../objects/global/GlobalDefinitionsMiscellaneous.scala | 5 +++-- src/main/scala/net/psforever/util/Config.scala | 3 ++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf index 806a23ff1..d912b5911 100644 --- a/src/main/resources/application.conf +++ b/src/main/resources/application.conf @@ -88,6 +88,9 @@ game { # Purchases timers for the battleframe robotics vehicles all update at the same time when either of them would update shared-bfr-cooldown = yes + # How long the countdown timer is when a facility (main overworld base) is hacked + facility-hack-time = 15.minutes + # HART system, shuttles and facilities hart { # How long the shuttle is not boarding passengers (going through the motions) diff --git a/src/main/scala/net/psforever/objects/global/GlobalDefinitionsMiscellaneous.scala b/src/main/scala/net/psforever/objects/global/GlobalDefinitionsMiscellaneous.scala index 3c4650132..d60c9d5ed 100644 --- a/src/main/scala/net/psforever/objects/global/GlobalDefinitionsMiscellaneous.scala +++ b/src/main/scala/net/psforever/objects/global/GlobalDefinitionsMiscellaneous.scala @@ -19,6 +19,7 @@ import net.psforever.objects.vital.base.DamageType import net.psforever.objects.vital.etc.ExplodingRadialDegrade import net.psforever.objects.vital.prop.DamageWithPosition import net.psforever.types.{ExoSuitType, Vector3} +import net.psforever.util.Config import scala.collection.mutable import scala.concurrent.duration._ @@ -485,7 +486,7 @@ object GlobalDefinitionsMiscellaneous { repair_silo.TargetValidation += EffectTarget.Category.Vehicle -> EffectTarget.Validation.RepairSilo repair_silo.Damageable = false repair_silo.Repairable = false - + recharge_terminal.Name = "recharge_terminal" recharge_terminal.Interval = 1000 recharge_terminal.UseRadius = 20 @@ -707,7 +708,7 @@ object GlobalDefinitionsMiscellaneous { capture_terminal.Name = "capture_terminal" capture_terminal.Damageable = false capture_terminal.Repairable = false - capture_terminal.FacilityHackTime = 15.minutes + capture_terminal.FacilityHackTime = Config.app.game.facilityHackTime secondary_capture.Name = "secondary_capture" secondary_capture.Damageable = false diff --git a/src/main/scala/net/psforever/util/Config.scala b/src/main/scala/net/psforever/util/Config.scala index 419e66cb7..13d7d5c90 100644 --- a/src/main/scala/net/psforever/util/Config.scala +++ b/src/main/scala/net/psforever/util/Config.scala @@ -163,7 +163,8 @@ case class GameConfig( doorsCanBeOpenedByMedAppFromThisDistance: Float, experience: Experience, maxBattleRank: Int, - promotion: PromotionSystem + promotion: PromotionSystem, + facilityHackTime: FiniteDuration ) case class InstantActionConfig( From d9c913e3ffe9a2f818d9b1c2155708e57a51fee1 Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Sun, 19 Jan 2025 16:37:48 -0500 Subject: [PATCH 05/57] grant exp for captures --- .../actors/session/support/SessionAvatarHandlers.scala | 7 +++++-- .../participation/FacilityHackParticipation.scala | 4 ++-- .../participation/MajorFacilityHackParticipation.scala | 8 +++++--- .../structures/participation/TowerHackParticipation.scala | 5 ++++- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala b/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala index 995f7666d..1fa6bf7cb 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala @@ -59,7 +59,7 @@ class SessionAvatarHandlers( //TODO squad services deactivated, participation trophy rewards for now - 11-20-2023 //must be in a squad to earn experience val charId = player.CharId - val squadUI = sessionLogic.squad.squadUI + /*val squadUI = sessionLogic.squad.squadUI val participation = continent .Building(buildingId) .map { building => @@ -117,7 +117,10 @@ class SessionAvatarHandlers( exp.ToDatabase.reportFacilityCapture(charId, buildingId, zoneNumber, modifiedExp, expType="bep") avatarActor ! AvatarActor.AwardFacilityCaptureBep(modifiedExp) Some(modifiedExp) - } + }*/ + //if not in squad (temporary) + exp.ToDatabase.reportFacilityCapture(charId, buildingId, zoneNumber, cep, expType="bep") + avatarActor ! AvatarActor.AwardFacilityCaptureBep(cep) } /** diff --git a/src/main/scala/net/psforever/objects/serverobject/structures/participation/FacilityHackParticipation.scala b/src/main/scala/net/psforever/objects/serverobject/structures/participation/FacilityHackParticipation.scala index f85211860..90c750b00 100644 --- a/src/main/scala/net/psforever/objects/serverobject/structures/participation/FacilityHackParticipation.scala +++ b/src/main/scala/net/psforever/objects/serverobject/structures/participation/FacilityHackParticipation.scala @@ -41,13 +41,13 @@ trait FacilityHackParticipation extends ParticipationLogic { .filterNot { case (_, (_, _, t)) => curr - t > hackTime } .partition { case (p, _) => uniqueList2.contains(p) } } - val newParticipaants = list + val newParticipants = list .filterNot { p => playerContribution.exists { case (u, _) => p.CharId == u } } playerContribution = vanguardParticipants.map { case (u, (p, d, _)) => (u, (p, d + 1, curr)) } ++ - newParticipaants.map { p => (p.CharId, (p, 1, curr)) } ++ + newParticipants.map { p => (p.CharId, (p, 1, curr)) } ++ missingParticipants } } diff --git a/src/main/scala/net/psforever/objects/serverobject/structures/participation/MajorFacilityHackParticipation.scala b/src/main/scala/net/psforever/objects/serverobject/structures/participation/MajorFacilityHackParticipation.scala index 6b7b3f958..634ad019f 100644 --- a/src/main/scala/net/psforever/objects/serverobject/structures/participation/MajorFacilityHackParticipation.scala +++ b/src/main/scala/net/psforever/objects/serverobject/structures/participation/MajorFacilityHackParticipation.scala @@ -28,7 +28,9 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci def TryUpdate(): Unit = { val list = building.PlayersInSOI - updatePlayers(list) + if (list.nonEmpty) { + updatePlayers(list) + } val now = System.currentTimeMillis() if (now - lastInfoRequest > 60000L) { updatePopulationOverTime(list, now, before = 900000L) @@ -123,7 +125,7 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci hackStart, completionTime, opposingFaction, - contributionOpposing + contributionVictor ) ) //1) experience from killing opposingFaction across duration of hack @@ -336,7 +338,7 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci val towerRadius = math.pow(tower.Definition.SOIRadius.toDouble * 0.7d, 2d).toFloat list .map { case (p, f, kills) => - val filteredKills = kills.filter { kill => Vector3.DistanceSquared(kill.victim.Position.xy, towerPosition) <= towerRadius } + val filteredKills = kills.filter { kill => Vector3.DistanceSquared(kill.victim.Position.xy, towerPosition) >= towerRadius } (p, f, filteredKills) } .filter { case (_, _, kills) => kills.nonEmpty } diff --git a/src/main/scala/net/psforever/objects/serverobject/structures/participation/TowerHackParticipation.scala b/src/main/scala/net/psforever/objects/serverobject/structures/participation/TowerHackParticipation.scala index 824f60898..505c401ae 100644 --- a/src/main/scala/net/psforever/objects/serverobject/structures/participation/TowerHackParticipation.scala +++ b/src/main/scala/net/psforever/objects/serverobject/structures/participation/TowerHackParticipation.scala @@ -11,11 +11,14 @@ import net.psforever.util.Config final case class TowerHackParticipation(building: Building) extends FacilityHackParticipation { def TryUpdate(): Unit = { val list = building.PlayersInSOI - updatePlayers(building.PlayersInSOI) + if (list.nonEmpty) { + updatePlayers(list) + } val now = System.currentTimeMillis() if (now - lastInfoRequest > 60000L) { updatePopulationOverTime(list, now, before = 300000L) } + lastInfoRequest = now } def RewardFacilityCapture( From 3969543cde5731c3fc5d89e30d514d9868ef5b1f Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Mon, 20 Jan 2025 10:41:15 -0500 Subject: [PATCH 06/57] fix resecure reward --- .../local/support/HackCaptureActor.scala | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/main/scala/net/psforever/services/local/support/HackCaptureActor.scala b/src/main/scala/net/psforever/services/local/support/HackCaptureActor.scala index 1a88b0e00..d561cd758 100644 --- a/src/main/scala/net/psforever/services/local/support/HackCaptureActor.scala +++ b/src/main/scala/net/psforever/services/local/support/HackCaptureActor.scala @@ -106,7 +106,7 @@ class HackCaptureActor extends Actor { NotifyHackStateChange(target, isResecured = true) building.Participation.RewardFacilityCapture( target.Faction, - faction, + HackCaptureActor.GetAttackingFaction(building, faction), hacker, facilityHackTime, hackTime, @@ -347,6 +347,30 @@ object HackCaptureActor { .get } + def GetAttackingFaction( + building: Building, + excludeThisFaction: PlanetSideEmpire.Value + ): PlanetSideEmpire.Value = { + // Use PlayerContributionRaw to calculate attacking faction + val factionEfforts = building.Participation + .PlayerContributionRaw + .values + .foldLeft(Array.fill(4)(0L)) { case (efforts, (player, duration, _)) => + val factionId = player.Faction.id + efforts.update(factionId, efforts(factionId) + duration) + efforts + } + + // Exclude the specified faction + factionEfforts.update(excludeThisFaction.id, Long.MinValue) + + // Find the faction with the highest contribution + factionEfforts.indices + .maxByOption(factionEfforts) + .map(PlanetSideEmpire.apply) + .getOrElse(PlanetSideEmpire.NEUTRAL) + } + def GetHackingFaction(terminal: CaptureTerminal): Option[PlanetSideEmpire.Value] = { terminal.HackedBy.map { a => a.player.Faction } } From ed09a9a4e5deece8f620186a9a329ec8c5a8f966 Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Thu, 23 Jan 2025 13:31:21 -0500 Subject: [PATCH 07/57] reward contributors still in zone --- .../session/support/SessionAvatarHandlers.scala | 2 +- .../MajorFacilityHackParticipation.scala | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala b/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala index 1fa6bf7cb..a85c2dcfe 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala @@ -119,7 +119,7 @@ class SessionAvatarHandlers( Some(modifiedExp) }*/ //if not in squad (temporary) - exp.ToDatabase.reportFacilityCapture(charId, buildingId, zoneNumber, cep, expType="bep") + exp.ToDatabase.reportFacilityCapture(charId, zoneNumber, buildingId, cep, expType="bep") avatarActor ! AvatarActor.AwardFacilityCaptureBep(cep) } diff --git a/src/main/scala/net/psforever/objects/serverobject/structures/participation/MajorFacilityHackParticipation.scala b/src/main/scala/net/psforever/objects/serverobject/structures/participation/MajorFacilityHackParticipation.scala index 634ad019f..230aa2226 100644 --- a/src/main/scala/net/psforever/objects/serverobject/structures/participation/MajorFacilityHackParticipation.scala +++ b/src/main/scala/net/psforever/objects/serverobject/structures/participation/MajorFacilityHackParticipation.scala @@ -251,11 +251,14 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci overallTimeMultiplier * Config.app.game.experience.cep.rate + competitionBonus ).toLong - //8. reward participants - //Classically, only players in the SOI are rewarded, and the llu runner too + //8. reward participants that are still in the zone val hackerId = hacker.CharId + val contributingPlayers = contributionVictor + .filter { case (player, _, _) => player.Zone.id == building.Zone.id } + .map { case (player, _, _) => player } + .toList //terminal hacker (always cep) - if (playersInSoi.exists(_.CharId == hackerId) && flagCarrier.map(_.CharId).getOrElse(0L) != hackerId) { + if (contributingPlayers.exists(_.CharId == hackerId) && flagCarrier.map(_.CharId).getOrElse(0L) != hackerId) { ToDatabase.reportFacilityCapture( hackerId, zoneNumber, @@ -266,7 +269,7 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci events ! AvatarServiceMessage(hacker.Name, AvatarAction.AwardCep(hackerId, finalCep)) } //bystanders (cep if squad leader, bep otherwise) - playersInSoi + contributingPlayers .filterNot { _.CharId == hackerId } .foreach { player => val charId = player.CharId From c0a357974f785363cc1abb4e598ad2b7de62a68c Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Sun, 26 Jan 2025 16:25:37 -0500 Subject: [PATCH 08/57] doors change player's sideness --- .../net/psforever/objects/avatar/interaction/WithEntrance.scala | 2 +- .../objects/serverobject/environment/EnvironmentAttribute.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/scala/net/psforever/objects/avatar/interaction/WithEntrance.scala b/src/main/scala/net/psforever/objects/avatar/interaction/WithEntrance.scala index 8263b7627..9064efb16 100644 --- a/src/main/scala/net/psforever/objects/avatar/interaction/WithEntrance.scala +++ b/src/main/scala/net/psforever/objects/avatar/interaction/WithEntrance.scala @@ -65,7 +65,7 @@ class WithEntrance() obj: InteractsWithZone, door: Door ): Sidedness = { - //debugInteriorCheck(obj, door) + debugInteriorCheck(obj, door) strictInteriorCheck(obj, door) } diff --git a/src/main/scala/net/psforever/objects/serverobject/environment/EnvironmentAttribute.scala b/src/main/scala/net/psforever/objects/serverobject/environment/EnvironmentAttribute.scala index 232923956..74d9e4744 100644 --- a/src/main/scala/net/psforever/objects/serverobject/environment/EnvironmentAttribute.scala +++ b/src/main/scala/net/psforever/objects/serverobject/environment/EnvironmentAttribute.scala @@ -82,7 +82,7 @@ object EnvironmentAttribute { /** only interact with living player characters or vehicles */ def canInteractWith(obj: PlanetSideGameObject): Boolean = canInteractWithPlayersAndVehicles(obj) - def testingDepth(obj: _root_.net.psforever.objects.PlanetSideGameObject): Float = 0f + def testingDepth(obj: _root_.net.psforever.objects.PlanetSideGameObject): Float = 4f } /** From a758911a261ef21fea192a749d15fc569d8b98b8 Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Mon, 27 Jan 2025 13:46:32 -0500 Subject: [PATCH 09/57] comment out debugInteriorCheck --- .../net/psforever/objects/avatar/interaction/WithEntrance.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/net/psforever/objects/avatar/interaction/WithEntrance.scala b/src/main/scala/net/psforever/objects/avatar/interaction/WithEntrance.scala index 9064efb16..8263b7627 100644 --- a/src/main/scala/net/psforever/objects/avatar/interaction/WithEntrance.scala +++ b/src/main/scala/net/psforever/objects/avatar/interaction/WithEntrance.scala @@ -65,7 +65,7 @@ class WithEntrance() obj: InteractsWithZone, door: Door ): Sidedness = { - debugInteriorCheck(obj, door) + //debugInteriorCheck(obj, door) strictInteriorCheck(obj, door) } From 6597e8be32051e84c698350b72655e8c94e0f75e Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Tue, 28 Jan 2025 11:55:15 -0500 Subject: [PATCH 10/57] squad kill experience --- .../actors/session/AvatarActor.scala | 2 ++ .../session/normal/AvatarHandlerLogic.scala | 3 +++ .../support/SessionAvatarHandlers.scala | 27 ++++++++++++++++++- .../services/avatar/AvatarService.scala | 9 +++++++ .../avatar/AvatarServiceMessage.scala | 1 + .../avatar/AvatarServiceResponse.scala | 1 + 6 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/main/scala/net/psforever/actors/session/AvatarActor.scala b/src/main/scala/net/psforever/actors/session/AvatarActor.scala index 1dd805e3e..b0899301e 100644 --- a/src/main/scala/net/psforever/actors/session/AvatarActor.scala +++ b/src/main/scala/net/psforever/actors/session/AvatarActor.scala @@ -3039,6 +3039,8 @@ class AvatarActor( if (exp > 0L) { setBep(avatar.bep + exp, msg) zone.actor ! ZoneActor.RewardOurSupporters(playerSource, historyTranscript, killStat, exp) + zone.AvatarEvents ! AvatarServiceMessage( + player.Name, AvatarAction.ShareKillExperienceWithSquad(player, exp)) } } diff --git a/src/main/scala/net/psforever/actors/session/normal/AvatarHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/normal/AvatarHandlerLogic.scala index 96ef7e81c..eb430a45b 100644 --- a/src/main/scala/net/psforever/actors/session/normal/AvatarHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/AvatarHandlerLogic.scala @@ -473,6 +473,9 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A case AvatarResponse.FacilityCaptureRewards(buildingId, zoneNumber, cep) => ops.facilityCaptureRewards(buildingId, zoneNumber, cep) + case AvatarResponse.ShareKillExperienceWithSquad(killer, exp) => + ops.shareKillExperienceWithSquad(killer, exp) + case AvatarResponse.SendResponse(msg) => sendResponse(msg) diff --git a/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala b/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala index a85c2dcfe..37e2d03da 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala @@ -3,7 +3,7 @@ package net.psforever.actors.session.support import akka.actor.{ActorContext, typed} import net.psforever.objects.serverobject.mount.Mountable -import net.psforever.objects.{Default, PlanetSideGameObject} +import net.psforever.objects.{Default, PlanetSideGameObject, Player} import net.psforever.objects.sourcing.{PlayerSource, SourceEntry} import net.psforever.packet.game.objectcreate.ConstructorData import net.psforever.objects.zones.exp @@ -123,6 +123,31 @@ class SessionAvatarHandlers( avatarActor ! AvatarActor.AwardFacilityCaptureBep(cep) } + /** + * + * @param killer the player who got the kill + * @param exp the amount of bep they received for the kill + * Squad members of a "killer" will receive a split of the experience if they are both alive + * and in the same zone as the killer. The amount received is + * based on the size of the squad. Each squad member that meets the criteria will receive a fractional split. + */ + def shareKillExperienceWithSquad(killer: Player, exp: Long): Unit = { + //TODO consider squad experience waypoint in exp calculation + val squadUI = sessionLogic.squad.squadUI + val squadSize = squadUI.size + if (squadSize > 1) { + val expSplit = exp / squadSize + val squadMembers = squadUI.filterNot(_._1 == killer.CharId).map { case (_, member) => member }.toList.map(_.name) + val playersInZone = killer.Zone.Players.map { avatar => (avatar.id, avatar.basic.name) } + val squadMembersHere = playersInZone.filter(member => squadMembers.contains(member._2)) + squadMembersHere.foreach { member => + killer.Zone.AvatarEvents ! AvatarServiceMessage( + member._2, + AvatarAction.AwardBep(member._1, expSplit, ExperienceType.Normal)) + } + } + } + /** * Properly format a `DestroyDisplayMessage` packet * given sufficient information about a target (victim) and an actor (killer). diff --git a/src/main/scala/net/psforever/services/avatar/AvatarService.scala b/src/main/scala/net/psforever/services/avatar/AvatarService.scala index d7e585f0a..f61e43949 100644 --- a/src/main/scala/net/psforever/services/avatar/AvatarService.scala +++ b/src/main/scala/net/psforever/services/avatar/AvatarService.scala @@ -458,6 +458,15 @@ class AvatarService(zone: Zone) extends Actor { ) ) + case AvatarAction.ShareKillExperienceWithSquad(killer, exp) => + AvatarEvents.publish( + AvatarServiceResponse( + s"/$forChannel/Avatar", + Service.defaultPlayerGUID, + AvatarResponse.ShareKillExperienceWithSquad(killer, exp) + ) + ) + case _ => () } diff --git a/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala b/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala index ec2959b11..5cea9482d 100644 --- a/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala +++ b/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala @@ -160,6 +160,7 @@ object AvatarAction { final case class AwardBep(charId: Long, bep: Long, expType: ExperienceType) extends Action final case class AwardCep(charId: Long, bep: Long) extends Action final case class FacilityCaptureRewards(building_id: Int, zone_number: Int, exp: Long) extends Action + final case class ShareKillExperienceWithSquad(killer: Player, exp: Long) extends Action final case class TeardownConnection() extends Action // final case class PlayerStateShift(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action diff --git a/src/main/scala/net/psforever/services/avatar/AvatarServiceResponse.scala b/src/main/scala/net/psforever/services/avatar/AvatarServiceResponse.scala index 897d105b7..5c26d9a02 100644 --- a/src/main/scala/net/psforever/services/avatar/AvatarServiceResponse.scala +++ b/src/main/scala/net/psforever/services/avatar/AvatarServiceResponse.scala @@ -132,4 +132,5 @@ object AvatarResponse { final case class AwardBep(charId: Long, bep: Long, expType: ExperienceType) extends Response final case class AwardCep(charId: Long, bep: Long) extends Response final case class FacilityCaptureRewards(building_id: Int, zone_number: Int, exp: Long) extends Response + final case class ShareKillExperienceWithSquad(killer: Player, exp: Long) extends Response } From 53ba117f233083a35a7b1138c9908365ce360687 Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Wed, 29 Jan 2025 12:55:34 -0500 Subject: [PATCH 11/57] include towers --- src/main/scala/net/psforever/actors/zone/ZoneActor.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/scala/net/psforever/actors/zone/ZoneActor.scala b/src/main/scala/net/psforever/actors/zone/ZoneActor.scala index 63bf81700..c5747619e 100644 --- a/src/main/scala/net/psforever/actors/zone/ZoneActor.scala +++ b/src/main/scala/net/psforever/actors/zone/ZoneActor.scala @@ -182,7 +182,8 @@ class ZoneActor( case ZoneMapUpdate() => zone.Buildings - .filter(_._2.BuildingType == StructureType.Facility) + .filter(building => + building._2.BuildingType == StructureType.Facility || building._2.BuildingType == StructureType.Tower) .values .foreach(_.Actor ! BuildingActor.MapUpdate()) Behaviors.same From a685f71d6ed85d7249bca29e5047677e972bbe88 Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Thu, 30 Jan 2025 12:11:13 -0500 Subject: [PATCH 12/57] killed player lifespan calculation --- src/main/scala/net/psforever/objects/zones/exp/Support.scala | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/scala/net/psforever/objects/zones/exp/Support.scala b/src/main/scala/net/psforever/objects/zones/exp/Support.scala index 0233426ca..1ad7ead6e 100644 --- a/src/main/scala/net/psforever/objects/zones/exp/Support.scala +++ b/src/main/scala/net/psforever/objects/zones/exp/Support.scala @@ -29,8 +29,13 @@ object Support { //setup val historyList = history.toList val withKills = victim.progress.kills.nonEmpty + //TODO Issue #1259 - Use another method to capture time of death than current time ("kill shots" aren't working) + /* val fullLifespan = (historyList.headOption, historyList.lastOption) match { case (Some(spawn), Some(death)) => death.time - spawn.time + */ + val fullLifespan = historyList.headOption match { + case Some(spawn) => System.currentTimeMillis() - spawn.time case _ => 0L } val recordOfWornTimes = countTimeWhileExoSuitOrMounted(historyList) From dd87d5da89baf0c4f9b8e943763512aa5822cd40 Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Sun, 2 Feb 2025 21:33:32 -0500 Subject: [PATCH 13/57] misc deployables fixes --- .../session/support/ZoningOperations.scala | 5 +++++ .../objects/ce/DeployableBehavior.scala | 18 ++++++++++++------ 2 files changed, 17 insertions(+), 6 deletions(-) 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 5e7501c3e..cdbb2cdcf 100644 --- a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala @@ -2494,6 +2494,11 @@ class ZoningOperations( reclaimOurDeployables(continent.DeployableList, player.Name, reassignDeployablesTo(player.GUID)) ) ) + //do this to make my deployed telepad appear that way + continent.Vehicles.filter(router => router.Definition == GlobalDefinitions.router && router.OwnerName.contains(player.Name)) + .foreach { obj => + sessionLogic.general.toggleTeleportSystem(obj, TelepadLike.AppraiseTeleportationSystem(obj, continent)) + } //begin looking for conditions to set the avatar context.system.scheduler.scheduleOnce(delay = 250 millisecond, context.self, SessionActor.SetCurrentAvatar(player, 200)) } diff --git a/src/main/scala/net/psforever/objects/ce/DeployableBehavior.scala b/src/main/scala/net/psforever/objects/ce/DeployableBehavior.scala index b363b63e3..dc5bd06f5 100644 --- a/src/main/scala/net/psforever/objects/ce/DeployableBehavior.scala +++ b/src/main/scala/net/psforever/objects/ce/DeployableBehavior.scala @@ -68,7 +68,7 @@ trait DeployableBehavior { if DeployableObject.OwnerGuid.nonEmpty => val obj = DeployableObject if (constructed.contains(true)) { - loseOwnership(obj, PlanetSideEmpire.NEUTRAL) + loseOwnership(obj, obj.Faction) } else { obj.OwnerGuid = None } @@ -290,11 +290,17 @@ object DeployableBehavior { originalFaction.toString, LocalAction.DeployableMapIcon(Service.defaultPlayerGUID, DeploymentAction.Dismiss, info) ) + //remove deployable from original owner's toolbox and UI counter + zone.AllPlayers.filter(p => obj.OriginalOwnerName.contains(p.Name)) + .foreach { originalOwner => + originalOwner.avatar.deployables.Remove(obj) + originalOwner.Zone.LocalEvents ! LocalServiceMessage(originalOwner.Name, LocalAction.DeployableUIFor(obj.Definition.Item)) + } + //display to the given faction + localEvents ! LocalServiceMessage( + toFaction.toString, + LocalAction.DeployableMapIcon(Service.defaultPlayerGUID, DeploymentAction.Build, info) + ) } - //display to the given faction - localEvents ! LocalServiceMessage( - toFaction.toString, - LocalAction.DeployableMapIcon(Service.defaultPlayerGUID, DeploymentAction.Build, info) - ) } } From e9336f52a6e40d8a8f68720565e2876d739f5c69 Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Mon, 3 Feb 2025 06:46:22 -0500 Subject: [PATCH 14/57] boomers still go neutral --- .../net/psforever/objects/ce/DeployableBehavior.scala | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/scala/net/psforever/objects/ce/DeployableBehavior.scala b/src/main/scala/net/psforever/objects/ce/DeployableBehavior.scala index dc5bd06f5..fbdfa85ca 100644 --- a/src/main/scala/net/psforever/objects/ce/DeployableBehavior.scala +++ b/src/main/scala/net/psforever/objects/ce/DeployableBehavior.scala @@ -68,7 +68,12 @@ trait DeployableBehavior { if DeployableObject.OwnerGuid.nonEmpty => val obj = DeployableObject if (constructed.contains(true)) { - loseOwnership(obj, obj.Faction) + if (obj.Definition.DeployCategory == DeployableCategory.Boomers) { + loseOwnership(obj, PlanetSideEmpire.NEUTRAL) + } + else { + loseOwnership(obj, obj.Faction) + } } else { obj.OwnerGuid = None } From 009e9bc3617b720b94beba82f062249b97a83d48 Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Wed, 19 Feb 2025 21:49:17 -0500 Subject: [PATCH 15/57] lock the driver door --- .../actors/session/support/ZoningOperations.scala | 10 +++++++--- src/main/scala/net/psforever/objects/Vehicles.scala | 4 +++- 2 files changed, 10 insertions(+), 4 deletions(-) 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 cdbb2cdcf..49bb0833c 100644 --- a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala @@ -2495,9 +2495,13 @@ class ZoningOperations( ) ) //do this to make my deployed telepad appear that way - continent.Vehicles.filter(router => router.Definition == GlobalDefinitions.router && router.OwnerName.contains(player.Name)) - .foreach { obj => - sessionLogic.general.toggleTeleportSystem(obj, TelepadLike.AppraiseTeleportationSystem(obj, continent)) + if (continent.DeployableList.exists(telepad => telepad.Definition == GlobalDefinitions.router_telepad_deployable + && telepad.OwnerName.contains(player.Name))) + { + continent.Vehicles.filter(router => router.Definition == GlobalDefinitions.router && router.Faction == player.Faction) + .foreach { obj => + sessionLogic.general.toggleTeleportSystem(obj, TelepadLike.AppraiseTeleportationSystem(obj, continent)) + } } //begin looking for conditions to set the avatar context.system.scheduler.scheduleOnce(delay = 250 millisecond, context.self, SessionActor.SetCurrentAvatar(player, 200)) diff --git a/src/main/scala/net/psforever/objects/Vehicles.scala b/src/main/scala/net/psforever/objects/Vehicles.scala index 6a66afe33..259dc2845 100644 --- a/src/main/scala/net/psforever/objects/Vehicles.scala +++ b/src/main/scala/net/psforever/objects/Vehicles.scala @@ -42,11 +42,13 @@ object Vehicles { case Some(tplayer) => tplayer.avatar.vehicle = Some(vehicle.GUID) vehicle.AssignOwnership(playerOpt) + val locked = VehicleLockState.Locked.id + Array(0, 3).foreach(group => vehicle.PermissionGroup(group, locked)) + Vehicles.ReloadAccessPermissions(vehicle, tplayer.Faction.toString) vehicle.Zone.VehicleEvents ! VehicleServiceMessage( vehicle.Zone.id, VehicleAction.Ownership(tplayer.GUID, vehicle.GUID) ) - Vehicles.ReloadAccessPermissions(vehicle, tplayer.Faction.toString) Some(vehicle) case None => None From 4600e5ebc3decd972fdfa7d1597bc832cb494fb5 Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Wed, 21 May 2025 07:23:07 -0400 Subject: [PATCH 16/57] slower captureall, squads off, log typo fix --- .../session/csr/SquadHandlerLogic.scala | 12 ++++---- .../session/normal/SquadHandlerLogic.scala | 8 ++--- .../session/support/ChatOperations.scala | 30 +++++++++++++++---- .../WeaponAndProjectileOperations.scala | 2 +- 4 files changed, 36 insertions(+), 16 deletions(-) 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 f6be56232..70e789621 100644 --- a/src/main/scala/net/psforever/actors/session/csr/SquadHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/csr/SquadHandlerLogic.scala @@ -28,21 +28,21 @@ class SquadHandlerLogic(val ops: SessionSquadHandlers, implicit val context: Act /* packet */ def handleSquadDefinitionAction(pkt: SquadDefinitionActionMessage): Unit = { - if (!player.spectator) { - 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 = { - if (!player.spectator) { + /* 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 = { 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 71bbc20a0..b80eb01cd 100644 --- a/src/main/scala/net/psforever/actors/session/normal/SquadHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/SquadHandlerLogic.scala @@ -28,17 +28,17 @@ class SquadHandlerLogic(val ops: SessionSquadHandlers, implicit val context: Act /* packet */ def handleSquadDefinitionAction(pkt: SquadDefinitionActionMessage): Unit = { - val SquadDefinitionActionMessage(u1, u2, action) = pkt - squadService ! SquadServiceMessage(player, continent, SquadServiceAction.Definition(u1, u2, action)) + /*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 + /*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 = { 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 0e72be694..3a585f7bd 100644 --- a/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala @@ -15,6 +15,7 @@ import net.psforever.services.local.{LocalAction, LocalServiceMessage} import net.psforever.types.ChatMessageType.CMT_QUIT import org.log4s.Logger +import java.util.concurrent.{Executors, TimeUnit} import scala.annotation.unused import scala.collection.{Seq, mutable} import scala.concurrent.duration._ @@ -59,6 +60,7 @@ class ChatOperations( private var channels: List[ChatChannel] = List() private var silenceTimer: Cancellable = Default.Cancellable private[session] var transitoryCommandEntered: Option[ChatMessageType] = None + private val scheduler = Executors.newScheduledThreadPool(2) /** * when another player is listed as one of our ignored players, * and that other player sends an emote, @@ -352,8 +354,25 @@ class ChatOperations( //evaluate results (resolvedFacilities, resolvedFaction, resolvedTimer) match { case (Some(buildings), Some(faction), Some(_)) => - buildings.foreach { building => //TODO implement timer + //schedule processing of buildings with a delay + processBuildingsWithDelay(buildings, faction, 1000) //delay of 1000ms between each building operation + true + case _ => + false + } + } + + def processBuildingsWithDelay( + buildings: Seq[Building], + faction: PlanetSideEmpire.Value, + delayMillis: Long + ): Unit = { + val buildingIterator = buildings.iterator + scheduler.scheduleAtFixedRate( + () => { + if (buildingIterator.hasNext) { + val building = buildingIterator.next() val terminal = building.CaptureTerminal.get val zone = building.Zone val zoneActor = zone.actor @@ -373,10 +392,11 @@ class ChatOperations( //push for map updates again zoneActor ! ZoneActor.ZoneMapUpdate() } - true - case _ => - false - } + }, + 0, + delayMillis, + TimeUnit.MILLISECONDS + ) } def commandVoice(session: Session, message: ChatMsg, contents: String, toChannel: ChatChannel): Unit = { 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 5633e76f7..5ade93f01 100644 --- a/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala @@ -343,7 +343,7 @@ class WeaponAndProjectileOperations( 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") + log.info(s"${player.Name} changed ${player.Sex.possessive} ${obj.Definition.Name}'s fire mode to #$modeIndex") } sendResponse(ChangeFireModeMessage(item_guid, modeIndex)) continent.AvatarEvents ! AvatarServiceMessage( From 9590ac4721cdb781e9c1e7c86b96fad8a4527ef8 Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Wed, 21 May 2025 16:10:54 -0400 Subject: [PATCH 17/57] all weapons and vehicles on Deso --- .../overrides/game_objects31.adb.lst | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/server/src/main/resources/overrides/game_objects31.adb.lst b/server/src/main/resources/overrides/game_objects31.adb.lst index 86dcd92ee..d0101a6eb 100644 --- a/server/src/main/resources/overrides/game_objects31.adb.lst +++ b/server/src/main/resources/overrides/game_objects31.adb.lst @@ -156,3 +156,101 @@ add_property portable_order_terminal forsale_six_shooter ordertype_weapon add_property portable_order_terminal forsale_six_shooter_ammo add_property portable_order_terminal forsale_winchester ordertype_weapon add_property portable_order_terminal forsale_winchester_ammo +add_property bullet_105mm purchase_empire all +add_property bullet_150mm purchase_empire all +add_property bullet_15mm purchase_empire all +add_property anniversary_gun purchase_empire all +add_property anniversary_guna purchase_empire all +add_property anniversary_gunb purchase_empire all +add_property apc_nc purchase_empire all +add_property apc_tr purchase_empire all +add_property apc_vs purchase_empire all +add_property aphelion purchase_empire all +add_property aphelion_flight purchase_empire all +add_property aphelion_gunner purchase_empire all +add_property aphelion_armor_siphon purchase_empire all +add_property aphelion_immolation_cannon purchase_empire all +add_property aphelion_immolation_cannon_ammo purchase_empire all +add_property aphelion_laser purchase_empire all +add_property aphelion_laser_ammo purchase_empire all +add_property aphelion_ntu_siphon purchase_empire all +add_property aphelion_plasma_rocket_ammo purchase_empire all +add_property aphelion_plasma_rocket_pod purchase_empire all +add_property aphelion_ppa purchase_empire all +add_property aphelion_ppa_ammo purchase_empire all +add_property aphelion_starfire purchase_empire all +add_property aphelion_starfire_ammo purchase_empire all +add_property aurora purchase_empire all +add_property battlewagon purchase_empire all +add_property beamer purchase_empire all +add_property burster_ammo purchase_empire all +add_property colossus purchase_empire all +add_property colossus_flight purchase_empire all +add_property colossus_gunner purchase_empire all +add_property colossus_100mm_cannon_ammo purchase_empire all +add_property colossus_armor_siphon purchase_empire all +add_property colossus_burster purchase_empire all +add_property colossus_burster_ammo purchase_empire all +add_property colossus_chaingun purchase_empire all +add_property colossus_chaingun_ammo purchase_empire all +add_property colossus_cluster_bomb_ammo purchase_empire all +add_property colossus_cluster_bomb_pod purchase_empire all +add_property colossus_dual_100mm_cannons purchase_empire all +add_property colossus_ntu_siphon purchase_empire all +add_property colossus_tank_cannon purchase_empire all +add_property colossus_tank_cannon_ammo purchase_empire all +add_property comet_ammo purchase_empire all +add_property cycler purchase_empire all +add_property cycler_v2 purchase_empire all +add_property cycler_v3 purchase_empire all +add_property cycler_v4 purchase_empire all +add_property dualcycler_ammo purchase_empire all +add_property energy_cell purchase_empire all +add_property falcon_ammo purchase_empire all +add_property firebird_missile purchase_empire all +add_property flux_cannon_thresher_battery purchase_empire all +add_property fluxpod_ammo purchase_empire all +add_property gauss purchase_empire all +add_property gauss_cannon_ammo purchase_empire all +add_property heavy_rail_beam_battery purchase_empire all +add_property hunter_seeker_missile purchase_empire all +add_property hunterseeker purchase_empire all +add_property isp purchase_empire all +add_property lancer purchase_empire all +add_property lancer_cartridge purchase_empire all +add_property lasher purchase_empire all +add_property magrider purchase_empire all +add_property mini_chaingun purchase_empire all +add_property peregrine purchase_empire all +add_property peregrine_flight purchase_empire all +add_property peregrine_gunner purchase_empire all +add_property peregrine_armor_siphon purchase_empire all +add_property peregrine_dual_machine_gun purchase_empire all +add_property peregrine_dual_machine_gun_ammo purchase_empire all +add_property peregrine_dual_rocket_pods purchase_empire all +add_property peregrine_mechhammer purchase_empire all +add_property peregrine_mechhammer_ammo purchase_empire all +add_property peregrine_ntu_siphon purchase_empire all +add_property peregrine_particle_cannon purchase_empire all +add_property peregrine_particle_cannon_ammo purchase_empire all +add_property peregrine_rocket_pod_ammo purchase_empire all +add_property peregrine_sparrow purchase_empire all +add_property peregrine_sparrow_ammo purchase_empire all +add_property phoenix_missile purchase_empire all +add_property pounder_ammo purchase_empire all +add_property prowler purchase_empire all +add_property pulsar purchase_empire all +add_property pulse_battery purchase_empire all +add_property quasar_ammo purchase_empire all +add_property r_shotgun purchase_empire all +add_property repeater purchase_empire all +add_property scattercannon_ammo purchase_empire all +add_property sparrow_ammo purchase_empire all +add_property starfire_ammo purchase_empire all +add_property striker purchase_empire all +add_property striker_missile_ammo purchase_empire all +add_property threemanheavybuggy purchase_empire all +add_property thunderer purchase_empire all +add_property twomanheavybuggy purchase_empire all +add_property twomanhoverbuggy purchase_empire all +add_property vanguard purchase_empire all From 0621cb4fcd21fd8b1ad5661ba895f09c90e6068d Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Thu, 22 May 2025 09:20:30 -0400 Subject: [PATCH 18/57] allow enemy equip in fav --- .../scala/net/psforever/objects/avatar/PlayerControl.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala index ee5fef561..16df2a05d 100644 --- a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala +++ b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala @@ -448,11 +448,11 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm //loadout for a MAX player.ResistArmMotion(PlayerControl.maxRestriction) player.DrawnSlot = Player.HandsDownSlot - (newHolsters.filter(_.start == 4), newInventory.filterNot(dropPred)) + (newHolsters.filter(_.start == 4), newInventory) } else { //loadout for a vanilla exo-suit player.ResistArmMotion(Player.neverRestrict) - (newHolsters.filterNot(dropPred), newInventory.filterNot(dropPred)) + (newHolsters, newInventory) } } else { //proposed loadout conforms to a different inventory layout than the projected exo-suit @@ -460,7 +460,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm //holsters (matching holsters will be inserted, the rest will deposited into the inventory) val (finalHolsters, leftoversForInventory) = Players.fillEmptyHolsters( player.Holsters().iterator, - (newHolsters.filterNot(_.obj.Size == EquipmentSize.Max) ++ newInventory).filterNot(dropPred) + (newHolsters.filterNot(_.obj.Size == EquipmentSize.Max) ++ newInventory) ) //inventory (items will be placed to accommodate the change, or dropped) val (finalInventory, _) = GridInventory.recoverInventory(leftoversForInventory, player.Inventory) From 5d00a10e80fe168f4fa84aaee669aa475acb8d1c Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Thu, 22 May 2025 12:08:56 -0400 Subject: [PATCH 19/57] don't drop --- .../scala/net/psforever/objects/avatar/PlayerControl.scala | 6 +++--- .../serverobject/containable/ContainableBehavior.scala | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala index 16df2a05d..ee5fef561 100644 --- a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala +++ b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala @@ -448,11 +448,11 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm //loadout for a MAX player.ResistArmMotion(PlayerControl.maxRestriction) player.DrawnSlot = Player.HandsDownSlot - (newHolsters.filter(_.start == 4), newInventory) + (newHolsters.filter(_.start == 4), newInventory.filterNot(dropPred)) } else { //loadout for a vanilla exo-suit player.ResistArmMotion(Player.neverRestrict) - (newHolsters, newInventory) + (newHolsters.filterNot(dropPred), newInventory.filterNot(dropPred)) } } else { //proposed loadout conforms to a different inventory layout than the projected exo-suit @@ -460,7 +460,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm //holsters (matching holsters will be inserted, the rest will deposited into the inventory) val (finalHolsters, leftoversForInventory) = Players.fillEmptyHolsters( player.Holsters().iterator, - (newHolsters.filterNot(_.obj.Size == EquipmentSize.Max) ++ newInventory) + (newHolsters.filterNot(_.obj.Size == EquipmentSize.Max) ++ newInventory).filterNot(dropPred) ) //inventory (items will be placed to accommodate the change, or dropped) val (finalInventory, _) = GridInventory.recoverInventory(leftoversForInventory, player.Inventory) diff --git a/src/main/scala/net/psforever/objects/serverobject/containable/ContainableBehavior.scala b/src/main/scala/net/psforever/objects/serverobject/containable/ContainableBehavior.scala index d3680de99..04ffa27a0 100644 --- a/src/main/scala/net/psforever/objects/serverobject/containable/ContainableBehavior.scala +++ b/src/main/scala/net/psforever/objects/serverobject/containable/ContainableBehavior.scala @@ -678,8 +678,8 @@ object ContainableBehavior { val faction = GlobalDefinitions.isFactionEquipment(objDef) GlobalDefinitions.isCavernEquipment(objDef) || objDef == GlobalDefinitions.router_telepad || - entry.obj.isInstanceOf[BoomerTrigger] || - (faction != tplayer.Faction && faction != PlanetSideEmpire.NEUTRAL) + entry.obj.isInstanceOf[BoomerTrigger] /*|| + (faction != tplayer.Faction && faction != PlanetSideEmpire.NEUTRAL)*/ } } From bc978f177be6cd6dd1b5858d65d3e13ba1e4eee4 Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Mon, 26 May 2025 07:17:26 -0400 Subject: [PATCH 20/57] normal minus bfr --- .../resources/overrides/game_objects0.adb.lst | 9 ++ .../overrides/game_objects31.adb.lst | 98 ------------------- .../containable/ContainableBehavior.scala | 4 +- 3 files changed, 11 insertions(+), 100 deletions(-) diff --git a/server/src/main/resources/overrides/game_objects0.adb.lst b/server/src/main/resources/overrides/game_objects0.adb.lst index 5e08278e5..d476e61a9 100644 --- a/server/src/main/resources/overrides/game_objects0.adb.lst +++ b/server/src/main/resources/overrides/game_objects0.adb.lst @@ -96,3 +96,12 @@ add_property suppressor holstertime 600 add_property trek equiptime 500 add_property trek holstertime 500 add_property vulture requirement_award0 false +add_property aphelion allowed false +add_property aphelion_flight allowed false +add_property aphelion_gunner allowed false +add_property colossus allowed false +add_property colossus_flight allowed false +add_property colossus_gunner allowed false +add_property peregrine allowed false +add_property peregrine_flight allowed false +add_property peregrine_gunner allowed false diff --git a/server/src/main/resources/overrides/game_objects31.adb.lst b/server/src/main/resources/overrides/game_objects31.adb.lst index d0101a6eb..86dcd92ee 100644 --- a/server/src/main/resources/overrides/game_objects31.adb.lst +++ b/server/src/main/resources/overrides/game_objects31.adb.lst @@ -156,101 +156,3 @@ add_property portable_order_terminal forsale_six_shooter ordertype_weapon add_property portable_order_terminal forsale_six_shooter_ammo add_property portable_order_terminal forsale_winchester ordertype_weapon add_property portable_order_terminal forsale_winchester_ammo -add_property bullet_105mm purchase_empire all -add_property bullet_150mm purchase_empire all -add_property bullet_15mm purchase_empire all -add_property anniversary_gun purchase_empire all -add_property anniversary_guna purchase_empire all -add_property anniversary_gunb purchase_empire all -add_property apc_nc purchase_empire all -add_property apc_tr purchase_empire all -add_property apc_vs purchase_empire all -add_property aphelion purchase_empire all -add_property aphelion_flight purchase_empire all -add_property aphelion_gunner purchase_empire all -add_property aphelion_armor_siphon purchase_empire all -add_property aphelion_immolation_cannon purchase_empire all -add_property aphelion_immolation_cannon_ammo purchase_empire all -add_property aphelion_laser purchase_empire all -add_property aphelion_laser_ammo purchase_empire all -add_property aphelion_ntu_siphon purchase_empire all -add_property aphelion_plasma_rocket_ammo purchase_empire all -add_property aphelion_plasma_rocket_pod purchase_empire all -add_property aphelion_ppa purchase_empire all -add_property aphelion_ppa_ammo purchase_empire all -add_property aphelion_starfire purchase_empire all -add_property aphelion_starfire_ammo purchase_empire all -add_property aurora purchase_empire all -add_property battlewagon purchase_empire all -add_property beamer purchase_empire all -add_property burster_ammo purchase_empire all -add_property colossus purchase_empire all -add_property colossus_flight purchase_empire all -add_property colossus_gunner purchase_empire all -add_property colossus_100mm_cannon_ammo purchase_empire all -add_property colossus_armor_siphon purchase_empire all -add_property colossus_burster purchase_empire all -add_property colossus_burster_ammo purchase_empire all -add_property colossus_chaingun purchase_empire all -add_property colossus_chaingun_ammo purchase_empire all -add_property colossus_cluster_bomb_ammo purchase_empire all -add_property colossus_cluster_bomb_pod purchase_empire all -add_property colossus_dual_100mm_cannons purchase_empire all -add_property colossus_ntu_siphon purchase_empire all -add_property colossus_tank_cannon purchase_empire all -add_property colossus_tank_cannon_ammo purchase_empire all -add_property comet_ammo purchase_empire all -add_property cycler purchase_empire all -add_property cycler_v2 purchase_empire all -add_property cycler_v3 purchase_empire all -add_property cycler_v4 purchase_empire all -add_property dualcycler_ammo purchase_empire all -add_property energy_cell purchase_empire all -add_property falcon_ammo purchase_empire all -add_property firebird_missile purchase_empire all -add_property flux_cannon_thresher_battery purchase_empire all -add_property fluxpod_ammo purchase_empire all -add_property gauss purchase_empire all -add_property gauss_cannon_ammo purchase_empire all -add_property heavy_rail_beam_battery purchase_empire all -add_property hunter_seeker_missile purchase_empire all -add_property hunterseeker purchase_empire all -add_property isp purchase_empire all -add_property lancer purchase_empire all -add_property lancer_cartridge purchase_empire all -add_property lasher purchase_empire all -add_property magrider purchase_empire all -add_property mini_chaingun purchase_empire all -add_property peregrine purchase_empire all -add_property peregrine_flight purchase_empire all -add_property peregrine_gunner purchase_empire all -add_property peregrine_armor_siphon purchase_empire all -add_property peregrine_dual_machine_gun purchase_empire all -add_property peregrine_dual_machine_gun_ammo purchase_empire all -add_property peregrine_dual_rocket_pods purchase_empire all -add_property peregrine_mechhammer purchase_empire all -add_property peregrine_mechhammer_ammo purchase_empire all -add_property peregrine_ntu_siphon purchase_empire all -add_property peregrine_particle_cannon purchase_empire all -add_property peregrine_particle_cannon_ammo purchase_empire all -add_property peregrine_rocket_pod_ammo purchase_empire all -add_property peregrine_sparrow purchase_empire all -add_property peregrine_sparrow_ammo purchase_empire all -add_property phoenix_missile purchase_empire all -add_property pounder_ammo purchase_empire all -add_property prowler purchase_empire all -add_property pulsar purchase_empire all -add_property pulse_battery purchase_empire all -add_property quasar_ammo purchase_empire all -add_property r_shotgun purchase_empire all -add_property repeater purchase_empire all -add_property scattercannon_ammo purchase_empire all -add_property sparrow_ammo purchase_empire all -add_property starfire_ammo purchase_empire all -add_property striker purchase_empire all -add_property striker_missile_ammo purchase_empire all -add_property threemanheavybuggy purchase_empire all -add_property thunderer purchase_empire all -add_property twomanheavybuggy purchase_empire all -add_property twomanhoverbuggy purchase_empire all -add_property vanguard purchase_empire all diff --git a/src/main/scala/net/psforever/objects/serverobject/containable/ContainableBehavior.scala b/src/main/scala/net/psforever/objects/serverobject/containable/ContainableBehavior.scala index 04ffa27a0..d3680de99 100644 --- a/src/main/scala/net/psforever/objects/serverobject/containable/ContainableBehavior.scala +++ b/src/main/scala/net/psforever/objects/serverobject/containable/ContainableBehavior.scala @@ -678,8 +678,8 @@ object ContainableBehavior { val faction = GlobalDefinitions.isFactionEquipment(objDef) GlobalDefinitions.isCavernEquipment(objDef) || objDef == GlobalDefinitions.router_telepad || - entry.obj.isInstanceOf[BoomerTrigger] /*|| - (faction != tplayer.Faction && faction != PlanetSideEmpire.NEUTRAL)*/ + entry.obj.isInstanceOf[BoomerTrigger] || + (faction != tplayer.Faction && faction != PlanetSideEmpire.NEUTRAL) } } From a1b7fe01ff4eb266a54723fd258582df60ac8688 Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Mon, 26 May 2025 17:54:39 -0400 Subject: [PATCH 21/57] sideness check --- .../net/psforever/objects/avatar/interaction/WithEntrance.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/net/psforever/objects/avatar/interaction/WithEntrance.scala b/src/main/scala/net/psforever/objects/avatar/interaction/WithEntrance.scala index 8263b7627..9064efb16 100644 --- a/src/main/scala/net/psforever/objects/avatar/interaction/WithEntrance.scala +++ b/src/main/scala/net/psforever/objects/avatar/interaction/WithEntrance.scala @@ -65,7 +65,7 @@ class WithEntrance() obj: InteractsWithZone, door: Door ): Sidedness = { - //debugInteriorCheck(obj, door) + debugInteriorCheck(obj, door) strictInteriorCheck(obj, door) } From 2f2fcd94f15c1e9d9762491f8d0e5c865f678677 Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Mon, 26 May 2025 21:01:22 -0400 Subject: [PATCH 22/57] force spit sideness --- .../psforever/actors/session/normal/GeneralLogic.scala | 8 +++++++- .../objects/avatar/interaction/WithEntrance.scala | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) 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 3a84a4324..32e9bc6c2 100644 --- a/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala @@ -18,6 +18,7 @@ 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 +import net.psforever.objects.serverobject.interior.Sidedness.OutsideOf import net.psforever.objects.serverobject.llu.CaptureFlag import net.psforever.objects.serverobject.locks.IFFLock import net.psforever.objects.serverobject.mblocker.Locker @@ -406,7 +407,12 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex } log.info(s"${player.Name} is constructing a $ammoType deployable") sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") - ops.handleDeployObject(continent, ammoType, pos, orient, player.WhichSide, player.Faction, player, obj) + if (obj.AmmoType == DeployedItem.spitfire_turret || obj.AmmoType == DeployedItem.spitfire_cloaked) { + ops.handleDeployObject(continent, ammoType, pos, orient, OutsideOf, player.Faction, player, obj) + } + else { + ops.handleDeployObject(continent, ammoType, pos, orient, player.WhichSide, player.Faction, player, obj) + } case Some(obj) => log.warn(s"DeployObject: what is $obj, ${player.Name}? It's not a construction tool!") case None => diff --git a/src/main/scala/net/psforever/objects/avatar/interaction/WithEntrance.scala b/src/main/scala/net/psforever/objects/avatar/interaction/WithEntrance.scala index 9064efb16..8263b7627 100644 --- a/src/main/scala/net/psforever/objects/avatar/interaction/WithEntrance.scala +++ b/src/main/scala/net/psforever/objects/avatar/interaction/WithEntrance.scala @@ -65,7 +65,7 @@ class WithEntrance() obj: InteractsWithZone, door: Door ): Sidedness = { - debugInteriorCheck(obj, door) + //debugInteriorCheck(obj, door) strictInteriorCheck(obj, door) } From a98b5fd9f84301f1d19f7320e370c1cff7ac2426 Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Mon, 2 Jun 2025 13:12:59 -0400 Subject: [PATCH 23/57] more sideness --- .../net/psforever/actors/session/normal/GeneralLogic.scala | 3 ++- .../psforever/actors/session/normal/VehicleHandlerLogic.scala | 2 ++ .../actors/session/support/SessionMountHandlers.scala | 2 ++ 3 files changed, 6 insertions(+), 1 deletion(-) 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 32e9bc6c2..35fb4f457 100644 --- a/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala @@ -407,7 +407,8 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex } log.info(s"${player.Name} is constructing a $ammoType deployable") sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_use") - if (obj.AmmoType == DeployedItem.spitfire_turret || obj.AmmoType == DeployedItem.spitfire_cloaked) { + if (ammoType == DeployedItem.spitfire_turret || ammoType == DeployedItem.spitfire_cloaked || + ammoType == DeployedItem.spitfire_aa || ammoType == DeployedItem.he_mine || ammoType == DeployedItem.jammer_mine) { ops.handleDeployObject(continent, ammoType, pos, orient, OutsideOf, player.Faction, player, obj) } else { diff --git a/src/main/scala/net/psforever/actors/session/normal/VehicleHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/normal/VehicleHandlerLogic.scala index 55d01d2a1..64a7a67a8 100644 --- a/src/main/scala/net/psforever/actors/session/normal/VehicleHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/VehicleHandlerLogic.scala @@ -8,6 +8,7 @@ import net.psforever.objects.avatar.SpecialCarry 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.interior.Sidedness.OutsideOf import net.psforever.objects.serverobject.mount.Mountable import net.psforever.objects.serverobject.pad.VehicleSpawnPad import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent @@ -187,6 +188,7 @@ class VehicleHandlerLogic(val ops: SessionVehicleHandlers, implicit val context: s"${player.Sex.possessive} ride" } log.info(s"${player.Name} has been kicked from $typeOfRide!") + player.WhichSide = OutsideOf case VehicleResponse.KickPassenger(_, wasKickedByDriver, _) => //seat number (first field) seems to be correct if passenger is kicked manually by driver 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 bc9f30b12..3ee687b12 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionMountHandlers.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionMountHandlers.scala @@ -3,6 +3,7 @@ package net.psforever.actors.session.support import akka.actor.{ActorContext, typed} import net.psforever.objects.serverobject.affinity.FactionAffinity +import net.psforever.objects.serverobject.interior.Sidedness.OutsideOf import net.psforever.objects.{PlanetSideGameObject, Tool, Vehicle} import net.psforever.objects.vehicles.{CargoBehavior, MountableWeapons} import net.psforever.objects.vital.InGameHistory @@ -197,6 +198,7 @@ class SessionMountHandlers( */ def DismountVehicleAction(tplayer: Player, obj: PlanetSideGameObject with FactionAffinity with InGameHistory, seatNum: Int): Unit = { DismountAction(tplayer, obj, seatNum) + tplayer.WhichSide = OutsideOf //until vehicles maintain synchronized momentum without a driver obj match { case v: Vehicle From ca15790a36f0d65b78727370588bd44046727f39 Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Tue, 3 Jun 2025 07:28:31 -0400 Subject: [PATCH 24/57] don't shoot mossie --- .../net/psforever/objects/equipment/EffectTarget.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/scala/net/psforever/objects/equipment/EffectTarget.scala b/src/main/scala/net/psforever/objects/equipment/EffectTarget.scala index 101b1cc5a..689fcf652 100644 --- a/src/main/scala/net/psforever/objects/equipment/EffectTarget.scala +++ b/src/main/scala/net/psforever/objects/equipment/EffectTarget.scala @@ -324,7 +324,7 @@ object EffectTarget { def FacilityTurretValidateAircraftTarget(target: PlanetSideGameObject): Boolean = target match { case v: Vehicle - if GlobalDefinitions.isFlightVehicle(v.Definition) && v.Seats.values.exists(_.isOccupied) => + if GlobalDefinitions.isFlightVehicle(v.Definition) && v.Seats.values.exists(_.isOccupied) && v.Definition != GlobalDefinitions.mosquito => val now = System.currentTimeMillis() val pos = v.Position lazy val sector = v.Zone.blockMap.sector(pos, range = 51f) @@ -332,10 +332,10 @@ object EffectTarget { .collect { case t: Tool => now - t.LastDischarge } .exists(_ < 2000L) // from the perspective of a mosquito, at 5th gauge, forward velocity is 59~60 - lazy val movingFast = Vector3.MagnitudeSquared(v.Velocity.getOrElse(Vector3.Zero).xy) > 3721f //61 + //lazy val movingFast = Vector3.MagnitudeSquared(v.Velocity.getOrElse(Vector3.Zero).xy) > 3721f //61 lazy val isMoving = v.isMoving(test = 1d) if (v.Cloaked || radarCloakedAms(sector, pos) || radarCloakedAegis(sector, pos)) false - else if (v.Definition == GlobalDefinitions.mosquito) movingFast + //else if (v.Definition == GlobalDefinitions.mosquito) movingFast else v.isFlying && (isMoving || entityTookDamage(v, now) || usedEquipment) case _ => false From 863fa4ca7575540e4081418acf144a06c4bc0b0c Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Tue, 3 Jun 2025 09:02:28 -0400 Subject: [PATCH 25/57] don't force mines --- .../net/psforever/actors/session/normal/GeneralLogic.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 35fb4f457..fdfa1c9b4 100644 --- a/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala @@ -408,7 +408,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") if (ammoType == DeployedItem.spitfire_turret || ammoType == DeployedItem.spitfire_cloaked || - ammoType == DeployedItem.spitfire_aa || ammoType == DeployedItem.he_mine || ammoType == DeployedItem.jammer_mine) { + ammoType == DeployedItem.spitfire_aa) { ops.handleDeployObject(continent, ammoType, pos, orient, OutsideOf, player.Faction, player, obj) } else { From d1e8da208b75b3a5955affe94194ece7f56f89db Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Sun, 8 Jun 2025 19:48:28 -0400 Subject: [PATCH 26/57] bail malfunction, gen normal state, stop vehicle momentum --- .../session/csr/MountHandlerLogic.scala | 4 +++ .../session/normal/MountHandlerLogic.scala | 4 +++ .../support/SessionMountHandlers.scala | 10 +++--- .../generator/GeneratorControl.scala | 4 +++ .../vehicles/control/VehicleControl.scala | 33 ++++++++++++++++++- 5 files changed, 49 insertions(+), 6 deletions(-) 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 5ca48d683..1846f069b 100644 --- a/src/main/scala/net/psforever/actors/session/csr/MountHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/csr/MountHandlerLogic.scala @@ -297,6 +297,10 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act sendResponse(ChatMsg(ChatMessageType.UNK_224, "@SA_CannotBailAtThisTime")) } + case Mountable.CanNotDismount(obj: Vehicle, _, BailType.Bailed) + if obj.Health <= (obj.MaxHealth * .35).round && GlobalDefinitions.isFlightVehicle(obj.Definition) => + sendResponse(ChatMsg(ChatMessageType.UNK_224, "@BailingMechanismFailure_Pilot")) + case Mountable.CanNotDismount(obj: Vehicle, _, BailType.Bailed) if { continent 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 2a6249d62..ccd995544 100644 --- a/src/main/scala/net/psforever/actors/session/normal/MountHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/MountHandlerLogic.scala @@ -312,6 +312,10 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act if obj.DeploymentState == DriveState.AutoPilot => sendResponse(ChatMsg(ChatMessageType.UNK_224, "@SA_CannotBailAtThisTime")) + case Mountable.CanNotDismount(obj: Vehicle, _, BailType.Bailed) + if obj.Health <= (obj.MaxHealth * .35).round && GlobalDefinitions.isFlightVehicle(obj.Definition) => + sendResponse(ChatMsg(ChatMessageType.UNK_224, "@BailingMechanismFailure_Pilot")) + case Mountable.CanNotDismount(obj: Vehicle, _, BailType.Bailed) if { continent 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 3ee687b12..905d2ca01 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionMountHandlers.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionMountHandlers.scala @@ -202,10 +202,10 @@ class SessionMountHandlers( //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 { _ => + if seatNum == 0 => + /*sessionLogic.vehicles.serverVehicleControlVelocity.collect { _ => sessionLogic.vehicles.ServerVehicleOverrideStop(v) - } + }*/ v.Velocity = Vector3.Zero continent.VehicleEvents ! VehicleServiceMessage( continent.id, @@ -213,9 +213,9 @@ class SessionMountHandlers( tplayer.GUID, v.GUID, unk1 = 0, - v.Position, + tplayer.Position, v.Orientation, - vel = None, + v.Velocity, v.Flying, unk3 = 0, unk4 = 0, diff --git a/src/main/scala/net/psforever/objects/serverobject/generator/GeneratorControl.scala b/src/main/scala/net/psforever/objects/serverobject/generator/GeneratorControl.scala index 5ff176897..07e2b9d9f 100644 --- a/src/main/scala/net/psforever/objects/serverobject/generator/GeneratorControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/generator/GeneratorControl.scala @@ -192,6 +192,10 @@ class GeneratorControl(gen: Generator) if(newHealth == target.Definition.MaxHealth) { stopAutoRepair() } + if(gen.Condition == PlanetSideGeneratorState.Critical && newHealth > (target.MaxHealth / 2)) { + gen.Condition = PlanetSideGeneratorState.Normal + GeneratorControl.UpdateOwner(gen, Some(GeneratorControl.Event.Normal)) + } newHealth } diff --git a/src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala b/src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala index 66b6f2700..4a4f97b6a 100644 --- a/src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala +++ b/src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala @@ -27,7 +27,7 @@ import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource} import net.psforever.objects.vehicles._ import net.psforever.objects.vehicles.interaction.WithWater import net.psforever.objects.vital.interaction.DamageResult -import net.psforever.objects.vital.{InGameActivity, ShieldCharge, SpawningActivity, VehicleDismountActivity, VehicleMountActivity} +import net.psforever.objects.vital.{DamagingActivity, InGameActivity, ShieldCharge, SpawningActivity, VehicleDismountActivity, VehicleMountActivity} import net.psforever.objects.zones._ import net.psforever.packet.PlanetSideGamePacket import net.psforever.packet.game._ @@ -39,6 +39,7 @@ import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration._ +import scala.util.Random /** * An `Actor` that handles messages being dispatched to a specific `Vehicle`.
@@ -140,6 +141,36 @@ class VehicleControl(vehicle: Vehicle) }) => sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num, bailType)) + case Mountable.TryDismount(user, seat_num, bailType) + if vehicle.Health <= (vehicle.Definition.MaxHealth * .1).round && bailType == BailType.Bailed + && GlobalDefinitions.isFlightVehicle(vehicle.Definition) + && (seat_num == 0 || vehicle.SeatPermissionGroup(seat_num).getOrElse(0) == AccessPermissionGroup.Gunner) + && (vehicle.History.findLast { entry => entry.isInstanceOf[DamagingActivity] } match { + case Some(entry) if System.currentTimeMillis() - entry.time < 4000L => true + case _ if Random.nextInt(10) == 1 => false + case _ => true }) => + sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num, bailType)) + + case Mountable.TryDismount(user, seat_num, bailType) + if vehicle.Health <= (vehicle.Definition.MaxHealth * .2).round && bailType == BailType.Bailed + && GlobalDefinitions.isFlightVehicle(vehicle.Definition) + && (seat_num == 0 || vehicle.SeatPermissionGroup(seat_num).getOrElse(0) == AccessPermissionGroup.Gunner) + && (vehicle.History.findLast { entry => entry.isInstanceOf[DamagingActivity] } match { + case Some(entry) if System.currentTimeMillis() - entry.time < 3500L => true + case _ if Random.nextInt(5) == 1 => false + case _ => true }) => + sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num, bailType)) + + case Mountable.TryDismount(user, seat_num, bailType) + if vehicle.Health <= (vehicle.Definition.MaxHealth * .35).round && bailType == BailType.Bailed + && GlobalDefinitions.isFlightVehicle(vehicle.Definition) + && (seat_num == 0 || vehicle.SeatPermissionGroup(seat_num).getOrElse(0) == AccessPermissionGroup.Gunner) + && (vehicle.History.findLast { entry => entry.isInstanceOf[DamagingActivity] } match { + case Some(entry) if System.currentTimeMillis() - entry.time < 3000L => true + case _ if Random.nextInt(4) == 1 => false + case _ => true }) => + sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num, bailType)) + case Mountable.TryDismount(user, seat_num, bailType) if vehicle.DeploymentState == DriveState.AutoPilot => sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num, bailType)) From 4cf54d09373b997b28e7d7de52efa9702d7923c3 Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Mon, 9 Jun 2025 19:49:17 -0400 Subject: [PATCH 27/57] claim the cont --- .../local/support/HackCaptureActor.scala | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/src/main/scala/net/psforever/services/local/support/HackCaptureActor.scala b/src/main/scala/net/psforever/services/local/support/HackCaptureActor.scala index d561cd758..dccc0dd7e 100644 --- a/src/main/scala/net/psforever/services/local/support/HackCaptureActor.scala +++ b/src/main/scala/net/psforever/services/local/support/HackCaptureActor.scala @@ -18,6 +18,8 @@ import net.psforever.services.local.support.HackCaptureActor.GetHackingFaction import net.psforever.services.local.{LocalAction, LocalServiceMessage} import net.psforever.types.{ChatMessageType, PlanetSideEmpire, PlanetSideGUID} +import java.util.concurrent.{Executors, TimeUnit} +import scala.collection.Seq import scala.concurrent.duration.{FiniteDuration, _} import scala.util.Random @@ -30,6 +32,7 @@ class HackCaptureActor extends Actor { private var clearTrigger: Cancellable = Default.Cancellable /** list of currently hacked server objects */ private var hackedObjects: List[HackCaptureActor.HackEntry] = Nil + private val scheduler = Executors.newScheduledThreadPool(2) def receive: Receive = { case HackCaptureActor.StartCaptureTerminalHack(target, _, _, _, _) @@ -266,6 +269,20 @@ class HackCaptureActor extends Actor { .collect { case p if p.Faction == hackedByFaction => events ! LocalServiceMessage(p.Name, msg) } + val zoneBases = building.Zone.Buildings.filter(base => + base._2.BuildingType == StructureType.Facility) + val ownedBases = building.Zone.Buildings.filter(base => + base._2.BuildingType == StructureType.Facility && base._2.Faction == hackedByFaction + && base._2.GUID != building.GUID) + val zoneTowers = building.Zone.Buildings.filter(tower => + tower._2.BuildingType == StructureType.Tower && tower._2.Faction != hackedByFaction) + // All major facilities in zone are now owned by the hacking faction. Capture all towers in the zone + // Base that was just hacked is not counted (hence the size - 1) because it wasn't always in ownedBases (async?) + println(s"bases: ${zoneBases.size} owned: ${ownedBases.size} towers: ${zoneTowers.size}") + if (zoneBases.size - 1 == ownedBases.size && zoneTowers.nonEmpty) + { + processBuildingsWithDelay(zoneTowers.values.toSeq, hackedByFaction, 1000) + } } else { log.info("Base hack completed, but base was out of NTU.") } @@ -283,6 +300,43 @@ class HackCaptureActor extends Actor { clearTrigger = context.system.scheduler.scheduleOnce(short_timeout, self, HackCaptureActor.ProcessCompleteHacks()) } } + + def processBuildingsWithDelay( + buildings: Seq[Building], + faction: PlanetSideEmpire.Value, + delayMillis: Long + ): Unit = { + val buildingIterator = buildings.iterator + scheduler.scheduleAtFixedRate( + () => { + if (buildingIterator.hasNext) { + val building = buildingIterator.next() + val terminal = building.CaptureTerminal.get + val zone = building.Zone + val zoneActor = zone.actor + val buildingActor = building.Actor + //clear any previous hack + if (building.CaptureTerminalIsHacked) { + zone.LocalEvents ! LocalServiceMessage( + zone.id, + LocalAction.ResecureCaptureTerminal(terminal, PlayerSource.Nobody) + ) + } + //push any updates this might cause + zoneActor ! ZoneActor.ZoneMapUpdate() + //convert faction affiliation + buildingActor ! BuildingActor.SetFaction(faction) + buildingActor ! BuildingActor.AmenityStateChange(terminal, Some(false)) + //push for map updates again + zoneActor ! ZoneActor.ZoneMapUpdate() + } + }, + 0, + delayMillis, + TimeUnit.MILLISECONDS + ) + } + } object HackCaptureActor { From acb9cae3fcb347000cfe94c76211bfcdf1224c37 Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Mon, 9 Jun 2025 20:34:45 -0400 Subject: [PATCH 28/57] oops --- .../net/psforever/services/local/support/HackCaptureActor.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/scala/net/psforever/services/local/support/HackCaptureActor.scala b/src/main/scala/net/psforever/services/local/support/HackCaptureActor.scala index dccc0dd7e..89443524d 100644 --- a/src/main/scala/net/psforever/services/local/support/HackCaptureActor.scala +++ b/src/main/scala/net/psforever/services/local/support/HackCaptureActor.scala @@ -278,7 +278,6 @@ class HackCaptureActor extends Actor { tower._2.BuildingType == StructureType.Tower && tower._2.Faction != hackedByFaction) // All major facilities in zone are now owned by the hacking faction. Capture all towers in the zone // Base that was just hacked is not counted (hence the size - 1) because it wasn't always in ownedBases (async?) - println(s"bases: ${zoneBases.size} owned: ${ownedBases.size} towers: ${zoneTowers.size}") if (zoneBases.size - 1 == ownedBases.size && zoneTowers.nonEmpty) { processBuildingsWithDelay(zoneTowers.values.toSeq, hackedByFaction, 1000) From e32e67a00dc86a9eedd71e79c3de9feb1a86834a Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Mon, 9 Jun 2025 20:37:38 -0400 Subject: [PATCH 29/57] oops --- .../net/psforever/services/local/support/HackCaptureActor.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/net/psforever/services/local/support/HackCaptureActor.scala b/src/main/scala/net/psforever/services/local/support/HackCaptureActor.scala index 89443524d..db33a6a3d 100644 --- a/src/main/scala/net/psforever/services/local/support/HackCaptureActor.scala +++ b/src/main/scala/net/psforever/services/local/support/HackCaptureActor.scala @@ -276,7 +276,7 @@ class HackCaptureActor extends Actor { && base._2.GUID != building.GUID) val zoneTowers = building.Zone.Buildings.filter(tower => tower._2.BuildingType == StructureType.Tower && tower._2.Faction != hackedByFaction) - // All major facilities in zone are now owned by the hacking faction. Capture all towers in the zone + // All major facilities in zone are now owned by the hacking faction. Capture all remaining towers in the zone // Base that was just hacked is not counted (hence the size - 1) because it wasn't always in ownedBases (async?) if (zoneBases.size - 1 == ownedBases.size && zoneTowers.nonEmpty) { From 895ed5331cf7674fb04cf0b970ad6e984b26d2e2 Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Mon, 9 Jun 2025 20:39:13 -0400 Subject: [PATCH 30/57] oops --- .../net/psforever/services/local/support/HackCaptureActor.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/net/psforever/services/local/support/HackCaptureActor.scala b/src/main/scala/net/psforever/services/local/support/HackCaptureActor.scala index db33a6a3d..89443524d 100644 --- a/src/main/scala/net/psforever/services/local/support/HackCaptureActor.scala +++ b/src/main/scala/net/psforever/services/local/support/HackCaptureActor.scala @@ -276,7 +276,7 @@ class HackCaptureActor extends Actor { && base._2.GUID != building.GUID) val zoneTowers = building.Zone.Buildings.filter(tower => tower._2.BuildingType == StructureType.Tower && tower._2.Faction != hackedByFaction) - // All major facilities in zone are now owned by the hacking faction. Capture all remaining towers in the zone + // All major facilities in zone are now owned by the hacking faction. Capture all towers in the zone // Base that was just hacked is not counted (hence the size - 1) because it wasn't always in ownedBases (async?) if (zoneBases.size - 1 == ownedBases.size && zoneTowers.nonEmpty) { From b63e402003bbe90426da09a697e5b00d58527dc8 Mon Sep 17 00:00:00 2001 From: Resaec Date: Wed, 11 Jun 2025 00:11:49 +0200 Subject: [PATCH 31/57] prevent ! commands from getting send to chat if no handler was found --- .../psforever/actors/session/csr/ChatLogic.scala | 2 +- .../actors/session/spectator/ChatLogic.scala | 13 ++++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) 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 e7a5a9c1e..24c5c8851 100644 --- a/src/main/scala/net/psforever/actors/session/csr/ChatLogic.scala +++ b/src/main/scala/net/psforever/actors/session/csr/ChatLogic.scala @@ -236,7 +236,7 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext message.note ) ) - false + true } } else { false 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 a75ea4dcc..ae2857066 100644 --- a/src/main/scala/net/psforever/actors/session/spectator/ChatLogic.scala +++ b/src/main/scala/net/psforever/actors/session/spectator/ChatLogic.scala @@ -143,7 +143,18 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext case "list" => ops.customCommandList(session, params, message) case "nearby" => ops.customCommandNearby(session) case "loc" => ops.customCommandLoc(session, message) - case _ => false + case _ => + // command was not handled + sendResponse( + ChatMsg( + ChatMessageType.CMT_GMOPEN, // CMT_GMTELL + message.wideContents, + "Server", + s"Unknown command !$command", + message.note + ) + ) + true } } else { false From 73b27ef43cc73a1b3a0bd9158b66cf170aadfae9 Mon Sep 17 00:00:00 2001 From: Resaec Date: Wed, 11 Jun 2025 00:54:47 +0200 Subject: [PATCH 32/57] Colorful server names --- src/main/resources/application.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf index d912b5911..cbb9bc88e 100644 --- a/src/main/resources/application.conf +++ b/src/main/resources/application.conf @@ -23,7 +23,7 @@ world { ports = [] # The name of the server as displayed in the server browser. - server-name = PSForever + server-name = "\\#1EACF9P\\#E87BE8S\\#F93F4E4ever" # How the server is displayed in the server browser. # One of: released beta development From c634aea786d5b1d0e0dc8557df021e2de155d5d4 Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Sun, 15 Jun 2025 17:19:11 -0400 Subject: [PATCH 33/57] heal & repair messages --- .../scala/net/psforever/objects/Players.scala | 2 ++ .../objects/avatar/PlayerControl.scala | 19 +++++++++++++ .../repair/RepairableEntity.scala | 27 +++++++++++++++++-- 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/main/scala/net/psforever/objects/Players.scala b/src/main/scala/net/psforever/objects/Players.scala index 7ec07f3c2..241078f2b 100644 --- a/src/main/scala/net/psforever/objects/Players.scala +++ b/src/main/scala/net/psforever/objects/Players.scala @@ -80,6 +80,8 @@ object Players { ) ) target.Zone.AvatarEvents ! AvatarServiceMessage(name, AvatarAction.Revive(target.GUID)) + val reviveMessage = s"@YouHaveBeenMessage^revived~^$medicName~" + PlayerControl.sendResponse(target.Zone, name, ChatMsg(ChatMessageType.UNK_227, reviveMessage)) } /** diff --git a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala index ee5fef561..375f4cec5 100644 --- a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala +++ b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala @@ -5,6 +5,7 @@ import akka.actor.{Actor, ActorRef, Props, typed} import net.psforever.actors.session.AvatarActor import net.psforever.login.WorldSession.{DropEquipmentFromInventory, HoldNewEquipmentUp, PutNewEquipmentInInventoryOrDrop, RemoveOldEquipmentFromInventory} import net.psforever.objects._ +import net.psforever.objects.avatar.PlayerControl.sendResponse import net.psforever.objects.ce.Deployable import net.psforever.objects.definition.DeployAnimation import net.psforever.objects.definition.converter.OCM @@ -147,6 +148,15 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm newHealth - originalHealth ) ) + val amount = newHealth - originalHealth + val healMessageSelf = s"@SelfHitHealedMessage^healed~^$amount~^health~" + val healMessageOther = s"@WereHitByHealedMessage^healed~^$amount~^health~^$uname~" + if (player == user) { + sendResponse(user.Zone, user.Name, ChatMsg(ChatMessageType.UNK_227, healMessageSelf)) + } + else { + sendResponse(player.Zone, player.Name, ChatMsg(ChatMessageType.UNK_227, healMessageOther)) + } } if (player != user) { //"Someone is trying to heal you" @@ -210,6 +220,15 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm newArmor - originalArmor ) ) + val amount = newArmor - originalArmor + val repairMessageSelf = s"@SelfHitHealedMessage^repaired~^$amount~^armor~" + val repairMessageOther = s"@WereHitByHealedMessage^repaired~^$amount~^armor~^$uname~" + if (player == user) { + sendResponse(user.Zone, user.Name, ChatMsg(ChatMessageType.UNK_227, repairMessageSelf)) + } + else { + sendResponse(player.Zone, player.Name, ChatMsg(ChatMessageType.UNK_227, repairMessageOther)) + } } if (player != user) { if (player.isAlive) { diff --git a/src/main/scala/net/psforever/objects/serverobject/repair/RepairableEntity.scala b/src/main/scala/net/psforever/objects/serverobject/repair/RepairableEntity.scala index 11b51ad8a..3efb93c97 100644 --- a/src/main/scala/net/psforever/objects/serverobject/repair/RepairableEntity.scala +++ b/src/main/scala/net/psforever/objects/serverobject/repair/RepairableEntity.scala @@ -1,11 +1,13 @@ //Copyright (c) 2020 PSForever package net.psforever.objects.serverobject.repair +import net.psforever.objects.avatar.PlayerControl.sendResponse +import net.psforever.objects.ce.DeployableCategory import net.psforever.objects.sourcing.PlayerSource import net.psforever.objects.vital.RepairFromEquipment import net.psforever.objects.{Player, Tool} -import net.psforever.packet.game.{InventoryStateMessage, RepairMessage} -import net.psforever.types.{PlanetSideEmpire, Vector3} +import net.psforever.packet.game.{ChatMsg, InventoryStateMessage, RepairMessage} +import net.psforever.types.{ChatMessageType, PlanetSideEmpire, Vector3} import net.psforever.services.Service import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} @@ -113,6 +115,27 @@ trait RepairableEntity extends Repairable { RepairMessage(target.GUID, updatedHealth * 100 / definition.MaxHealth) ) ) + //if vehicle and vehicle is owned by another player, send repair chat message to the vehicle's owner + if (target.Zone.Vehicles.exists(_.GUID == target.GUID)) { + val vehicle = target.Zone.Vehicles.filter(_.GUID == target.GUID).head + val vehicleOwner = vehicle.OwnerName.getOrElse("someone") + val amount = updatedHealth - originalHealth + if (vehicleOwner != "someone" && vehicle.OwnerGuid.get != player.GUID && amount > 0) { + val repairMessageOther = s"@YourWasHealedMessage^@${vehicle.Definition.Name}~^repaired~^$amount~^armor~^${player.Name}~" + sendResponse(vehicle.Zone, vehicleOwner, ChatMsg(ChatMessageType.UNK_227, repairMessageOther)) + } + } + //same check for field turret + if (target.Zone.DeployableList.exists(_.GUID == target.GUID)) { + val turret = target.Zone.DeployableList.filter(_.GUID == target.GUID).head + val turretOwner = turret.OwnerName.getOrElse("someone") + val amount = updatedHealth - originalHealth + if (turret.Definition.DeployCategory == DeployableCategory.FieldTurrets && turretOwner != "someone" + && turret.OwnerGuid.get != player.GUID && amount > 0) { + val repairMessageOther = s"@YourWasHealedMessage^@${turret.Definition.Name}~^repaired~^$amount~^armor~^${player.Name}~" + sendResponse(turret.Zone, turretOwner, ChatMsg(ChatMessageType.UNK_227, repairMessageOther)) + } + } } protected def PerformRepairs(target: Repairable.Target, amount: Int): Int = { From 42fb0d692d3c0676547ad7f49ca31e19067b6105 Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Mon, 16 Jun 2025 20:17:35 -0400 Subject: [PATCH 34/57] xp for each kill --- .../actors/session/AvatarActor.scala | 36 ++++++++++--------- .../local/support/CaptureFlagManager.scala | 2 +- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/main/scala/net/psforever/actors/session/AvatarActor.scala b/src/main/scala/net/psforever/actors/session/AvatarActor.scala index b0899301e..c68b0f47c 100644 --- a/src/main/scala/net/psforever/actors/session/AvatarActor.scala +++ b/src/main/scala/net/psforever/actors/session/AvatarActor.scala @@ -946,12 +946,16 @@ object AvatarActor { def setBepOnly(avatarId: Long, bep: Long): Future[Long] = { import ctx._ import scala.concurrent.ExecutionContext.Implicits.global - val out: Promise[Long] = Promise() - val result = ctx.run(query[persistence.Avatar].filter(_.id == lift(avatarId)).update(_.bep -> lift(bep))) - result.onComplete { _ => - out.completeWith(Future(bep)) + ctx.transaction { implicit ec => + for { + currBep <- ctx.run(query[persistence.Avatar].filter(_.id == lift(avatarId)).map(_.bep)) + .map(_.headOption.getOrElse(0L)) + + newBep = currBep + bep + + _ <- ctx.run(query[persistence.Avatar].filter(_.id == lift(avatarId)).update(_.bep -> lift(newBep))) + } yield newBep } - out.future } def loadExperienceDebt(avatarId: Long): Future[Long] = { @@ -1843,7 +1847,7 @@ class AvatarActor( implantTimers.foreach(_.cancel()) supportExperienceTimer.cancel() if (supportExperiencePool > 0) { - AvatarActor.setBepOnly(avatar.id, avatar.bep + supportExperiencePool) + AvatarActor.setBepOnly(avatar.id, supportExperiencePool) } AvatarActor.saveAvatarData(avatar) saveLockerFunc() @@ -2882,7 +2886,7 @@ class AvatarActor( def setBep(bep: Long, modifier: ExperienceType): Unit = { import ctx._ AvatarActor.setBepOnly(avatar.id, bep).onComplete { - case Success(_) => + case Success(newBep) => val sess = session.get val zone = sess.zone val zoneId = zone.id @@ -2891,10 +2895,10 @@ class AvatarActor( val pguid = player.GUID val localModifier = modifier val current = BattleRank.withExperience(avatar.bep).value - val next = BattleRank.withExperience(bep).value + val next = BattleRank.withExperience(newBep).value val br24 = BattleRank.BR24.value - sessionActor ! SessionActor.SendResponse(BattleExperienceMessage(pguid, bep, localModifier)) - events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(pguid, 17, bep)) + sessionActor ! SessionActor.SendResponse(BattleExperienceMessage(pguid, newBep, localModifier)) + events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(pguid, 17, newBep)) if (current < br24 && next >= br24 || current >= br24 && next < br24) { setCosmetics(Set()).onComplete { _ => val evts = events @@ -2906,7 +2910,7 @@ class AvatarActor( // when the level is reduced, take away any implants over the implant slot limit val implants = avatar.implants.zipWithIndex.map { case (implant, index) => - if (index >= BattleRank.withExperience(bep).implantSlots && implant.isDefined) { + if (index >= BattleRank.withExperience(newBep).implantSlots && implant.isDefined) { ctx .run( query[persistence.Implant] @@ -2925,7 +2929,7 @@ class AvatarActor( implant } } - avatar = avatar.copy(bep = bep, implants = implants) + avatar = avatar.copy(bep = newBep, implants = implants) case Failure(exception) => log.error(exception)("db failure") } @@ -2948,11 +2952,11 @@ class AvatarActor( } def awardSupportExperience(bep: Long, previousDelay: Long): Unit = { - setBep(avatar.bep + bep, ExperienceType.Support) + setBep(bep, ExperienceType.Support) } def actuallyAwardSupportExperience(bep: Long, delayBy: Long): Unit = { - setBep(avatar.bep + bep, ExperienceType.Support) + setBep(bep, ExperienceType.Support) supportExperiencePool = supportExperiencePool - bep if (supportExperiencePool > 0) { resetSupportExperienceTimer(bep, delayBy) @@ -2986,7 +2990,7 @@ class AvatarActor( } private def setBepAction(modifier: ExperienceType)(value: Long): Unit = { - setBep(avatar.bep + value, modifier) + setBep(value, modifier) } private def setSupportAction(value: Long): Unit = { @@ -3037,7 +3041,7 @@ class AvatarActor( ) } if (exp > 0L) { - setBep(avatar.bep + exp, msg) + setBep(exp, msg) zone.actor ! ZoneActor.RewardOurSupporters(playerSource, historyTranscript, killStat, exp) zone.AvatarEvents ! AvatarServiceMessage( player.Name, AvatarAction.ShareKillExperienceWithSquad(player, exp)) diff --git a/src/main/scala/net/psforever/services/local/support/CaptureFlagManager.scala b/src/main/scala/net/psforever/services/local/support/CaptureFlagManager.scala index df0d5f129..e6450a7a8 100644 --- a/src/main/scala/net/psforever/services/local/support/CaptureFlagManager.scala +++ b/src/main/scala/net/psforever/services/local/support/CaptureFlagManager.scala @@ -92,7 +92,7 @@ class CaptureFlagManager(zone: Zone) extends Actor { case CaptureFlagLostReasonEnum.Resecured => CaptureFlagManager.ChatBroadcast( zone, - CaptureFlagChatMessageStrings.CTF_Failed_SourceResecured(flag.Owner.asInstanceOf[Building].Name, flag.Faction) + CaptureFlagChatMessageStrings.CTF_Failed_SourceResecured(flag.Owner.asInstanceOf[Building].Name, flag.Owner.asInstanceOf[Building].Faction) ) case CaptureFlagLostReasonEnum.TimedOut => val building = flag.Owner.asInstanceOf[Building] From 8f04b634c532937ec9bb59120f06400d19cb7d75 Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Tue, 17 Jun 2025 14:16:54 -0400 Subject: [PATCH 35/57] fix-weaponstatsession-error --- .../objects/zones/exp/ToDatabase.scala | 39 +++++++++++-------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/src/main/scala/net/psforever/objects/zones/exp/ToDatabase.scala b/src/main/scala/net/psforever/objects/zones/exp/ToDatabase.scala index aca7a2fe3..97ed44e7b 100644 --- a/src/main/scala/net/psforever/objects/zones/exp/ToDatabase.scala +++ b/src/main/scala/net/psforever/objects/zones/exp/ToDatabase.scala @@ -10,6 +10,8 @@ import net.psforever.types.Vector3 import net.psforever.util.Database.ctx import net.psforever.util.Database.ctx._ +import scala.concurrent.Future + object ToDatabase { /** * Insert an entry into the database's `killactivity` table. @@ -101,22 +103,27 @@ object ToDatabase { * insert a new entry into the table. * Shots fired. */ - def reportToolDischarge(avatarId: Long, stats: EquipmentStat): Unit = { - ctx.run(query[persistence.Weaponstatsession] - .insert( - _.avatarId -> lift(avatarId), - _.weaponId -> lift(stats.objectId), - _.shotsFired -> lift(stats.shotsFired), - _.shotsLanded -> lift(stats.shotsLanded), - _.kills -> lift(0), - _.assists -> lift(0), - _.sessionId -> lift(-1L) - ) - .onConflictUpdate(_.avatarId, _.weaponId, _.sessionId)( - (t, e) => t.shotsFired -> (t.shotsFired + e.shotsFired), - (t, e) => t.shotsLanded -> (t.shotsLanded + e.shotsLanded) - ) - ) + def reportToolDischarge(avatarId: Long, stats: EquipmentStat): Future[Unit] = { + import ctx._ + import scala.concurrent.ExecutionContext.Implicits.global + ctx.transaction { implicit ec => + ctx.run( + query[persistence.Weaponstatsession] + .insert( + _.avatarId -> lift(avatarId), + _.weaponId -> lift(stats.objectId), + _.shotsFired -> lift(stats.shotsFired), + _.shotsLanded -> lift(stats.shotsLanded), + _.kills -> lift(0), + _.assists -> lift(0), + _.sessionId -> lift(-1L) + ) + .onConflictUpdate(_.avatarId, _.weaponId, _.sessionId)( + (t, e) => t.shotsFired -> (t.shotsFired + e.shotsFired), + (t, e) => t.shotsLanded -> (t.shotsLanded + e.shotsLanded) + ) + ).map(_ => ()) + } } /** From 53429ea71fc0e3a86e83147943622c83153974bc Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Tue, 17 Jun 2025 16:31:05 -0400 Subject: [PATCH 36/57] false-hope --- .../objects/zones/exp/ToDatabase.scala | 39 ++++++++----------- 1 file changed, 16 insertions(+), 23 deletions(-) diff --git a/src/main/scala/net/psforever/objects/zones/exp/ToDatabase.scala b/src/main/scala/net/psforever/objects/zones/exp/ToDatabase.scala index 97ed44e7b..9bb015e0e 100644 --- a/src/main/scala/net/psforever/objects/zones/exp/ToDatabase.scala +++ b/src/main/scala/net/psforever/objects/zones/exp/ToDatabase.scala @@ -1,7 +1,6 @@ // Copyright (c) 2022 PSForever package net.psforever.objects.zones.exp -import scala.concurrent.ExecutionContext.Implicits.global import net.psforever.objects.avatar.scoring.EquipmentStat import net.psforever.objects.serverobject.hackable.Hackable.HackInfo import net.psforever.objects.sourcing.VehicleSource @@ -10,7 +9,6 @@ import net.psforever.types.Vector3 import net.psforever.util.Database.ctx import net.psforever.util.Database.ctx._ -import scala.concurrent.Future object ToDatabase { /** @@ -103,27 +101,22 @@ object ToDatabase { * insert a new entry into the table. * Shots fired. */ - def reportToolDischarge(avatarId: Long, stats: EquipmentStat): Future[Unit] = { - import ctx._ - import scala.concurrent.ExecutionContext.Implicits.global - ctx.transaction { implicit ec => - ctx.run( - query[persistence.Weaponstatsession] - .insert( - _.avatarId -> lift(avatarId), - _.weaponId -> lift(stats.objectId), - _.shotsFired -> lift(stats.shotsFired), - _.shotsLanded -> lift(stats.shotsLanded), - _.kills -> lift(0), - _.assists -> lift(0), - _.sessionId -> lift(-1L) - ) - .onConflictUpdate(_.avatarId, _.weaponId, _.sessionId)( - (t, e) => t.shotsFired -> (t.shotsFired + e.shotsFired), - (t, e) => t.shotsLanded -> (t.shotsLanded + e.shotsLanded) - ) - ).map(_ => ()) - } + def reportToolDischarge(avatarId: Long, stats: EquipmentStat): Unit = { + ctx.run(query[persistence.Weaponstatsession] + .insert( + _.avatarId -> lift(avatarId), + _.weaponId -> lift(stats.objectId), + _.shotsFired -> lift(stats.shotsFired), + _.shotsLanded -> lift(stats.shotsLanded), + _.kills -> lift(0), + _.assists -> lift(0), + _.sessionId -> lift(-1L) + ) + .onConflictUpdate(_.avatarId, _.weaponId, _.sessionId)( + (t, e) => t.shotsFired -> (t.shotsFired + e.shotsFired), + (t, e) => t.shotsLanded -> (t.shotsLanded + e.shotsLanded) + ) + ) } /** From 1900fd028ed75373d1250fd060fe1df191d55c31 Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Tue, 17 Jun 2025 19:30:29 -0400 Subject: [PATCH 37/57] needed that --- src/main/scala/net/psforever/objects/zones/exp/ToDatabase.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/net/psforever/objects/zones/exp/ToDatabase.scala b/src/main/scala/net/psforever/objects/zones/exp/ToDatabase.scala index 9bb015e0e..aca7a2fe3 100644 --- a/src/main/scala/net/psforever/objects/zones/exp/ToDatabase.scala +++ b/src/main/scala/net/psforever/objects/zones/exp/ToDatabase.scala @@ -1,6 +1,7 @@ // Copyright (c) 2022 PSForever package net.psforever.objects.zones.exp +import scala.concurrent.ExecutionContext.Implicits.global import net.psforever.objects.avatar.scoring.EquipmentStat import net.psforever.objects.serverobject.hackable.Hackable.HackInfo import net.psforever.objects.sourcing.VehicleSource @@ -9,7 +10,6 @@ import net.psforever.types.Vector3 import net.psforever.util.Database.ctx import net.psforever.util.Database.ctx._ - object ToDatabase { /** * Insert an entry into the database's `killactivity` table. From f74da61fa5ebf457bd120346478a5e7fc930fb08 Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Mon, 23 Jun 2025 13:10:48 -0400 Subject: [PATCH 38/57] Mine Distance (#1276) * changing to square distance during mine checks; repeating entity geometric distance check function * added separate trigger radius rules for players and vehicles --- .../scala/net/psforever/objects/Player.scala | 4 +-- .../scala/net/psforever/objects/Vehicle.scala | 4 +-- .../interaction/TriggerOnPlayerRule.scala | 14 +++++++++ .../objects/ce/InteractWithMines.scala | 29 +++++++++++++++---- .../interaction/TriggerOnVehicleRule.scala | 14 +++++++++ .../net/psforever/objects/zones/Zone.scala | 18 ++++++++++++ 6 files changed, 73 insertions(+), 10 deletions(-) create mode 100644 src/main/scala/net/psforever/objects/avatar/interaction/TriggerOnPlayerRule.scala create mode 100644 src/main/scala/net/psforever/objects/vehicles/interaction/TriggerOnVehicleRule.scala diff --git a/src/main/scala/net/psforever/objects/Player.scala b/src/main/scala/net/psforever/objects/Player.scala index 8415e3b6e..fd00fb54d 100644 --- a/src/main/scala/net/psforever/objects/Player.scala +++ b/src/main/scala/net/psforever/objects/Player.scala @@ -1,7 +1,7 @@ // Copyright (c) 2017 PSForever package net.psforever.objects -import net.psforever.objects.avatar.interaction.{WithEntrance, WithGantry, WithLava, WithWater} +import net.psforever.objects.avatar.interaction.{TriggerOnPlayerRule, WithEntrance, WithGantry, WithLava, WithWater} import net.psforever.objects.avatar.{Avatar, LoadoutManager, SpecialCarry} import net.psforever.objects.ballistics.InteractWithRadiationClouds import net.psforever.objects.ce.{Deployable, InteractWithMines, InteractWithTurrets} @@ -47,7 +47,7 @@ class Player(var avatar: Avatar) new WithGantry(avatar.name), new WithMovementTrigger() ))) - interaction(new InteractWithMines(range = 10)) + interaction(new InteractWithMines(range = 10, TriggerOnPlayerRule)) interaction(new InteractWithTurrets()) interaction(new InteractWithRadiationClouds(range = 10f, Some(this))) diff --git a/src/main/scala/net/psforever/objects/Vehicle.scala b/src/main/scala/net/psforever/objects/Vehicle.scala index e60259ede..386aa98f4 100644 --- a/src/main/scala/net/psforever/objects/Vehicle.scala +++ b/src/main/scala/net/psforever/objects/Vehicle.scala @@ -16,7 +16,7 @@ import net.psforever.objects.serverobject.hackable.Hackable import net.psforever.objects.serverobject.interior.{InteriorAwareFromInteraction, Sidedness} import net.psforever.objects.serverobject.structures.AmenityOwner import net.psforever.objects.vehicles._ -import net.psforever.objects.vehicles.interaction.{WithLava, WithWater} +import net.psforever.objects.vehicles.interaction.{TriggerOnVehicleRule, WithLava, WithWater} import net.psforever.objects.vital.resistance.StandardResistanceProfile import net.psforever.objects.vital.Vitality import net.psforever.objects.vital.resolution.DamageResistanceModel @@ -101,7 +101,7 @@ class Vehicle(private val vehicleDef: VehicleDefinition) new WithDeath(), new WithMovementTrigger() ))) - interaction(new InteractWithMines(range = 20)) + interaction(new InteractWithMines(range = 20, TriggerOnVehicleRule)) interaction(new InteractWithTurrets()) interaction(new InteractWithRadiationCloudsSeatedInVehicle(obj = this, range = 20)) diff --git a/src/main/scala/net/psforever/objects/avatar/interaction/TriggerOnPlayerRule.scala b/src/main/scala/net/psforever/objects/avatar/interaction/TriggerOnPlayerRule.scala new file mode 100644 index 000000000..95da30ac9 --- /dev/null +++ b/src/main/scala/net/psforever/objects/avatar/interaction/TriggerOnPlayerRule.scala @@ -0,0 +1,14 @@ +// Copyright (c) 2025 PSForever +package net.psforever.objects.avatar.interaction + +import net.psforever.objects.ce.TriggerTest +import net.psforever.objects.ExplosiveDeployable +import net.psforever.objects.geometry.d3.VolumetricGeometry +import net.psforever.objects.zones.Zone + +case object TriggerOnPlayerRule + extends TriggerTest { + def test(g: VolumetricGeometry, obj: ExplosiveDeployable, radius: Float): Boolean = { + Zone.distanceCheck(g, obj, radius) + } +} diff --git a/src/main/scala/net/psforever/objects/ce/InteractWithMines.scala b/src/main/scala/net/psforever/objects/ce/InteractWithMines.scala index 39e3f3fa2..9e7c66f83 100644 --- a/src/main/scala/net/psforever/objects/ce/InteractWithMines.scala +++ b/src/main/scala/net/psforever/objects/ce/InteractWithMines.scala @@ -1,8 +1,9 @@ // Copyright (c) 2021 PSForever package net.psforever.objects.ce +import net.psforever.objects.geometry.d3.VolumetricGeometry import net.psforever.objects.zones.blockmap.SectorPopulation -import net.psforever.objects.zones.{InteractsWithZone, Zone, ZoneInteraction, ZoneInteractionType} +import net.psforever.objects.zones.{InteractsWithZone, ZoneInteraction, ZoneInteractionType} import net.psforever.objects.{BoomerDeployable, ExplosiveDeployable} import net.psforever.types.PlanetSideGUID @@ -13,7 +14,7 @@ case object MineInteraction extends ZoneInteractionType * "Interact", here, is a graceful word for "trample upon" and the consequence should be an explosion * and maybe death. */ -class InteractWithMines(val range: Float) +class InteractWithMines(val range: Float, rule: TriggerTest) extends ZoneInteraction { /** * mines that, though detected, are skipped from being alerted; @@ -23,7 +24,7 @@ class InteractWithMines(val range: Float) */ private var skipTargets: List[PlanetSideGUID] = List() - def Type = MineInteraction + def Type: MineInteraction.type = MineInteraction /** * Trample upon active mines in our current detection sector and alert those mines. @@ -32,12 +33,12 @@ class InteractWithMines(val range: Float) */ def interaction(sector: SectorPopulation, target: InteractsWithZone): Unit = { val faction = target.Faction + lazy val targetGeometry = target.Definition.Geometry(target) val targets = sector .deployableList .filter { - case _: BoomerDeployable => false //boomers are specific types of ExplosiveDeployable but do not count here - case ex: ExplosiveDeployable => ex.Faction != faction && - Zone.distanceCheck(target, ex, ex.Definition.triggerRadius) + case _: BoomerDeployable => false //boomers are a specific type of ExplosiveDeployable that do not count here + case ex: ExplosiveDeployable => ex.Faction != faction && rule.test(targetGeometry, ex, ex.Definition.triggerRadius) case _ => false } val notSkipped = targets.filterNot { t => skipTargets.contains(t.GUID) } @@ -56,3 +57,19 @@ class InteractWithMines(val range: Float) skipTargets = List() } } + +/** + * The testing rule used to determine if a target is within range + * to agitate the game world deployable extra-territorial munitions. + */ +trait TriggerTest { + /** + * Perform the test + * @param g the geometric representation of a game entity + * @param obj a game entity + * @param distance the maximum distance permissible between game entities + * @return `true`, if the two entities are near enough to each other; + * `false`, otherwise + */ + def test(g: VolumetricGeometry, obj: ExplosiveDeployable, distance: Float): Boolean +} diff --git a/src/main/scala/net/psforever/objects/vehicles/interaction/TriggerOnVehicleRule.scala b/src/main/scala/net/psforever/objects/vehicles/interaction/TriggerOnVehicleRule.scala new file mode 100644 index 000000000..719e28a13 --- /dev/null +++ b/src/main/scala/net/psforever/objects/vehicles/interaction/TriggerOnVehicleRule.scala @@ -0,0 +1,14 @@ +// Copyright (c) 2025 PSForever +package net.psforever.objects.vehicles.interaction + +import net.psforever.objects.ce.TriggerTest +import net.psforever.objects.ExplosiveDeployable +import net.psforever.objects.geometry.d3.VolumetricGeometry +import net.psforever.objects.zones.Zone + +case object TriggerOnVehicleRule + extends TriggerTest { + def test(g: VolumetricGeometry, obj: ExplosiveDeployable, radius: Float): Boolean = { + Zone.distanceCheck(g, obj, radius * radius) + } +} diff --git a/src/main/scala/net/psforever/objects/zones/Zone.scala b/src/main/scala/net/psforever/objects/zones/Zone.scala index 4a44e36d6..45eb69a12 100644 --- a/src/main/scala/net/psforever/objects/zones/Zone.scala +++ b/src/main/scala/net/psforever/objects/zones/Zone.scala @@ -1901,6 +1901,24 @@ object Zone { distanceCheck(obj1.Definition.Geometry(obj1), obj2.Definition.Geometry(obj2), maxDistance) } + /** + * Two game entities are considered "near" each other if they are within a certain distance of one another. + * A default function literal mainly used for `serverSideDamage`. + * Best used when one entity - `obj1` - is to be reused in tests against multiple other entities + * as it skips repeatedly calculating the volumetric geometry of the repeating entity. + * @see `ObjectDefinition.Geometry` + * @see `serverSideDamage` + * @param obj1 the geometric representation of a game entity + * @param obj2 a game entity + * @param maxDistance the square of the maximum distance permissible between game entities + * before they are no longer considered "near" + * @return `true`, if the two entities are near enough to each other; + * `false`, otherwise + */ + def distanceCheck(obj1: VolumetricGeometry, obj2: PlanetSideGameObject, maxDistance: Float): Boolean = { + distanceCheck(obj1, obj2.Definition.Geometry(obj2), maxDistance) + } + /** * Two game entities are considered "near" each other if they are within a certain distance of one another. * @param g1 the geometric representation of a game entity From 4d3a3a390fcf68864aeb8ed9daaf7cedab6cd5bf Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Sun, 29 Jun 2025 15:58:47 -0400 Subject: [PATCH 39/57] max armor reminder, log kicked player --- .../net/psforever/actors/session/support/ZoningOperations.scala | 1 + .../psforever/services/account/AccountPersistenceService.scala | 1 + 2 files changed, 2 insertions(+) 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 49bb0833c..514b7c3a6 100644 --- a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala @@ -3134,6 +3134,7 @@ class ZoningOperations( statisticsPacketFunc() if (tplayer.ExoSuit == ExoSuitType.MAX) { sendResponse(PlanetsideAttributeMessage(guid, 7, tplayer.Capacitor.toLong)) + sendResponse(PlanetsideAttributeMessage(guid, 4, tplayer.Armor)) } // AvatarAwardMessage //populateAvatarAwardRibbonsFunc(1, 20L) diff --git a/src/main/scala/net/psforever/services/account/AccountPersistenceService.scala b/src/main/scala/net/psforever/services/account/AccountPersistenceService.scala index 1d2fe0f4f..3ff3206af 100644 --- a/src/main/scala/net/psforever/services/account/AccountPersistenceService.scala +++ b/src/main/scala/net/psforever/services/account/AccountPersistenceService.scala @@ -318,6 +318,7 @@ class PersistenceMonitor( } kicked = true kickTime = time.orElse(Some(300L)) + PerformLogout() case Logout(_) => kickTime match { From e76c33b0285f0536395878e796149880094b991e Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Mon, 30 Jun 2025 13:00:58 -0400 Subject: [PATCH 40/57] tell others my armor --- .../net/psforever/actors/session/support/ZoningOperations.scala | 1 + 1 file changed, 1 insertion(+) 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 514b7c3a6..f7efba70c 100644 --- a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala @@ -3135,6 +3135,7 @@ class ZoningOperations( if (tplayer.ExoSuit == ExoSuitType.MAX) { sendResponse(PlanetsideAttributeMessage(guid, 7, tplayer.Capacitor.toLong)) sendResponse(PlanetsideAttributeMessage(guid, 4, tplayer.Armor)) + continent.AvatarEvents ! AvatarServiceMessage(continent.id, AvatarAction.PlanetsideAttributeToAll(guid, 4, tplayer.Armor)) } // AvatarAwardMessage //populateAvatarAwardRibbonsFunc(1, 20L) From e3231bae022784f477fa81b9eb5c7b6d983769d4 Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Mon, 30 Jun 2025 20:28:16 -0400 Subject: [PATCH 41/57] tell me armor for any max --- .../net/psforever/actors/session/support/ZoningOperations.scala | 2 ++ 1 file changed, 2 insertions(+) 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 f7efba70c..3cb7d7643 100644 --- a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala @@ -3137,6 +3137,8 @@ class ZoningOperations( sendResponse(PlanetsideAttributeMessage(guid, 4, tplayer.Armor)) continent.AvatarEvents ! AvatarServiceMessage(continent.id, AvatarAction.PlanetsideAttributeToAll(guid, 4, tplayer.Armor)) } + // for issue #1269 + continent.AllPlayers.filter(_.ExoSuit == ExoSuitType.MAX).foreach(max => sendResponse(PlanetsideAttributeMessage(max.GUID, 4, max.Armor))) // AvatarAwardMessage //populateAvatarAwardRibbonsFunc(1, 20L) From b82e0280f6e2979aa7c73ac4b6a71e39359ba936 Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Sun, 6 Jul 2025 15:24:07 -0400 Subject: [PATCH 42/57] gate fixes --- .../session/support/ZoningOperations.scala | 3 ++- .../net/psforever/objects/SpawnPoint.scala | 4 +--- .../global/GlobalDefinitionsBuilding.scala | 8 ++++---- .../scala/net/psforever/zones/Zones.scala | 20 ++++++++++++++++++- 4 files changed, 26 insertions(+), 9 deletions(-) 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 3cb7d7643..a60ac1edc 100644 --- a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala @@ -1269,6 +1269,7 @@ class ZoningOperations( ICS.FindZone(_.id == zoneId, context.self) )) } else { + vehicle.Velocity = None sessionLogic.general.unaccessContainer(vehicle) LoadZoneCommonTransferActivity() player.VehicleSeated = vehicle.GUID @@ -2965,7 +2966,7 @@ class ZoningOperations( /** * na * @param target player being spawned - * @param position where player is being placed in the game wqrld + * @param position where player is being placed in the game world * @param orientation in what direction the player is facing in the game world * @param onThisSide description of the containing environment * @param goingToZone common designation for the zone diff --git a/src/main/scala/net/psforever/objects/SpawnPoint.scala b/src/main/scala/net/psforever/objects/SpawnPoint.scala index d6f800dde..e3fc8ca52 100644 --- a/src/main/scala/net/psforever/objects/SpawnPoint.scala +++ b/src/main/scala/net/psforever/objects/SpawnPoint.scala @@ -140,8 +140,6 @@ object SpawnPoint { def CavernGate(innerRadius: Float)(obj: SpawnPoint, target: PlanetSideGameObject): (Vector3, Vector3) = { val (a, b) = metaGate(obj, target, innerRadius) target match { - case v: Vehicle if GlobalDefinitions.isFlightVehicle(v.Definition) => - (a.xy + Vector3.z((target.Position.z + a.z) * 0.5f), b) case m: MountableEntity => m.BailProtection = true (a + Vector3.z(obj.Definition.UseRadius * 0.5f), b) @@ -154,7 +152,7 @@ object SpawnPoint { val (a, b) = metaGate(obj, target, innerRadius) target match { case v: Vehicle if GlobalDefinitions.isFlightVehicle(v.Definition) => - (a.xy + Vector3.z((target.Position.z + a.z) * 0.5f), b) + (a, b) case _ => (a + Vector3.z(flightlessZOffset), b) } diff --git a/src/main/scala/net/psforever/objects/global/GlobalDefinitionsBuilding.scala b/src/main/scala/net/psforever/objects/global/GlobalDefinitionsBuilding.scala index 74a487821..ca18a2e0a 100644 --- a/src/main/scala/net/psforever/objects/global/GlobalDefinitionsBuilding.scala +++ b/src/main/scala/net/psforever/objects/global/GlobalDefinitionsBuilding.scala @@ -129,8 +129,8 @@ object GlobalDefinitionsBuilding { hst.NoWarp += colossus_flight hst.NoWarp += peregrine_gunner hst.NoWarp += peregrine_flight - hst.SpecificPointFunc = SpawnPoint.CavernGate(innerRadius = 6f) - + hst.SpecificPointFunc = SpawnPoint.CavernGate(innerRadius = 12f) + warpgate.Name = "warpgate" warpgate.UseRadius = 67.81070029f warpgate.SOIRadius = 302 //301.8713f @@ -141,8 +141,8 @@ object GlobalDefinitionsBuilding { warpgate_cavern.UseRadius = 19.72639434f warpgate_cavern.SOIRadius = 41 warpgate_cavern.VehicleAllowance = true - warpgate_cavern.SpecificPointFunc = SpawnPoint.CavernGate(innerRadius = 4.5f) - + warpgate_cavern.SpecificPointFunc = SpawnPoint.CavernGate(innerRadius = 12f) + warpgate_small.Name = "warpgate_small" warpgate_small.UseRadius = 69.03687655f warpgate_small.SOIRadius = 103 diff --git a/src/main/scala/net/psforever/zones/Zones.scala b/src/main/scala/net/psforever/zones/Zones.scala index 6f9694688..f1463dfda 100644 --- a/src/main/scala/net/psforever/zones/Zones.scala +++ b/src/main/scala/net/psforever/zones/Zones.scala @@ -295,7 +295,25 @@ object Zones { WarpGate.Structure(Vector3(structure.absX, structure.absY, structure.absZ), GlobalDefinitions.hst) ) ) - case objectType if warpGateTypes.contains(objectType) => + case objectType @ "warpgate_cavern" if warpGateTypes.contains(objectType) => + zoneMap.addLocalBuilding( + structure.objectName, + structure.guid, + structure.mapId.get, + FoundationBuilder( + WarpGate.Structure(Vector3(structure.absX, structure.absY, structure.absZ), GlobalDefinitions.warpgate_cavern) + ) + ) + case objectType @ "warpgate_small" if warpGateTypes.contains(objectType) => + zoneMap.addLocalBuilding( + structure.objectName, + structure.guid, + structure.mapId.get, + FoundationBuilder( + WarpGate.Structure(Vector3(structure.absX, structure.absY, structure.absZ), GlobalDefinitions.warpgate_small) + ) + ) + case objectType @ "warpgate" if warpGateTypes.contains(objectType) => zoneMap.addLocalBuilding( structure.objectName, structure.guid, From 95b8b3459473e1f814e01af13c15122bddf7dc74 Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Thu, 17 Jul 2025 15:35:10 -0400 Subject: [PATCH 43/57] cud emp and os --- .../resources/overrides/game_objects0.adb.lst | 2 +- .../actors/session/AvatarActor.scala | 66 +++++++++++- .../actors/session/SessionActor.scala | 3 +- .../normal/WeaponAndProjectileLogic.scala | 6 +- .../spectator/WeaponAndProjectileLogic.scala | 13 +-- .../WeaponAndProjectileOperations.scala | 80 +++++++++++++- .../session/support/ZoningOperations.scala | 2 +- .../net/psforever/objects/OrbitalStrike.scala | 61 +++++++++++ .../net/psforever/objects/SpecialEmp.scala | 102 ++++++++++++++++++ .../net/psforever/objects/avatar/Avatar.scala | 7 ++ .../objects/avatar/PlayerControl.scala | 6 ++ .../net/psforever/objects/zones/Zone.scala | 47 ++++++++ .../game/PlanetsideAttributeMessage.scala | 4 + .../psforever/packet/game/UplinkRequest.scala | 6 +- 14 files changed, 382 insertions(+), 23 deletions(-) create mode 100644 src/main/scala/net/psforever/objects/OrbitalStrike.scala diff --git a/server/src/main/resources/overrides/game_objects0.adb.lst b/server/src/main/resources/overrides/game_objects0.adb.lst index d476e61a9..1ea38dca0 100644 --- a/server/src/main/resources/overrides/game_objects0.adb.lst +++ b/server/src/main/resources/overrides/game_objects0.adb.lst @@ -23,7 +23,7 @@ add_property boomer_trigger equiptime 500 add_property chainblade equiptime 250 add_property chainblade holstertime 250 add_property colossus_flight requirement_award0 false -add_property command_detonater allowed false +add_property command_detonater allowed true add_property command_detonater equiptime 500 add_property command_detonater holstertime 500 add_property cycler equiptime 600 diff --git a/src/main/scala/net/psforever/actors/session/AvatarActor.scala b/src/main/scala/net/psforever/actors/session/AvatarActor.scala index c68b0f47c..12291c4ac 100644 --- a/src/main/scala/net/psforever/actors/session/AvatarActor.scala +++ b/src/main/scala/net/psforever/actors/session/AvatarActor.scala @@ -154,6 +154,10 @@ object AvatarActor { final case class UpdatePurchaseTime(definition: BasicDefinition, time: LocalDateTime = LocalDateTime.now()) extends Command + /** rchase time for the use of calculating cooldowns */ + final case class UpdateCUDTime(action: String, time: LocalDateTime = LocalDateTime.now()) + extends Command + /** Set use time for the use of calculating cooldowns */ final case class UpdateUseTime(definition: BasicDefinition, time: LocalDateTime = LocalDateTime.now()) extends Command @@ -459,7 +463,15 @@ object AvatarActor { case _ => () } } catch { - case _: Exception => () + case _: Exception => + val cooldown = LocalDateTime.parse(b) + name match { + case "orbital_strike" if now.compareTo(cooldown.plusMillis(3.hours.toMillis.toInt)) == -1 => + cooldowns.put(name, cooldown) + case "emp_blast" | "reveal_friendlies" | "reveal_enemies" if now.compareTo(cooldown.plusMillis(20.minutes.toMillis.toInt)) == -1 => + cooldowns.put(name, cooldown) + case _ => () + } } case _ => log.warn(s"ignoring invalid cooldown string: '$value'") @@ -1549,6 +1561,21 @@ class AvatarActor( } Behaviors.same + case UpdateCUDTime(action, time) => + var theTimes = avatar.cooldowns.purchase + var updateTheTimes: Boolean = false + Avatar.cudCooldowns.get(action) match { + case Some(_) => + //only send for items with cooldowns + updateTheTimes = true + theTimes = theTimes.updated(action, time) + case _ => () + } + if (updateTheTimes) { + avatarCopy(avatar.copy(cooldowns = avatar.cooldowns.copy(purchase = theTimes))) + } + Behaviors.same + case UpdateUseTime(definition, time) => if (!Avatar.useCooldowns.contains(definition)) { log.warn(s"${avatar.name} is updating a use time for item '${definition.Name}' that has no cooldown") @@ -2655,6 +2682,43 @@ class AvatarActor( } if (keysToDrop.nonEmpty) { val cdown = avatar.cooldowns + val cud = Array("orbital_strike", "emp_blast", "reveal_friendlies", "reveal_enemies") + keysToDrop.foreach { key => + if (cud.contains(key)) { + avatar + .cooldowns + .purchase + .find { case (name, _) => name.equals(key) } + .flatMap { case (name, purchaseTime) => + val secondsSincePurchase = Seconds.secondsBetween(purchaseTime, LocalDateTime.now()).getSeconds + Avatar + .cudCooldowns + .find(_._1.equals(name)) + .collect { + case (action, cooldown) => + (action, cooldown.toSeconds - secondsSincePurchase) + } + .orElse { + None + } + .collect { + case (action, remainingTime) if remainingTime > 0 => + val convertTime = remainingTime * 1000 + keysToDrop = keysToDrop.filterNot(_ == action) + action match { + case "orbital_strike" => + sessionActor ! SessionActor.SendResponse(PlanetsideAttributeMessage(session.get.player.GUID, 60, convertTime)) + case "emp_blast" => + sessionActor ! SessionActor.SendResponse(PlanetsideAttributeMessage(session.get.player.GUID, 59, convertTime)) + case "reveal_enemies" => + sessionActor ! SessionActor.SendResponse(PlanetsideAttributeMessage(session.get.player.GUID, 58, convertTime)) + case "reveal_friendlies" => + sessionActor ! SessionActor.SendResponse(PlanetsideAttributeMessage(session.get.player.GUID, 57, convertTime)) + } + } + } + } + } avatarCopy(avatar.copy(cooldowns = cdown.copy(purchase = cdown.purchase.removedAll(keysToDrop)))) } } diff --git a/src/main/scala/net/psforever/actors/session/SessionActor.scala b/src/main/scala/net/psforever/actors/session/SessionActor.scala index 899d32651..25b5c377e 100644 --- a/src/main/scala/net/psforever/actors/session/SessionActor.scala +++ b/src/main/scala/net/psforever/actors/session/SessionActor.scala @@ -517,7 +517,8 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con case packet: WeaponLazeTargetPositionMessage => logic.shooting.handleWeaponLazeTargetPosition(packet) - case _: UplinkRequest => () + case packet: UplinkRequest => + logic.shooting.handleUplinkRequest(packet) case packet: HitMessage => logic.shooting.handleDirectHit(packet) 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 0d33f5aba..785cb06c2 100644 --- a/src/main/scala/net/psforever/actors/session/normal/WeaponAndProjectileLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/WeaponAndProjectileLogic.scala @@ -8,8 +8,8 @@ import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObjec import net.psforever.objects.{BoomerDeployable, BoomerTrigger, Player, SpecialEmp, Tool, Vehicle} import net.psforever.objects.vital.base.{DamageResolution, DamageType} import net.psforever.objects.zones.{Zone, ZoneProjectile} -import net.psforever.packet.game.{AIDamage, AvatarGrenadeStateMessage, ChangeAmmoMessage, ChangeFireModeMessage, ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, HitMessage, LashMessage, LongRangeProjectileInfoMessage, ProjectileStateMessage, ReloadMessage, SplashHitMessage, UplinkRequest, WeaponDelayFireMessage, WeaponDryFireMessage, WeaponFireMessage, WeaponLazeTargetPositionMessage} -import net.psforever.types.Vector3 +import net.psforever.packet.game.{AIDamage, AvatarGrenadeStateMessage, ChangeAmmoMessage, ChangeFireModeMessage, ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, HitMessage, LashMessage, LongRangeProjectileInfoMessage, OrbitalStrikeWaypointMessage, ProjectileStateMessage, ReloadMessage, SplashHitMessage, TriggerEffectMessage, TriggeredEffectLocation, UplinkRequest, UplinkRequestType, UplinkResponse, WeaponDelayFireMessage, WeaponDryFireMessage, WeaponFireMessage, WeaponLazeTargetPositionMessage} +import net.psforever.types.{ValidPlanetSideGUID, Vector3} object WeaponAndProjectileLogic { def apply(ops: WeaponAndProjectileOperations): WeaponAndProjectileLogic = { @@ -50,7 +50,7 @@ class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit } def handleUplinkRequest(packet: UplinkRequest): Unit = { - sessionLogic.administrativeKick(player) + ops.handleUplinkRequest(packet) } def handleAvatarGrenadeState(pkt: AvatarGrenadeStateMessage): Unit = { 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 89a95dac1..ab5002760 100644 --- a/src/main/scala/net/psforever/actors/session/spectator/WeaponAndProjectileLogic.scala +++ b/src/main/scala/net/psforever/actors/session/spectator/WeaponAndProjectileLogic.scala @@ -28,18 +28,7 @@ class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit def handleWeaponLazeTargetPosition(pkt: WeaponLazeTargetPositionMessage): Unit = { /* intentionally blank */ } - def handleUplinkRequest(packet: UplinkRequest): Unit = { - val UplinkRequest(code, _, _) = packet - val playerFaction = player.Faction - //todo this is not correct - code match { - case UplinkRequestType.RevealFriendlies => - sendResponse(UplinkResponse(code.value, continent.LivePlayers.count(_.Faction == playerFaction))) - case UplinkRequestType.RevealEnemies => - sendResponse(UplinkResponse(code.value, continent.LivePlayers.count(_.Faction != playerFaction))) - case _ => () - } - } + def handleUplinkRequest(packet: UplinkRequest): Unit = { /* intentionally blank */ } def handleAvatarGrenadeState(pkt: AvatarGrenadeStateMessage): Unit = { /* intentionally blank */ } 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 5ade93f01..eebb56466 100644 --- a/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala @@ -3,6 +3,8 @@ package net.psforever.actors.session.support import akka.actor.{ActorContext, typed} import net.psforever.login.WorldSession.{CountAmmunition, CountGrenades, FindAmmoBoxThatUses, FindEquipmentStock, FindToolThatUses, PutEquipmentInInventoryOrDrop, PutNewEquipmentInInventoryOrDrop, RemoveOldEquipmentFromInventory} +import net.psforever.objects.OrbitalStrike.{cr4_os, cr5_os} +import net.psforever.objects.SpecialEmp.{cr3_emp, cr4_emp, cr5_emp} import net.psforever.objects.ballistics.ProjectileQuality import net.psforever.objects.definition.{ProjectileDefinition, SpecialExoSuitDefinition} import net.psforever.objects.entity.SimpleWorldEntity @@ -23,7 +25,9 @@ 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, Vector3} +import net.psforever.packet.game.UplinkRequest +import net.psforever.services.local.{LocalAction, LocalServiceMessage} +import net.psforever.types.{ChatMessageType, PlanetSideEmpire, ValidPlanetSideGUID, Vector3} import net.psforever.util.Config import scala.collection.mutable @@ -89,6 +93,7 @@ class WeaponAndProjectileOperations( ) extends CommonSessionInterfacingFunctionality { var shooting: mutable.Set[PlanetSideGUID] = mutable.Set.empty //ChangeFireStateMessage_Start var prefire: mutable.Set[PlanetSideGUID] = mutable.Set.empty //if WeaponFireMessage precedes ChangeFireStateMessage_Start + private[session] var orbitalStrikePos: Option[Vector3] = None private[session] var shootingStart: mutable.HashMap[PlanetSideGUID, Long] = mutable.HashMap[PlanetSideGUID, Long]() private[session] var shootingStop: mutable.HashMap[PlanetSideGUID, Long] = mutable.HashMap[PlanetSideGUID, Long]() private[session] val shotsFired: mutable.HashMap[Int,Int] = mutable.HashMap[Int,Int]() @@ -292,6 +297,79 @@ class WeaponAndProjectileOperations( } } + def handleUplinkRequest(pkt: UplinkRequest): Unit = { + val UplinkRequest(code, pos, _) = pkt + val playerFaction = player.Faction + code match { + case UplinkRequestType.RevealFriendlies => () + /*sendResponse(UplinkResponse(code.value, 0)) + sendResponse(PlanetsideAttributeMessage(player.GUID, 57, 10000)) //1200000 + avatarActor ! AvatarActor.UpdateCUDTime("reveal_friendlies")*/ + case UplinkRequestType.RevealEnemies => () + /*sendResponse(UplinkResponse(code.value, 0)) + sendResponse(PlanetsideAttributeMessage(player.GUID, 58, 10000)) //1200000 + avatarActor ! AvatarActor.UpdateCUDTime("reveal_enemies") + These are seen in a few logs, but didn't work. Unclear what these do or what '10' in Event1 represents + sendResponse(UplinkPositionEvent(5, Event0(5))) + sendResponse(UplinkPositionEvent(4, Event1(4, 10))) + sendResponse(UplinkPositionEvent(6, Event0(6)))*/ + case UplinkRequestType.ElectroMagneticPulse => + val cr = player.avatar.cr.value + val empSize = cr match { + case 3 => cr3_emp + case 4 => cr4_emp + case 5 => cr5_emp + } + val empColor = if (playerFaction != PlanetSideEmpire.NEUTRAL) { s"explosion_emp_${playerFaction.toString.toLowerCase}" } else { "explosion_emp_bo" } + sendResponse(UplinkResponse(code.value, 0)) + sendResponse(PlanetsideAttributeMessage(player.GUID, 59, 10000)) //1200000 + avatarActor ! AvatarActor.UpdateCUDTime("emp_blast") + player.Zone.LocalEvents ! LocalServiceMessage(s"${player.Zone.id}", + LocalAction.SendPacket(TriggerEffectMessage(ValidPlanetSideGUID(0), empColor, None, Some(TriggeredEffectLocation(player.Position, Vector3(0, 0, 90)))))) + context.system.scheduler.scheduleOnce(delay = 1 seconds) { + Zone.serverSideDamage(player.Zone, player, empSize, SpecialEmp.createEmpInteraction(empSize, player.Position), + ExplosiveDeployableControl.detectionForExplosiveSource(player), Zone.findAllTargets) + } + case UplinkRequestType.OrbitalStrike => + player.Zone.LocalEvents ! LocalServiceMessage(s"$playerFaction", LocalAction.SendPacket(OrbitalStrikeWaypointMessage(player.GUID, pos.get.x, pos.get.y))) + sendResponse(UplinkResponse(code.value, 0)) + orbitalStrikePos = pos + case UplinkRequestType.Unknown5 => + val cr = player.avatar.cr.value + val strikeType = playerFaction match { + case PlanetSideEmpire.NC => + if (cr == 4) {"explosion_bluedeath_nc"} else {"explosion_bluedeath_nc_lrg"} + case PlanetSideEmpire.TR => + if (cr == 4) {"explosion_bluedeath_tr"} else {"explosion_bluedeath_tr_lrg"} + case PlanetSideEmpire.VS => + if (cr == 4) {"explosion_bluedeath_vs"} else {"explosion_bluedeath_vs_lrg"} + case PlanetSideEmpire.NEUTRAL => + if (cr == 4) {"explosion_bluedeath_bo"} else {"explosion_bluedeath_bo_lrg"} + } + val osSize = cr match { + case 4 => cr4_os + case 5 => cr5_os + } + sendResponse(UplinkResponse(code.value, 0)) + sendResponse(PlanetsideAttributeMessage(player.GUID, 60, 10000)) //10800000 + avatarActor ! AvatarActor.UpdateCUDTime("orbital_strike") + context.system.scheduler.scheduleOnce(delay = 5 seconds) { + player.Zone.LocalEvents ! LocalServiceMessage(s"${player.Zone.id}", + LocalAction.SendPacket(TriggerEffectMessage(ValidPlanetSideGUID(0), strikeType, None, Some(TriggeredEffectLocation(orbitalStrikePos.get, Vector3(0, 0, 90)))))) + player.Zone.LocalEvents ! LocalServiceMessage(s"$playerFaction", LocalAction.SendPacket(OrbitalStrikeWaypointMessage(player.GUID, None))) + context.system.scheduler.scheduleOnce(delay = 5 seconds) { + val sectorTargets = Zone.findOrbitalStrikeTargets(player.Zone, orbitalStrikePos.get, osSize.DamageRadius, Zone.getOrbitbalStrikeTargets) + val withinRange = sectorTargets.filter {target => Zone.orbitalStrikeDistanceCheck(orbitalStrikePos.get, target.Position, osSize.DamageRadius)} + withinRange.foreach { target => + target.Actor ! Vitality.Damage(DamageInteraction(SourceEntry(target), OrbitalStrike(PlayerSource(player)), target.Position).calculate()) + } + orbitalStrikePos = None + } + } + case _ => () + } + } + def handleChangeAmmo(pkt: ChangeAmmoMessage): Unit = { val ChangeAmmoMessage(item_guid, _) = pkt val (thing, equipment) = sessionLogic.findContainedEquipment() 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 a60ac1edc..9e62d70ee 100644 --- a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala @@ -2613,7 +2613,6 @@ class ZoningOperations( sendResponse(ObjectCreateDetailedMessage(ObjectClass.avatar, guid, data)) log.debug(s"AvatarRejoin: ${player.Name} - $guid -> $data") } - avatarActor ! AvatarActor.RefreshPurchaseTimes() setupAvatarFunc = AvatarCreate //begin looking for conditions to set the avatar context.system.scheduler.scheduleOnce(delay = 750 millisecond, context.self, SessionActor.SetCurrentAvatar(player, 200)) @@ -3276,6 +3275,7 @@ class ZoningOperations( ZoningOperations.reportProgressionSystem(context.self) } } + avatarActor ! AvatarActor.RefreshPurchaseTimes() } /** diff --git a/src/main/scala/net/psforever/objects/OrbitalStrike.scala b/src/main/scala/net/psforever/objects/OrbitalStrike.scala new file mode 100644 index 000000000..33634d852 --- /dev/null +++ b/src/main/scala/net/psforever/objects/OrbitalStrike.scala @@ -0,0 +1,61 @@ +// Copyright (c) 2025 PSForever +package net.psforever.objects + +import net.psforever.objects.sourcing.{PlayerSource, SourceEntry} +import net.psforever.objects.vital.{NoResistanceSelection, SimpleResolutions} +import net.psforever.objects.vital.base.{DamageReason, DamageResolution, DamageType} +import net.psforever.objects.vital.damage.DamageCalculations +import net.psforever.objects.vital.prop.{DamageProperties, DamageWithPosition} +import net.psforever.objects.vital.resolution.{DamageAndResistance, DamageResistanceModel} + +final case class OrbitalStrike(player: PlayerSource) + extends DamageReason { + def resolution: DamageResolution.Value = DamageResolution.Hit + + def same(test: DamageReason): Boolean = { + test.source eq source + } + + def source: DamageProperties = OrbitalStrike.source + + def damageModel: DamageAndResistance = OrbitalStrike.drm + + override def adversary : Option[SourceEntry] = Some(player) + +} + +object OrbitalStrike { + + final val cr4_os = new DamageWithPosition { + CausesDamageType = DamageType.Splash + SympatheticExplosion = true + Damage0 = 10000 + DamageAtEdge = 0.1f + DamageRadius = 10f + } + + final val cr5_os = new DamageWithPosition { + CausesDamageType = DamageType.Splash + SympatheticExplosion = true + Damage0 = 10000 + DamageAtEdge = 0.1f + DamageRadius = 20f + } + + private val source = new DamageProperties { + Damage0 = 10000 + Damage1 = 10000 + Damage2 = 10000 + Damage3 = 10000 + Damage4 = 10000 + DamageToHealthOnly = true + DamageToVehicleOnly = true + DamageToBattleframeOnly = true + } + + private val drm = new DamageResistanceModel { + DamageUsing = DamageCalculations.AgainstExoSuit + ResistUsing = NoResistanceSelection + Model = SimpleResolutions.calculate + } +} diff --git a/src/main/scala/net/psforever/objects/SpecialEmp.scala b/src/main/scala/net/psforever/objects/SpecialEmp.scala index 5578690af..b58a0e377 100644 --- a/src/main/scala/net/psforever/objects/SpecialEmp.scala +++ b/src/main/scala/net/psforever/objects/SpecialEmp.scala @@ -65,6 +65,108 @@ object SpecialEmp { innateDamage = emp } + final val cr3_emp = new DamageWithPosition { + CausesDamageType = DamageType.Splash + SympatheticExplosion = true + Damage0 = 0 + DamageAtEdge = 1.0f + DamageRadius = 10f + AdditionalEffect = true + JammedEffectDuration += TargetValidation( + EffectTarget.Category.Player, + EffectTarget.Validation.Player + ) -> 1000 + JammedEffectDuration += TargetValidation( + EffectTarget.Category.Vehicle, + EffectTarget.Validation.AMS + ) -> 5000 + JammedEffectDuration += TargetValidation( + EffectTarget.Category.Deployable, + EffectTarget.Validation.MotionSensor + ) -> 30000 + JammedEffectDuration += TargetValidation( + EffectTarget.Category.Deployable, + EffectTarget.Validation.Spitfire + ) -> 30000 + JammedEffectDuration += TargetValidation( + EffectTarget.Category.Turret, + EffectTarget.Validation.Turret + ) -> 30000 + JammedEffectDuration += TargetValidation( + EffectTarget.Category.Vehicle, + EffectTarget.Validation.VehicleNotAMS + ) -> 10000 + Modifiers = MaxDistanceCutoff + } + + final val cr4_emp = new DamageWithPosition { + CausesDamageType = DamageType.Splash + SympatheticExplosion = true + Damage0 = 0 + DamageAtEdge = 1.0f + DamageRadius = 15f + AdditionalEffect = true + JammedEffectDuration += TargetValidation( + EffectTarget.Category.Player, + EffectTarget.Validation.Player + ) -> 1000 + JammedEffectDuration += TargetValidation( + EffectTarget.Category.Vehicle, + EffectTarget.Validation.AMS + ) -> 5000 + JammedEffectDuration += TargetValidation( + EffectTarget.Category.Deployable, + EffectTarget.Validation.MotionSensor + ) -> 30000 + JammedEffectDuration += TargetValidation( + EffectTarget.Category.Deployable, + EffectTarget.Validation.Spitfire + ) -> 30000 + JammedEffectDuration += TargetValidation( + EffectTarget.Category.Turret, + EffectTarget.Validation.Turret + ) -> 30000 + JammedEffectDuration += TargetValidation( + EffectTarget.Category.Vehicle, + EffectTarget.Validation.VehicleNotAMS + ) -> 10000 + Modifiers = MaxDistanceCutoff + } + + final val cr5_emp = new DamageWithPosition { + CausesDamageType = DamageType.Splash + SympatheticExplosion = true + Damage0 = 0 + DamageAtEdge = 1.0f + DamageRadius = 20f + AdditionalEffect = true + JammedEffectDuration += TargetValidation( + EffectTarget.Category.Player, + EffectTarget.Validation.Player + ) -> 1000 + JammedEffectDuration += TargetValidation( + EffectTarget.Category.Vehicle, + EffectTarget.Validation.AMS + ) -> 5000 + JammedEffectDuration += TargetValidation( + EffectTarget.Category.Deployable, + EffectTarget.Validation.MotionSensor + ) -> 30000 + JammedEffectDuration += TargetValidation( + EffectTarget.Category.Deployable, + EffectTarget.Validation.Spitfire + ) -> 30000 + JammedEffectDuration += TargetValidation( + EffectTarget.Category.Turret, + EffectTarget.Validation.Turret + ) -> 30000 + JammedEffectDuration += TargetValidation( + EffectTarget.Category.Vehicle, + EffectTarget.Validation.VehicleNotAMS + ) -> 10000 + Modifiers = MaxDistanceCutoff + } + /** * Trigger an electromagnetic pulse. */ diff --git a/src/main/scala/net/psforever/objects/avatar/Avatar.scala b/src/main/scala/net/psforever/objects/avatar/Avatar.scala index 4e349ec55..8f0a41a16 100644 --- a/src/main/scala/net/psforever/objects/avatar/Avatar.scala +++ b/src/main/scala/net/psforever/objects/avatar/Avatar.scala @@ -68,6 +68,13 @@ object Avatar { GlobalDefinitions.trhev_pounder -> 5.minutes ) + val cudCooldowns: Map[String, FiniteDuration] = Map( + "orbital_strike" -> 1.minutes, // 3.hours + "emp_blast" -> 1.minutes, // 20.minutes + "reveal_friendlies" -> 1.minutes, // 20.minutes + "reveal_enemies" -> 1.minutes // 20.minutes + ) + val useCooldowns: Map[BasicDefinition, FiniteDuration] = Map( GlobalDefinitions.medkit -> 5.seconds, GlobalDefinitions.super_armorkit -> 20.minutes, diff --git a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala index 375f4cec5..4e151f973 100644 --- a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala +++ b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala @@ -36,6 +36,7 @@ import net.psforever.objects.vital.collision.CollisionReason import net.psforever.objects.vital.etc.{PainboxReason, SuicideReason} import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult} import net.psforever.packet.PlanetSideGamePacket +import org.joda.time.{LocalDateTime, Seconds} import scala.concurrent.duration._ @@ -359,6 +360,11 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm equipment match { case Some(holsteredEquipment) => log.info(s"${player.Name} has put ${player.Sex.possessive} ${holsteredEquipment.Definition.Name} down") + //make sure the player didn't just initialte an orbital strike. If not (the if below is true), make sure waypoint is removed + if (holsteredEquipment.Definition == GlobalDefinitions.command_detonater && player.avatar.cr.value > 3 && + !player.avatar.cooldowns.purchase.exists(os => os._1 == "orbital_strike" && Seconds.secondsBetween(os._2, LocalDateTime.now()).getSeconds < 10)) { + player.Zone.LocalEvents ! LocalServiceMessage(s"${player.Faction}", LocalAction.SendPacket(OrbitalStrikeWaypointMessage(player.GUID, None))) + } case None => log.info(s"${player.Name} lowers ${player.Sex.possessive} hand") } diff --git a/src/main/scala/net/psforever/objects/zones/Zone.scala b/src/main/scala/net/psforever/objects/zones/Zone.scala index 45eb69a12..cbf828d07 100644 --- a/src/main/scala/net/psforever/objects/zones/Zone.scala +++ b/src/main/scala/net/psforever/objects/zones/Zone.scala @@ -1845,6 +1845,53 @@ object Zone { playerTargets ++ vehicleTargets ++ deployableTargets ++ soiTargets } + /** + * na + * @see `DamageWithPosition` + * @see `Zone.blockMap.sector` + * @param zone the zone in which the explosion should occur + * @param sourcePosition a position that is used as the origin of the explosion + * @param radius idistance + * @param getTargetsFromSector get this list of entities from a sector + * @return a list of affected entities + */ + def findOrbitalStrikeTargets( + zone: Zone, + sourcePosition: Vector3, + radius: Float, + getTargetsFromSector: SectorPopulation => List[PlanetSideServerObject with Vitality] + ): List[PlanetSideServerObject with Vitality] = { + getTargetsFromSector(zone.blockMap.sector(sourcePosition.xy, radius)) + } + + def getOrbitbalStrikeTargets(sector: SectorPopulation): List[PlanetSideServerObject with Vitality] = { + //collect all targets that can be damaged + //players + val playerTargets = sector.livePlayerList.filter { player => player.VehicleSeated.isEmpty && player.WhichSide == Sidedness.OutsideOf } + //vehicles + val vehicleTargets = sector.vehicleList.filterNot { _.Destroyed } + //deployables + val deployableTargets = sector.deployableList.filter { obj => !obj.Destroyed && obj.WhichSide == Sidedness.OutsideOf } + //amenities + val soiTargets = sector.amenityList.collect { + case amenity: Vitality if !amenity.Destroyed && amenity.WhichSide == Sidedness.OutsideOf && amenity.CanDamage => amenity } + //altogether ... + playerTargets ++ vehicleTargets ++ deployableTargets ++ soiTargets + } + + /** + * Check if targets returned from sector are within range of the imminent Orbital Strike + * @param p1 OS position + * @param p2 target position + * @param maxDistance radius of the Orbital Strike + * @return `true`, if the two entities are near enough to each other; + * `false`, otherwise + */ + def orbitalStrikeDistanceCheck(p1: Vector3, p2: Vector3, maxDistance: Float): Boolean = { + val radius = maxDistance * maxDistance + Vector3.DistanceSquared(p1.xy, p2.xy) <= radius + } + /** * na * @param instigation what previous event happened, if any, that caused this explosion diff --git a/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala b/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala index aaf908d14..87f94fb5f 100644 --- a/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala +++ b/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala @@ -162,6 +162,10 @@ import scodec.codecs._ * * `55 - "Someone is attempting to Heal you". Value is 1`
* `56 - "Someone is attempting to Repair you". Value is 1`
+ * `57 - CUD Reveal Friendlies cooldown timer`
+ * `58 - CUD Reveal Enemies cooldown timer`
+ * `59 - CUD EMP Blast cooldown timer`
+ * `60 - CUD Orbital Strike cooldown timer`
* `64 - ????? related to using router telepads` * `67 - Enables base shields (from cavern module/lock)`
* `73 - "You are locked into the Core Beam. Charging your Module now.". Value is 1 to active`
diff --git a/src/main/scala/net/psforever/packet/game/UplinkRequest.scala b/src/main/scala/net/psforever/packet/game/UplinkRequest.scala index f4a66e591..2d4197269 100644 --- a/src/main/scala/net/psforever/packet/game/UplinkRequest.scala +++ b/src/main/scala/net/psforever/packet/game/UplinkRequest.scala @@ -26,15 +26,15 @@ object UplinkRequestType extends IntEnum[UplinkRequestType] { case object OrbitalStrike extends UplinkRequestType(value = 4) - case object Unknown5 extends UplinkRequestType(value = 5) + case object Unknown5 extends UplinkRequestType(value = 5) // pull trigger to start orbital strike countdown case object Function6 extends UplinkRequestType(value = 6) - case object Function7 extends UplinkRequestType(value = 7) + case object Function7 extends UplinkRequestType(value = 7) // sent back by client after reveal enemies response case object Function8 extends UplinkRequestType(value = 8) - case object Unknown9 extends UplinkRequestType(value = 9) + case object Unknown9 extends UplinkRequestType(value = 9) // recall squad to sanc case object UnknownA extends UplinkRequestType(value = 10) From c9b13394e9fdd8f980e476edbd1237759633caf1 Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Thu, 17 Jul 2025 20:19:49 -0400 Subject: [PATCH 44/57] just one os please --- .../support/WeaponAndProjectileOperations.scala | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) 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 eebb56466..7dba959fc 100644 --- a/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala @@ -94,6 +94,7 @@ class WeaponAndProjectileOperations( var shooting: mutable.Set[PlanetSideGUID] = mutable.Set.empty //ChangeFireStateMessage_Start var prefire: mutable.Set[PlanetSideGUID] = mutable.Set.empty //if WeaponFireMessage precedes ChangeFireStateMessage_Start private[session] var orbitalStrikePos: Option[Vector3] = None + private[session] var orbitalStrikeInProgress: Boolean = false private[session] var shootingStart: mutable.HashMap[PlanetSideGUID, Long] = mutable.HashMap[PlanetSideGUID, Long]() private[session] var shootingStop: mutable.HashMap[PlanetSideGUID, Long] = mutable.HashMap[PlanetSideGUID, Long]() private[session] val shotsFired: mutable.HashMap[Int,Int] = mutable.HashMap[Int,Int]() @@ -335,6 +336,8 @@ class WeaponAndProjectileOperations( sendResponse(UplinkResponse(code.value, 0)) orbitalStrikePos = pos case UplinkRequestType.Unknown5 => + if (!orbitalStrikeInProgress) { + orbitalStrikeInProgress = true val cr = player.avatar.cr.value val strikeType = playerFaction match { case PlanetSideEmpire.NC => @@ -359,13 +362,18 @@ class WeaponAndProjectileOperations( player.Zone.LocalEvents ! LocalServiceMessage(s"$playerFaction", LocalAction.SendPacket(OrbitalStrikeWaypointMessage(player.GUID, None))) context.system.scheduler.scheduleOnce(delay = 5 seconds) { val sectorTargets = Zone.findOrbitalStrikeTargets(player.Zone, orbitalStrikePos.get, osSize.DamageRadius, Zone.getOrbitbalStrikeTargets) - val withinRange = sectorTargets.filter {target => Zone.orbitalStrikeDistanceCheck(orbitalStrikePos.get, target.Position, osSize.DamageRadius)} + val withinRange = sectorTargets.filter { target => Zone.orbitalStrikeDistanceCheck(orbitalStrikePos.get, target.Position, osSize.DamageRadius) } withinRange.foreach { target => target.Actor ! Vitality.Damage(DamageInteraction(SourceEntry(target), OrbitalStrike(PlayerSource(player)), target.Position).calculate()) } orbitalStrikePos = None + orbitalStrikeInProgress = false + } } } + else { + sendResponse(UplinkResponse(code.value, 0)) + } case _ => () } } From 1b6e50cf0d06ecdbf07b003ccdbba7ec7bb4cfa0 Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Fri, 18 Jul 2025 21:29:42 -0400 Subject: [PATCH 45/57] we can reveal --- .../WeaponAndProjectileOperations.scala | 50 +++++++++++++------ .../net/psforever/objects/avatar/Avatar.scala | 8 +-- .../objects/definition/BasicDefinition.scala | 8 +++ .../global/GlobalDefinitionsVehicle.scala | 39 +++++++++++++++ 4 files changed, 87 insertions(+), 18 deletions(-) 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 7dba959fc..24ba6f335 100644 --- a/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala @@ -302,18 +302,40 @@ class WeaponAndProjectileOperations( val UplinkRequest(code, pos, _) = pkt val playerFaction = player.Faction code match { - case UplinkRequestType.RevealFriendlies => () - /*sendResponse(UplinkResponse(code.value, 0)) - sendResponse(PlanetsideAttributeMessage(player.GUID, 57, 10000)) //1200000 - avatarActor ! AvatarActor.UpdateCUDTime("reveal_friendlies")*/ - case UplinkRequestType.RevealEnemies => () - /*sendResponse(UplinkResponse(code.value, 0)) - sendResponse(PlanetsideAttributeMessage(player.GUID, 58, 10000)) //1200000 - avatarActor ! AvatarActor.UpdateCUDTime("reveal_enemies") - These are seen in a few logs, but didn't work. Unclear what these do or what '10' in Event1 represents - sendResponse(UplinkPositionEvent(5, Event0(5))) - sendResponse(UplinkPositionEvent(4, Event1(4, 10))) - sendResponse(UplinkPositionEvent(6, Event0(6)))*/ + case UplinkRequestType.RevealFriendlies => + val revealZone = player.Zone.Number + sendResponse(UplinkResponse(code.value, 0)) + sendResponse(PlanetsideAttributeMessage(player.GUID, 57, 1200000)) + avatarActor ! AvatarActor.UpdateCUDTime("reveal_friendlies") + sendResponse(UplinkPositionEvent(5, Event0(5))) + sendResponse(UplinkPositionEvent(4, Event1(4, revealZone))) + sendResponse(UplinkPositionEvent(6, Event0(6))) + val friendlies = player.Zone.LivePlayers.filter { friend => friend.Faction == player.Faction } + val friendlyVehicles = player.Zone.Vehicles.filter { vehicle => vehicle.Faction == player.Faction && !vehicle.Destroyed } + friendlies.foreach { f => + sendResponse(UplinkPositionEvent(0, Event2(0, Vector3(f.Position.x, f.Position.y, 0.0f), 255, revealZone, 0, 1118938442, 300000, 299080, Some(true)))) + } + friendlyVehicles.foreach { v => + sendResponse(UplinkPositionEvent(0, Event2(0, Vector3(v.Position.x, v.Position.y, 0.0f), v.Definition.MapRevealId, revealZone, 0, 1118938442, 300000, 299080, Some(true)))) + } + case UplinkRequestType.RevealEnemies => + val revealZone = player.Zone.Number + sendResponse(UplinkResponse(code.value, 0)) + sendResponse(PlanetsideAttributeMessage(player.GUID, 58, 1200000)) + avatarActor ! AvatarActor.UpdateCUDTime("reveal_enemies") + sendResponse(UplinkPositionEvent(5, Event0(5))) + sendResponse(UplinkPositionEvent(4, Event1(4, revealZone))) + sendResponse(UplinkPositionEvent(6, Event0(6))) + val enemies = player.Zone.LivePlayers.filter { enemy => enemy.Faction != player.Faction && + Zone.orbitalStrikeDistanceCheck(player.Position, enemy.Position, 200f)} //reusing distance check + val enemyVehicles = player.Zone.Vehicles.filter { vehicle => vehicle.Faction != player.Faction && !vehicle.Destroyed && + Zone.orbitalStrikeDistanceCheck(player.Position, vehicle.Position, 200f)} //reusing distance check + enemies.foreach { e => + sendResponse(UplinkPositionEvent(1, Event2(1, Vector3(e.Position.x, e.Position.y, 0.0f), 255, revealZone, 0, 1118938442, 300000, 299080, Some(false)))) + } + enemyVehicles.foreach { v => + sendResponse(UplinkPositionEvent(1, Event2(1, Vector3(v.Position.x, v.Position.y, 0.0f), v.Definition.MapRevealId, revealZone, 0, 1118938442, 300000, 299080, Some(false)))) + } case UplinkRequestType.ElectroMagneticPulse => val cr = player.avatar.cr.value val empSize = cr match { @@ -323,7 +345,7 @@ class WeaponAndProjectileOperations( } val empColor = if (playerFaction != PlanetSideEmpire.NEUTRAL) { s"explosion_emp_${playerFaction.toString.toLowerCase}" } else { "explosion_emp_bo" } sendResponse(UplinkResponse(code.value, 0)) - sendResponse(PlanetsideAttributeMessage(player.GUID, 59, 10000)) //1200000 + sendResponse(PlanetsideAttributeMessage(player.GUID, 59, 1200000)) avatarActor ! AvatarActor.UpdateCUDTime("emp_blast") player.Zone.LocalEvents ! LocalServiceMessage(s"${player.Zone.id}", LocalAction.SendPacket(TriggerEffectMessage(ValidPlanetSideGUID(0), empColor, None, Some(TriggeredEffectLocation(player.Position, Vector3(0, 0, 90)))))) @@ -354,7 +376,7 @@ class WeaponAndProjectileOperations( case 5 => cr5_os } sendResponse(UplinkResponse(code.value, 0)) - sendResponse(PlanetsideAttributeMessage(player.GUID, 60, 10000)) //10800000 + sendResponse(PlanetsideAttributeMessage(player.GUID, 60, 10800000)) avatarActor ! AvatarActor.UpdateCUDTime("orbital_strike") context.system.scheduler.scheduleOnce(delay = 5 seconds) { player.Zone.LocalEvents ! LocalServiceMessage(s"${player.Zone.id}", diff --git a/src/main/scala/net/psforever/objects/avatar/Avatar.scala b/src/main/scala/net/psforever/objects/avatar/Avatar.scala index 8f0a41a16..633943e26 100644 --- a/src/main/scala/net/psforever/objects/avatar/Avatar.scala +++ b/src/main/scala/net/psforever/objects/avatar/Avatar.scala @@ -69,10 +69,10 @@ object Avatar { ) val cudCooldowns: Map[String, FiniteDuration] = Map( - "orbital_strike" -> 1.minutes, // 3.hours - "emp_blast" -> 1.minutes, // 20.minutes - "reveal_friendlies" -> 1.minutes, // 20.minutes - "reveal_enemies" -> 1.minutes // 20.minutes + "orbital_strike" -> 3.hours, + "emp_blast" -> 20.minutes, + "reveal_friendlies" -> 20.minutes, + "reveal_enemies" -> 20.minutes ) val useCooldowns: Map[BasicDefinition, FiniteDuration] = Map( diff --git a/src/main/scala/net/psforever/objects/definition/BasicDefinition.scala b/src/main/scala/net/psforever/objects/definition/BasicDefinition.scala index 092d763a6..9649420f2 100644 --- a/src/main/scala/net/psforever/objects/definition/BasicDefinition.scala +++ b/src/main/scala/net/psforever/objects/definition/BasicDefinition.scala @@ -4,6 +4,7 @@ package net.psforever.objects.definition abstract class BasicDefinition { private var name: String = "definition" private var descriptor: Option[String] = None + private var mapRevealId: Int = 0 def Name: String = name @@ -20,4 +21,11 @@ abstract class BasicDefinition { descriptor = description Descriptor } + + def MapRevealId: Int = mapRevealId //for vehicle IDs used by reveal friendlies and enemies with a CUD + + def MapRevealId_=(mapRevealId: Int): Int = { + this.mapRevealId = mapRevealId + MapRevealId + } } diff --git a/src/main/scala/net/psforever/objects/global/GlobalDefinitionsVehicle.scala b/src/main/scala/net/psforever/objects/global/GlobalDefinitionsVehicle.scala index 5e7ae6dad..c2fee38f6 100644 --- a/src/main/scala/net/psforever/objects/global/GlobalDefinitionsVehicle.scala +++ b/src/main/scala/net/psforever/objects/global/GlobalDefinitionsVehicle.scala @@ -83,6 +83,7 @@ object GlobalDefinitionsVehicle { fury.collision.z = CollisionZData(Array((8f, 1), (24f, 35), (40f, 100), (48f, 175), (52f, 350))) fury.maxForwardSpeed = 90f fury.mass = 32.1f + fury.MapRevealId = 14 quadassault.Name = "quadassault" // Basilisk quadassault.MaxHealth = 650 @@ -119,6 +120,7 @@ object GlobalDefinitionsVehicle { quadassault.collision.z = CollisionZData(Array((8f, 1), (24f, 35), (40f, 100), (48f, 175), (52f, 350))) quadassault.maxForwardSpeed = 90f quadassault.mass = 32.1f + quadassault.MapRevealId = 30 quadstealth.Name = "quadstealth" // Wraith quadstealth.MaxHealth = 650 @@ -155,6 +157,7 @@ object GlobalDefinitionsVehicle { quadstealth.collision.z = CollisionZData(Array((8f, 1), (24f, 35), (40f, 100), (48f, 175), (52f, 350))) quadstealth.maxForwardSpeed = 90f quadstealth.mass = 32.1f + quadstealth.MapRevealId = 15 two_man_assault_buggy.Name = "two_man_assault_buggy" // Harasser two_man_assault_buggy.MaxHealth = 1250 @@ -193,6 +196,7 @@ object GlobalDefinitionsVehicle { two_man_assault_buggy.collision.z = CollisionZData(Array((7f, 1), (21f, 50), (35f, 150), (42f, 300), (45.5f, 600))) two_man_assault_buggy.maxForwardSpeed = 85f two_man_assault_buggy.mass = 52.4f + two_man_assault_buggy.MapRevealId = 1 skyguard.Name = "skyguard" skyguard.MaxHealth = 1000 @@ -232,6 +236,7 @@ object GlobalDefinitionsVehicle { skyguard.collision.z = CollisionZData(Array((7f, 1), (21f, 50), (35f, 150), (42f, 300), (45.4f, 600))) skyguard.maxForwardSpeed = 90f skyguard.mass = 78.9f + skyguard.MapRevealId = 16 threemanheavybuggy.Name = "threemanheavybuggy" // Marauder threemanheavybuggy.MaxHealth = 1700 @@ -274,6 +279,7 @@ object GlobalDefinitionsVehicle { threemanheavybuggy.collision.z = CollisionZData(Array((6f, 1), (18f, 50), (30f, 150), (36f, 300), (39f, 900))) threemanheavybuggy.maxForwardSpeed = 80f threemanheavybuggy.mass = 96.3f + threemanheavybuggy.MapRevealId = 18 twomanheavybuggy.Name = "twomanheavybuggy" // Enforcer twomanheavybuggy.MaxHealth = 1800 @@ -313,6 +319,7 @@ object GlobalDefinitionsVehicle { twomanheavybuggy.collision.z = CollisionZData(Array((6f, 1), (18f, 50), (30f, 150), (36f, 300), (39f, 900))) twomanheavybuggy.maxForwardSpeed = 80f twomanheavybuggy.mass = 83.2f + twomanhoverbuggy.MapRevealId = 20 twomanhoverbuggy.Name = "twomanhoverbuggy" // Thresher twomanhoverbuggy.MaxHealth = 1600 @@ -355,6 +362,7 @@ object GlobalDefinitionsVehicle { twomanhoverbuggy.collision.z = CollisionZData(Array((6f, 1), (18f, 50), (30f, 150), (36f, 300), (39f, 900))) twomanhoverbuggy.maxForwardSpeed = 85f twomanhoverbuggy.mass = 55.5f + twomanhoverbuggy.MapRevealId = 21 mediumtransport.Name = "mediumtransport" // Deliverer mediumtransport.MaxHealth = 2500 @@ -401,6 +409,7 @@ object GlobalDefinitionsVehicle { mediumtransport.collision.z = CollisionZData(Array((5f, 1), (15f, 50), (25f, 200), (30f, 750), (32.5f, 2000))) mediumtransport.maxForwardSpeed = 70f mediumtransport.mass = 108.5f + mediumtransport.MapRevealId = 10 battlewagon.Name = "battlewagon" // Raider battlewagon.MaxHealth = 2500 @@ -450,6 +459,7 @@ object GlobalDefinitionsVehicle { battlewagon.collision.z = CollisionZData(Array((5f, 1), (15f, 50), (25f, 200), (30f, 750), (32.5f, 2000))) //inherited from mediumtransport battlewagon.maxForwardSpeed = 65f battlewagon.mass = 108.5f + battlewagon.MapRevealId = 10 thunderer.Name = "thunderer" thunderer.MaxHealth = 2500 @@ -496,6 +506,7 @@ object GlobalDefinitionsVehicle { thunderer.collision.z = CollisionZData(Array((5f, 1), (15f, 50), (25f, 200), (30f, 750), (32.5f, 2000))) thunderer.maxForwardSpeed = 65f thunderer.mass = 108.5f + thunderer.MapRevealId = 29 aurora.Name = "aurora" aurora.MaxHealth = 2500 @@ -542,6 +553,7 @@ object GlobalDefinitionsVehicle { aurora.collision.z = CollisionZData(Array((5f, 1), (15f, 50), (25f, 200), (30f, 750), (32.5f, 2000))) aurora.maxForwardSpeed = 65f aurora.mass = 108.5f + aurora.MapRevealId = 28 apc_tr.Name = "apc_tr" // Juggernaut apc_tr.MaxHealth = 6000 @@ -612,6 +624,7 @@ object GlobalDefinitionsVehicle { apc_tr.collision.z = CollisionZData(Array((2f, 1), (6f, 50), (10f, 300), (12f, 1000), (13f, 3000))) apc_tr.maxForwardSpeed = 60f apc_tr.mass = 128.4f + apc_tr.MapRevealId = 32 apc_nc.Name = "apc_nc" // Vindicator apc_nc.MaxHealth = 6000 @@ -682,6 +695,7 @@ object GlobalDefinitionsVehicle { apc_nc.collision.z = CollisionZData(Array((2f, 1), (6f, 50), (10f, 300), (12f, 1000), (13f, 3000))) apc_nc.maxForwardSpeed = 60f apc_nc.mass = 128.4f + apc_nc.MapRevealId = 33 apc_vs.Name = "apc_vs" // Leviathan apc_vs.MaxHealth = 6000 @@ -752,6 +766,7 @@ object GlobalDefinitionsVehicle { apc_vs.collision.z = CollisionZData(Array((2f, 1), (6f, 50), (10f, 300), (12f, 1000), (13f, 3000))) apc_vs.maxForwardSpeed = 60f apc_vs.mass = 128.4f + apc_vs.MapRevealId = 41 lightning.Name = "lightning" lightning.MaxHealth = 2000 @@ -790,6 +805,7 @@ object GlobalDefinitionsVehicle { lightning.collision.z = CollisionZData(Array((6f, 1), (18f, 50), (30f, 150), (36f, 300), (39f, 750))) lightning.maxForwardSpeed = 74f lightning.mass = 100.2f + lightning.MapRevealId = 7 prowler.Name = "prowler" prowler.MaxHealth = 4800 @@ -833,6 +849,7 @@ object GlobalDefinitionsVehicle { prowler.collision.z = CollisionZData(Array((5f, 1), (15f, 50), (25f, 250), (30f, 600), (32.5f, 1500))) prowler.maxForwardSpeed = 57f prowler.mass = 510.5f + prowler.MapRevealId = 13 vanguard.Name = "vanguard" vanguard.MaxHealth = 5400 @@ -872,6 +889,7 @@ object GlobalDefinitionsVehicle { vanguard.collision.z = CollisionZData(Array((5f, 1), (15f, 50), (25f, 100), (30f, 250), (32.5f, 600))) vanguard.maxForwardSpeed = 60f vanguard.mass = 460.4f + vanguard.MapRevealId = 22 magrider.Name = "magrider" magrider.MaxHealth = 4200 @@ -913,6 +931,7 @@ object GlobalDefinitionsVehicle { magrider.collision.z = CollisionZData(Array((5f, 1), (15f, 50), (25f, 250), (30f, 600), (32.5f, 1500))) magrider.maxForwardSpeed = 65f magrider.mass = 75.3f + magrider.MapRevealId = 8 val utilityConverter = new UtilityVehicleConverter ant.Name = "ant" @@ -952,6 +971,7 @@ object GlobalDefinitionsVehicle { ant.collision.z = CollisionZData(Array((2f, 1), (6f, 50), (10f, 250), (12f, 500), (13f, 750))) ant.maxForwardSpeed = 65f ant.mass = 80.5f + ant.MapRevealId = 1 ams.Name = "ams" ams.MaxHealth = 3000 @@ -995,6 +1015,7 @@ object GlobalDefinitionsVehicle { ams.collision.z = CollisionZData(Array((2f, 1), (6f, 50), (10f, 250), (12f, 805), (13f, 3000))) ams.maxForwardSpeed = 70f ams.mass = 136.8f + ams.MapRevealId = 2 val variantConverter = new VariantVehicleConverter router.Name = "router" @@ -1039,6 +1060,7 @@ object GlobalDefinitionsVehicle { router.collision.z = CollisionZData(Array((6f, 1), (18f, 50), (30f, 150), (36f, 350), (39f, 900))) router.maxForwardSpeed = 60f router.mass = 60f + router.MapRevealId = 25 switchblade.Name = "switchblade" switchblade.MaxHealth = 1750 @@ -1085,6 +1107,7 @@ object GlobalDefinitionsVehicle { switchblade.collision.z = CollisionZData(Array((6f, 1), (18f, 50), (30f, 150), (36f, 350), (39f, 800))) switchblade.maxForwardSpeed = 80f switchblade.mass = 63.9f + switchblade.MapRevealId = 26 flail.Name = "flail" flail.MaxHealth = 2400 @@ -1127,6 +1150,7 @@ object GlobalDefinitionsVehicle { flail.collision.z = CollisionZData(Array((6f, 1), (18f, 50), (30f, 150), (36f, 350), (39f, 900))) flail.maxForwardSpeed = 55f flail.mass = 73.5f + flail.MapRevealId = 24 } /** @@ -1179,6 +1203,7 @@ object GlobalDefinitionsVehicle { mosquito.collision.z = CollisionZData(Array((3f, 1), (9f, 25), (15f, 50), (18f, 75), (19.5f, 100))) mosquito.maxForwardSpeed = 120f mosquito.mass = 53.6f + mosquito.MapRevealId = 11 lightgunship.Name = "lightgunship" // Reaver lightgunship.MaxHealth = 855 // Temporary - Correct Reaver Health from pre-"Coder Madness 2" Event @@ -1219,6 +1244,7 @@ object GlobalDefinitionsVehicle { lightgunship.collision.z = CollisionZData(Array((3f, 1), (9f, 30), (15f, 60), (18f, 90), (19.5f, 125))) lightgunship.maxForwardSpeed = 104f lightgunship.mass = 51.1f + lightgunship.MapRevealId = 6 wasp.Name = "wasp" wasp.MaxHealth = 515 @@ -1257,6 +1283,7 @@ object GlobalDefinitionsVehicle { wasp.collision.z = CollisionZData(Array((3f, 1), (9f, 25), (15f, 50), (18f, 75), (19.5f, 100))) //mosquito numbers wasp.maxForwardSpeed = 120f wasp.mass = 53.6f + wasp.MapRevealId = 11 liberator.Name = "liberator" liberator.MaxHealth = 2500 @@ -1305,6 +1332,7 @@ object GlobalDefinitionsVehicle { liberator.collision.z = CollisionZData(Array((3f, 1), (9f, 30), (15f, 60), (18f, 90), (19.5f, 125))) liberator.maxForwardSpeed = 90f liberator.mass = 82f + liberator.MapRevealId = 5 vulture.Name = "vulture" vulture.MaxHealth = 2500 @@ -1354,6 +1382,7 @@ object GlobalDefinitionsVehicle { vulture.collision.z = CollisionZData(Array((3f, 1), (9f, 30), (15f, 60), (18f, 90), (19.5f, 125))) vulture.maxForwardSpeed = 97f vulture.mass = 82f + vulture.MapRevealId = 5 dropship.Name = "dropship" // Galaxy dropship.MaxHealth = 5000 @@ -1430,6 +1459,7 @@ object GlobalDefinitionsVehicle { dropship.collision.z = CollisionZData(Array((3f, 5), (9f, 125), (15f, 250), (18f, 500), (19.5f, 1000))) dropship.maxForwardSpeed = 80f dropship.mass = 133f + dropship.MapRevealId = 4 galaxy_gunship.Name = "galaxy_gunship" galaxy_gunship.MaxHealth = 6000 @@ -1490,6 +1520,7 @@ object GlobalDefinitionsVehicle { galaxy_gunship.collision.z = CollisionZData(Array((3f, 5), (9f, 125), (15f, 250), (18f, 500), (19.5f, 1000))) galaxy_gunship.maxForwardSpeed = 85f galaxy_gunship.mass = 133f + galaxy_gunship.MapRevealId = 31 lodestar.Name = "lodestar" lodestar.MaxHealth = 5000 @@ -1542,6 +1573,7 @@ object GlobalDefinitionsVehicle { lodestar.collision.z = CollisionZData(Array((3f, 5), (9f, 125), (15f, 250), (18f, 500), (19.5f, 1000))) lodestar.maxForwardSpeed = 80f lodestar.mass = 128.2f + lodestar.MapRevealId = 34 phantasm.Name = "phantasm" phantasm.MaxHealth = 2500 @@ -1587,6 +1619,7 @@ object GlobalDefinitionsVehicle { phantasm.collision.z = CollisionZData(Array((3f, 1), (9f, 30), (15f, 60), (18f, 90), (19.5f, 125))) phantasm.maxForwardSpeed = 140f phantasm.mass = 100f + phantasm.MapRevealId = 35 droppod.Name = "droppod" droppod.MaxHealth = 20000 @@ -1724,6 +1757,7 @@ object GlobalDefinitionsVehicle { aphelion_gunner.collision.z = CollisionZData(Array((25f, 2), (40f, 4), (60f, 8), (85f, 16), (115f, 32))) aphelion_gunner.maxForwardSpeed = 17 aphelion_gunner.mass = 615.1f + aphelion_gunner.MapRevealId = 44 colossus_gunner.Name = "colossus_gunner" colossus_gunner.MaxHealth = 4500 @@ -1775,6 +1809,7 @@ object GlobalDefinitionsVehicle { colossus_gunner.collision.z = CollisionZData(Array((25f, 2), (40f, 4), (60f, 8), (85f, 16), (115f, 32))) colossus_gunner.maxForwardSpeed = 17 colossus_gunner.mass = 709.7f + colossus_gunner.MapRevealId = 48 peregrine_gunner.Name = "peregrine_gunner" peregrine_gunner.MaxHealth = 4500 @@ -1826,6 +1861,7 @@ object GlobalDefinitionsVehicle { peregrine_gunner.collision.z = CollisionZData(Array((25f, 2), (40f, 4), (60f, 8), (85f, 16), (115f, 32))) peregrine_gunner.maxForwardSpeed = 17 peregrine_gunner.mass = 713f + peregrine_gunner.MapRevealId = 52 val battleFrameFlightConverter = new BattleFrameFlightConverter aphelion_flight.Name = "aphelion_flight" @@ -1881,6 +1917,7 @@ object GlobalDefinitionsVehicle { aphelion_flight.collision.z = CollisionZData(Array((25f, 2), (40f, 4), (60f, 8), (85f, 16), (115f, 32))) aphelion_flight.maxForwardSpeed = 35 aphelion_flight.mass = 615.1f + aphelion_flight.MapRevealId = 45 colossus_flight.Name = "colossus_flight" colossus_flight.MaxHealth = 3500 @@ -1935,6 +1972,7 @@ object GlobalDefinitionsVehicle { colossus_flight.collision.z = CollisionZData(Array((25f, 2), (40f, 4), (60f, 8), (85f, 16), (115f, 32))) colossus_flight.maxForwardSpeed = 34 colossus_flight.mass = 709.7f + colossus_flight.MapRevealId = 49 peregrine_flight.Name = "peregrine_flight" peregrine_flight.MaxHealth = 3500 @@ -1989,5 +2027,6 @@ object GlobalDefinitionsVehicle { peregrine_flight.collision.z = CollisionZData(Array((25f, 2), (40f, 4), (60f, 8), (85f, 16), (115f, 32))) peregrine_flight.maxForwardSpeed = 35 peregrine_flight.mass = 713f + peregrine_flight.MapRevealId = 51 } } From 3055f73a68fdcb9ccfff5b5a78160ae0c6e76e14 Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Sat, 19 Jul 2025 22:10:07 -0400 Subject: [PATCH 46/57] wrong name --- .../actors/session/support/WeaponAndProjectileOperations.scala | 2 +- src/main/scala/net/psforever/objects/avatar/PlayerControl.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 24ba6f335..cf4c36ad4 100644 --- a/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala @@ -363,7 +363,7 @@ class WeaponAndProjectileOperations( val cr = player.avatar.cr.value val strikeType = playerFaction match { case PlanetSideEmpire.NC => - if (cr == 4) {"explosion_bluedeath_nc"} else {"explosion_bluedeath_nc_lrg"} + if (cr == 4) {"explosion_bluedeath"} else {"explosion_bluedeath_lrg"} case PlanetSideEmpire.TR => if (cr == 4) {"explosion_bluedeath_tr"} else {"explosion_bluedeath_tr_lrg"} case PlanetSideEmpire.VS => diff --git a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala index 4e151f973..34d6b19a0 100644 --- a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala +++ b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala @@ -362,7 +362,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm log.info(s"${player.Name} has put ${player.Sex.possessive} ${holsteredEquipment.Definition.Name} down") //make sure the player didn't just initialte an orbital strike. If not (the if below is true), make sure waypoint is removed if (holsteredEquipment.Definition == GlobalDefinitions.command_detonater && player.avatar.cr.value > 3 && - !player.avatar.cooldowns.purchase.exists(os => os._1 == "orbital_strike" && Seconds.secondsBetween(os._2, LocalDateTime.now()).getSeconds < 10)) { + !player.avatar.cooldowns.purchase.exists(os => os._1 == "orbital_strike" && Seconds.secondsBetween(os._2, LocalDateTime.now()).getSeconds < 12)) { player.Zone.LocalEvents ! LocalServiceMessage(s"${player.Faction}", LocalAction.SendPacket(OrbitalStrikeWaypointMessage(player.GUID, None))) } case None => From 198f4cebb959ac4c882dfed09bc294743c6c6f07 Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Sun, 20 Jul 2025 07:48:13 -0400 Subject: [PATCH 47/57] packet order --- .../session/support/WeaponAndProjectileOperations.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 cf4c36ad4..5be9ab5b6 100644 --- a/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala @@ -309,15 +309,15 @@ class WeaponAndProjectileOperations( avatarActor ! AvatarActor.UpdateCUDTime("reveal_friendlies") sendResponse(UplinkPositionEvent(5, Event0(5))) sendResponse(UplinkPositionEvent(4, Event1(4, revealZone))) - sendResponse(UplinkPositionEvent(6, Event0(6))) val friendlies = player.Zone.LivePlayers.filter { friend => friend.Faction == player.Faction } val friendlyVehicles = player.Zone.Vehicles.filter { vehicle => vehicle.Faction == player.Faction && !vehicle.Destroyed } friendlies.foreach { f => - sendResponse(UplinkPositionEvent(0, Event2(0, Vector3(f.Position.x, f.Position.y, 0.0f), 255, revealZone, 0, 1118938442, 300000, 299080, Some(true)))) + sendResponse(UplinkPositionEvent(0, Event2(0, Vector3(f.Position.x, f.Position.y, 0.0f), 255, revealZone, 0, 1127348721, 300000, 298858, Some(true)))) } friendlyVehicles.foreach { v => - sendResponse(UplinkPositionEvent(0, Event2(0, Vector3(v.Position.x, v.Position.y, 0.0f), v.Definition.MapRevealId, revealZone, 0, 1118938442, 300000, 299080, Some(true)))) + sendResponse(UplinkPositionEvent(0, Event2(0, Vector3(v.Position.x, v.Position.y, 0.0f), v.Definition.MapRevealId, revealZone, 0, 1127348721, 300000, 298858, Some(true)))) } + sendResponse(UplinkPositionEvent(6, Event0(6))) case UplinkRequestType.RevealEnemies => val revealZone = player.Zone.Number sendResponse(UplinkResponse(code.value, 0)) @@ -325,7 +325,6 @@ class WeaponAndProjectileOperations( avatarActor ! AvatarActor.UpdateCUDTime("reveal_enemies") sendResponse(UplinkPositionEvent(5, Event0(5))) sendResponse(UplinkPositionEvent(4, Event1(4, revealZone))) - sendResponse(UplinkPositionEvent(6, Event0(6))) val enemies = player.Zone.LivePlayers.filter { enemy => enemy.Faction != player.Faction && Zone.orbitalStrikeDistanceCheck(player.Position, enemy.Position, 200f)} //reusing distance check val enemyVehicles = player.Zone.Vehicles.filter { vehicle => vehicle.Faction != player.Faction && !vehicle.Destroyed && @@ -336,6 +335,7 @@ class WeaponAndProjectileOperations( enemyVehicles.foreach { v => sendResponse(UplinkPositionEvent(1, Event2(1, Vector3(v.Position.x, v.Position.y, 0.0f), v.Definition.MapRevealId, revealZone, 0, 1118938442, 300000, 299080, Some(false)))) } + sendResponse(UplinkPositionEvent(6, Event0(6))) case UplinkRequestType.ElectroMagneticPulse => val cr = player.avatar.cr.value val empSize = cr match { From 697d5cfba9039758d5a019f0733992ea5a0385d8 Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Mon, 21 Jul 2025 07:51:23 -0400 Subject: [PATCH 48/57] found a friendly --- .../support/WeaponAndProjectileOperations.scala | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 5be9ab5b6..8e18811e3 100644 --- a/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala @@ -308,14 +308,14 @@ class WeaponAndProjectileOperations( sendResponse(PlanetsideAttributeMessage(player.GUID, 57, 1200000)) avatarActor ! AvatarActor.UpdateCUDTime("reveal_friendlies") sendResponse(UplinkPositionEvent(5, Event0(5))) - sendResponse(UplinkPositionEvent(4, Event1(4, revealZone))) + sendResponse(UplinkPositionEvent(3, Event1(3, revealZone))) val friendlies = player.Zone.LivePlayers.filter { friend => friend.Faction == player.Faction } val friendlyVehicles = player.Zone.Vehicles.filter { vehicle => vehicle.Faction == player.Faction && !vehicle.Destroyed } friendlies.foreach { f => - sendResponse(UplinkPositionEvent(0, Event2(0, Vector3(f.Position.x, f.Position.y, 0.0f), 255, revealZone, 0, 1127348721, 300000, 298858, Some(true)))) + sendResponse(UplinkPositionEvent(0, Event2(0, Vector3(f.Position.x, f.Position.y, 0.0f), 255, revealZone, 0, 1117348721, 300000, 299497, Some(true)))) } friendlyVehicles.foreach { v => - sendResponse(UplinkPositionEvent(0, Event2(0, Vector3(v.Position.x, v.Position.y, 0.0f), v.Definition.MapRevealId, revealZone, 0, 1127348721, 300000, 298858, Some(true)))) + sendResponse(UplinkPositionEvent(0, Event2(0, Vector3(v.Position.x, v.Position.y, 0.0f), v.Definition.MapRevealId, revealZone, 0, 1127348721, 300000, 299497, Some(true)))) } sendResponse(UplinkPositionEvent(6, Event0(6))) case UplinkRequestType.RevealEnemies => @@ -330,10 +330,10 @@ class WeaponAndProjectileOperations( val enemyVehicles = player.Zone.Vehicles.filter { vehicle => vehicle.Faction != player.Faction && !vehicle.Destroyed && Zone.orbitalStrikeDistanceCheck(player.Position, vehicle.Position, 200f)} //reusing distance check enemies.foreach { e => - sendResponse(UplinkPositionEvent(1, Event2(1, Vector3(e.Position.x, e.Position.y, 0.0f), 255, revealZone, 0, 1118938442, 300000, 299080, Some(false)))) + sendResponse(UplinkPositionEvent(1, Event2(1, Vector3(e.Position.x, e.Position.y, 0.0f), 255, revealZone, 0, 1138938442, 300000, 299080, Some(false)))) } enemyVehicles.foreach { v => - sendResponse(UplinkPositionEvent(1, Event2(1, Vector3(v.Position.x, v.Position.y, 0.0f), v.Definition.MapRevealId, revealZone, 0, 1118938442, 300000, 299080, Some(false)))) + sendResponse(UplinkPositionEvent(1, Event2(1, Vector3(v.Position.x, v.Position.y, 0.0f), v.Definition.MapRevealId, revealZone, 0, 1148938442, 300000, 299080, Some(false)))) } sendResponse(UplinkPositionEvent(6, Event0(6))) case UplinkRequestType.ElectroMagneticPulse => From 4f50884c7102abcddb0c5cac97457d794eac2885 Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Mon, 21 Jul 2025 10:38:08 -0400 Subject: [PATCH 49/57] density-alerts --- src/main/resources/application.conf | 12 ++++++ .../session/support/ZoningOperations.scala | 2 +- .../psforever/actors/zone/BuildingActor.scala | 7 +++ .../serverobject/structures/Building.scala | 43 ++++++++++++++++++- .../MajorFacilityHackParticipation.scala | 11 +++++ .../zones/SphereOfInfluenceActor.scala | 9 +++- .../scala/net/psforever/util/Config.scala | 9 +++- 7 files changed, 89 insertions(+), 4 deletions(-) diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf index cbb9bc88e..6d56004c6 100644 --- a/src/main/resources/application.conf +++ b/src/main/resources/application.conf @@ -508,6 +508,18 @@ game { capture-experience-points-modifier = 1f # Don't forget to pay back that debt. } + + alert { + # When a certain number of enemy players are within the SOI of a facility, an alert (DensityLevelUpdateMessage) + # will be dispatched to all players. Players of the owning faction will receive a chat warning (if in + # the same zone) and the map will flash the alert level over the facility until it changes + # Wiki says 25-30 + yellow = 1 + # Wiki says 30-60 + orange = 2 + # Wiki says 60+ + red = 3 + } } anti-cheat { 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 a60ac1edc..503e7757a 100644 --- a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala @@ -972,7 +972,7 @@ class ZoningOperations( */ def initFacility(continentNumber: Int, buildingNumber: Int, building: Building): Unit = { sendResponse(building.infoUpdateMessage()) - sendResponse(DensityLevelUpdateMessage(continentNumber, buildingNumber, List(0, 0, 0, 0, 0, 0, 0, 0))) + sendResponse(building.densityLevelUpdateMessage(building)) } /** diff --git a/src/main/scala/net/psforever/actors/zone/BuildingActor.scala b/src/main/scala/net/psforever/actors/zone/BuildingActor.scala index 8cfd403d8..334921248 100644 --- a/src/main/scala/net/psforever/actors/zone/BuildingActor.scala +++ b/src/main/scala/net/psforever/actors/zone/BuildingActor.scala @@ -74,6 +74,8 @@ object BuildingActor { final case class PowerOff() extends Command + final case class DensityLevelUpdate(building: Building) extends Command + /** * Set a facility affiliated to one faction to be affiliated to a different faction. * @param details building and event system references @@ -226,6 +228,7 @@ class BuildingActor( case MapUpdate() => details.galaxyService ! GalaxyServiceMessage(GalaxyAction.MapUpdate(details.building.infoUpdateMessage())) + details.galaxyService ! GalaxyServiceMessage(GalaxyAction.SendResponse(details.building.densityLevelUpdateMessage(building))) Behaviors.same case AmenityStateChange(amenity, data) => @@ -245,6 +248,10 @@ class BuildingActor( case Ntu(msg) => logic.ntu(details, msg) + + case DensityLevelUpdate(building) => + details.galaxyService ! GalaxyServiceMessage(GalaxyAction.SendResponse(details.building.densityLevelUpdateMessage(building))) + Behaviors.same } } } diff --git a/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala b/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala index cf2634b26..adcf381b1 100644 --- a/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala +++ b/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala @@ -11,13 +11,14 @@ import net.psforever.objects.serverobject.resourcesilo.ResourceSilo import net.psforever.objects.serverobject.tube.SpawnTube import net.psforever.objects.zones.Zone import net.psforever.objects.zones.blockmap.BlockMapEntity -import net.psforever.packet.game.BuildingInfoUpdateMessage +import net.psforever.packet.game.{BuildingInfoUpdateMessage, DensityLevelUpdateMessage} import net.psforever.types._ import scalax.collection.{Graph, GraphEdge} import akka.actor.typed.scaladsl.adapter._ import net.psforever.objects.serverobject.llu.{CaptureFlag, CaptureFlagSocket} import net.psforever.objects.serverobject.structures.participation.{MajorFacilityHackParticipation, NoParticipation, ParticipationLogic, TowerHackParticipation} import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal +import net.psforever.util.Config class Building( private val name: String, @@ -236,6 +237,46 @@ class Building( ) } + def densityLevelUpdateMessage(building: Building): DensityLevelUpdateMessage = { + if (building.PlayersInSOI.nonEmpty) { + val factionCounts: Map[PlanetSideEmpire.Value, Int] = + building.PlayersInSOI.groupBy(_.Faction).view.mapValues(_.size).toMap + val otherEmpireCounts: Map[PlanetSideEmpire.Value, Int] = PlanetSideEmpire.values.map { + faction => + val otherCount = factionCounts.filterNot(_._1 == faction).values.sum + faction -> otherCount + }.toMap + val trAlert = otherEmpireCounts.getOrElse(PlanetSideEmpire.TR, 0) match { + case count if count >= Config.app.game.alert.red => 3 + case count if count >= Config.app.game.alert.orange => 2 + case count if count >= Config.app.game.alert.yellow => 1 + case _ => 0 + } + val ncAlert = otherEmpireCounts.getOrElse(PlanetSideEmpire.NC, 0) match { + case count if count >= Config.app.game.alert.red => 3 + case count if count >= Config.app.game.alert.orange => 2 + case count if count >= Config.app.game.alert.yellow => 1 + case _ => 0 + } + val vsAlert = otherEmpireCounts.getOrElse(PlanetSideEmpire.VS, 0) match { + case count if count >= Config.app.game.alert.red => 3 + case count if count >= Config.app.game.alert.orange => 2 + case count if count >= Config.app.game.alert.yellow => 1 + case _ => 0 + } + val boAlert = otherEmpireCounts.getOrElse(PlanetSideEmpire.NEUTRAL, 0) match { + case count if count >= Config.app.game.alert.red => 3 + case count if count >= Config.app.game.alert.orange => 2 + case count if count >= Config.app.game.alert.yellow => 1 + case _ => 0 + } + DensityLevelUpdateMessage(Zone.Number, MapId, List(0, trAlert, 0, ncAlert, 0, vsAlert, 0, boAlert)) + } + else { //nobody is in this SOI + DensityLevelUpdateMessage(Zone.Number, MapId, List(0, 0, 0, 0, 0, 0, 0, 0)) + } + } + def hasLatticeBenefit(wantedBenefit: LatticeBenefit): Boolean = { val baseDownState = (NtuSource match { case Some(ntu) => ntu.NtuCapacitor < 1f diff --git a/src/main/scala/net/psforever/objects/serverobject/structures/participation/MajorFacilityHackParticipation.scala b/src/main/scala/net/psforever/objects/serverobject/structures/participation/MajorFacilityHackParticipation.scala index 230aa2226..bb319dccd 100644 --- a/src/main/scala/net/psforever/objects/serverobject/structures/participation/MajorFacilityHackParticipation.scala +++ b/src/main/scala/net/psforever/objects/serverobject/structures/participation/MajorFacilityHackParticipation.scala @@ -9,6 +9,7 @@ import net.psforever.types.{ChatMessageType, PlanetSideEmpire, Vector3} import net.psforever.util.Config import akka.pattern.ask import akka.util.Timeout +import net.psforever.actors.zone.BuildingActor import net.psforever.objects.Player import net.psforever.objects.avatar.scoring.Kill import net.psforever.objects.serverobject.hackable.Hackable @@ -26,6 +27,8 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci private var hotSpotLayersOverTime: Seq[List[HotSpotInfo]] = Seq[List[HotSpotInfo]]() + var lastEnemyCount: List[Player] = List.empty + def TryUpdate(): Unit = { val list = building.PlayersInSOI if (list.nonEmpty) { @@ -37,6 +40,14 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci updateHotSpotInfoOverTime() updateTime(now) } + val enemies = list.filter(p => p.Faction != building.Faction) ++ + building.Zone.blockMap.sector(building).corpseList + .filter(p => Vector3.DistanceSquared(building.Position.xy, p.Position.xy) < building.Definition.SOIRadius * building.Definition.SOIRadius) + //alert defenders (actually goes to all clients) of population change for base alerts + if (Math.abs(enemies.length - lastEnemyCount.length) >= 1) { + building.Actor ! BuildingActor.DensityLevelUpdate(building) + } + lastEnemyCount = enemies building.CaptureTerminal .map(_.HackedBy) .collect { diff --git a/src/main/scala/net/psforever/objects/zones/SphereOfInfluenceActor.scala b/src/main/scala/net/psforever/objects/zones/SphereOfInfluenceActor.scala index 0510d5c07..4c5a4171a 100644 --- a/src/main/scala/net/psforever/objects/zones/SphereOfInfluenceActor.scala +++ b/src/main/scala/net/psforever/objects/zones/SphereOfInfluenceActor.scala @@ -55,9 +55,16 @@ class SphereOfInfluenceActor(zone: Zone) extends Actor { sois.foreach { case (facility, radius) => val facilityXY = facility.Position.xy - facility.PlayersInSOI = zone.blockMap.sector(facility) + val playersOnFoot = zone.blockMap.sector(facility) .livePlayerList .filter(p => Vector3.DistanceSquared(facilityXY, p.Position.xy) < radius) + + val vehicleOccupants = zone.blockMap.sector(facility) + .vehicleList + .filter(v => Vector3.DistanceSquared(facilityXY, v.Position.xy) < radius) + .flatMap(_.Seats.values.flatMap(_.occupants)) + + facility.PlayersInSOI = playersOnFoot ++ vehicleOccupants } populateTick.cancel() populateTick = context.system.scheduler.scheduleOnce(5 seconds, self, SOI.Populate()) diff --git a/src/main/scala/net/psforever/util/Config.scala b/src/main/scala/net/psforever/util/Config.scala index 13d7d5c90..ef1314248 100644 --- a/src/main/scala/net/psforever/util/Config.scala +++ b/src/main/scala/net/psforever/util/Config.scala @@ -164,7 +164,8 @@ case class GameConfig( experience: Experience, maxBattleRank: Int, promotion: PromotionSystem, - facilityHackTime: FiniteDuration + facilityHackTime: FiniteDuration, + alert: DensityAlert ) case class InstantActionConfig( @@ -326,3 +327,9 @@ case class PromotionSystem( supportExperiencePointsModifier: Float, captureExperiencePointsModifier: Float ) + +case class DensityAlert( + yellow: Int, + orange: Int, + red: Int +) From 01cdc2db4cf1ed32ea9a1f901d3d76ea13652f48 Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Tue, 22 Jul 2025 12:28:54 -0400 Subject: [PATCH 50/57] adjusted condition and weather --- src/main/resources/application.conf | 6 ++-- .../session/support/ZoningOperations.scala | 28 ++++++++++++++++++- .../MajorFacilityHackParticipation.scala | 16 +++++++++-- 3 files changed, 44 insertions(+), 6 deletions(-) diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf index 6d56004c6..27b1e4a5b 100644 --- a/src/main/resources/application.conf +++ b/src/main/resources/application.conf @@ -514,11 +514,11 @@ game { # will be dispatched to all players. Players of the owning faction will receive a chat warning (if in # the same zone) and the map will flash the alert level over the facility until it changes # Wiki says 25-30 - yellow = 1 + yellow = 20 # Wiki says 30-60 - orange = 2 + orange = 30 # Wiki says 60+ - red = 3 + red = 60 } } 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 503e7757a..35ccd88ba 100644 --- a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala @@ -18,7 +18,7 @@ import net.psforever.objects.serverobject.turret.auto.AutomatedTurret import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource} import net.psforever.objects.vital.{InGameHistory, IncarnationActivity, ReconstructionActivity, SpawningActivity} import net.psforever.objects.zones.blockmap.BlockMapEntity -import net.psforever.packet.game.{CampaignStatistic, ChangeFireStateMessage_Start, HackState7, MailMessage, ObjectDetectedMessage, SessionStatistic, TriggeredSound} +import net.psforever.packet.game.{CampaignStatistic, ChangeFireStateMessage_Start, HackState7, MailMessage, ObjectDetectedMessage, SessionStatistic, TriggeredSound, WeatherMessage, CloudInfo, StormInfo} import net.psforever.services.chat.DefaultChannel import scala.collection.mutable @@ -2504,6 +2504,19 @@ class ZoningOperations( sessionLogic.general.toggleTeleportSystem(obj, TelepadLike.AppraiseTeleportationSystem(obj, continent)) } } + //make weather happen + sendResponse(WeatherMessage(List(),List( + StormInfo(Vector3(0.1f, 0.15f, 0.0f), 240, 217), + StormInfo(Vector3(0.5f, 0.11f, 0.0f), 240, 215), + StormInfo(Vector3(0.15f, 0.4f, 0.0f), 249, 215), + StormInfo(Vector3(0.15f, 0.87f, 0.0f), 240, 215), + StormInfo(Vector3(0.3f, 0.65f, 0.0f), 240, 215), + StormInfo(Vector3(0.5f, 0.475f, 0.0f), 245, 215), + StormInfo(Vector3(0.725f, 0.38f, 0.0f), 243, 215), + StormInfo(Vector3(0.9f, 0.57f, 0.0f), 244, 215), + StormInfo(Vector3(0.9f, 0.9f, 0.0f), 243, 215), + StormInfo(Vector3(0.1f, 0.2f, 0.0f), 241, 215), + StormInfo(Vector3(0.95f, 0.2f, 0.0f), 241, 215)))) //begin looking for conditions to set the avatar context.system.scheduler.scheduleOnce(delay = 250 millisecond, context.self, SessionActor.SetCurrentAvatar(player, 200)) } @@ -2615,6 +2628,19 @@ class ZoningOperations( } avatarActor ! AvatarActor.RefreshPurchaseTimes() setupAvatarFunc = AvatarCreate + //make weather happen + sendResponse(WeatherMessage(List(),List( + StormInfo(Vector3(0.1f, 0.15f, 0.0f), 240, 217), + StormInfo(Vector3(0.5f, 0.11f, 0.0f), 240, 215), + StormInfo(Vector3(0.15f, 0.4f, 0.0f), 249, 215), + StormInfo(Vector3(0.15f, 0.87f, 0.0f), 240, 215), + StormInfo(Vector3(0.3f, 0.65f, 0.0f), 240, 215), + StormInfo(Vector3(0.5f, 0.475f, 0.0f), 245, 215), + StormInfo(Vector3(0.725f, 0.38f, 0.0f), 243, 215), + StormInfo(Vector3(0.9f, 0.57f, 0.0f), 244, 215), + StormInfo(Vector3(0.9f, 0.9f, 0.0f), 243, 215), + StormInfo(Vector3(0.1f, 0.2f, 0.0f), 241, 215), + StormInfo(Vector3(0.95f, 0.2f, 0.0f), 241, 215)))) //begin looking for conditions to set the avatar context.system.scheduler.scheduleOnce(delay = 750 millisecond, context.self, SessionActor.SetCurrentAvatar(player, 200)) } diff --git a/src/main/scala/net/psforever/objects/serverobject/structures/participation/MajorFacilityHackParticipation.scala b/src/main/scala/net/psforever/objects/serverobject/structures/participation/MajorFacilityHackParticipation.scala index bb319dccd..fdd519ec5 100644 --- a/src/main/scala/net/psforever/objects/serverobject/structures/participation/MajorFacilityHackParticipation.scala +++ b/src/main/scala/net/psforever/objects/serverobject/structures/participation/MajorFacilityHackParticipation.scala @@ -28,6 +28,7 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci private var hotSpotLayersOverTime: Seq[List[HotSpotInfo]] = Seq[List[HotSpotInfo]]() var lastEnemyCount: List[Player] = List.empty + var alertTimeMillis: Long = 0L def TryUpdate(): Unit = { val list = building.PlayersInSOI @@ -44,10 +45,21 @@ final case class MajorFacilityHackParticipation(building: Building) extends Faci building.Zone.blockMap.sector(building).corpseList .filter(p => Vector3.DistanceSquared(building.Position.xy, p.Position.xy) < building.Definition.SOIRadius * building.Definition.SOIRadius) //alert defenders (actually goes to all clients) of population change for base alerts - if (Math.abs(enemies.length - lastEnemyCount.length) >= 1) { + //straight away if higher alert, delay if pop decreases enough to lower alert + if ((enemies.length >= Config.app.game.alert.yellow && lastEnemyCount.length < Config.app.game.alert.yellow) || + (enemies.length >= Config.app.game.alert.orange && lastEnemyCount.length < Config.app.game.alert.orange) || + (enemies.length >= Config.app.game.alert.red && lastEnemyCount.length < Config.app.game.alert.red) || + (enemies.length < Config.app.game.alert.yellow && lastEnemyCount.length >= Config.app.game.alert.yellow && + now - alertTimeMillis > 30000L && Math.abs(enemies.length - lastEnemyCount.length) >= 3) || + (enemies.length < Config.app.game.alert.orange && lastEnemyCount.length >= Config.app.game.alert.orange && + now - alertTimeMillis > 30000L && Math.abs(enemies.length - lastEnemyCount.length) >= 3) || + (enemies.length < Config.app.game.alert.red && lastEnemyCount.length >= Config.app.game.alert.red && + now - alertTimeMillis > 30000L && Math.abs(enemies.length - lastEnemyCount.length) >= 3)) + { building.Actor ! BuildingActor.DensityLevelUpdate(building) + alertTimeMillis = now + lastEnemyCount = enemies } - lastEnemyCount = enemies building.CaptureTerminal .map(_.HackedBy) .collect { From 39e70ff3193cc8b92151eed04705d79b45f26169 Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Thu, 24 Jul 2025 13:18:01 -0400 Subject: [PATCH 51/57] time change why not --- .../net/psforever/actors/session/support/ZoningOperations.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 1932cdb63..fd7e3145c 100644 --- a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala @@ -292,7 +292,7 @@ class ZoningOperations( continent.VehicleEvents ! Service.Join(continentId) continent.VehicleEvents ! Service.Join(factionChannel) if (sessionLogic.connectionState != 100) configZone(continent) - sendResponse(TimeOfDayMessage(1191182336)) + sendResponse(TimeOfDayMessage(1135214592)) //custom sendResponse(ReplicationStreamMessage(5, Some(6), Vector.empty)) //clear squad list sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(0), 112, 0)) // disable festive backpacks From 2b1d01c4bafb700658617720368dfe7e3b6467e2 Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Mon, 28 Jul 2025 21:26:39 -0400 Subject: [PATCH 52/57] misc animations and turret range --- .../actors/session/csr/GeneralLogic.scala | 5 +++++ .../actors/session/normal/GeneralLogic.scala | 5 +++++ .../session/normal/VehicleHandlerLogic.scala | 2 +- .../spectator/VehicleHandlerLogic.scala | 2 +- .../session/support/ZoningOperations.scala | 12 ++++++++--- .../damage/DamageableEntity.scala | 12 +++++++---- .../turret/auto/AutomatedTurretBehavior.scala | 21 +++++++++++++------ .../game/GenericObjectActionMessage.scala | 1 + .../avatar/support/CorpseRemovalActor.scala | 2 +- 9 files changed, 46 insertions(+), 16 deletions(-) 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 68f59b434..d9c76b6ef 100644 --- a/src/main/scala/net/psforever/actors/session/csr/GeneralLogic.scala +++ b/src/main/scala/net/psforever/actors/session/csr/GeneralLogic.scala @@ -33,6 +33,7 @@ import net.psforever.packet.PlanetSideGamePacket import net.psforever.packet.game.{ActionCancelMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, BugReportMessage, ChangeFireModeMessage, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestMessage, ChatMsg, CollisionIs, ConnectToWorldRequestMessage, CreateShortcutMessage, DeadState, DeployObjectMessage, DisplayedAwardMessage, DropItemMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FriendsRequest, GenericAction, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, InvalidTerrainMessage, LootItemMessage, MoveItemMessage, ObjectDetectedMessage, ObjectHeldMessage, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, RequestDestroyMessage, TargetingImplantRequest, TerrainCondition, TradeMessage, UnuseItemMessage, UseItemMessage, VoiceHostInfo, VoiceHostRequest, ZipLineMessage} import net.psforever.services.RemoverActor import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} +import net.psforever.services.local.{LocalAction, LocalServiceMessage} import net.psforever.types.{CapacitorStateType, ChatMessageType, Cosmetic, ExoSuitType, PlanetSideEmpire, PlanetSideGUID, Vector3} import scala.util.Success @@ -181,7 +182,11 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex def handleEmote(pkt: EmoteMsg): Unit = { val EmoteMsg(avatarGuid, emote) = pkt + val pZone = player.Zone sendResponse(EmoteMsg(avatarGuid, emote)) + pZone.blockMap.sector(player).livePlayerList.collect { case t if t.GUID != player.GUID => + pZone.LocalEvents ! LocalServiceMessage(t.Name, LocalAction.SendResponse(EmoteMsg(avatarGuid, emote))) + } } def handleDropItem(pkt: DropItemMessage): Unit = { 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 fdfa1c9b4..23e9d38ed 100644 --- a/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala @@ -40,6 +40,7 @@ import net.psforever.packet.PlanetSideGamePacket 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, InvalidTerrainMessage, LootItemMessage, MoveItemMessage, ObjectDetectedMessage, ObjectHeldMessage, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, RequestDestroyMessage, TargetingImplantRequest, 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, ExoSuitType, ImplantType, PlanetSideEmpire, PlanetSideGUID, Vector3} import net.psforever.util.Config @@ -199,7 +200,11 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex def handleEmote(pkt: EmoteMsg): Unit = { val EmoteMsg(avatarGuid, emote) = pkt + val pZone = player.Zone sendResponse(EmoteMsg(avatarGuid, emote)) + pZone.blockMap.sector(player).livePlayerList.collect { case t if t.GUID != player.GUID => + pZone.LocalEvents ! LocalServiceMessage(t.Name, LocalAction.SendResponse(EmoteMsg(avatarGuid, emote))) + } } def handleDropItem(pkt: DropItemMessage): Unit = { diff --git a/src/main/scala/net/psforever/actors/session/normal/VehicleHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/normal/VehicleHandlerLogic.scala index 64a7a67a8..f6102dedb 100644 --- a/src/main/scala/net/psforever/actors/session/normal/VehicleHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/VehicleHandlerLogic.scala @@ -231,7 +231,7 @@ class VehicleHandlerLogic(val ops: SessionVehicleHandlers, implicit val context: sendResponse(ObjectCreateDetailedMessage(itemType, itemGuid, ObjectCreateMessageParent(vehicleGuid, slot), itemData)) case VehicleResponse.UnloadVehicle(_, vehicleGuid) => - sendResponse(ObjectDeleteMessage(vehicleGuid, unk1=0)) + sendResponse(ObjectDeleteMessage(vehicleGuid, unk1=1)) if (sessionLogic.zoning.spawn.prevSpawnPoint.map(_.Owner).exists { case ams: Vehicle => ams.GUID == vehicleGuid && 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 ac930161c..d68308a68 100644 --- a/src/main/scala/net/psforever/actors/session/spectator/VehicleHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/spectator/VehicleHandlerLogic.scala @@ -197,7 +197,7 @@ class VehicleHandlerLogic(val ops: SessionVehicleHandlers, implicit val context: sendResponse(PlanetsideAttributeMessage(vehicleGuid, seatGroup, permission)) case VehicleResponse.UnloadVehicle(_, vehicleGuid) => - sendResponse(ObjectDeleteMessage(vehicleGuid, unk1=0)) + sendResponse(ObjectDeleteMessage(vehicleGuid, unk1=1)) case VehicleResponse.UnstowEquipment(itemGuid) if isNotSameTarget => //TODO prefer ObjectDetachMessage, but how to force ammo pools to update properly? 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 fd7e3145c..f46ba3185 100644 --- a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala @@ -18,7 +18,8 @@ import net.psforever.objects.serverobject.turret.auto.AutomatedTurret import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource} import net.psforever.objects.vital.{InGameHistory, IncarnationActivity, ReconstructionActivity, SpawningActivity} import net.psforever.objects.zones.blockmap.BlockMapEntity -import net.psforever.packet.game.{CampaignStatistic, ChangeFireStateMessage_Start, HackState7, MailMessage, ObjectDetectedMessage, SessionStatistic, TriggeredSound, WeatherMessage, CloudInfo, StormInfo} +import net.psforever.packet.game.GenericAction.FirstPersonViewWithEffect +import net.psforever.packet.game.{CampaignStatistic, ChangeFireStateMessage_Start, CloudInfo, GenericActionMessage, GenericObjectActionEnum, HackState7, MailMessage, ObjectDetectedMessage, SessionStatistic, StormInfo, TriggeredSound, WeatherMessage} import net.psforever.services.chat.DefaultChannel import scala.collection.mutable @@ -2972,10 +2973,10 @@ class ZoningOperations( case _ if player.HasGUID => // player is deconstructing self or instant action val player_guid = player.GUID - sendResponse(ObjectDeleteMessage(player_guid, 4)) + sendResponse(ObjectDeleteMessage(player_guid, unk1=1)) continent.AvatarEvents ! AvatarServiceMessage( continent.id, - AvatarAction.ObjectDelete(player_guid, player_guid, 4) + AvatarAction.ObjectDelete(player_guid, player_guid, unk=1) ) InGameHistory.SpawnReconstructionActivity(player, toZoneNumber, betterSpawnPoint) LoadZoneAsPlayerUsing(player, pos, ori, toSide, zoneId) @@ -3776,6 +3777,11 @@ class ZoningOperations( player.death_by = 1 } GoToDeploymentMap() + val pZone = player.Zone + sendResponse(GenericActionMessage(FirstPersonViewWithEffect)) + pZone.blockMap.sector(player).livePlayerList.collect { case t if t.GUID != player.GUID => + pZone.LocalEvents ! LocalServiceMessage(t.Name, LocalAction.SendGenericObjectActionMessage(t.GUID, player.GUID, GenericObjectActionEnum.PlayerDeconstructs)) + } } def stopDeconstructing(): Unit = { diff --git a/src/main/scala/net/psforever/objects/serverobject/damage/DamageableEntity.scala b/src/main/scala/net/psforever/objects/serverobject/damage/DamageableEntity.scala index b3d74ac17..e594de2db 100644 --- a/src/main/scala/net/psforever/objects/serverobject/damage/DamageableEntity.scala +++ b/src/main/scala/net/psforever/objects/serverobject/damage/DamageableEntity.scala @@ -2,6 +2,7 @@ package net.psforever.objects.serverobject.damage import net.psforever.objects.equipment.JammableUnit +import net.psforever.objects.serverobject.tube.SpawnTube import net.psforever.objects.vital.interaction.DamageResult import net.psforever.objects.vital.resolution.ResolutionCalculations import net.psforever.objects.zones.Zone @@ -199,9 +200,12 @@ object DamageableEntity { val tguid = target.GUID val attribution = attributionTo(cause, target.Zone) zone.AvatarEvents ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(tguid, 0, target.Health)) - zone.AvatarEvents ! AvatarServiceMessage( - zoneId, - AvatarAction.Destroy(tguid, attribution, Service.defaultPlayerGUID, target.Position) - ) + if (target.isInstanceOf[SpawnTube]) {}//do nothing to prevent issue #1057 + else { + zone.AvatarEvents ! AvatarServiceMessage( + zoneId, + AvatarAction.Destroy(tguid, attribution, Service.defaultPlayerGUID, target.Position) + ) + } } } diff --git a/src/main/scala/net/psforever/objects/serverobject/turret/auto/AutomatedTurretBehavior.scala b/src/main/scala/net/psforever/objects/serverobject/turret/auto/AutomatedTurretBehavior.scala index fad4c5c34..cd116dfd5 100644 --- a/src/main/scala/net/psforever/objects/serverobject/turret/auto/AutomatedTurretBehavior.scala +++ b/src/main/scala/net/psforever/objects/serverobject/turret/auto/AutomatedTurretBehavior.scala @@ -205,7 +205,8 @@ trait AutomatedTurretBehavior { val now = System.currentTimeMillis() if ( currentTargetToken.isEmpty && - target.Faction != AutomatedTurretObject.Faction + target.Faction != AutomatedTurretObject.Faction && + now >= currentTargetLastShotTime ) { currentTargetLastShotTime = now currentTargetLocation = Some(target.Position) @@ -215,11 +216,19 @@ trait AutomatedTurretBehavior { true } else if ( currentTargetToken.contains(SourceUniqueness(target)) && - now - currentTargetLastShotTime < autoStats.map(_.cooldowns.missedShot).getOrElse(0L)) { - currentTargetLastShotTime = now - currentTargetLocation = Some(target.Position) - cancelSelfReportedAutoFire() - true + now - currentTargetLastShotTime < autoStats.map(_.cooldowns.missedShot).getOrElse(0L) + ) { + val escapeRange = autoStats.map(_.ranges.escape).getOrElse(400f) + val distSq = Vector3.DistanceSquared(target.Position, AutomatedTurretObject.Position) + val escapeSq = escapeRange * escapeRange + if (distSq <= escapeSq) { + currentTargetLastShotTime = now + currentTargetLocation = Some(target.Position) + cancelSelfReportedAutoFire() + true + } else { + false + } } else { false } diff --git a/src/main/scala/net/psforever/packet/game/GenericObjectActionMessage.scala b/src/main/scala/net/psforever/packet/game/GenericObjectActionMessage.scala index 916ee0310..aa71a534c 100644 --- a/src/main/scala/net/psforever/packet/game/GenericObjectActionMessage.scala +++ b/src/main/scala/net/psforever/packet/game/GenericObjectActionMessage.scala @@ -91,4 +91,5 @@ object GenericObjectActionEnum extends Enumeration { * Target: CaptureTerminal */ val FlagSpawned = Value(14) + val PlayerDeconstructs = Value(6) } diff --git a/src/main/scala/net/psforever/services/avatar/support/CorpseRemovalActor.scala b/src/main/scala/net/psforever/services/avatar/support/CorpseRemovalActor.scala index 062c7f15c..974f0b687 100644 --- a/src/main/scala/net/psforever/services/avatar/support/CorpseRemovalActor.scala +++ b/src/main/scala/net/psforever/services/avatar/support/CorpseRemovalActor.scala @@ -25,7 +25,7 @@ class CorpseRemovalActor extends RemoverActor() { entry.zone.Population ! Zone.Corpse.Remove(entry.obj.asInstanceOf[Player]) context.parent ! AvatarServiceMessage( entry.zone.id, - AvatarAction.ObjectDelete(Service.defaultPlayerGUID, entry.obj.GUID) + AvatarAction.ObjectDelete(Service.defaultPlayerGUID, entry.obj.GUID, unk=1) ) } From aa31d6e3954074a10e0ecaf7fd704c746a96c868 Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Wed, 30 Jul 2025 10:39:50 -0400 Subject: [PATCH 53/57] gm sees anims and crouch bfr --- .../actors/session/csr/GeneralLogic.scala | 3 +++ .../actors/session/normal/GeneralLogic.scala | 3 +++ .../session/support/ZoningOperations.scala | 3 +++ .../scala/net/psforever/objects/Vehicles.scala | 16 +++++++++++++++- .../packet/game/GenericObjectActionMessage.scala | 1 + 5 files changed, 25 insertions(+), 1 deletion(-) 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 d9c76b6ef..2cd1782ec 100644 --- a/src/main/scala/net/psforever/actors/session/csr/GeneralLogic.scala +++ b/src/main/scala/net/psforever/actors/session/csr/GeneralLogic.scala @@ -187,6 +187,9 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex pZone.blockMap.sector(player).livePlayerList.collect { case t if t.GUID != player.GUID => pZone.LocalEvents ! LocalServiceMessage(t.Name, LocalAction.SendResponse(EmoteMsg(avatarGuid, emote))) } + pZone.AllPlayers.collect { case t if t.GUID != player.GUID && !t.allowInteraction => + pZone.LocalEvents ! LocalServiceMessage(t.Name, LocalAction.SendResponse(EmoteMsg(avatarGuid, emote))) + } } def handleDropItem(pkt: DropItemMessage): Unit = { 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 23e9d38ed..99cf5103c 100644 --- a/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala @@ -205,6 +205,9 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex pZone.blockMap.sector(player).livePlayerList.collect { case t if t.GUID != player.GUID => pZone.LocalEvents ! LocalServiceMessage(t.Name, LocalAction.SendResponse(EmoteMsg(avatarGuid, emote))) } + pZone.AllPlayers.collect { case t if t.GUID != player.GUID && !t.allowInteraction => + pZone.LocalEvents ! LocalServiceMessage(t.Name, LocalAction.SendResponse(EmoteMsg(avatarGuid, emote))) + } } def handleDropItem(pkt: DropItemMessage): Unit = { 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 f46ba3185..c2404d291 100644 --- a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala @@ -3782,6 +3782,9 @@ class ZoningOperations( pZone.blockMap.sector(player).livePlayerList.collect { case t if t.GUID != player.GUID => pZone.LocalEvents ! LocalServiceMessage(t.Name, LocalAction.SendGenericObjectActionMessage(t.GUID, player.GUID, GenericObjectActionEnum.PlayerDeconstructs)) } + pZone.AllPlayers.collect { case t if t.GUID != player.GUID && !t.allowInteraction => + pZone.LocalEvents ! LocalServiceMessage(t.Name, LocalAction.SendGenericObjectActionMessage(t.GUID, player.GUID, GenericObjectActionEnum.PlayerDeconstructs)) + } } def stopDeconstructing(): Unit = { diff --git a/src/main/scala/net/psforever/objects/Vehicles.scala b/src/main/scala/net/psforever/objects/Vehicles.scala index 259dc2845..019f4d7f2 100644 --- a/src/main/scala/net/psforever/objects/Vehicles.scala +++ b/src/main/scala/net/psforever/objects/Vehicles.scala @@ -10,7 +10,7 @@ import net.psforever.objects.serverobject.transfer.TransferContainer import net.psforever.objects.serverobject.structures.WarpGate import net.psforever.objects.vehicles._ import net.psforever.objects.zones.Zone -import net.psforever.packet.game.{ChatMsg, HackMessage, HackState, HackState1, HackState7, TriggeredSound} +import net.psforever.packet.game.{ChatMsg, FrameVehicleStateMessage, GenericObjectActionEnum, GenericObjectActionMessage, HackMessage, HackState, HackState1, HackState7, TriggeredSound, VehicleStateMessage} import net.psforever.types.{ChatMessageType, DriveState, PlanetSideEmpire, PlanetSideGUID, Vector3} import net.psforever.services.Service import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} @@ -274,6 +274,20 @@ object Vehicles { VehicleAction.KickPassenger(player.GUID, 4, unk2 = false, tGuid) ) } + // In case BFR is occupied and may or may not be crouched + if (GlobalDefinitions.isBattleFrameVehicle(target.Definition) && target.Seat(0).isDefined) { + zone.LocalEvents ! LocalServiceMessage( + zoneid, + LocalAction.SendGenericObjectActionMessage(PlanetSideGUID(-1), target.GUID, GenericObjectActionEnum.BFRShieldsDown)) + zone.LocalEvents ! LocalServiceMessage( + zoneid, + LocalAction.SendResponse( + FrameVehicleStateMessage(target.GUID, 0, target.Position, target.Orientation, Some(Vector3(0f, 0f, 0f)), unk2=false, 0, 0, is_crouched=true, is_airborne=false, ascending_flight=false, 10, 0, 0))) + zone.LocalEvents ! LocalServiceMessage( + zoneid, + LocalAction.SendResponse( + VehicleStateMessage(target.GUID, 0, target.Position, target.Orientation, Some(Vector3(0f, 0f, 0f)), None, 0, 0, 15, is_decelerating=false, is_cloaked=false))) + } }) // If the vehicle can fly and is flying: deconstruct it; and well played to whomever managed to hack a plane in mid air if (target.Definition.CanFly && target.isFlying) { diff --git a/src/main/scala/net/psforever/packet/game/GenericObjectActionMessage.scala b/src/main/scala/net/psforever/packet/game/GenericObjectActionMessage.scala index aa71a534c..8d4b7e4cb 100644 --- a/src/main/scala/net/psforever/packet/game/GenericObjectActionMessage.scala +++ b/src/main/scala/net/psforever/packet/game/GenericObjectActionMessage.scala @@ -92,4 +92,5 @@ object GenericObjectActionEnum extends Enumeration { */ val FlagSpawned = Value(14) val PlayerDeconstructs = Value(6) + val BFRShieldsDown = Value(45) } From a00a95d15234ea5860674cc189f08473224f461c Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Thu, 31 Jul 2025 01:12:16 -0400 Subject: [PATCH 54/57] allowing scaled back bfr accessibility (#1280) --- .../resources/overrides/game_objects0.adb.lst | 30 +++++++++++++---- .../WeaponAndProjectileOperations.scala | 2 +- .../BattleFrameFlightConverter.scala | 2 +- .../BattleFrameRoboticsConverter.scala | 2 +- .../global/GlobalDefinitionsProjectile.scala | 4 +-- .../global/GlobalDefinitionsVehicle.scala | 32 +++++++++---------- .../objects/vehicles/VehicleSubsystem.scala | 2 +- .../objects/vehicles/control/BfrControl.scala | 15 +++++---- .../vehicles/control/BfrFlightControl.scala | 8 ++--- .../resolution/ResolutionCalculations.scala | 2 +- 10 files changed, 59 insertions(+), 40 deletions(-) diff --git a/server/src/main/resources/overrides/game_objects0.adb.lst b/server/src/main/resources/overrides/game_objects0.adb.lst index 1ea38dca0..b62b955a0 100644 --- a/server/src/main/resources/overrides/game_objects0.adb.lst +++ b/server/src/main/resources/overrides/game_objects0.adb.lst @@ -96,12 +96,30 @@ add_property suppressor holstertime 600 add_property trek equiptime 500 add_property trek holstertime 500 add_property vulture requirement_award0 false -add_property aphelion allowed false +add_property aphelion allowed true add_property aphelion_flight allowed false -add_property aphelion_gunner allowed false -add_property colossus allowed false +add_property aphelion_gunner allowed true +add_property aphelion_armor_siphon allowed false +add_property aphelion_armor_siphon_left allowed false +add_property aphelion_armor_siphon_right allowed false +add_property aphelion_ntu_siphon allowed false +add_property aphelion_ntu_siphon_left allowed false +add_property aphelion_ntu_siphon_right allowed false +add_property colossus allowed true add_property colossus_flight allowed false -add_property colossus_gunner allowed false -add_property peregrine allowed false +add_property colossus_gunner allowed true +add_property colossus_armor_siphon allowed false +add_property colossus_armor_siphon_left allowed false +add_property colossus_armor_siphon_right allowed false +add_property colossus_ntu_siphon allowed false +add_property colossus_ntu_siphon_left allowed false +add_property colossus_ntu_siphon_right allowed false +add_property peregrine allowed true add_property peregrine_flight allowed false -add_property peregrine_gunner allowed false +add_property peregrine_gunner allowed true +add_property peregrine_armor_siphon allowed false +add_property peregrine_armor_siphon_left allowed false +add_property peregrine_armor_siphon_right allowed false +add_property peregrine_ntu_siphon allowed false +add_property peregrine_ntu_siphon_left allowed false +add_property peregrine_ntu_siphon_right allowed false 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 8e18811e3..047983bd9 100644 --- a/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala @@ -870,7 +870,7 @@ class WeaponAndProjectileOperations( "BattleframeLeftArm" } else { "BattleframeRightArm" - }).get.Enabled + }).exists(_.Enabled) if (!mountIsEnabled) { //can't stop the local discharge, but it will not actually shoot anything; assert the magazine sendResponse(QuantityUpdateMessage(tool.AmmoSlot.Box.GUID, tool.Magazine)) diff --git a/src/main/scala/net/psforever/objects/definition/converter/BattleFrameFlightConverter.scala b/src/main/scala/net/psforever/objects/definition/converter/BattleFrameFlightConverter.scala index 7ed207187..9efc677c2 100644 --- a/src/main/scala/net/psforever/objects/definition/converter/BattleFrameFlightConverter.scala +++ b/src/main/scala/net/psforever/objects/definition/converter/BattleFrameFlightConverter.scala @@ -110,6 +110,6 @@ class BattleFrameFlightConverter extends ObjectCreateConverter[Vehicle]() { } def showBfrShield(obj: Vehicle): Boolean = { - obj.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator).get.Enabled && obj.Shields > 0 + obj.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator).exists(_.Enabled) && obj.Shields > 0 } } diff --git a/src/main/scala/net/psforever/objects/definition/converter/BattleFrameRoboticsConverter.scala b/src/main/scala/net/psforever/objects/definition/converter/BattleFrameRoboticsConverter.scala index 411ff6a79..ea12b8120 100644 --- a/src/main/scala/net/psforever/objects/definition/converter/BattleFrameRoboticsConverter.scala +++ b/src/main/scala/net/psforever/objects/definition/converter/BattleFrameRoboticsConverter.scala @@ -110,6 +110,6 @@ class BattleFrameRoboticsConverter extends ObjectCreateConverter[Vehicle]() { } def showBfrShield(obj: Vehicle): Boolean = { - obj.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator).get.Enabled && obj.Shields > 0 + obj.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator).exists(_.Enabled) && obj.Shields > 0 } } diff --git a/src/main/scala/net/psforever/objects/global/GlobalDefinitionsProjectile.scala b/src/main/scala/net/psforever/objects/global/GlobalDefinitionsProjectile.scala index 4771f421b..be9f4e581 100644 --- a/src/main/scala/net/psforever/objects/global/GlobalDefinitionsProjectile.scala +++ b/src/main/scala/net/psforever/objects/global/GlobalDefinitionsProjectile.scala @@ -2056,7 +2056,7 @@ object GlobalDefinitionsProjectile { aphelion_plasma_rocket_projectile.DamageAtEdge = .1f aphelion_plasma_rocket_projectile.DamageRadius = 3f aphelion_plasma_rocket_projectile.ProjectileDamageType = DamageType.Splash - aphelion_plasma_rocket_projectile.DamageProxy = 96 //aphelion_plama_cloud + //aphelion_plasma_rocket_projectile.DamageProxy = 96 //aphelion_plama_cloud aphelion_plasma_rocket_projectile.InitialVelocity = 75 aphelion_plasma_rocket_projectile.Lifespan = 5f ProjectileDefinition.CalculateDerivedFields(aphelion_plasma_rocket_projectile) @@ -2221,7 +2221,7 @@ object GlobalDefinitionsProjectile { peregrine_particle_cannon_projectile.DamageAtEdge = 0.1f peregrine_particle_cannon_projectile.DamageRadius = 3f peregrine_particle_cannon_projectile.ProjectileDamageType = DamageType.Splash - peregrine_particle_cannon_projectile.DamageProxy = 655 //peregrine_particle_cannon_radiation_cloud + //peregrine_particle_cannon_projectile.DamageProxy = 655 //peregrine_particle_cannon_radiation_cloud peregrine_particle_cannon_projectile.InitialVelocity = 500 peregrine_particle_cannon_projectile.Lifespan = .6f ProjectileDefinition.CalculateDerivedFields(peregrine_particle_cannon_projectile) diff --git a/src/main/scala/net/psforever/objects/global/GlobalDefinitionsVehicle.scala b/src/main/scala/net/psforever/objects/global/GlobalDefinitionsVehicle.scala index c2fee38f6..50876dd42 100644 --- a/src/main/scala/net/psforever/objects/global/GlobalDefinitionsVehicle.scala +++ b/src/main/scala/net/psforever/objects/global/GlobalDefinitionsVehicle.scala @@ -46,7 +46,7 @@ object GlobalDefinitionsVehicle { restriction = MaxOnly } - val controlSubsystem = List(VehicleSubsystemEntry.Controls) + val controlSubsystem: List[VehicleSubsystemEntry] = List(VehicleSubsystemEntry.Controls) fury.Name = "fury" fury.MaxHealth = 650 @@ -1162,7 +1162,7 @@ object GlobalDefinitionsVehicle { bailable = true } - val flightSubsystems = List(VehicleSubsystemEntry.Controls, VehicleSubsystemEntry.Ejection) + val flightSubsystems: List[VehicleSubsystemEntry] = List(VehicleSubsystemEntry.Controls, VehicleSubsystemEntry.Ejection) val variantConverter = new VariantVehicleConverter mosquito.Name = "mosquito" @@ -1684,20 +1684,20 @@ object GlobalDefinitionsVehicle { bailable = true } val normalSeat = new SeatDefinition() - val bfrSubsystems = List( - VehicleSubsystemEntry.BattleframeMovementServos, - VehicleSubsystemEntry.BattleframeSensorArray, - VehicleSubsystemEntry.BattleframeShieldGenerator, - VehicleSubsystemEntry.BattleframeTrunk + val bfrSubsystems: List[VehicleSubsystemEntry] = List( +// VehicleSubsystemEntry.BattleframeMovementServos, +// VehicleSubsystemEntry.BattleframeSensorArray, + VehicleSubsystemEntry.BattleframeShieldGenerator//, +// VehicleSubsystemEntry.BattleframeTrunk ) - val bfrGunnerSubsystems = List( + val bfrGunnerSubsystems: List[VehicleSubsystemEntry] = List( VehicleSubsystemEntry.BattleframeLeftArm, - VehicleSubsystemEntry.BattleframeRightArm, - VehicleSubsystemEntry.BattleframeLeftWeapon, - VehicleSubsystemEntry.BattleframeRightWeapon, - VehicleSubsystemEntry.BattleframeGunnerWeapon + VehicleSubsystemEntry.BattleframeRightArm//, +// VehicleSubsystemEntry.BattleframeLeftWeapon, +// VehicleSubsystemEntry.BattleframeRightWeapon, +// VehicleSubsystemEntry.BattleframeGunnerWeapon ) ++ bfrSubsystems - val bfrFlightSubsystems = List( + val bfrFlightSubsystems: List[VehicleSubsystemEntry] = List( VehicleSubsystemEntry.BattleframeFlightLeftArm, VehicleSubsystemEntry.BattleframeFlightRightArm, VehicleSubsystemEntry.BattleframeFlightLeftWeapon, @@ -1735,7 +1735,7 @@ object GlobalDefinitionsVehicle { aphelion_gunner.AutoPilotSpeeds = (5, 1) aphelion_gunner.Packet = battleFrameConverter aphelion_gunner.DestroyedModel = None - aphelion_gunner.destructionDelay = Some(4000L) + //aphelion_gunner.destructionDelay = Some(4000L) aphelion_gunner.JackingDuration = Array(0, 62, 60, 30) aphelion_gunner.RadiationShielding = 0.5f aphelion_gunner.DamageUsing = DamageCalculations.AgainstBfr @@ -1787,7 +1787,7 @@ object GlobalDefinitionsVehicle { colossus_gunner.AutoPilotSpeeds = (5, 1) colossus_gunner.Packet = battleFrameConverter colossus_gunner.DestroyedModel = None - colossus_gunner.destructionDelay = Some(4000L) + //colossus_gunner.destructionDelay = Some(4000L) colossus_gunner.JackingDuration = Array(0, 62, 60, 30) colossus_gunner.RadiationShielding = 0.5f colossus_gunner.DamageUsing = DamageCalculations.AgainstBfr @@ -1839,7 +1839,7 @@ object GlobalDefinitionsVehicle { peregrine_gunner.AutoPilotSpeeds = (5, 1) peregrine_gunner.Packet = battleFrameConverter peregrine_gunner.DestroyedModel = None - peregrine_gunner.destructionDelay = Some(4000L) + //peregrine_gunner.destructionDelay = Some(4000L) peregrine_gunner.JackingDuration = Array(0, 62, 60, 30) peregrine_gunner.RadiationShielding = 0.5f peregrine_gunner.DamageUsing = DamageCalculations.AgainstBfr diff --git a/src/main/scala/net/psforever/objects/vehicles/VehicleSubsystem.scala b/src/main/scala/net/psforever/objects/vehicles/VehicleSubsystem.scala index 29033e8ba..364ce5431 100644 --- a/src/main/scala/net/psforever/objects/vehicles/VehicleSubsystem.scala +++ b/src/main/scala/net/psforever/objects/vehicles/VehicleSubsystem.scala @@ -18,7 +18,7 @@ sealed abstract class VehicleSubsystemConditionModifier( ) extends IntEnumEntry object VehicleSubsystemConditionModifier extends IntEnum[VehicleSubsystemConditionModifier] { - val values = findValues + val values: IndexedSeq[VehicleSubsystemConditionModifier] = findValues case object Off extends VehicleSubsystemConditionModifier(value = 1065353216, multiplier = 0f, addend = 0) diff --git a/src/main/scala/net/psforever/objects/vehicles/control/BfrControl.scala b/src/main/scala/net/psforever/objects/vehicles/control/BfrControl.scala index 69d60d4b1..9dbc3436d 100644 --- a/src/main/scala/net/psforever/objects/vehicles/control/BfrControl.scala +++ b/src/main/scala/net/psforever/objects/vehicles/control/BfrControl.scala @@ -134,7 +134,7 @@ class BfrControl(vehicle: Vehicle) item.asInstanceOf[Tool], dimorph.transform(Handiness.Generic).asInstanceOf[ToolDefinition] ) - case _ => ; //no dimorphic entry; place as-is + case _ => () //no dimorphic entry; place as-is } val guid0 = PlanetSideGUID(0) //if the weapon arm is disabled, enable it for later (makes life easy) @@ -173,7 +173,8 @@ class BfrControl(vehicle: Vehicle) override def dismountCleanup(seatBeingDismounted: Int, player: Player): Unit = { super.dismountCleanup(seatBeingDismounted, player) if (!vehicle.Seats.values.exists(_.isOccupied)) { - vehicle.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator) match { + vehicle + .Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator) match { case Some(subsys) => if (vehicle.Shields > 0) { vehicleSubsystemMessages( @@ -190,7 +191,7 @@ class BfrControl(vehicle: Vehicle) } ) } - case _ => ; + case _ => () } } } @@ -203,7 +204,7 @@ class BfrControl(vehicle: Vehicle) if !subsys.Enabled && vehicle.Shields > 0 && subsys.Enabled_=(state = true) => //if the shield is damaged, it does not turn on until the damaged is cleared vehicleSubsystemMessages(subsys.changedMessages(vehicle)) - case _ => ; + case _ => () } } } @@ -425,9 +426,9 @@ class BfrControl(vehicle: Vehicle) zone.id, VehicleAction.GenericObjectAction(doNotSendTo, useThisGuid, action) ) - case _ => ; + case _ => () } - case _ => ; + case _ => () } } } @@ -602,7 +603,7 @@ class BfrControl(vehicle: Vehicle) ) ) } - case _ => ; + case _ => () } } } diff --git a/src/main/scala/net/psforever/objects/vehicles/control/BfrFlightControl.scala b/src/main/scala/net/psforever/objects/vehicles/control/BfrFlightControl.scala index 144ecb7d9..78d9e6387 100644 --- a/src/main/scala/net/psforever/objects/vehicles/control/BfrFlightControl.scala +++ b/src/main/scala/net/psforever/objects/vehicles/control/BfrFlightControl.scala @@ -58,20 +58,20 @@ class BfrFlightControl(vehicle: Vehicle) case Some(drain) if localFlyingValue.isEmpty => //shields off disableShield() - vehicle.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator).get.Enabled = false + vehicle.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator).map(s => s.Enabled = false) vehicle.Shields -= drain showShieldCharge() case None if localFlyingValue.isEmpty => //shields off disableShield() - vehicle.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator).get.Enabled = false + vehicle.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator).map(s => s.Enabled = false) case Some(drain) => vehicle.Shields -= drain showShieldCharge() case _ => ; } } - if (vehicle.Subsystems(VehicleSubsystemEntry.BattleframeFlightPod).get.Jammed) { + if (vehicle.Subsystems(VehicleSubsystemEntry.BattleframeFlightPod).exists(_.Jammed)) { } @@ -79,7 +79,7 @@ class BfrFlightControl(vehicle: Vehicle) if (flying.nonEmpty) { flying = None vehicle.Flying = None - vehicle.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator).get.Enabled = true + vehicle.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator).map(s => s.Enabled = true) if (vehicle.Shields > 0) { enableShield() } diff --git a/src/main/scala/net/psforever/objects/vital/resolution/ResolutionCalculations.scala b/src/main/scala/net/psforever/objects/vital/resolution/ResolutionCalculations.scala index e6112726b..cc6884073 100644 --- a/src/main/scala/net/psforever/objects/vital/resolution/ResolutionCalculations.scala +++ b/src/main/scala/net/psforever/objects/vital/resolution/ResolutionCalculations.scala @@ -354,7 +354,7 @@ object ResolutionCalculations { { data.cause.source.DamageToBattleframeOnly || data.cause.source.DamageToVehicleOnly || - !obj.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator).get.Enabled || + !obj.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator).exists(_.Enabled) || obj.Shields == 0 } ) From 662844b8454dd8a04c89e4c35ca0668c7c50fda7 Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Thu, 31 Jul 2025 01:12:28 -0400 Subject: [PATCH 55/57] aded an early test to determine if player account database is active (#1281) --- .../net/psforever/actors/net/LoginActor.scala | 371 +++++++++++------- 1 file changed, 228 insertions(+), 143 deletions(-) diff --git a/src/main/scala/net/psforever/actors/net/LoginActor.scala b/src/main/scala/net/psforever/actors/net/LoginActor.scala index 4b9a87418..5eb8bb14e 100644 --- a/src/main/scala/net/psforever/actors/net/LoginActor.scala +++ b/src/main/scala/net/psforever/actors/net/LoginActor.scala @@ -24,7 +24,6 @@ import org.joda.time.LocalDateTime import scala.collection.mutable import scala.concurrent.Future import scala.concurrent.duration._ -import scala.util.matching.Regex import scala.util.{Failure, Success} object LoginActor { @@ -33,94 +32,31 @@ object LoginActor { private case object UpdateServerList extends Command final case class ReceptionistListing(listing: Receptionist.Listing) extends Command -} -class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], connectionId: String, sessionId: Long) - extends Actor - with MDCContextAware { - - import scala.concurrent.ExecutionContext.Implicits.global - - val usernameRegex: Regex = """[A-Za-z0-9]{3,}""".r - - var leftRef: ActorRef = Default.Actor - var rightRef: ActorRef = Default.Actor - var accountIntermediary: ActorRef = Default.Actor - var sockets: typed.ActorRef[SocketPane.Command] = Default.typed.Actor - - var updateServerListTask: Cancellable = Default.Cancellable - - var ipAddress: String = "" - var hostName: String = "" - var canonicalHostName: String = "" - var port: Int = 0 - - val serverName: String = Config.app.world.serverName - val gameTestServerAddress = new InetSocketAddress(InetAddress.getByName(Config.app.public), Config.app.world.port) - - private val bcryptRounds = 12 - - ServiceManager.serviceManager ! Lookup("accountIntermediary") - ServiceManager.receptionist ! Receptionist.Find(SocketPane.SocketPaneKey, context.self) - - override def postStop(): Unit = { - if (updateServerListTask != null) - updateServerListTask.cancel() - } - - def receive: Receive = { - case ServiceManager.LookupResult("accountIntermediary", endpoint) => - accountIntermediary = endpoint - - case SocketPane.SocketPaneKey.Listing(listings) => - sockets = listings.head - - case ReceiveIPAddress(address) => - ipAddress = address.Address - hostName = address.HostName - canonicalHostName = address.CanonicalHostName - port = address.Port - - case LoginActor.UpdateServerList => - updateServerList() - - case packet: PlanetSideGamePacket => - handleGamePkt(packet) - - case SocketPane.NextPort(_, _, portNum) => - val address = gameTestServerAddress.getAddress.getHostAddress - log.info(s"Connecting to ${address.toLowerCase}: $portNum ...") - val response = ConnectToWorldMessage(serverName, address, portNum) - middlewareActor ! MiddlewareActor.Send(response) - middlewareActor ! MiddlewareActor.Close() - - case default => - failWithError(s"Invalid packet class received: $default") - } - - def handleGamePkt(pkt: PlanetSideGamePacket): Unit = - pkt match { - case LoginMessage(majorVersion, minorVersion, buildDate, username, password, token, revision) => - // TODO: prevent multiple LoginMessages from being processed in a row!! We need a state machine - val clientVersion = s"Client Version: $majorVersion.$minorVersion.$revision, $buildDate" - if (token.isDefined) - log.debug(s"New login UN:$username Token:${token.get}. $clientVersion") - else { - log.debug(s"New login UN:$username. $clientVersion") - } - requestAccountLogin(username, password, token) - - case ConnectToWorldRequestMessage(name, _, _, _, _, _, _, _) => - log.info(s"Request to connect to world '$name' ...") - sockets ! SocketPane.GetNextPort("world", context.self) - - case _ => - log.warning(s"Unhandled GamePacket $pkt") + /** + * What does a token do? + * No one knows. + * @return a 32-bit ascii string + */ + private def generateToken(): String = { + val r = new scala.util.Random + val sb = new mutable.StringBuilder + for (_ <- 1 to 31) { + sb.append(r.nextPrintableChar()) } + sb.toString + } - // generates a password from username and password combination - // mimics the process the launcher follows and hashes the password salted by the username - def generateNewPassword(username: String, password: String): String = { + /** + * Generates a new password from username and password combination, + * hashing the initial password when salted by the username, + * mimicking the process the launcher follows. + * @param username part of the original details + * @param password part of the original details + * @param rounds number of times cryptographic mutation occurs + * @return new password + */ + private def generateNewPassword(username: String, password: String, rounds: Int): String = { // salt password hash with username (like the launcher does) (username + password) val saltedPassword = username.concat(password) // https://stackoverflow.com/a/46332228 @@ -129,20 +65,178 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne .digest(saltedPassword.getBytes("UTF-8")) .map("%02x".format(_)).mkString // bcrypt hash for DB storage - val bcryptedPassword = hashedPassword.bcryptBounded(bcryptRounds) + val bcryptedPassword = hashedPassword.bcryptBounded(rounds) bcryptedPassword } - def requestAccountLogin(username: String, passwordOpt: Option[String], tokenOpt: Option[String]): Unit = { - tokenOpt match { - case Some(token) => accountLoginWithToken(token) - case None => accountLogin(username, passwordOpt.getOrElse("")) + /** + * Remove flavor from the server name that should not show up in the log. + * @param name original name + * @return sanitized name + */ + private def sanitizeServerName(name: String): String = { + //remove color codes from the server name - look for '\\#' followed by six characters or numbers + name.replaceAll("\\\\#[\\da-fA-F]{6}","") + } +} + +class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], connectionId: String, sessionId: Long) + extends Actor + with MDCContextAware { + import scala.concurrent.ExecutionContext.Implicits.global + + //private val usernameRegex: Regex = """[A-Za-z\d]{3,}""".r might be useful one day + private var accountIntermediary: ActorRef = Default.Actor + private var sockets: typed.ActorRef[SocketPane.Command] = Default.typed.Actor + + private var updateServerListTask: Cancellable = Default.Cancellable + + private var ipAddress: String = "" + private var hostName: String = "" + private var canonicalHostName: String = "" + private var port: Int = 0 + + private val serverName: String = Config.app.world.serverName + private val gameTestServerAddress = new InetSocketAddress(InetAddress.getByName(Config.app.public), Config.app.world.port) + + private val bcryptRounds = 12 + + override def preStart(): Unit = { + super.preStart() + ServiceManager.serviceManager ! Lookup("accountIntermediary") + ServiceManager.receptionist ! Receptionist.Find(SocketPane.SocketPaneKey, context.self) + } + + override def postStop(): Unit = { + if (updateServerListTask != null) + updateServerListTask.cancel() + } + + def receive: Receive = beforeLoginBehavior + + private def persistentSetupMixinBehavior: Receive = { + case ServiceManager.LookupResult("accountIntermediary", endpoint) => + accountIntermediary = endpoint + + case SocketPane.SocketPaneKey.Listing(listings) => + sockets = listings.head + } + + private def idlingBehavior: Receive = persistentSetupMixinBehavior.orElse { + case _ => () + } + + private def beforeLoginBehavior: Receive = persistentSetupMixinBehavior.orElse { + case ReceiveIPAddress(address) => + ipAddress = address.Address + hostName = address.HostName + canonicalHostName = address.CanonicalHostName + port = address.Port + context.become(idlingBehavior) + runLoginTest() + + case _ => () + } + + private def accountLoginBehavior: Receive = persistentSetupMixinBehavior.orElse { + case packet: PlanetSideGamePacket => + handleGamePktDuringLogin(packet) + + case default => + failWithError(s"Invalid packet class received: $default") + } + + private def displayingServerListBehavior: Receive = persistentSetupMixinBehavior.orElse { + case packet: PlanetSideGamePacket => + handleGamePktDuringWorldSelect(packet) + + case LoginActor.UpdateServerList => + updateServerList() + + case SocketPane.NextPort(_, _, portNum) => + val address = gameTestServerAddress.getAddress.getHostAddress + log.info(s"Connecting to ${address.toLowerCase}: $portNum ...") + val response = ConnectToWorldMessage(serverName, address, portNum) + context.become(idlingBehavior) + middlewareActor ! MiddlewareActor.Send(response) + middlewareActor ! MiddlewareActor.Close() + + case default => + failWithError(s"Invalid packet class received: $default") + } + + private def waitingForServerTransferBehavior: Receive = persistentSetupMixinBehavior.orElse { + case SocketPane.NextPort(_, _, portNum) => + val address = gameTestServerAddress.getAddress.getHostAddress + log.info(s"Connecting to ${address.toLowerCase}: $portNum ...") + val response = ConnectToWorldMessage(serverName, address, portNum) + context.become(idlingBehavior) + middlewareActor ! MiddlewareActor.Send(response) + middlewareActor ! MiddlewareActor.Close() + + case _ => () + } + + private def handleGamePktDuringLogin(pkt: PlanetSideGamePacket): Unit = { + pkt match { + case LoginMessage(majorVersion, minorVersion, buildDate, username, _, Some(token), revision) => + val clientVersion = s"Client Version: $majorVersion.$minorVersion.$revision, $buildDate" + log.debug(s"New login UN:$username Token:$token. $clientVersion") + context.become(idlingBehavior) + accountLoginWithToken(token) + + case LoginMessage(majorVersion, minorVersion, buildDate, username, password, None, revision) => + val clientVersion = s"Client Version: $majorVersion.$minorVersion.$revision, $buildDate" + log.debug(s"New login UN:$username. $clientVersion") + context.become(idlingBehavior) + accountLogin(username, password.getOrElse("")) + + case _ => + log.warning(s"Unhandled GamePacket $pkt") } } - def accountLogin(username: String, password: String): Unit = { + private def handleGamePktDuringWorldSelect(pkt: PlanetSideGamePacket): Unit = { + pkt match { + case ConnectToWorldRequestMessage(name, _, _, _, _, _, _, _) => + val sanitizedName = LoginActor.sanitizeServerName(name) + log.info(s"Request to connect to world '$sanitizedName' ...") + context.become(waitingForServerTransferBehavior) + sockets ! SocketPane.GetNextPort("world", context.self) + + case _ => + log.warning(s"Unhandled GamePacket $pkt") + } + } + + private def runLoginTest(): Unit = { import ctx._ - val newToken = this.generateToken() + val result = for { + accountsExact <- ctx.run(query[persistence.Account].filter(_.username == lift("PSForever"))) + accountOption <- accountsExact.headOption match { + case Some(account) => + Future.successful(Some(account)) + case None => + Future.successful(None) + } + } yield accountOption + + result.onComplete { + case Success(Some(_)) => + context.become(accountLoginBehavior) // account found + case Success(None) => + middlewareActor ! MiddlewareActor.Send(DisconnectMessage("Character database not found; stopping ...")) + middlewareActor ! MiddlewareActor.Close() + case Failure(e) => + log.error(e.getMessage) + middlewareActor ! MiddlewareActor.Send(DisconnectMessage("Encountered login error; stopping ...")) + middlewareActor ! MiddlewareActor.Close() + } + } + + private def accountLogin(username: String, password: String): Unit = { + import ctx._ + val newToken = LoginActor.generateToken() val result = for { // backwards compatibility: prefer exact match first, then try lowercase accountsExact <- ctx.run(query[persistence.Account].filter(_.username == lift(username))) @@ -160,7 +254,7 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne case None => if (Config.app.login.createMissingAccounts) { // generate bcrypted passwords - val bcryptedPassword = generateNewPassword(username, password) + val bcryptedPassword = LoginActor.generateNewPassword(username, password, bcryptRounds) val passhash = password.bcryptBounded(bcryptRounds) // save bcrypted password hash to DB ctx.run( @@ -201,7 +295,7 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne if (account.password == "") { // generate bcrypted password // use username as provided by the user (db entry could be wrong), that is the way the launcher does it - val bcryptedPassword = generateNewPassword(username, password) + val bcryptedPassword = LoginActor.generateNewPassword(username, password, bcryptRounds) // update account, set password ctx.run( query[persistence.Account] @@ -210,31 +304,33 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne ) } loginSuccessfulResponse(username, newToken) + context.become(displayingServerListBehavior) updateServerListTask = context.system.scheduler.scheduleWithFixedDelay(0 seconds, 5 seconds, self, LoginActor.UpdateServerList) future case (_, false) => - loginPwdFailureResponse(username, newToken) - Future.successful(None) + loginFailurePasswordResponse(username, newToken) + loginFailureAction() case (true, _) => loginAccountFailureResponse(username, newToken) - Future.successful(None) + loginFailureAction() } - case None => Future.successful(None) + case None => + loginFailureAction() } } yield login result.onComplete { - case Success(_) => + case Success(_) => () case Failure(e) => log.error(e.getMessage) } } - def accountLoginWithToken(token: String): Unit = { + private def accountLoginWithToken(token: String): Unit = { import ctx._ - val newToken = this.generateToken() + val newToken = LoginActor.generateToken() val result = for { accountsExact <- ctx.run(query[persistence.Account].filter(_.token.getOrNull == lift(token))) accountOption <- accountsExact.headOption match { @@ -267,33 +363,35 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne _.port -> lift(port) ) ) - loginSuccessfulResponseToken(account.username, token, newToken) + loginSuccessfulResponseWithToken(account.username, token, newToken) + context.become(displayingServerListBehavior) updateServerListTask = context.system.scheduler.scheduleWithFixedDelay(0 seconds, 5 seconds, self, LoginActor.UpdateServerList) future case (_, false) => loginFailureResponseToken(account.username, token, newToken) - Future.successful(None) + loginFailureAction() case (true, _) => loginAccountFailureResponseToken(account.username, token, newToken) - Future.successful(None) + loginFailureAction() } - case None => Future.successful(None) + case None => + loginFailureAction() } } yield login result.onComplete { - case Success(_) => + case Success(_) => () case Failure(e) => log.error(e.getMessage) } } - def loginSuccessfulResponse(username: String, newToken: String): Unit = { + private def loginSuccessfulResponse(username: String, token: String): Unit = { middlewareActor ! MiddlewareActor.Send( LoginRespMessage( - newToken, + token, LoginError.Success, StationError.AccountActive, StationSubscriptionStatus.Active, @@ -304,26 +402,21 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne ) } - def loginSuccessfulResponseToken(username: String, token: String, newToken: String): Unit = { - log.info(s"User $username logged in unsing token $token") - middlewareActor ! MiddlewareActor.Send( - LoginRespMessage( - newToken, - LoginError.Success, - StationError.AccountActive, - StationSubscriptionStatus.Active, - 0, - username, - 10001 - ) - ) + private def loginSuccessfulResponseWithToken(username: String, token: String, newToken: String): Unit = { + log.info(s"User $username logged in using token $token") + loginSuccessfulResponse(username, newToken) } - def loginPwdFailureResponse(username: String, newToken: String): Unit = { + private def loginFailureAction(): Future[Any] = { + context.become(accountLoginBehavior) + Future.successful(None) + } + + private def loginFailurePasswordResponse(username: String, token: String): Unit = { log.warning(s"Failed login to account $username") middlewareActor ! MiddlewareActor.Send( LoginRespMessage( - newToken, + token, LoginError.BadUsernameOrPassword, StationError.AccountActive, StationSubscriptionStatus.Active, @@ -334,7 +427,7 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne ) } - def loginFailureResponseToken(token: String, newToken: String): Unit = { + private def loginFailureResponseToken(token: String, newToken: String): Unit = { log.warning(s"Failed login using unknown token $token") middlewareActor ! MiddlewareActor.Send( LoginRespMessage( @@ -349,7 +442,7 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne ) } - def loginFailureResponseTokenExpired(token: String, newToken: String): Unit = { + private def loginFailureResponseTokenExpired(token: String, newToken: String): Unit = { log.warning(s"Failed login using expired token $token") middlewareActor ! MiddlewareActor.Send( LoginRespMessage( @@ -364,11 +457,11 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne ) } - def loginFailureResponse(username: String, newToken: String): Unit = { + private def loginFailureResponse(username: String, token: String): Unit = { log.warning(s"DB problem username: $username") middlewareActor ! MiddlewareActor.Send( LoginRespMessage( - newToken, + token, LoginError.unk1, StationError.AccountActive, StationSubscriptionStatus.Active, @@ -379,7 +472,7 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne ) } - def loginFailureResponseToken(username: String, token: String, newToken: String): Unit = { + private def loginFailureResponseToken(username: String, token: String, newToken: String): Unit = { log.warning(s"DB problem username $username token: $token") middlewareActor ! MiddlewareActor.Send( LoginRespMessage( @@ -394,11 +487,11 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne ) } - def loginAccountFailureResponse(username: String, newToken: String): Unit = { + private def loginAccountFailureResponse(username: String, token: String): Unit = { log.warning(s"Account $username inactive") middlewareActor ! MiddlewareActor.Send( LoginRespMessage( - newToken, + token, LoginError.BadUsernameOrPassword, StationError.AccountClosed, StationSubscriptionStatus.Active, @@ -409,7 +502,7 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne ) } - def loginAccountFailureResponseToken(username: String, token: String, newToken: String): Unit = { + private def loginAccountFailureResponseToken(username: String, token: String, newToken: String): Unit = { log.warning(s"Account $username inactive token: $token ") middlewareActor ! MiddlewareActor.Send( LoginRespMessage( @@ -424,16 +517,8 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne ) } - def generateToken(): String = { - val r = new scala.util.Random - val sb = new mutable.StringBuilder - for (_ <- 1 to 31) { - sb.append(r.nextPrintableChar()) - } - sb.toString - } - - def updateServerList(): Unit = { + private def updateServerList(): Unit = { + //todo list of game servers from database, eventually, which is a separation of game server from login server middlewareActor ! MiddlewareActor.Send( VNLWorldStatusMessage( "Welcome to PlanetSide! ", @@ -450,7 +535,7 @@ class LoginActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], conne ) } - def failWithError(error: String): Unit = { + private def failWithError(error: String): Unit = { log.error(error) middlewareActor ! MiddlewareActor.Close() } From c1d9f679b2adb979ba6f124101ee2f32d20633da Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Thu, 31 Jul 2025 01:13:32 -0400 Subject: [PATCH 56/57] No Safe Spaces (#1283) * local zone maintains information about weapon fire capability per faction * map reload by faction to represent a change in weapons fire permissions via LMM --- .../actors/session/csr/ChatLogic.scala | 130 ++++++++++++++++++ .../session/normal/LocalHandlerLogic.scala | 23 +++- .../actors/session/spectator/ChatLogic.scala | 2 +- .../session/support/ChatOperations.scala | 26 +++- .../session/support/ZoningOperations.scala | 4 +- .../net/psforever/objects/zones/Zone.scala | 46 +++++++ .../services/local/LocalService.scala | 8 ++ .../services/local/LocalServiceMessage.scala | 1 + .../services/local/LocalServiceResponse.scala | 2 + 9 files changed, 235 insertions(+), 7 deletions(-) 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 24c5c8851..f90de34da 100644 --- a/src/main/scala/net/psforever/actors/session/csr/ChatLogic.scala +++ b/src/main/scala/net/psforever/actors/session/csr/ChatLogic.scala @@ -18,6 +18,7 @@ import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.chat.{ChatChannel, DefaultChannel, SpectatorChannel} import net.psforever.types.ChatMessageType.{CMT_TOGGLESPECTATORMODE, CMT_TOGGLE_GM} import net.psforever.types.{ChatMessageType, PlanetSideEmpire} +import net.psforever.zones.Zones import scala.util.Success @@ -225,6 +226,7 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext case "hidespectators" => customCommandHideSpectators() case "sayspectator" => customCommandSpeakAsSpectator(params, message) case "setempire" => customCommandSetEmpire(params) + case "weaponlock" => customCommandZoneWeaponUnlock(session, params) case _ => // command was not handled sendResponse( @@ -411,6 +413,134 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext } } + def customCommandZoneWeaponUnlock(session: Session, params: Seq[String]): Boolean = { + val usageMessage: Boolean = params.exists(_.matches("--help")) || params.exists(_.matches("-h")) + val formattedParams = ops.cliCommaSeparatedParams(params) + //handle params + val (zoneList, verifiedZones, factionList, verifiedFactions, stateOpt) = (formattedParams.headOption, formattedParams.lift(1), formattedParams.lift(2)) match { + case _ if usageMessage => + (Nil, Nil, Nil, Nil, None) + + case (None, None, None) => + ( + Seq(session.zone.id), + Seq(session.zone), + PlanetSideEmpire.values.map(_.toString()).toSeq, + PlanetSideEmpire.values.toSeq, + Some(true) + ) + + case (Some(zoneOrFaction), Some(factionOrZone), stateOpt) => + val factionOrZoneSplit = factionOrZone.split(",").toSeq + val zoneOrFactionSplit = zoneOrFaction.split(",").toSeq + val tryToFactions = factionOrZoneSplit.flatten(s => ops.captureBaseParamFaction(session, Some(s))) + if (tryToFactions.isEmpty) { + ( + factionOrZoneSplit, + customCommandZoneParse(factionOrZoneSplit), + zoneOrFactionSplit, + zoneOrFactionSplit.flatten(s => ops.captureBaseParamFaction(session, Some(s))), + customCommandOnOffStateOrNone(stateOpt) + ) + } else { + ( + zoneOrFactionSplit, + customCommandZoneParse(zoneOrFactionSplit), + factionOrZoneSplit, + tryToFactions, + customCommandOnOffStateOrNone(stateOpt) + ) + } + + case (Some(zoneOrFaction), stateOpt, None) => + val zoneOrFactionSplit = zoneOrFaction.split(",").toSeq + val tryToFactions = zoneOrFactionSplit.flatten(s => ops.captureBaseParamFaction(session, Some(s))) + if (tryToFactions.isEmpty) { + ( + zoneOrFactionSplit, + customCommandZoneParse(zoneOrFactionSplit), + PlanetSideEmpire.values.map(_.toString()).toSeq, + PlanetSideEmpire.values.toSeq, + customCommandOnOffStateOrNone(stateOpt) + ) + } else { + ( + Seq(session.zone.id), + Seq(session.zone), + zoneOrFactionSplit, + tryToFactions, + customCommandOnOffStateOrNone(stateOpt) + ) + } + + case (stateOpt, None, None) => + ( + Seq(session.zone.id), + Seq(session.zone), + PlanetSideEmpire.values.map(_.toString()).toSeq, + PlanetSideEmpire.values.toSeq, + customCommandOnOffState(stateOpt) + ) + } + //resolve + if (usageMessage) { + sendResponse(ChatMsg(ChatMessageType.UNK_227, "!weaponlock [zone[,...]] [faction[,...]] [o[n]|of[f]]")) + } else if (zoneList.isEmpty || verifiedZones.isEmpty || zoneList.size != verifiedZones.size) { + sendResponse(ChatMsg(ChatMessageType.UNK_227, "some zones can not be verified")) + } else if (factionList.isEmpty || verifiedFactions.isEmpty || factionList.size != verifiedFactions.size) { + sendResponse(ChatMsg(ChatMessageType.UNK_227, "some factions can not be verified")) + } else if (stateOpt.isEmpty) { + sendResponse(ChatMsg(ChatMessageType.UNK_227, "state must be on or off")) + } else { + val state = !stateOpt.get + verifiedZones.foreach { zone => + val events = zone.AvatarEvents + val zoneId = zone.id + //val reloadZoneMsg = AvatarAction.ReloadZone(zone) + zone + .UpdateLiveFireAllowed(state, verifiedFactions) + .foreach { + case (_, false, _) => () + case (faction, true, _) => + //events ! AvatarServiceMessage(s"$faction", reloadZoneMsg) + } + } + } + true + } + + private def customCommandOnOffStateOrNone(stateOpt: Option[String]): Option[Boolean] = { + stateOpt match { + case None => + Some(true) + case _ => + customCommandOnOffState(stateOpt) + } + } + + private def customCommandOnOffState(stateOpt: Option[String]): Option[Boolean] = { + stateOpt match { + case Some("o") | Some("on") => + Some(false) + case Some("of") | Some("off") => + Some(true) + case _ => + None + } + } + + def customCommandZoneParse(potentialZones: Seq[String]): Seq[Zone] = { + potentialZones.flatten { potentialZone => + if (potentialZone.toIntOption.nonEmpty) { + val xInt = potentialZone.toInt + Zones.zones.find(_.Number == xInt) + } else { + Zones.zones.find(z => z.id.equals(potentialZone)) + } + } + } + + override def stop(): Unit = { super.stop() seeSpectatorsIn.foreach(_ => customCommandHideSpectators()) 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 3b7538a5a..1b1764743 100644 --- a/src/main/scala/net/psforever/actors/session/normal/LocalHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/LocalHandlerLogic.scala @@ -8,9 +8,9 @@ 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.{InterstellarClusterService, Service} import net.psforever.services.local.LocalResponse -import net.psforever.types.{ChatMessageType, PlanetSideGUID} +import net.psforever.types.{ChatMessageType, PlanetSideGUID, SpawnGroup} object LocalHandlerLogic { def apply(ops: SessionLocalHandlers): LocalHandlerLogic = { @@ -240,6 +240,25 @@ class LocalHandlerLogic(val ops: SessionLocalHandlers, implicit val context: Act sendResponse(InventoryStateMessage(weapon.AmmoSlot.Box.GUID, weapon.GUID, weapon.Magazine)) } + case LocalResponse.ForceZoneChange(zone) => + //todo we might be able to piggyback this for squad recalls later + if(session.zone eq zone) { + sessionLogic.zoning.zoneReload = true + zone.AvatarEvents ! Service.Leave() + zone.LocalEvents ! Service.Leave() + zone.VehicleEvents ! Service.Leave() + zone.AvatarEvents ! Service.Join(player.Name) //must manually restore this subscriptions + sessionLogic.zoning.spawn.handleNewPlayerLoaded(player) //will restart subscriptions and dispatch a LoadMapMessage + } else { + import akka.actor.typed.scaladsl.adapter._ + sessionLogic.cluster ! InterstellarClusterService.GetRandomSpawnPoint( + zone.Number, + player.Faction, + Seq(SpawnGroup.Facility, SpawnGroup.Tower, SpawnGroup.AMS), + context.self + ) + } + case _ => () } } 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 ae2857066..c17b2fdc1 100644 --- a/src/main/scala/net/psforever/actors/session/spectator/ChatLogic.scala +++ b/src/main/scala/net/psforever/actors/session/spectator/ChatLogic.scala @@ -140,7 +140,7 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext case _ => ("", Seq("")) } command match { - case "list" => ops.customCommandList(session, params, message) + case "list" => ops.customCommandList(session, params.toSeq, message) case "nearby" => ops.customCommandNearby(session) case "loc" => ops.customCommandLoc(session, message) case _ => 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 399fede66..7827cd234 100644 --- a/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ChatOperations.scala @@ -17,7 +17,7 @@ import org.log4s.Logger import java.util.concurrent.{Executors, TimeUnit} import scala.annotation.unused -import scala.collection.{Seq, mutable} +import scala.collection.mutable import scala.concurrent.duration._ // import net.psforever.actors.zone.BuildingActor @@ -1034,7 +1034,7 @@ class ChatOperations( } } - private def captureBaseParamFaction( + def captureBaseParamFaction( @unused session: Session, token: Option[String] ): Option[PlanetSideEmpire.Value] = { @@ -1307,6 +1307,28 @@ class ChatOperations( str.replaceAll("\\s+", " ").trim.split("\\s").toList.filter(!_.equals("")) } + def cliCommaSeparatedParams(params: Seq[String]): Seq[String] = { + var len = 0 + var appendNext = false + var formattedParams: Seq[String] = Seq() + params.foreach { + case "," => + appendNext = true + case param if appendNext || param.startsWith(",") => + formattedParams = formattedParams.slice(0, len - 1) :+ formattedParams(len - 1) + "," + param.replaceAll(",", "") + appendNext = param.endsWith(",") + case param if param.endsWith(",") => + formattedParams = formattedParams :+ param.take(param.length-1) + len += 1 + appendNext = true + case param => + formattedParams = formattedParams :+ param + len += 1 + appendNext = false + } + formattedParams + } + def commandIncomingSend(message: ChatMsg): Unit = { sendResponse(message) } 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 c2404d291..7f9aa7315 100644 --- a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala @@ -2160,8 +2160,8 @@ class ZoningOperations( tplayer.avatar = avatar session = session.copy(player = tplayer) //LoadMapMessage causes the client to send BeginZoningMessage, eventually leading to SetCurrentAvatar - val weaponsEnabled = !(mapName.equals("map11") || mapName.equals("map12") || mapName.equals("map13")) - sendResponse(LoadMapMessage(mapName, id, 40100, 25, weaponsEnabled, map.checksum)) + //val weaponsEnabled = !(mapName.equals("map11") || mapName.equals("map12") || mapName.equals("map13")) + sendResponse(LoadMapMessage(mapName, id, 40100, 25, zone.LiveFireAllowed(tplayer.Faction), map.checksum)) if (isAcceptableNextSpawnPoint) { //important! the LoadMapMessage must be processed by the client before the avatar is created player.allowInteraction = true diff --git a/src/main/scala/net/psforever/objects/zones/Zone.scala b/src/main/scala/net/psforever/objects/zones/Zone.scala index cbf828d07..4deac1700 100644 --- a/src/main/scala/net/psforever/objects/zones/Zone.scala +++ b/src/main/scala/net/psforever/objects/zones/Zone.scala @@ -182,6 +182,12 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) { */ private var vehicleEvents: ActorRef = Default.Actor + /** + * Is any player permitted to engage in weapons discharge in this zone? + */ + private var liveFireAllowed: mutable.HashMap[PlanetSideEmpire.Value, Boolean] = + mutable.HashMap.from(PlanetSideEmpire.values.map { f => (f, true) }) + /** * When the zone has completed initializing, fulfill this promise. * @see `init(ActorContext)` @@ -593,6 +599,46 @@ class Zone(val id: String, val map: ZoneMap, zoneNumber: Int) { vehicleEvents = bus VehicleEvents } + + def LiveFireAllowed(): Boolean = liveFireAllowed.exists { case (_, v) => v } + + def LiveFireAllowed(faction: PlanetSideEmpire.Value): Boolean = liveFireAllowed.getOrElse(faction, false) + + def UpdateLiveFireAllowed(state: Boolean): List[(PlanetSideEmpire.Value, Boolean, Boolean)] = { + val output = liveFireAllowed.map { case (f, v) => + (f, v == state, state) + } + output.foreach { case (f, _, v) => + liveFireAllowed.update(f, v) + } + output.toList + } + + def UpdateLiveFireAllowed(state: Boolean, faction: PlanetSideEmpire.Value): List[(PlanetSideEmpire.Value, Boolean, Boolean)] = { + val output = liveFireAllowed.map { case (f, v) => + if (f == faction) { + (f, v == state, state) + } else { + (f, false, v) + } + } + liveFireAllowed.update(faction, state) + output.toList + } + + def UpdateLiveFireAllowed(state: Boolean, factions: Seq[PlanetSideEmpire.Value]): List[(PlanetSideEmpire.Value, Boolean, Boolean)] = { + val output = liveFireAllowed.map { case (f, v) => + if (factions.contains(f)) { + (f, v == state, state) + } else { + (f, false, v) + } + } + factions.foreach { f => + liveFireAllowed.update(f, state) + } + output.toList + } } /** diff --git a/src/main/scala/net/psforever/services/local/LocalService.scala b/src/main/scala/net/psforever/services/local/LocalService.scala index 91d900d3b..751dce97b 100644 --- a/src/main/scala/net/psforever/services/local/LocalService.scala +++ b/src/main/scala/net/psforever/services/local/LocalService.scala @@ -306,6 +306,14 @@ class LocalService(zone: Zone) extends Actor { LocalResponse.RechargeVehicleWeapon(vehicle_guid, weapon_guid) ) ) + case LocalAction.ForceZoneChange(zone) => + LocalEvents.publish( + LocalServiceResponse( + s"/$forChannel/Local", + Service.defaultPlayerGUID, + LocalResponse.ForceZoneChange(zone) + ) + ) case _ => ; } diff --git a/src/main/scala/net/psforever/services/local/LocalServiceMessage.scala b/src/main/scala/net/psforever/services/local/LocalServiceMessage.scala index de4545cc5..ced629458 100644 --- a/src/main/scala/net/psforever/services/local/LocalServiceMessage.scala +++ b/src/main/scala/net/psforever/services/local/LocalServiceMessage.scala @@ -138,4 +138,5 @@ object LocalAction { mountable_guid: PlanetSideGUID, weapon_guid: PlanetSideGUID ) extends Action + final case class ForceZoneChange(zone: Zone) extends Action } diff --git a/src/main/scala/net/psforever/services/local/LocalServiceResponse.scala b/src/main/scala/net/psforever/services/local/LocalServiceResponse.scala index ffb1bb3ff..8834ab197 100644 --- a/src/main/scala/net/psforever/services/local/LocalServiceResponse.scala +++ b/src/main/scala/net/psforever/services/local/LocalServiceResponse.scala @@ -6,6 +6,7 @@ import net.psforever.objects.{PlanetSideGameObject, TelepadDeployable, Vehicle} import net.psforever.objects.ce.{Deployable, DeployedItem} import net.psforever.objects.serverobject.terminals.{ProximityUnit, Terminal} import net.psforever.objects.vehicles.Utility +import net.psforever.objects.zones.Zone import net.psforever.packet.game.GenericObjectActionEnum.GenericObjectActionEnum import net.psforever.packet.game.PlanetsideAttributeEnum.PlanetsideAttributeEnum import net.psforever.packet.PlanetSideGamePacket @@ -86,4 +87,5 @@ object LocalResponse { final case class TriggerSound(sound: TriggeredSound.Value, pos: Vector3, unk: Int, volume: Float) extends Response final case class UpdateForceDomeStatus(building_guid: PlanetSideGUID, activated: Boolean) extends Response final case class RechargeVehicleWeapon(mountable_guid: PlanetSideGUID, weapon_guid: PlanetSideGUID) extends Response + final case class ForceZoneChange(zone: Zone) extends Response } From bb858203e49df5f93928f448e712ee4774ba5a4f Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Thu, 31 Jul 2025 01:14:40 -0400 Subject: [PATCH 57/57] Lukewarm Potato (#1285) * removing the cep reward for killing a flag carrier * log flag carrier kill to database as a facility capture event * need to acronym the exp event type, three characters only --- src/main/resources/application.conf | 2 +- .../actors/session/support/GeneralOperations.scala | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf index 27b1e4a5b..20885b9e5 100644 --- a/src/main/resources/application.conf +++ b/src/main/resources/application.conf @@ -469,7 +469,7 @@ game { # If a player died while carrying an lattice logic unit, # and satisfies the carrying duration, # award the player who is accredited with the kill command experience. - llu-slayer-credit = 200 + llu-slayer-credit = 0 # The maximum command experience that can be earned in a facility capture based on squad size maximum-per-squad-size = [990, 1980, 3466, 4950, 6436, 7920, 9406, 10890, 12376, 13860] # When the cep has to be capped for squad size, add a small value to the capped value 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 181a1e746..e72e57a56 100644 --- a/src/main/scala/net/psforever/actors/session/support/GeneralOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/GeneralOperations.scala @@ -15,6 +15,7 @@ import net.psforever.objects.serverobject.turret.FacilityTurret import net.psforever.objects.sourcing.{DeployableSource, PlayerSource, VehicleSource} import net.psforever.objects.vehicles.Utility.InternalTelepad import net.psforever.objects.zones.blockmap.BlockMapEntity +import net.psforever.objects.zones.exp.ToDatabase import net.psforever.services.RemoverActor import net.psforever.services.local.{LocalAction, LocalServiceMessage} @@ -444,6 +445,13 @@ class GeneralOperations( attacker.Name, AvatarAction.AwardCep(attacker.CharId, Config.app.game.experience.cep.lluSlayerCredit) ) + ToDatabase.reportFacilityCapture( + attacker.CharId, + continent.Number, + llu.Owner.GUID.guid, + Config.app.game.experience.cep.lluSlayerCredit, + expType = "lsc" //three characters - "llu slayer credit" + ) } } if (!CaptureFlagManager.ReasonToLoseFlagViolently(continent, Some(guid), player)) {