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 c07af5d5e..7bcdd81e3 100644
--- a/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala
+++ b/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala
@@ -3,7 +3,6 @@ package net.psforever.actors.session.support
import akka.actor.{ActorContext, typed}
import net.psforever.objects.definition.ProjectileDefinition
-import net.psforever.objects.serverobject.structures.Amenity
import net.psforever.objects.zones.Zoning
import net.psforever.objects.serverobject.turret.{AutomatedTurret, AutomatedTurretBehavior, VanuSentry}
import net.psforever.objects.zones.exp.ToDatabase
@@ -450,16 +449,9 @@ private[support] class WeaponAndProjectileOperations(
turret.Actor ! AutomatedTurretBehavior.ConfirmShot(target)
None
- case turret: AutomatedTurret with OwnableByPlayer => //most likely a deployable
+ case turret: AutomatedTurret =>
turret.Actor ! AutomatedTurretBehavior.ConfirmShot(target)
- val owner = continent.GUID(turret.OwnerGuid)
- .collect { case obj: PlanetSideGameObject with FactionAffinity => SourceEntry(obj) }
- .getOrElse(SourceEntry(turret))
- CompileAutomatedTurretDamageData(turret, owner, projectileTypeId)
-
- case turret: Amenity with AutomatedTurret => //most likely a facility turret
- turret.Actor ! AutomatedTurretBehavior.ConfirmShot(target)
- CompileAutomatedTurretDamageData(turret, SourceEntry(turret.Owner), projectileTypeId)
+ CompileAutomatedTurretDamageData(turret, turret.TurretOwner, projectileTypeId)
}
.collect {
case Some((obj, tool, owner, projectileInfo)) =>
diff --git a/src/main/scala/net/psforever/objects/GlobalDefinitions.scala b/src/main/scala/net/psforever/objects/GlobalDefinitions.scala
index f3f0212df..c1717dc14 100644
--- a/src/main/scala/net/psforever/objects/GlobalDefinitions.scala
+++ b/src/main/scala/net/psforever/objects/GlobalDefinitions.scala
@@ -9078,7 +9078,8 @@ object GlobalDefinitions {
targetTriggerRange = 50f,
targetEscapeRange = 50f,
targetValidation = List(EffectTarget.Validation.PlayerOnRadar, EffectTarget.Validation.Vehicle),
- retaliatoryDuration = 8000L
+ retaliatoryDuration = 8000L,
+ refireTime = 200.milliseconds //150.milliseconds
)
spitfire_turret.innateDamage = new DamageWithPosition {
CausesDamageType = DamageType.One
@@ -9111,7 +9112,8 @@ object GlobalDefinitions {
targetTriggerRange = 30f,
targetEscapeRange = 50f,
targetValidation = List(EffectTarget.Validation.PlayerOnRadar, EffectTarget.Validation.VehiclesOnRadar),
- retaliatoryDuration = 8000L
+ retaliatoryDuration = 8000L,
+ refireTime = 200.milliseconds //150.milliseconds
)
spitfire_cloaked.innateDamage = new DamageWithPosition {
CausesDamageType = DamageType.One
@@ -9146,6 +9148,7 @@ object GlobalDefinitions {
targetValidation = List(EffectTarget.Validation.AircraftOnRadar),
retaliatoryDuration = 2000L,
retaliationOverridesTarget = false,
+ refireTime = 350.milliseconds, //300.milliseconds
cylindricalCheck = true,
cylindricalHeight = 25f
)
@@ -10058,7 +10061,8 @@ object GlobalDefinitions {
retaliatoryDuration = 8000L,
cylindricalCheck = true,
cylindricalHeight = 25f,
- detectionSpeed = 2.seconds
+ detectionSpeed = 2.seconds,
+ refireTime = 362.milliseconds //312.milliseconds
)
manned_turret.innateDamage = new DamageWithPosition {
CausesDamageType = DamageType.One
diff --git a/src/main/scala/net/psforever/objects/TurretDeployable.scala b/src/main/scala/net/psforever/objects/TurretDeployable.scala
index 19bc10f82..504f7e0b6 100644
--- a/src/main/scala/net/psforever/objects/TurretDeployable.scala
+++ b/src/main/scala/net/psforever/objects/TurretDeployable.scala
@@ -13,6 +13,7 @@ import net.psforever.objects.serverobject.damage.Damageable.Target
import net.psforever.objects.serverobject.hackable.Hackable
import net.psforever.objects.serverobject.mount.Mountable
import net.psforever.objects.serverobject.turret.{AutomatedTurret, AutomatedTurretBehavior, MountableTurretControl, TurretDefinition, WeaponTurret}
+import net.psforever.objects.sourcing.SourceEntry
import net.psforever.objects.vital.damage.DamageCalculations
import net.psforever.objects.vital.interaction.DamageResult
import net.psforever.objects.vital.{SimpleResolutions, StandardVehicleResistance}
@@ -28,6 +29,15 @@ class TurretDeployable(tdef: TurretDeployableDefinition)
with AutomatedTurret {
WeaponTurret.LoadDefinition(turret = this)
+ def TurretOwner: SourceEntry = {
+ Seats
+ .headOption
+ .collect { case (_, a) => a }
+ .flatMap(_.occupant)
+ .map(SourceEntry(_))
+ .getOrElse(SourceEntry(this))
+ }
+
override def Definition: TurretDeployableDefinition = tdef
}
@@ -105,7 +115,7 @@ class TurretControl(turret: TurretDeployable)
AutomaticOperation = false
//look in direction of cause of jamming
val zone = JammableObject.Zone
- AutomatedTurretBehavior.getAttackerFromCause(zone, cause).foreach {
+ AutomatedTurretBehavior.getAttackVectorFromCause(zone, cause).foreach {
attacker =>
val channel = zone.id
val guid = AutomatedTurretObject.GUID
diff --git a/src/main/scala/net/psforever/objects/serverobject/turret/AffectedByAutomaticTurretFire.scala b/src/main/scala/net/psforever/objects/serverobject/turret/AffectedByAutomaticTurretFire.scala
new file mode 100644
index 000000000..36e74ecde
--- /dev/null
+++ b/src/main/scala/net/psforever/objects/serverobject/turret/AffectedByAutomaticTurretFire.scala
@@ -0,0 +1,69 @@
+// Copyright (c) 2024 PSForever
+package net.psforever.objects.serverobject.turret
+
+import akka.actor.Actor
+import net.psforever.objects.Tool
+import net.psforever.objects.ballistics.{Projectile, ProjectileQuality}
+import net.psforever.objects.serverobject.damage.Damageable
+import net.psforever.objects.sourcing.SourceEntry
+import net.psforever.objects.vital.base.DamageResolution
+import net.psforever.objects.vital.interaction.DamageInteraction
+import net.psforever.objects.vital.projectile.ProjectileReason
+import net.psforever.types.Vector3
+
+/**
+ * With a timed messaging cycle from `AutomatedTurretBehavior`,
+ * an implementation of this trait should be able to simulate being damaged by a source of automated weapon's fire
+ * without needing a player character to experience the damage directly as is usual for a client's user.
+ * As a drawback, however, it's not possible to validate against collision detection of any sort
+ * so damage could be applied through trees and rocks and walls and other users.
+ */
+trait AffectedByAutomaticTurretFire extends Damageable {
+ _: Actor =>
+
+ def AffectedObject: AutomatedTurret.Target
+
+ val takeAutomatedDamage: Receive = {
+ case AffectedByAutomaticTurretFire.AiDamage(turret) =>
+ performAutomatedDamage(turret)
+ }
+
+ private def performAutomatedDamage(turret: AutomatedTurret): Unit = {
+ val target = AffectedObject
+ val tool = turret.Weapons.values.head.Equipment.collect { case t : Tool => t }.get
+ val projectileInfo = tool.Projectile
+ val targetPos = target.Position
+ val turretPos = turret.Position
+ val correctedTargetPosition = targetPos + Vector3.z(value = 1f)
+ val angle = Vector3.Unit(targetPos - turretPos)
+ turret.Actor ! AutomatedTurretBehavior.ConfirmShot(target, Some(SourceEntry(target)))
+ val projectile = new Projectile(
+ projectileInfo,
+ tool.Definition,
+ tool.FireMode,
+ None,
+ turret.TurretOwner,
+ turret.Definition.ObjectId,
+ turretPos + Vector3.z(value = 1f),
+ angle,
+ Some(angle * projectileInfo.FinalVelocity)
+ )
+ val modProjectile = ProjectileQuality.modifiers(
+ projectile,
+ DamageResolution.Hit,
+ target,
+ correctedTargetPosition,
+ None
+ )
+ val resolvedProjectile = DamageInteraction(
+ SourceEntry(target),
+ ProjectileReason(DamageResolution.Hit, modProjectile, target.DamageModel),
+ correctedTargetPosition
+ )
+ PerformDamage(target, resolvedProjectile.calculate())
+ }
+}
+
+object AffectedByAutomaticTurretFire {
+ case class AiDamage(turret: AutomatedTurret)
+}
diff --git a/src/main/scala/net/psforever/objects/serverobject/turret/AutomatedTurret.scala b/src/main/scala/net/psforever/objects/serverobject/turret/AutomatedTurret.scala
new file mode 100644
index 000000000..04b60d5b6
--- /dev/null
+++ b/src/main/scala/net/psforever/objects/serverobject/turret/AutomatedTurret.scala
@@ -0,0 +1,63 @@
+// Copyright (c) 2024 PSForever
+package net.psforever.objects.serverobject.turret
+
+import net.psforever.objects.definition.ObjectDefinition
+import net.psforever.objects.serverobject.PlanetSideServerObject
+import net.psforever.objects.sourcing.{SourceEntry, SourceUniqueness}
+import net.psforever.objects.vital.Vitality
+
+trait AutomatedTurret
+ extends PlanetSideServerObject
+ with WeaponTurret {
+ import AutomatedTurret.Target
+ private var currentTarget: Option[Target] = None
+
+ private var targets: List[Target] = List[Target]()
+
+ def TurretOwner: SourceEntry
+
+ def Target: Option[Target] = currentTarget
+
+ def Target_=(newTarget: Target): Option[Target] = {
+ Target_=(Some(newTarget))
+ }
+
+ def Target_=(newTarget: Option[Target]): Option[Target] = {
+ if (newTarget.isDefined != currentTarget.isDefined) {
+ currentTarget = newTarget
+ }
+ currentTarget
+ }
+
+ def Targets: List[Target] = targets
+
+ def Detected(target: Target): Option[Target] = {
+ val unique = SourceEntry(target).unique
+ targets.find(SourceEntry(_).unique == unique)
+ }
+
+ def Detected(target: SourceUniqueness): Option[Target] = {
+ targets.find(SourceEntry(_).unique == target)
+ }
+
+ def AddTarget(target: Target): Unit = {
+ targets = targets :+ target
+ }
+
+ def RemoveTarget(target: Target): Unit = {
+ val unique = SourceEntry(target).unique
+ targets = targets.filterNot(SourceEntry(_).unique == unique)
+ }
+
+ def Clear(): List[Target] = {
+ val oldTargets = targets
+ targets = Nil
+ oldTargets
+ }
+
+ def Definition: ObjectDefinition with TurretDefinition
+}
+
+object AutomatedTurret {
+ type Target = PlanetSideServerObject with Vitality
+}
diff --git a/src/main/scala/net/psforever/objects/serverobject/turret/AutomatedTurretBehavior.scala b/src/main/scala/net/psforever/objects/serverobject/turret/AutomatedTurretBehavior.scala
index 7ac17f70e..884d28fbe 100644
--- a/src/main/scala/net/psforever/objects/serverobject/turret/AutomatedTurretBehavior.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/turret/AutomatedTurretBehavior.scala
@@ -1,8 +1,7 @@
-// Copyright (c) 2023 PSForever
+// Copyright (c) 2024 PSForever
package net.psforever.objects.serverobject.turret
-import akka.actor.{Actor, ActorRef, Cancellable}
-import net.psforever.objects.definition.ObjectDefinition
+import akka.actor.{Actor, Cancellable}
import net.psforever.objects.{Default, Player, Vehicle}
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.damage.DamageableEntity
@@ -10,85 +9,34 @@ import net.psforever.objects.sourcing.{SourceEntry, SourceUniqueness}
import net.psforever.objects.vital.Vitality
import net.psforever.objects.vital.interaction.DamageResult
import net.psforever.objects.zones.{InteractsWithZone, Zone}
-import net.psforever.packet.PlanetSideGamePacket
-import net.psforever.packet.game.{ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, ObjectDetectedMessage}
-import net.psforever.services.Service
-import net.psforever.services.local.{LocalAction, LocalServiceMessage}
-import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
import net.psforever.types.{PlanetSideGUID, Vector3}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
-trait AutomatedTurret
- extends PlanetSideServerObject
- with WeaponTurret {
- import AutomatedTurret.Target
- private var currentTarget: Option[Target] = None
-
- private var targets: List[Target] = List[Target]()
-
- def Target: Option[Target] = currentTarget
-
- def Target_=(newTarget: Target): Option[Target] = {
- Target_=(Some(newTarget))
- }
-
- def Target_=(newTarget: Option[Target]): Option[Target] = {
- if (newTarget.isDefined != currentTarget.isDefined) {
- currentTarget = newTarget
- }
- currentTarget
- }
-
- def Targets: List[Target] = targets
-
- def Detected(target: Target): Option[Target] = {
- val unique = SourceEntry(target).unique
- targets.find(SourceEntry(_).unique == unique)
- }
-
- def Detected(target: SourceUniqueness): Option[Target] = {
- targets.find(SourceEntry(_).unique == target)
- }
-
- def AddTarget(target: Target): Unit = {
- targets = targets :+ target
- }
-
- def RemoveTarget(target: Target): Unit = {
- val unique = SourceEntry(target).unique
- targets = targets.filterNot(SourceEntry(_).unique == unique)
- }
-
- def Clear(): List[Target] = {
- val oldTargets = targets
- targets = Nil
- oldTargets
- }
-
- def Definition: ObjectDefinition with TurretDefinition
-}
-
-object AutomatedTurret {
- type Target = PlanetSideServerObject with Vitality
-}
-
trait AutomatedTurretBehavior {
_: Actor with DamageableEntity =>
import AutomatedTurret.Target
-
- private var automaticOperation: Boolean = false
-
- private var currentTargetToken: Option[SourceUniqueness] = None
-
- private var currentTargetLastShotReported: Long = 0L
-
- private var periodicValidationTest: Cancellable = Default.Cancellable
-
+ /** a local reference to the automated turret data on the entity's definition */
private lazy val autoStats: Option[Automation] = AutomatedTurretObject.Definition.AutoFire
-
+ /** whether the automated turret is functional or if anything is blocking its operation */
+ private var automaticOperation: Boolean = false
+ /** quick reference of the current target, if any */
+ private var currentTargetToken: Option[SourceUniqueness] = None
+ /** time of the last confirmed shot hitting the target */
+ private var currentTargetLastShotTime: Long = 0L
+ /** game world position when the last shot's confirmation was recorded */
+ private var currentTargetLocation: Option[Vector3] = None
+ /** targets queued during evaluation of the "next test", and targets to be being confirmed during "this test" */
private var previousTestedTargets: Set[Target] = Set()
+ /** timer managing the available target qualifications test
+ * whether or not a previously valid target is still a valid target */
+ private var periodicValidationTest: Cancellable = Default.Cancellable
+ /** timer managing the trailing target qualifications self test
+ * where the a source will shot directly at some target */
+ private var selfReportedRefire: Cancellable = Default.Cancellable
+
+ private var confirmShotFunc: Target=>Unit = normalConfirmShot
def AutomatedTurretObject: AutomatedTurret
@@ -96,8 +44,11 @@ trait AutomatedTurretBehavior {
case AutomatedTurretBehavior.Alert(target) =>
bringAttentionToTarget(target)
- case AutomatedTurretBehavior.ConfirmShot(target) =>
- confirmShot(target)
+ case AutomatedTurretBehavior.ConfirmShot(target, None) =>
+ confirmShotFunc(target)
+
+ case AutomatedTurretBehavior.ConfirmShot(target, _) =>
+ confirmShotFunc(target)
case AutomatedTurretBehavior.Unalert(target) =>
disregardTarget(target)
@@ -113,6 +64,15 @@ trait AutomatedTurretBehavior {
def AutomaticOperation: Boolean = automaticOperation
+ /**
+ * In relation to whether the automated turret is operational,
+ * set the value of a flag to record this condition.
+ * Additionally, perform actions relevant to the state changes:
+ * turning on when previously inactive;
+ * and, turning off when previously active.
+ * @param state new state
+ * @return state that results from this action
+ */
def AutomaticOperation_=(state: Boolean): Boolean = {
val previousState = automaticOperation
if (autoStats.isDefined) {
@@ -120,25 +80,50 @@ trait AutomatedTurretBehavior {
if (!previousState && state) {
trySelectNewTarget()
} else if (previousState && !state) {
- AutomatedTurretObject.Target.foreach {
- noLongerEngageDetectedTarget
- }
+ cancelSelfReportedAutoFire()
+ AutomatedTurretObject.Target.foreach(noLongerEngageDetectedTarget)
}
state
} else {
- false
+ previousState
}
}
+ /**
+ * A checklist of conditions that must be met before automatic operation of the turret should be possible.
+ * Should not actually change the current activation state of the turret.
+ * @return `true`, if it would be possible for automated behavior to become operational;
+ * `false`, otherwise
+ */
protected def AutomaticOperationFunctionalityChecks: Boolean
- protected def CurrentTargetLastShotReported: Long = currentTargetLastShotReported
+ /**
+ * The last time weapons fire from the turret was confirmed by this control agency.
+ * Exists for subclass access.
+ * @return the time
+ */
+ protected def CurrentTargetLastShotReported: Long = currentTargetLastShotTime
+ /**
+ * Set a new last time weapons fire from the turret was confirmed by this control agency.
+ * Exists for subclass access.
+ * @param value the new time
+ * @return the time
+ */
protected def CurrentTargetLastShotReported_=(value: Long): Long = {
- currentTargetLastShotReported = value
+ currentTargetLastShotTime = value
CurrentTargetLastShotReported
}
+ /* Actor level functions */
+
+ /**
+ * Add a new potential target to the turret's list of known targets
+ * only if this is a new potential target.
+ * If the provided target is the first potential target known to the turret,
+ * begin the timer that determines when or if that target is no longer considered qualified.
+ * @param target something the turret can potentially shoot at
+ */
private def bringAttentionToTarget(target: Target): Unit = {
val targets = AutomatedTurretObject.Targets
val size = targets.size
@@ -150,18 +135,13 @@ trait AutomatedTurretBehavior {
}
}
- private def confirmShot(target: Target): Unit = {
- val now = System.currentTimeMillis()
- if (currentTargetToken.isEmpty) {
- currentTargetLastShotReported = now
- engageNewDetectedTarget(target)
- } else if (
- currentTargetToken.contains(SourceEntry(target).unique) &&
- now - currentTargetLastShotReported < autoStats.map(_.missedShotCooldown).getOrElse(0L)) {
- currentTargetLastShotReported = now
- }
- }
-
+ /**
+ * Remove a target from the turret's list of known targets.
+ * If the provided target is the last potential target known to the turret,
+ * cancel the timer that determines when or if targets are to be considered qualified.
+ * If we are shooting at the target, stop shooting at it.
+ * @param target something the turret can potentially shoot at
+ */
private def disregardTarget(target: Target): Unit = {
val targets = AutomatedTurretObject.Targets
val size = targets.size
@@ -176,42 +156,103 @@ trait AutomatedTurretBehavior {
}
}
+ /**
+ * Undo all the things.
+ * It's like nothing ever happened.
+ */
private def resetAlerts(): Unit = {
+ cancelPeriodicTargetChecks()
+ cancelSelfReportedAutoFire()
AutomatedTurretObject.Target.foreach(noLongerEngageDetectedTarget)
AutomatedTurretObject.Target = None
AutomatedTurretObject.Clear()
- testTargetListQualifications(beforeSize = 1)
previousTestedTargets.foreach(noLongerEngageTestedTarget)
previousTestedTargets = Set()
currentTargetToken = None
+ currentTargetLocation = None
}
+ /* Normal automated turret behavior */
+
+ /**
+ * Process feedback from automatic turret weapon fire.
+ * The most common situation in which this is encountered is when the turret is instructed to shoot at something
+ * and that something reports being hit with the resulting projectile
+ * and, as a result, a message is sent to the turret to encourage it to continue to shoot.
+ * If there is no primary target yet, this target becomes primary.
+ * @param target something the turret can potentially shoot at
+ */
+ private def normalConfirmShot(target: Target): Unit = {
+ val now = System.currentTimeMillis()
+ if (currentTargetToken.isEmpty) {
+ currentTargetLastShotTime = now
+ currentTargetLocation = Some(target.Position)
+ engageNewDetectedTarget(target)
+ } else if (
+ currentTargetToken.contains(SourceEntry(target).unique) &&
+ now - currentTargetLastShotTime < autoStats.map(_.missedShotCooldown).getOrElse(0L)) {
+ currentTargetLastShotTime = now
+ currentTargetLocation = Some(target.Position)
+ }
+ }
+
+ /**
+ * Point the business end of the turret's weapon at a provided target
+ * and begin shooting at that target.
+ * The turret will rotate to follow the target's movements in the game world.
+ * Perform some cleanup of potential targets and
+ * perform setup of variables useful to maintain firepower against the target.
+ * @param target something the turret can potentially shoot at
+ */
protected def engageNewDetectedTarget(target: Target): Unit = {
val zone = target.Zone
val zoneid = zone.id
previousTestedTargets.filterNot(_ == target).foreach(noLongerEngageTestedTarget)
previousTestedTargets = Set(target)
currentTargetToken = Some(SourceEntry(target).unique)
+ currentTargetLocation = Some(target.Position)
AutomatedTurretObject.Target = target
AutomatedTurretBehavior.startTracking(target, zoneid, AutomatedTurretObject.GUID, List(target.GUID))
AutomatedTurretBehavior.startShooting(target, zoneid, AutomatedTurretObject.Weapons.values.head.Equipment.get.GUID)
}
+ /**
+ * If the provided target is the current target:
+ * Stop pointing the business end of the turret's weapon at a provided target.
+ * Stop shooting at the target.
+ * @param target something the turret can potentially shoot at
+ * @return something the turret was potentially shoot at
+ */
protected def noLongerDetectTargetIfCurrent(target: Target): Option[Target] = {
if (currentTargetToken.contains(SourceEntry(target).unique)) {
+ cancelSelfReportedAutoFire()
noLongerEngageDetectedTarget(target)
} else {
AutomatedTurretObject.Target
}
}
+ /**
+ * Stop pointing the business end of the turret's weapon at a provided target.
+ * Stop shooting at the target.
+ * Adjust some local values to disengage from the target.
+ * @param target something the turret can potentially shoot at
+ * @return something the turret was potentially shoot at
+ */
protected def noLongerEngageDetectedTarget(target: Target): Option[Target] = {
AutomatedTurretObject.Target = None
currentTargetToken = None
+ currentTargetLocation = None
noLongerEngageTestedTarget(target)
None
}
+ /**
+ * Stop pointing the business end of the turret's weapon at a provided target.
+ * Stop shooting at the target.
+ * @param target something the turret can potentially shoot at
+ * @return something the turret was potentially shoot at
+ */
private def noLongerEngageTestedTarget(target: Target): Option[Target] = {
val zone = target.Zone
val zoneid = zone.id
@@ -220,6 +261,20 @@ trait AutomatedTurretBehavior {
None
}
+ /**
+ * While the automated turret is operational and active,
+ * and while the turret does not have a current target to point towards and shoot at,
+ * collect all of the potential targets known to the turret
+ * and perform test shots that would only be visible to certain client perspectives.
+ * If those perspectives report back about those test shots being confirmed hits,
+ * the first reported confirmed test shot will be the chosen target.
+ * We will potentially have an old list of targets that were tested the previous pass
+ * and can be compared against a fresher list of targets.
+ * Explicitly order certain unrepresented targets to stop being tested
+ * in case the packets between the server and the client do not get transmitted properly
+ * or the turret is not assembled correctly in its automatic fire definition.
+ * @return something the turret can potentially shoot at
+ */
protected def trySelectNewTarget(): Option[Target] = {
AutomatedTurretObject.Target.orElse {
val turretPosition = AutomatedTurretObject.Position
@@ -238,19 +293,7 @@ trait AutomatedTurretBehavior {
target
}
.sortBy(target => Vector3.DistanceSquared(target.Position, turretPosition))
- .collect {
- case target: Player =>
- AutomatedTurretBehavior.Generic.testNewDetected(target, target.Name, turretGuid, weaponGuid)
- Seq((target, target))
- case target: Vehicle =>
- target.Seats.values
- .flatMap(_.occupants)
- .collectFirst { passenger =>
- AutomatedTurretBehavior.Generic.testNewDetected(passenger, passenger.Name, turretGuid, weaponGuid)
- (target, passenger)
- }
- }
- .flatten
+ .flatMap { processForTestingDetectedTarget(_, turretGuid, weaponGuid) }
.unzip
//call an explicit stop for these targets
(for {
@@ -264,6 +307,45 @@ trait AutomatedTurretBehavior {
}
}
+ /**
+ * Dispatch packets in the direction of a client perspective
+ * to determine if this target can be reliably struck with a projectile from the turret's weapon.
+ * This resolves to a player avatar entity usually and is communicated on that player's personal name channel.
+ * @param target something the turret can potentially shoot at
+ * @param turretGuid turret
+ * @param weaponGuid turret's weapon
+ * @return a tuple composed of:
+ * something the turret can potentially shoot at
+ * something that will report whether the test shot struck the target
+ */
+ private def processForTestingDetectedTarget(
+ target: Target,
+ turretGuid: PlanetSideGUID,
+ weaponGuid: PlanetSideGUID
+ ): Option[(Target, Target)] = {
+ target match {
+ case target: Player =>
+ AutomatedTurretDispatch.Generic.testNewDetected(target, target.Name, turretGuid, weaponGuid)
+ Some((target, target))
+ case target: Vehicle =>
+ target.Seats.values
+ .flatMap(_.occupants)
+ .collectFirst { passenger =>
+ AutomatedTurretDispatch.Generic.testNewDetected(passenger, passenger.Name, turretGuid, weaponGuid)
+ (target, passenger)
+ }
+ case _ =>
+ None
+ }
+ }
+
+ /**
+ * Cull all targets that have been detected by this turret at some point
+ * by determining which targets are either destroyed
+ * or by determining which targets are too far away to be detected anymore.
+ * If there are no more available targets, cancel the timer that governs this evaluation.
+ * @return a list of somethings the turret can potentially shoot at that were removed
+ */
private def performPeriodicTargetValidation(): List[Target] = {
val size = AutomatedTurretObject.Targets.size
val list = performDistanceCheck()
@@ -272,6 +354,12 @@ trait AutomatedTurretBehavior {
list
}
+ /**
+ * Cull all targets that have been detected by this turret at some point
+ * by determining which targets are either destroyed
+ * or by determining which targets are too far away to be detected anymore.
+ * @return a list of somethings the turret can potentially shoot at that were removed
+ */
private def performDistanceCheck(): List[Target] = {
//cull targets
val pos = AutomatedTurretObject.Position
@@ -286,50 +374,73 @@ trait AutomatedTurretBehavior {
removedTargets
}
+ /**
+ * An important process loop in the target engagement and target management of an automated turret.
+ * If a target has been selected, perform a test to determine whether it remains the selected ("current") target.
+ * If there is no target selected, or the previous selected target was demoted from being selected,
+ * determine if enough time has passed before testing all available targets to find a new selected target.
+ */
private def performCurrentTargetDecayCheck(): Unit = {
val now = System.currentTimeMillis()
- val selectDelay = autoStats.map(_.targetSelectCooldown).getOrElse(3000L)
AutomatedTurretObject.Target
.collect { target =>
//test target
generalDecayCheck(
+ target,
now,
autoStats.map(_.targetEscapeRange).getOrElse(400f),
- selectDelay,
+ autoStats.map(_.targetSelectCooldown).getOrElse(3000L),
autoStats.map(_.missedShotCooldown).getOrElse(3000L),
autoStats.map(_.targetEliminationCooldown).getOrElse(0L)
- )(target)
+ )
}
.orElse {
//no target; unless we are deactivated or have any unfinished delays, search for new target
- if (automaticOperation && now - currentTargetLastShotReported >= 0) {
+ cancelSelfReportedAutoFire()
+ currentTargetLocation = None
+ if (automaticOperation && now - currentTargetLastShotTime >= 0) {
trySelectNewTarget()
}
None
}
}
+ /**
+ * An important process loop in the target engagement and target management of an automated turret.
+ * If a target has been selected, perform a test to determine whether it remains the selected ("current") target.
+ * If the target has been destroyed,
+ * moved beyond the turret's maximum engagement range,
+ * or has been missing for a certain amount of time,
+ * declare the the turret should no longer be shooting at (whatever) it (was).
+ * Apply appropriate cooldown 6to instruct the turret to wait before attempting to select a new current target.
+ * @param target something the turret can potentially shoot at
+ * @return something the turret can potentially shoot at
+ */
private def generalDecayCheck(
+ target: Target,
now: Long,
escapeRange: Float,
selectDelay: Long,
cooldownDelay: Long,
eliminationDelay: Long
- )(target: Target): Option[Target] = {
+ ): Option[Target] = {
if (target.Destroyed) {
//if the target died while we were shooting at it
+ cancelSelfReportedAutoFire()
noLongerEngageDetectedTarget(target)
- currentTargetLastShotReported = now + eliminationDelay
+ currentTargetLastShotTime = now + eliminationDelay
None
} else if (AutomatedTurretBehavior.shapedDistanceCheckAgainstValue(autoStats, target.Position, AutomatedTurretObject.Position, escapeRange)) {
//if the target made sufficient distance from the turret
+ cancelSelfReportedAutoFire()
noLongerEngageDetectedTarget(target)
- currentTargetLastShotReported = now + cooldownDelay
+ currentTargetLastShotTime = now + cooldownDelay
None
- } else if (now - currentTargetLastShotReported >= cooldownDelay) {
+ } else if (now - currentTargetLastShotTime >= cooldownDelay) {
//if the target goes mia through lack of response
+ trySelfReportedAutofireIfStationary() //certain targets can go "unresponsive" even though they should still be reachable
noLongerEngageDetectedTarget(target)
- currentTargetLastShotReported = now + selectDelay
+ currentTargetLastShotTime = now + selectDelay
None
} else {
//continue shooting
@@ -337,10 +448,25 @@ trait AutomatedTurretBehavior {
}
}
+ /**
+ * If there are no available targets,
+ * and no current target,
+ * stop the evaluation of available targets.
+ * @param beforeSize size of the list of available targets before some operation took place
+ * @return `true`, if the evaluation of available targets was stopped;
+ * `false`, otherwise
+ */
private def testTargetListQualifications(beforeSize: Int): Boolean = {
- beforeSize > 0 && AutomatedTurretObject.Targets.isEmpty && periodicValidationTest.cancel()
+ beforeSize > 0 && AutomatedTurretObject.Targets.isEmpty && cancelPeriodicTargetChecks()
}
+ /**
+ * If there is no current target,
+ * start or restart the evaluation of available targets.
+ * @param beforeSize size of the list of available targets before some operation took place
+ * @return `true`, if the evaluation of available targets was stopped;
+ * `false`, otherwise
+ */
private def retimePeriodicTargetChecks(beforeSize: Int): Boolean = {
if (beforeSize == 0 && AutomatedTurretObject.Targets.nonEmpty && autoStats.isDefined) {
val repeated = autoStats.map(_.detectionSpeed).getOrElse(0.seconds)
@@ -351,6 +477,10 @@ trait AutomatedTurretBehavior {
}
}
+ /**
+ * Start or restart the evaluation of available targets immediately.
+ * @param repeated delay in between evaluation periods
+ */
private def retimePeriodicTargetChecks(repeated: FiniteDuration): Unit = {
periodicValidationTest.cancel()
periodicValidationTest = context.system.scheduler.scheduleWithFixedDelay(
@@ -361,15 +491,39 @@ trait AutomatedTurretBehavior {
)
}
- def automaticTurretPostStop(): Unit = {
+ /**
+ * Stop evaluation of available targets,
+ * including tests for targets being removed from selection for the current target,
+ * and tests whether the current target should remain a valid target.
+ * @return `true`, because we can not fail
+ * @see `Default.Cancellable`
+ */
+ private def cancelPeriodicTargetChecks(): Boolean = {
periodicValidationTest.cancel()
- AutomatedTurretObject.Target.foreach { noLongerEngageDetectedTarget }
+ periodicValidationTest = Default.Cancellable
+ true
+ }
+
+ /**
+ * Undo all the things, even the turret's knowledge of available targets.
+ * It's like nothing ever happened.
+ * @see `Actor.postStop`
+ */
+ def automaticTurretPostStop(): Unit = {
+ resetAlerts()
AutomatedTurretObject.Targets.foreach { AutomatedTurretObject.RemoveTarget }
}
+ /**
+ * Retaliation is when a turret returns fire on a potential target that had just previously dealt damage to it.
+ * Occasionally, the turret will drop its current target for the retaliatory target.
+ * @param target something the turret can potentially shoot at
+ * @param cause information about the damaging incident that caused the turret to consider retaliation
+ * @return something the turret can potentially shoot at
+ */
protected def attemptRetaliation(target: Target, cause: DamageResult): Option[Target] = {
if (automaticOperation && autoStats.exists(_.retaliatoryDuration > 0)) {
- AutomatedTurretBehavior.getAttackerFromCause(target.Zone, cause).collect {
+ AutomatedTurretBehavior.getAttackVectorFromCause(target.Zone, cause).collect {
case attacker if attacker.Faction != target.Faction =>
performRetaliation(attacker)
attacker
@@ -379,18 +533,120 @@ trait AutomatedTurretBehavior {
}
}
+ /**
+ * Retaliation is when a turret returns fire on a potential target that had just previously dealt damage to it.
+ * Occasionally, the turret will drop its current target for the retaliatory target.
+ * @param target something the turret can potentially shoot at
+ * @return something the turret can potentially shoot at
+ */
private def performRetaliation(target: Target): Option[Target] = {
AutomatedTurretObject.Target
.collect {
- case _ if autoStats.exists(_.retaliationOverridesTarget) =>
+ case existingTarget if autoStats.exists(_.retaliationOverridesTarget) =>
+ cancelSelfReportedAutoFire()
+ noLongerEngageDetectedTarget(existingTarget)
engageNewDetectedTarget(target)
target
+ case existingTarget =>
+ existingTarget
}
.orElse {
engageNewDetectedTarget(target)
Some(target)
}
}
+
+ /* Self-reporting automatic turret behavior */
+
+ /**
+ * Process confirmation shot feedback from self-reported automatic turret weapon fire.
+ * If the target has moved from the last time reported, cancel self-reported fire and revert to standard turret operation.
+ * Fire a normal test shot specifically at that target to determine if it is yet out of range.
+ * @param target something the turret can potentially shoot at
+ */
+ private def movementCancelSelfReportingFireConfirmShot(target: Target): Unit = {
+ normalConfirmShot(target)
+ if (currentTargetLocation.exists(loc => Vector3.DistanceSquared(loc, target.Position) > 1f)) {
+ cancelSelfReportedAutoFire()
+ noLongerEngageDetectedTarget(target)
+ processForTestingDetectedTarget(
+ target,
+ AutomatedTurretObject.GUID,
+ AutomatedTurretObject.Weapons.values.head.Equipment.get.GUID
+ )
+ } else {
+ tryStartSelfReportedAutofire(target)
+ }
+ }
+
+ /**
+ * If the target still is known to the turret,
+ * and if the target has not moved recently,
+ * but if none of the turret's projectiles have been confirmed shoots,
+ * it may still be reachable with weapons fire.
+ * Directly engage the target to simulate client perspective weapons fire damage.
+ * If you enter this mode, and the target can be damaged this way, the target needs to move in the game world to switch back.
+ * @return `true`, if the self-reporting test shot was discharged;
+ * `false`, otherwise
+ */
+ private def trySelfReportedAutofireIfStationary(): Boolean = {
+ AutomatedTurretObject.Target
+ .collect {
+ case target if currentTargetLocation.exists(loc => Vector3.DistanceSquared(loc, target.Position) > 1f) =>
+ trySelfReportedAutofireTest(target)
+ }
+ .getOrElse(false)
+ }
+
+ /**
+ * Directly engage the target to simulate client perspective weapons fire damage.
+ * If you enter this mode, and the target can be damaged this way, the target needs to move in the game world to switch back.
+ * @return `true`, if the self-reporting test shot was discharged;
+ * `false`, otherwise
+ */
+ private def trySelfReportedAutofireTest(target: Target): Boolean = {
+ if (selfReportedRefire.isCancelled) {
+ confirmShotFunc = movementCancelSelfReportingFireConfirmShot
+ target.Actor ! AffectedByAutomaticTurretFire.AiDamage(AutomatedTurretObject)
+ true
+ } else {
+ false
+ }
+ }
+
+ /**
+ * Directly engage the target to simulate client perspective weapons fire damage.
+ * If you enter this mode, and the target can be damaged this way, the target needs to move in the game world to switch out.
+ * @param target something the turret can potentially shoot at
+ * @return `true`, if the self-reporting operation was initiated;
+ * `false`, otherwise
+ */
+ private def tryStartSelfReportedAutofire(target: Target): Boolean = {
+ if (selfReportedRefire.isCancelled) {
+ confirmShotFunc = movementCancelSelfReportingFireConfirmShot
+ selfReportedRefire = context.system.scheduler.scheduleWithFixedDelay(
+ 0.seconds,
+ autoStats.map(_.refireTime).getOrElse(1.second),
+ target.Actor,
+ AffectedByAutomaticTurretFire.AiDamage(AutomatedTurretObject)
+ )
+ true
+ } else {
+ false
+ }
+ }
+
+ /**
+ * Stop directly communicating with a target to simulate weapons fire damage.
+ * @return `true`, because we can not fail
+ * @see `Default.Cancellable`
+ */
+ private def cancelSelfReportedAutoFire(): Boolean = {
+ selfReportedRefire.cancel()
+ selfReportedRefire = Default.Cancellable
+ confirmShotFunc = normalConfirmShot
+ true
+ }
}
object AutomatedTurretBehavior {
@@ -399,114 +655,97 @@ object AutomatedTurretBehavior {
final case class Unalert(target: Target)
- final case class ConfirmShot(target: Target)
+ final case class ConfirmShot(target: Target, reporter: Option[SourceEntry] = None)
final case object Reset
private case object PeriodicCheck
- trait AutomatedTurretDispatch {
- private val noTargets :List[PlanetSideGUID] = List(Service.defaultPlayerGUID)
-
- def getEventBus(target: Target): ActorRef
-
- def composeMessageEnvelope(channel: String, msg: PlanetSideGamePacket): Any
-
- def startTracking(target: Target, channel: String, turretGuid: PlanetSideGUID, list: List[PlanetSideGUID]): Unit = {
- getEventBus(target) ! composeMessageEnvelope(channel, startTrackingMsg(turretGuid, list))
- }
-
- def stopTracking(target: Target, channel: String, turretGuid: PlanetSideGUID): Unit = {
- getEventBus(target) ! composeMessageEnvelope(channel, stopTrackingMsg(turretGuid))
- }
-
- def startShooting(target: Target, channel: String, weaponGuid: PlanetSideGUID): Unit = {
- getEventBus(target) ! composeMessageEnvelope(channel, startShootingMsg(weaponGuid))
- }
-
- def stopShooting(target: Target, channel: String, weaponGuid: PlanetSideGUID): Unit = {
- getEventBus(target) ! composeMessageEnvelope(channel, stopShootingMsg(weaponGuid))
- }
-
- def testNewDetected(target: Target, channel: String, turretGuid: PlanetSideGUID, weaponGuid: PlanetSideGUID): Unit = {
- startTracking(target, channel, turretGuid, List(target.GUID))
- startShooting(target, channel, weaponGuid)
- stopShooting(target, channel, weaponGuid)
- }
-
- private def startTrackingMsg(guid: PlanetSideGUID, list: List[PlanetSideGUID]): PlanetSideGamePacket = {
- ObjectDetectedMessage(guid, guid, 0, list)
- }
-
- private def stopTrackingMsg(turretGuid: PlanetSideGUID): PlanetSideGamePacket = {
- ObjectDetectedMessage(turretGuid, turretGuid, 0, noTargets)
- }
-
- private def startShootingMsg(weaponGuid: PlanetSideGUID): PlanetSideGamePacket = {
- ChangeFireStateMessage_Start(weaponGuid)
- }
-
- private def stopShootingMsg(weaponGuid: PlanetSideGUID): PlanetSideGamePacket = {
- ChangeFireStateMessage_Stop(weaponGuid)
- }
- }
-
- object Generic extends AutomatedTurretDispatch {
- def getEventBus(target: Target): ActorRef = {
- target.Zone.LocalEvents
- }
-
- def composeMessageEnvelope(channel: String, msg: PlanetSideGamePacket): Any = {
- LocalServiceMessage(channel, LocalAction.SendResponse(msg))
- }
- }
-
- object Vehicle extends AutomatedTurretDispatch {
- def getEventBus(target: Target): ActorRef = {
- target.Zone.VehicleEvents
- }
-
- def composeMessageEnvelope(channel: String, msg: PlanetSideGamePacket): Any = {
- VehicleServiceMessage(channel, VehicleAction.SendResponse(Service.defaultPlayerGUID, msg))
- }
- }
-
+ /**
+ * Are we tracking a `Vehicle` entity?
+ * or, is it some other kind of entity?
+ * @param target something a turret can potentially shoot at
+ * @param channel scope of the message
+ * @param turretGuid turret
+ * @param list target's globally unique identifier, in list form
+ */
def startTracking(target: Target, channel: String, turretGuid: PlanetSideGUID, list: List[PlanetSideGUID]): Unit = {
target match {
- case v: Vehicle => Vehicle.startTracking(v, channel, turretGuid, list)
- case _ => Generic.startTracking(target, channel, turretGuid, list)
+ case v: Vehicle => AutomatedTurretDispatch.Vehicle.startTracking(v, channel, turretGuid, list)
+ case _ => AutomatedTurretDispatch.Generic.startTracking(target, channel, turretGuid, list)
}
}
+ /**
+ * Are we no longer tracking a `Vehicle` entity?
+ * or, was it some other kind of entity?
+ * @param target something a turret can potentially shoot at
+ * @param channel scope of the message
+ * @param turretGuid turret
+ */
def stopTracking(target: Target, channel: String, turretGuid: PlanetSideGUID): Unit = {
target match {
- case v: Vehicle => Vehicle.stopTracking(v, channel, turretGuid)
- case _ => Generic.stopTracking(target, channel, turretGuid)
+ case v: Vehicle => AutomatedTurretDispatch.Vehicle.stopTracking(v, channel, turretGuid)
+ case _ => AutomatedTurretDispatch.Generic.stopTracking(target, channel, turretGuid)
}
}
+ /**
+ * Are we shooting at a `Vehicle` entity?
+ * or, is it some other kind of entity?
+ * @param target something a turret can potentially shoot at
+ * @param channel scope of the message
+ * @param weaponGuid turret's weapon
+ */
def startShooting(target: Target, channel: String, weaponGuid: PlanetSideGUID): Unit = {
target match {
- case v: Vehicle => Vehicle.startShooting(v, channel, weaponGuid)
- case _ => Generic.startShooting(target, channel, weaponGuid)
+ case v: Vehicle => AutomatedTurretDispatch.Vehicle.startShooting(v, channel, weaponGuid)
+ case _ => AutomatedTurretDispatch.Generic.startShooting(target, channel, weaponGuid)
}
}
+ /**
+ * Are we no longer shooting at a `Vehicle` entity?
+ * or, was it some other kind of entity?
+ * @param target something a turret can potentially shoot at
+ * @param channel scope of the message
+ * @param weaponGuid turret's weapon
+ */
def stopShooting(target: Target, channel: String, weaponGuid: PlanetSideGUID): Unit = {
target match {
- case v: Vehicle => Vehicle.stopShooting(v, channel, weaponGuid)
- case _ => Generic.stopShooting(target, channel, weaponGuid)
+ case v: Vehicle => AutomatedTurretDispatch.Vehicle.stopShooting(v, channel, weaponGuid)
+ case _ => AutomatedTurretDispatch.Generic.stopShooting(target, channel, weaponGuid)
}
}
+ /**
+ * Will we be shooting at a `Vehicle` entity?
+ * or, will it be some other kind of entity?
+ * @param target something a turret can potentially shoot at
+ * @param channel scope of the message
+ * @param turretGuid turret
+ * @param weaponGuid turret's weapon
+ */
def testNewDetected(target: Target, channel: String, turretGuid: PlanetSideGUID, weaponGuid: PlanetSideGUID): Unit = {
target match {
- case v: Vehicle => Vehicle.testNewDetected(v, channel, turretGuid, weaponGuid)
- case _ => Generic.testNewDetected(target, channel, turretGuid, weaponGuid)
+ case v: Vehicle => AutomatedTurretDispatch.Vehicle.testNewDetected(v, channel, turretGuid, weaponGuid)
+ case _ => AutomatedTurretDispatch.Generic.testNewDetected(target, channel, turretGuid, weaponGuid)
}
}
- def getAttackerFromCause(zone: Zone, cause: DamageResult): Option[PlanetSideServerObject with Vitality] = {
+ /**
+ * Provided damage information and a zone in which the damage occurred,
+ * find a reference to the entity that caused the damage.
+ * The entity that caused the damage should also be damageable itself.
+ * Very important: do not return the owner of the entity that caused the damage;
+ * return the cause of the damage.
+ * Very important: does not properly trace damage from automatic weapons fire.
+ * @param zone where the damage occurred
+ * @param cause damage information
+ * @return entity that caused the damage
+ * @see `Vitality`
+ */
+ def getAttackVectorFromCause(zone: Zone, cause: DamageResult): Option[PlanetSideServerObject with Vitality] = {
import net.psforever.objects.sourcing._
cause
.interaction
@@ -541,20 +780,33 @@ object AutomatedTurretBehavior {
}
}
+ /**
+ * Perform special distance checks that are either spherical or cylindrical.
+ * Spherical distance checks are the default.
+ * @param stats check if doing cylindrical tests
+ * @param positionA one position in the game world
+ * @param positionB another position in the game world
+ * @param range input distance to test against
+ * @param result complies with standard `compareTo` operations;
+ * `foo.compareTo(bar)`,
+ * where "foo" is calculated using `Vector3.DistanceSquared` or the absolute value of the vertical distance,
+ * and "bar" is `range`-squared
+ * @return
+ */
def shapedDistanceCheckAgainstValue(
- stats: Option[Automation],
- position: Vector3,
- testPosition: Vector3,
- testRange: Float,
- result: Int = 1 //by default, calculation > input
- ): Boolean = {
- val testRangeSq = testRange * testRange
+ stats: Option[Automation],
+ positionA: Vector3,
+ positionB: Vector3,
+ range: Float,
+ result: Int = 1 //by default, calculation > input
+ ): Boolean = {
+ val testRangeSq = range * range
if (stats.exists(_.cylindricalCheck)) {
- val height = testRange + stats.map(_.cylindricalHeight).getOrElse(0f)
- math.abs(position.z - testPosition.z).compareTo(height) == result &&
- Vector3.DistanceSquared(position.xy, testPosition.xy).compareTo(testRangeSq) == result
+ val height = range + stats.map(_.cylindricalHeight).getOrElse(0f)
+ math.abs(positionA.z - positionB.z).compareTo(height) == result &&
+ Vector3.DistanceSquared(positionA.xy, positionB.xy).compareTo(testRangeSq) == result
} else {
- Vector3.DistanceSquared(position, testPosition).compareTo(testRangeSq) == result
+ Vector3.DistanceSquared(positionA, positionB).compareTo(testRangeSq) == result
}
}
}
diff --git a/src/main/scala/net/psforever/objects/serverobject/turret/AutomatedTurretDispatch.scala b/src/main/scala/net/psforever/objects/serverobject/turret/AutomatedTurretDispatch.scala
new file mode 100644
index 000000000..dddaa2c1d
--- /dev/null
+++ b/src/main/scala/net/psforever/objects/serverobject/turret/AutomatedTurretDispatch.scala
@@ -0,0 +1,116 @@
+// Copyright (c) 2024 PSForever
+package net.psforever.objects.serverobject.turret
+
+import akka.actor.ActorRef
+import net.psforever.objects.serverobject.turret.AutomatedTurret.Target
+import net.psforever.packet.PlanetSideGamePacket
+import net.psforever.packet.game.{ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, ObjectDetectedMessage}
+import net.psforever.services.Service
+import net.psforever.services.local.{LocalAction, LocalServiceMessage}
+import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
+import net.psforever.types.PlanetSideGUID
+
+/**
+ * Dispatch messages from an `AutomatedTurret` entity's control agency
+ * with respects to the kind of entity which is the target.
+ * The main sticking point is that the message bus destination matches the type of message envelope.
+ * The packet messages utilized are the same either way and are tied to the action rather than the transmission process.
+ * @see `ChangeFireStateMessage_Start`
+ * @see `ChangeFireStateMessage_Stop`
+ * @see `ObjectDetectedMessage`
+ * @see `PlanetSideGamePacket`
+ * @see `Zone`
+ */
+trait AutomatedTurretDispatch {
+ /**
+ * The event bus should be accessible from the target's knowledge of their zone.
+ * @param target something the turret can potentially shoot at
+ * @return event bus to use
+ */
+ def getEventBus(target: Target): ActorRef
+
+ /**
+ * The event bus should be accessible from the target's knowledge of their zone.
+ * @param channel the scope of the message transmission
+ * @param msg the packet to be dispatched
+ * @return messaging envelope to use
+ */
+ def composeMessageEnvelope(channel: String, msg: PlanetSideGamePacket): Any
+
+ /**
+ * Are we tracking an entity?
+ */
+ def startTracking(target: Target, channel: String, turretGuid: PlanetSideGUID, list: List[PlanetSideGUID]): Unit = {
+ getEventBus(target) ! composeMessageEnvelope(channel, startTrackingMsg(turretGuid, list))
+ }
+
+ /**
+ * Are we no longer tracking an entity?
+ */
+ def stopTracking(target: Target, channel: String, turretGuid: PlanetSideGUID): Unit = {
+ getEventBus(target) ! composeMessageEnvelope(channel, stopTrackingMsg(turretGuid))
+ }
+
+ /**
+ * Are we shooting at an entity?
+ */
+ def startShooting(target: Target, channel: String, weaponGuid: PlanetSideGUID): Unit = {
+ getEventBus(target) ! composeMessageEnvelope(channel, startShootingMsg(weaponGuid))
+ }
+
+ /**
+ * Are we no longer shooting at an entity?
+ */
+ def stopShooting(target: Target, channel: String, weaponGuid: PlanetSideGUID): Unit = {
+ getEventBus(target) ! composeMessageEnvelope(channel, stopShootingMsg(weaponGuid))
+ }
+
+ /**
+ * Will we be shooting at an entity?
+ */
+ def testNewDetected(target: Target, channel: String, turretGuid: PlanetSideGUID, weaponGuid: PlanetSideGUID): Unit = {
+ startTracking(target, channel, turretGuid, List(target.GUID))
+ startShooting(target, channel, weaponGuid)
+ stopShooting(target, channel, weaponGuid)
+ }
+
+ private def startTrackingMsg(guid: PlanetSideGUID, list: List[PlanetSideGUID]): PlanetSideGamePacket = {
+ ObjectDetectedMessage(guid, guid, 0, list)
+ }
+
+ private def stopTrackingMsg(turretGuid: PlanetSideGUID): PlanetSideGamePacket = {
+ ObjectDetectedMessage(turretGuid, turretGuid, 0, AutomatedTurretDispatch.noTargets)
+ }
+
+ private def startShootingMsg(weaponGuid: PlanetSideGUID): PlanetSideGamePacket = {
+ ChangeFireStateMessage_Start(weaponGuid)
+ }
+
+ private def stopShootingMsg(weaponGuid: PlanetSideGUID): PlanetSideGamePacket = {
+ ChangeFireStateMessage_Stop(weaponGuid)
+ }
+}
+
+object AutomatedTurretDispatch {
+ private val noTargets: List[PlanetSideGUID] = List(Service.defaultPlayerGUID)
+
+ object Generic extends AutomatedTurretDispatch {
+ def getEventBus(target: Target): ActorRef = {
+ target.Zone.LocalEvents
+ }
+
+ def composeMessageEnvelope(channel: String, msg: PlanetSideGamePacket): Any = {
+ LocalServiceMessage(channel, LocalAction.SendResponse(msg))
+ }
+ }
+
+ object Vehicle extends AutomatedTurretDispatch {
+ def getEventBus(target: Target): ActorRef = {
+ target.Zone.VehicleEvents
+ }
+
+ def composeMessageEnvelope(channel: String, msg: PlanetSideGamePacket): Any = {
+ VehicleServiceMessage(channel, VehicleAction.SendResponse(Service.defaultPlayerGUID, msg))
+ }
+ }
+}
diff --git a/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurret.scala b/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurret.scala
index ef5cf67ff..a9502800a 100644
--- a/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurret.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurret.scala
@@ -4,6 +4,7 @@ package net.psforever.objects.serverobject.turret
import net.psforever.objects.equipment.JammableUnit
import net.psforever.objects.serverobject.structures.{Amenity, AmenityOwner, Building}
import net.psforever.objects.serverobject.terminals.capture.CaptureTerminalAware
+import net.psforever.objects.sourcing.SourceEntry
import net.psforever.types.Vector3
class FacilityTurret(tDef: FacilityTurretDefinition)
@@ -11,7 +12,16 @@ class FacilityTurret(tDef: FacilityTurretDefinition)
with AutomatedTurret
with JammableUnit
with CaptureTerminalAware {
- WeaponTurret.LoadDefinition(this)
+ WeaponTurret.LoadDefinition(turret = this)
+
+ def TurretOwner: SourceEntry = {
+ Seats
+ .headOption
+ .collect { case (_, a) => a }
+ .flatMap(_.occupant)
+ .map(SourceEntry(_))
+ .getOrElse(SourceEntry(Owner))
+ }
override def Owner: AmenityOwner = {
if (Zone.map.cavern) {
diff --git a/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala b/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala
index a8e67b362..b48a45846 100644
--- a/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala
@@ -228,7 +228,7 @@ class FacilityTurretControl(turret: FacilityTurret)
AutomaticOperation = false
//look in direction of cause of jamming
val zone = JammableObject.Zone
- AutomatedTurretBehavior.getAttackerFromCause(zone, cause).foreach {
+ AutomatedTurretBehavior.getAttackVectorFromCause(zone, cause).foreach {
attacker =>
val channel = zone.id
val guid = AutomatedTurretObject.GUID
diff --git a/src/main/scala/net/psforever/objects/serverobject/turret/TurretDefinition.scala b/src/main/scala/net/psforever/objects/serverobject/turret/TurretDefinition.scala
index fadf6120e..d628e6fbe 100644
--- a/src/main/scala/net/psforever/objects/serverobject/turret/TurretDefinition.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/turret/TurretDefinition.scala
@@ -11,20 +11,21 @@ import scala.collection.mutable
import scala.concurrent.duration._
final case class Automation(
- targetDetectionRange: Float,
- targetTriggerRange: Float,
- targetEscapeRange: Float,
+ targetDetectionRange: Float, //m
+ targetTriggerRange: Float, //m
+ targetEscapeRange: Float, //m
targetValidation: List[PlanetSideGameObject => Boolean],
cylindricalCheck: Boolean = false,
- cylindricalHeight: Float = 0,
- retaliatoryDuration: Long = 0,
+ cylindricalHeight: Float = 0, //m
+ retaliatoryDuration: Long = 0, //ms
retaliationOverridesTarget: Boolean = true,
initialDetectionSpeed: FiniteDuration = Duration.Zero,
detectionSpeed: FiniteDuration = 1.seconds,
targetSelectCooldown: Long = 1500L, //ms
missedShotCooldown: Long = 3000L, //ms
targetEliminationCooldown: Long = 0L, //ms
- revertToDefaultFireMode: Boolean = true
+ revertToDefaultFireMode: Boolean = true,
+ refireTime: FiniteDuration = 1.seconds //60rpm
) {
assert(targetDetectionRange > targetTriggerRange, "trigger range must be less than detection range")
}
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 29dd31cea..d78c087ea 100644
--- a/src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala
+++ b/src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala
@@ -20,6 +20,7 @@ import net.psforever.objects.serverobject.hackable.GenericHackables
import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior}
import net.psforever.objects.serverobject.repair.RepairableVehicle
import net.psforever.objects.serverobject.terminals.Terminal
+import net.psforever.objects.serverobject.turret.AffectedByAutomaticTurretFire
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource}
import net.psforever.objects.vehicles._
import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult}
@@ -57,7 +58,8 @@ class VehicleControl(vehicle: Vehicle)
with ContainableBehavior
with AggravatedBehavior
with RespondsToZoneEnvironment
- with CargoBehavior {
+ with CargoBehavior
+ with AffectedByAutomaticTurretFire {
//make control actors belonging to utilities when making control actor belonging to vehicle
vehicle.Utilities.foreach { case (_, util) => util.Setup }
@@ -70,6 +72,7 @@ class VehicleControl(vehicle: Vehicle)
def ContainerObject: Vehicle = vehicle
def InteractiveObject: Vehicle = vehicle
def CargoObject: Vehicle = vehicle
+ def AffectedObject: Vehicle = vehicle
SetInteraction(EnvironmentAttribute.Water, doInteractingWithWater)
SetInteraction(EnvironmentAttribute.Lava, doInteractingWithLava)
@@ -114,6 +117,7 @@ class VehicleControl(vehicle: Vehicle)
.orElse(containerBehavior)
.orElse(environmentBehavior)
.orElse(cargoBehavior)
+ .orElse(takeAutomatedDamage)
.orElse {
case Vehicle.Ownership(None) =>
LoseOwnership()