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 7bcdd81e3..cfe1d5c38 100644 --- a/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala @@ -3,8 +3,9 @@ package net.psforever.actors.session.support import akka.actor.{ActorContext, typed} import net.psforever.objects.definition.ProjectileDefinition +import net.psforever.objects.serverobject.turret.auto.{AutomatedTurret, AutomatedTurretBehavior} import net.psforever.objects.zones.Zoning -import net.psforever.objects.serverobject.turret.{AutomatedTurret, AutomatedTurretBehavior, VanuSentry} +import net.psforever.objects.serverobject.turret.VanuSentry import net.psforever.objects.zones.exp.ToDatabase import scala.collection.mutable @@ -470,7 +471,7 @@ private[support] class WeaponAndProjectileOperations( val hitPos = target.Position + Vector3.z(value = 1f) ResolveProjectileInteraction(proj, DamageResolution.Hit, target, hitPos).collect { resprojectile => addShotsLanded(resprojectile.cause.attribution, shots = 1) - //sessionData.handleDealingDamage(target, resprojectile) + sessionData.handleDealingDamage(target, resprojectile) } } } diff --git a/src/main/scala/net/psforever/objects/GlobalDefinitions.scala b/src/main/scala/net/psforever/objects/GlobalDefinitions.scala index c1717dc14..0eb588538 100644 --- a/src/main/scala/net/psforever/objects/GlobalDefinitions.scala +++ b/src/main/scala/net/psforever/objects/GlobalDefinitions.scala @@ -24,7 +24,7 @@ import net.psforever.objects.serverobject.resourcesilo.ResourceSiloDefinition import net.psforever.objects.serverobject.structures.{AmenityDefinition, AutoRepairStats, BuildingDefinition, WarpGateDefinition} import net.psforever.objects.serverobject.terminals.capture.CaptureTerminalDefinition import net.psforever.objects.serverobject.terminals.implant.{ImplantTerminalDefinition, ImplantTerminalMechDefinition} -import net.psforever.objects.serverobject.turret.{Automation, FacilityTurretDefinition, TurretUpgrade} +import net.psforever.objects.serverobject.turret.{Automation, AutoChecks, FacilityTurretDefinition, AutoRanges, TurretUpgrade} import net.psforever.objects.vehicles.{DestroyedVehicle, InternalTelepadDefinition, UtilityType, VehicleSubsystemEntry} import net.psforever.objects.vital.base.DamageType import net.psforever.objects.vital.damage._ @@ -9074,11 +9074,19 @@ object GlobalDefinitions { spitfire_turret.Model = ComplexDeployableResolutions.calculate spitfire_turret.deployAnimation = DeployAnimation.Standard spitfire_turret.AutoFire = Automation( - targetDetectionRange = 60f, - targetTriggerRange = 50f, - targetEscapeRange = 50f, - targetValidation = List(EffectTarget.Validation.PlayerOnRadar, EffectTarget.Validation.Vehicle), - retaliatoryDuration = 8000L, + AutoRanges( + detection = 75f, + trigger = 50f, + escape = 50f + ), + AutoChecks( + validation = List( + EffectTarget.Validation.PlayerDetectedBySpitfireTurret, + EffectTarget.Validation.GroundVehicleDetectedByAutoTurret, + EffectTarget.Validation.AircraftDetectedByAutoTurret + ) + ), + retaliatoryDelay = 2000L, //8000L refireTime = 200.milliseconds //150.milliseconds ) spitfire_turret.innateDamage = new DamageWithPosition { @@ -9108,11 +9116,19 @@ object GlobalDefinitions { spitfire_cloaked.deployAnimation = DeployAnimation.Standard spitfire_cloaked.Model = ComplexDeployableResolutions.calculate spitfire_cloaked.AutoFire = Automation( - targetDetectionRange = 50f, - targetTriggerRange = 30f, - targetEscapeRange = 50f, - targetValidation = List(EffectTarget.Validation.PlayerOnRadar, EffectTarget.Validation.VehiclesOnRadar), - retaliatoryDuration = 8000L, + AutoRanges( + detection = 75f, + trigger = 50f, + escape = 75f + ), + AutoChecks( + validation = List( + EffectTarget.Validation.PlayerDetectedBySpitfireTurret, + EffectTarget.Validation.GroundVehicleDetectedByAutoTurret, + EffectTarget.Validation.AircraftDetectedByAutoTurret + ) + ), + retaliatoryDelay = 1L, //8000L refireTime = 200.milliseconds //150.milliseconds ) spitfire_cloaked.innateDamage = new DamageWithPosition { @@ -9142,15 +9158,19 @@ object GlobalDefinitions { spitfire_aa.deployAnimation = DeployAnimation.Standard spitfire_aa.Model = ComplexDeployableResolutions.calculate spitfire_aa.AutoFire = Automation( - targetDetectionRange = 100f, - targetTriggerRange = 90f, - targetEscapeRange = 200f, - targetValidation = List(EffectTarget.Validation.AircraftOnRadar), - retaliatoryDuration = 2000L, + AutoRanges( + detection = 125f, + trigger = 100f, + escape = 200f + ), + AutoChecks( + validation = List(EffectTarget.Validation.AircraftDetectedByAutoTurret) + ), + retaliatoryDelay = 2000L, //8000L retaliationOverridesTarget = false, - refireTime = 350.milliseconds, //300.milliseconds - cylindricalCheck = true, - cylindricalHeight = 25f + refireTime = 0.seconds, //300.milliseconds + cylindrical = true, + cylindricalExtraHeight = 50f ) spitfire_aa.innateDamage = new DamageWithPosition { CausesDamageType = DamageType.One @@ -10054,14 +10074,22 @@ object GlobalDefinitions { manned_turret.ReserveAmmunition = false manned_turret.RadiationShielding = 0.5f manned_turret.AutoFire = Automation( - targetDetectionRange = 100f, - targetTriggerRange = 90f, - targetEscapeRange = 200f, - targetValidation = List(EffectTarget.Validation.MaxOnRadar, EffectTarget.Validation.VehiclesOnRadar), - retaliatoryDuration = 8000L, - cylindricalCheck = true, - cylindricalHeight = 25f, - detectionSpeed = 2.seconds, + AutoRanges( + detection = 125f, + trigger = 100f, + escape = 200f + ), + AutoChecks( + validation = List( + EffectTarget.Validation.MaxDetectedByAutoTurret, + EffectTarget.Validation.GroundVehicleDetectedByAutoTurret, + EffectTarget.Validation.AircraftDetectedByAutoTurret + ) + ), + retaliatoryDelay = 4000L, //8000L + cylindrical = true, + cylindricalExtraHeight = 50f, + detectionSweepTime = 2.seconds, refireTime = 362.milliseconds //312.milliseconds ) manned_turret.innateDamage = new DamageWithPosition { diff --git a/src/main/scala/net/psforever/objects/TurretDeployable.scala b/src/main/scala/net/psforever/objects/TurretDeployable.scala index 504f7e0b6..d57dd98b1 100644 --- a/src/main/scala/net/psforever/objects/TurretDeployable.scala +++ b/src/main/scala/net/psforever/objects/TurretDeployable.scala @@ -12,8 +12,9 @@ import net.psforever.objects.serverobject.affinity.FactionAffinityBehavior 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.serverobject.turret.auto.{AutomatedTurret, AutomatedTurretBehavior} +import net.psforever.objects.serverobject.turret.{MountableTurretControl, TurretDefinition, WeaponTurret} +import net.psforever.objects.sourcing.{PlayerSource, SourceEntry} import net.psforever.objects.vital.damage.DamageCalculations import net.psforever.objects.vital.interaction.DamageResult import net.psforever.objects.vital.{SimpleResolutions, StandardVehicleResistance} @@ -31,10 +32,11 @@ class TurretDeployable(tdef: TurretDeployableDefinition) def TurretOwner: SourceEntry = { Seats + .values .headOption - .collect { case (_, a) => a } .flatMap(_.occupant) - .map(SourceEntry(_)) + .map(p => PlayerSource.inSeat(PlayerSource(p), SourceEntry(this), seatNumber=0)) + .orElse(Owners.map(PlayerSource(_, Position))) .getOrElse(SourceEntry(this)) } @@ -86,6 +88,7 @@ class TurretControl(turret: TurretDeployable) override def postStop(): Unit = { super.postStop() deployableBehaviorPostStop() + selfReportingDatabaseUpdate() automaticTurretPostStop() } @@ -111,7 +114,7 @@ class TurretControl(turret: TurretDeployable) override def TryJammerEffectActivate(target: Any, cause: DamageResult): Unit = { val startsUnjammed = !JammableObject.Jammed super.TryJammerEffectActivate(target, cause) - if (startsUnjammed && JammableObject.Jammed && AutomatedTurretObject.Definition.AutoFire.exists(_.retaliatoryDuration > 0)) { + if (startsUnjammed && JammableObject.Jammed && AutomatedTurretObject.Definition.AutoFire.exists(_.retaliatoryDelay > 0)) { AutomaticOperation = false //look in direction of cause of jamming val zone = JammableObject.Zone @@ -172,7 +175,7 @@ class TurretControl(turret: TurretDeployable) override def finalizeDeployable(callback: ActorRef): Unit = { super.finalizeDeployable(callback) - AutomaticOperation = true + AutomaticOperation = AutomaticOperationFunctionalityChecks } override def unregisterDeployable(obj: Deployable): Unit = { diff --git a/src/main/scala/net/psforever/objects/ce/InteractWithTurrets.scala b/src/main/scala/net/psforever/objects/ce/InteractWithTurrets.scala index 0556303a3..3ff7f2f92 100644 --- a/src/main/scala/net/psforever/objects/ce/InteractWithTurrets.scala +++ b/src/main/scala/net/psforever/objects/ce/InteractWithTurrets.scala @@ -3,7 +3,7 @@ package net.psforever.objects.ce import net.psforever.objects.GlobalDefinitions import net.psforever.objects.serverobject.PlanetSideServerObject -import net.psforever.objects.serverobject.turret.{AutomatedTurret, AutomatedTurretBehavior} +import net.psforever.objects.serverobject.turret.auto.{AutomatedTurret, AutomatedTurretBehavior} import net.psforever.objects.zones.blockmap.SectorPopulation import net.psforever.objects.zones.{InteractsWithZone, ZoneInteraction, ZoneInteractionType} import net.psforever.objects.sourcing.SourceEntry @@ -80,7 +80,7 @@ object InteractWithTurrets { GlobalDefinitions.manned_turret ) .flatMap(_.AutoFire) - .map(_.targetDetectionRange) + .map(_.ranges.detection) .max } } diff --git a/src/main/scala/net/psforever/objects/equipment/EffectTarget.scala b/src/main/scala/net/psforever/objects/equipment/EffectTarget.scala index c4c128719..db25de38a 100644 --- a/src/main/scala/net/psforever/objects/equipment/EffectTarget.scala +++ b/src/main/scala/net/psforever/objects/equipment/EffectTarget.scala @@ -2,10 +2,11 @@ package net.psforever.objects.equipment import net.psforever.objects._ -import net.psforever.objects.ce.DeployableCategory +import net.psforever.objects.ce.{DeployableCategory, DeployedItem} import net.psforever.objects.serverobject.turret.FacilityTurret import net.psforever.objects.vital.DamagingActivity -import net.psforever.types.{ExoSuitType, ImplantType} +import net.psforever.objects.zones.blockmap.SectorPopulation +import net.psforever.types.{DriveState, ExoSuitType, ImplantType, LatticeBenefit, PlanetSideEmpire, Vector3} final case class TargetValidation(category: EffectTarget.Category.Value, test: EffectTarget.Validation.Value) @@ -188,47 +189,169 @@ object EffectTarget { false } - def PlayerOnRadar(target: PlanetSideGameObject): Boolean = + def PlayerDetectedBySpitfireTurret(target: PlanetSideGameObject): Boolean = !target.Destroyed && (target match { case p: Player => - //TODO attacking breaks stealth - p.LastDamage.map(_.interaction.hitTime).exists(System.currentTimeMillis() - _ < 3000L) || - p.avatar.implants.flatten.find(a => a.definition.implantType == ImplantType.SilentRun).exists(_.active) || - (p.isMoving(test = 17d) && !(p.Crouching || p.Cloaked)) || - p.Jumping + val now = System.currentTimeMillis() + val pos = p.Position + val faction = p.Faction + val sector = p.Zone.blockMap.sector(p.Position, range = 51f) + lazy val tookDamage = p.LastDamage.exists(dam => dam.adversarial.nonEmpty && now - dam.interaction.hitTime < 2000L) + //todo equipment-use usually a violation for any equipment type + lazy val usedEquipment = (p.Holsters().flatMap(_.Equipment) ++ p.Inventory.Items.map(_.obj)) + .collect { + case t: Tool + if !(t.Projectile == GlobalDefinitions.no_projectile || t.Projectile.GrenadeProjectile || t.Size == EquipmentSize.Melee) => + now - t.LastDischarge + } + .exists(_ < 2000L) + lazy val silentRunActive = p.avatar.implants.flatten.find(a => a.definition.implantType == ImplantType.SilentRun).exists(_.active) + lazy val movingFast = p.isMoving(test = 17d) || p.Jumping + p.VehicleSeated.isEmpty && + (if (radarCloakedAms(sector, pos) || radarCloakedAegis(sector, pos)) false + else if (radarCloakedSensor(sector, pos, faction)) tookDamage || usedEquipment + else if (radarEnhancedInterlink(sector, pos, faction) || radarEnhancedSensor(sector, pos, faction)) true + else tookDamage || usedEquipment || !silentRunActive && movingFast) case _ => false }) - def MaxOnRadar(target: PlanetSideGameObject): Boolean = - !target.Destroyed && (target match { - case p: Player => - p.ExoSuit == ExoSuitType.MAX && p.isMoving(test = 17d) - case _ => - false - }) - - def VehiclesOnRadar(target: PlanetSideGameObject): Boolean = - !target.Destroyed && (target match { - case v: Vehicle => - val vdef = v.Definition - !(v.MountedIn.nonEmpty || - v.Cloaked || - GlobalDefinitions.isAtvVehicle(vdef) || - vdef == GlobalDefinitions.two_man_assault_buggy || - vdef == GlobalDefinitions.skyguard) - case _ => - false - }) - - - - def AircraftOnRadar(target: PlanetSideGameObject): Boolean = + def PlayerUndetectedByAutoTurret(target: PlanetSideGameObject): Boolean = target match { - case v: Vehicle => - GlobalDefinitions.isFlightVehicle(v.Definition) && v.Health > 0 && !v.Cloaked + case p: Player => + val pos = p.Position + val sector = p.Zone.blockMap.sector(p.Position, range = 51f) + p.VehicleSeated.nonEmpty || radarCloakedAms(sector, pos) || radarCloakedAegis(sector, pos) case _ => false } + + def MaxDetectedByAutoTurret(target: PlanetSideGameObject): Boolean = + !target.Destroyed && (target match { + case p: Player => + val now = System.currentTimeMillis() + val pos = p.Position + val faction = p.Faction + val sector = p.Zone.blockMap.sector(p.Position, range = 51f) + lazy val tookDamage = p.LastDamage.exists(dam => dam.adversarial.nonEmpty && now - dam.interaction.hitTime < 2000L) + lazy val usedEquipment = p.Holsters().flatMap(_.Equipment) + .collect { case t: Tool => now - t.LastDischarge } + .exists(_ < 2000L) + lazy val movingFast = p.isMoving(test = 17d) + p.ExoSuit == ExoSuitType.MAX && + p.VehicleSeated.isEmpty && + (if (radarCloakedAms(sector, pos) || radarCloakedAegis(sector, pos)) false + else if (radarCloakedSensor(sector, pos, faction)) tookDamage || usedEquipment + else if (radarEnhancedInterlink(sector, pos, faction) || radarEnhancedSensor(sector, pos, faction)) true + else tookDamage || usedEquipment || movingFast) + case _ => + false + }) + + def GroundVehicleDetectedByAutoTurret(target: PlanetSideGameObject): Boolean = + !target.Destroyed && (target match { + case v: Vehicle => + val now = System.currentTimeMillis() + val vdef = v.Definition + lazy val tookDamage = v.LastDamage.exists(dam => dam.adversarial.nonEmpty && now - dam.interaction.hitTime< 2000L) + lazy val usedEquipment = v.Weapons.values.flatMap(_.Equipment) + .collect { case t: Tool => now - t.LastDischarge } + .exists(_ < 2000L) + !GlobalDefinitions.isFlightVehicle(vdef) && v.MountedIn.isEmpty && ( + if (vdef == GlobalDefinitions.ams && v.DeploymentState == DriveState.Deployed) false + else if ( + v.Cloaked || + GlobalDefinitions.isAtvVehicle(vdef) || + vdef == GlobalDefinitions.two_man_assault_buggy || + vdef == GlobalDefinitions.skyguard + ) tookDamage || usedEquipment + else true) + case _ => + false + }) + + def VehicleUndetectedByAutoTurret(target: PlanetSideGameObject): Boolean = + target match { + case v: Vehicle => + (v.Definition == GlobalDefinitions.ams && v.DeploymentState == DriveState.Deployed) || v.MountedIn.nonEmpty || v.Cloaked + case _ => + false + } + + def AircraftDetectedByAutoTurret(target: PlanetSideGameObject): Boolean = + !target.Destroyed && (target match { + case v: Vehicle => + GlobalDefinitions.isFlightVehicle(v.Definition) && !v.Cloaked + case _ => + false + }) + } + + private def radarEnhancedInterlink( + sector: SectorPopulation, + position: Vector3, + faction: PlanetSideEmpire.Value + ): Boolean = { + sector.buildingList.collect { + case b => + b.Faction != faction && + b.hasLatticeBenefit(LatticeBenefit.InterlinkFacility) && + Vector3.DistanceSquared(b.Position, position).toDouble < math.pow(b.Definition.SOIRadius.toDouble, 2d) + }.contains(true) + } + + private def radarEnhancedSensor( + sector: SectorPopulation, + position: Vector3, + faction: PlanetSideEmpire.Value + ): Boolean = { + sector.deployableList.collect { + case d: SensorDeployable => + !d.Destroyed && + d.Definition.Item == DeployedItem.motionalarmsensor && + d.Faction != faction && + !d.Jammed && Vector3.DistanceSquared(d.Position, position) < 2500f + }.contains(true) + } + + private def radarCloakedAms( + sector: SectorPopulation, + position: Vector3 + ): Boolean = { + sector.vehicleList.collect { + case v => + !v.Destroyed && + v.Definition == GlobalDefinitions.ams && + v.DeploymentState == DriveState.Deployed && + !v.Jammed && + Vector3.DistanceSquared(v.Position, position) < 144f + }.contains(true) + } + + private def radarCloakedAegis( + sector: SectorPopulation, + position: Vector3 + ): Boolean = { + sector.deployableList.collect { + case d: ShieldGeneratorDeployable => + !d.Destroyed && + !d.Jammed && + Vector3.DistanceSquared(d.Position, position) < 100f + }.contains(true) + } + + private def radarCloakedSensor( + sector: SectorPopulation, + position: Vector3, + faction: PlanetSideEmpire.Value + ): Boolean = { + sector.deployableList.collect { + case d: SensorDeployable => + !d.Destroyed && + d.Definition.Item == DeployedItem.sensor_shield && + d.Faction == faction && + !d.Jammed && + Vector3.DistanceSquared(d.Position, position) < 900f + }.contains(true) } } 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 a9502800a..5ff5bd246 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.serverobject.turret.auto.AutomatedTurret import net.psforever.objects.sourcing.SourceEntry import net.psforever.types.Vector3 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 b48a45846..e40b06da5 100644 --- a/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala @@ -10,6 +10,7 @@ import net.psforever.objects.serverobject.mount.Mountable import net.psforever.objects.serverobject.repair.AmenityAutoRepair import net.psforever.objects.serverobject.structures.PoweredAmenityControl import net.psforever.objects.serverobject.terminals.capture.CaptureTerminalAwareBehavior +import net.psforever.objects.serverobject.turret.auto.{AutomatedTurret, AutomatedTurretBehavior} import net.psforever.objects.vital.interaction.DamageResult import net.psforever.packet.game.ChangeFireModeMessage import net.psforever.services.Service @@ -39,6 +40,8 @@ class FacilityTurretControl(turret: FacilityTurret) private var testToResetToDefaultFireMode: Boolean = false + AutomaticOperation = AutomaticOperationFunctionalityChecks + override def postStop(): Unit = { super.postStop() damageableWeaponTurretPostStop() @@ -126,6 +129,7 @@ class FacilityTurretControl(turret: FacilityTurret) super.DestructionAwareness(target, cause) tryAutoRepair() AutomaticOperation = false + selfReportingCleanUp() } override def PerformRepairs(target : Damageable.Target, amount : Int) : Int = { @@ -224,7 +228,7 @@ class FacilityTurretControl(turret: FacilityTurret) override def TryJammerEffectActivate(target: Any, cause: DamageResult): Unit = { val startsUnjammed = !JammableObject.Jammed super.TryJammerEffectActivate(target, cause) - if (startsUnjammed && JammableObject.Jammed && AutomatedTurretObject.Definition.AutoFire.exists(_.retaliatoryDuration > 0)) { + if (startsUnjammed && JammableObject.Jammed && AutomatedTurretObject.Definition.AutoFire.exists(_.retaliatoryDelay > 0)) { AutomaticOperation = false //look in direction of cause of jamming val zone = JammableObject.Zone 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 d628e6fbe..17916f5b1 100644 --- a/src/main/scala/net/psforever/objects/serverobject/turret/TurretDefinition.scala +++ b/src/main/scala/net/psforever/objects/serverobject/turret/TurretDefinition.scala @@ -11,25 +11,60 @@ import scala.collection.mutable import scala.concurrent.duration._ final case class Automation( - targetDetectionRange: Float, //m - targetTriggerRange: Float, //m - targetEscapeRange: Float, //m - targetValidation: List[PlanetSideGameObject => Boolean], - cylindricalCheck: Boolean = false, - cylindricalHeight: Float = 0, //m - retaliatoryDuration: Long = 0, //ms + ranges: AutoRanges, + checks: AutoChecks, + /** the boundary for target searching is typically a sphere of `ranges.detection` radius; + * instead, takes the shape of a cylinder of `ranges.detection` radius and height */ + cylindrical: Boolean = false, + /** if target searching is performed in the shape of a cylinder, + * add height on top of the cylinder's normal height */ + cylindricalExtraHeight: Float = 0, //m + /** how long after the last target engagement + * or how long into the current target engagement + * before the turret may counterattack damage; + * set to `0L` to never retaliate */ + retaliatoryDelay: Long = 0, //ms + /** if the turret has a current target, + * allow for retaliation against a different target */ retaliationOverridesTarget: Boolean = true, - initialDetectionSpeed: FiniteDuration = Duration.Zero, - detectionSpeed: FiniteDuration = 1.seconds, - targetSelectCooldown: Long = 1500L, //ms - missedShotCooldown: Long = 3000L, //ms - targetEliminationCooldown: Long = 0L, //ms + /** frequency at which the turret will test target for reachability */ + detectionSweepTime: FiniteDuration = 1.seconds, + cooldowns: AutoCooldowns = AutoCooldowns(), + /** if the turret weapon has multiple fire modes, + * revert to the base fire mode before engaging in target testing or other automatic operations */ revertToDefaultFireMode: Boolean = true, + /** the simulated weapon fire rate for self-reporting (internal damage loop) */ refireTime: FiniteDuration = 1.seconds //60rpm + ) + +final case class AutoRanges( + /** distance at which a target is first noticed */ + detection: Float, //m + /** distance at which the target is tested */ + trigger: Float, //m + /** distance away from the source of damage before the turret stops engaging */ + escape: Float //m ) { - assert(targetDetectionRange > targetTriggerRange, "trigger range must be less than detection range") + assert(detection >= trigger, "detection range must be greater than or equal to trigger range") + assert(escape >= trigger, "escape range must be greater than or equal to trigger range") } +final case class AutoChecks( + /** reasons why this target should be engaged */ + validation: List[PlanetSideGameObject => Boolean], + /** reasons why an ongoing target engagement should be stopped */ + blanking: List[PlanetSideGameObject => Boolean] = Nil + ) + +final case class AutoCooldowns( + /** when the target gets switched (generic) */ + targetSelect: Long = 1500L, //ms + /** when the target escapes being damaged */ + missedShot: Long = 3000L, //ms + /** when the target gets destroyed during an ongoing engagement */ + targetElimination: Long = 0L //ms + ) + /** * The definition for any `WeaponTurret`. */ diff --git a/src/main/scala/net/psforever/objects/serverobject/turret/AffectedByAutomaticTurretFire.scala b/src/main/scala/net/psforever/objects/serverobject/turret/auto/AffectedByAutomaticTurretFire.scala similarity index 85% rename from src/main/scala/net/psforever/objects/serverobject/turret/AffectedByAutomaticTurretFire.scala rename to src/main/scala/net/psforever/objects/serverobject/turret/auto/AffectedByAutomaticTurretFire.scala index 36e74ecde..3cbb81af1 100644 --- a/src/main/scala/net/psforever/objects/serverobject/turret/AffectedByAutomaticTurretFire.scala +++ b/src/main/scala/net/psforever/objects/serverobject/turret/auto/AffectedByAutomaticTurretFire.scala @@ -1,5 +1,5 @@ // Copyright (c) 2024 PSForever -package net.psforever.objects.serverobject.turret +package net.psforever.objects.serverobject.turret.auto import akka.actor.Actor import net.psforever.objects.Tool @@ -20,23 +20,22 @@ import net.psforever.types.Vector3 */ trait AffectedByAutomaticTurretFire extends Damageable { _: Actor => - def AffectedObject: AutomatedTurret.Target val takeAutomatedDamage: Receive = { - case AffectedByAutomaticTurretFire.AiDamage(turret) => + case AiDamage(turret) => performAutomatedDamage(turret) } - private def performAutomatedDamage(turret: AutomatedTurret): Unit = { + protected def performAutomatedDamage(turret: AutomatedTurret): Unit = { val target = AffectedObject - val tool = turret.Weapons.values.head.Equipment.collect { case t : Tool => t }.get + 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))) + turret.Actor ! SelfReportedConfirmShot(target) val projectile = new Projectile( projectileInfo, tool.Definition, @@ -63,7 +62,3 @@ trait AffectedByAutomaticTurretFire extends Damageable { 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/auto/AutomatedTurret.scala similarity index 80% rename from src/main/scala/net/psforever/objects/serverobject/turret/AutomatedTurret.scala rename to src/main/scala/net/psforever/objects/serverobject/turret/auto/AutomatedTurret.scala index 04b60d5b6..65df3c7a3 100644 --- a/src/main/scala/net/psforever/objects/serverobject/turret/AutomatedTurret.scala +++ b/src/main/scala/net/psforever/objects/serverobject/turret/auto/AutomatedTurret.scala @@ -1,8 +1,9 @@ // Copyright (c) 2024 PSForever -package net.psforever.objects.serverobject.turret +package net.psforever.objects.serverobject.turret.auto import net.psforever.objects.definition.ObjectDefinition import net.psforever.objects.serverobject.PlanetSideServerObject +import net.psforever.objects.serverobject.turret.{TurretDefinition, WeaponTurret} import net.psforever.objects.sourcing.{SourceEntry, SourceUniqueness} import net.psforever.objects.vital.Vitality @@ -14,6 +15,12 @@ trait AutomatedTurret private var targets: List[Target] = List[Target]() + /** + * The entity that claims responsibility for the actions of the turret + * or has authoritative management over the turret. + * When no one else steps up to the challenge, the turret can be its own person. + * @return owner entity + */ def TurretOwner: SourceEntry def Target: Option[Target] = currentTarget diff --git a/src/main/scala/net/psforever/objects/serverobject/turret/AutomatedTurretBehavior.scala b/src/main/scala/net/psforever/objects/serverobject/turret/auto/AutomatedTurretBehavior.scala similarity index 84% rename from src/main/scala/net/psforever/objects/serverobject/turret/AutomatedTurretBehavior.scala rename to src/main/scala/net/psforever/objects/serverobject/turret/auto/AutomatedTurretBehavior.scala index 884d28fbe..36d26487b 100644 --- a/src/main/scala/net/psforever/objects/serverobject/turret/AutomatedTurretBehavior.scala +++ b/src/main/scala/net/psforever/objects/serverobject/turret/auto/AutomatedTurretBehavior.scala @@ -1,14 +1,19 @@ // Copyright (c) 2024 PSForever -package net.psforever.objects.serverobject.turret +package net.psforever.objects.serverobject.turret.auto import akka.actor.{Actor, Cancellable} -import net.psforever.objects.{Default, Player, Vehicle} +import net.psforever.objects.avatar.scoring.EquipmentStat +import net.psforever.objects.equipment.EffectTarget import net.psforever.objects.serverobject.PlanetSideServerObject -import net.psforever.objects.serverobject.damage.DamageableEntity -import net.psforever.objects.sourcing.{SourceEntry, SourceUniqueness} +import net.psforever.objects.serverobject.damage.{Damageable, DamageableEntity} +import net.psforever.objects.serverobject.mount.Mountable +import net.psforever.objects.serverobject.turret.Automation +import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, SourceUniqueness} import net.psforever.objects.vital.Vitality import net.psforever.objects.vital.interaction.DamageResult +import net.psforever.objects.zones.exp.ToDatabase import net.psforever.objects.zones.{InteractsWithZone, Zone} +import net.psforever.objects.{Default, PlanetSideGameObject, Player, Vehicle} import net.psforever.types.{PlanetSideGUID, Vector3} import scala.concurrent.ExecutionContext.Implicits.global @@ -23,20 +28,26 @@ trait AutomatedTurretBehavior { private var automaticOperation: Boolean = false /** quick reference of the current target, if any */ private var currentTargetToken: Option[SourceUniqueness] = None + /** time of the current target's selection or the last target's selection */ + private var currentTargetSwitchTime: Long = 0L /** 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 + /** self-reported weapon fire produces projectiles that were shot; + * due to the call and response nature of this mode, they also count as shots that were landed */ + private var shotsFired: Int = 0 + /** self-reported weapon fire produces targets that were eliminated; + * due to the call and response nature of this mode, they also count as shots that were landed; + * this may duplicate information processed during some other database update call */ + private var targetsDestroyed: Int = 0 def AutomatedTurretObject: AutomatedTurret @@ -44,11 +55,11 @@ trait AutomatedTurretBehavior { case AutomatedTurretBehavior.Alert(target) => bringAttentionToTarget(target) - case AutomatedTurretBehavior.ConfirmShot(target, None) => - confirmShotFunc(target) - case AutomatedTurretBehavior.ConfirmShot(target, _) => - confirmShotFunc(target) + normalConfirmShot(target) + + case SelfReportedConfirmShot(target) => + movementCancelSelfReportingFireConfirmShot(target) case AutomatedTurretBehavior.Unalert(target) => disregardTarget(target) @@ -166,8 +177,6 @@ trait AutomatedTurretBehavior { AutomatedTurretObject.Target.foreach(noLongerEngageDetectedTarget) AutomatedTurretObject.Target = None AutomatedTurretObject.Clear() - previousTestedTargets.foreach(noLongerEngageTestedTarget) - previousTestedTargets = Set() currentTargetToken = None currentTargetLocation = None } @@ -181,18 +190,26 @@ trait AutomatedTurretBehavior { * 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 + * @return `true`, if the target submitted was recognized by the turret; + * `false`, if the target can not be the current target */ - private def normalConfirmShot(target: Target): Unit = { + private def normalConfirmShot(target: Target): Boolean = { val now = System.currentTimeMillis() if (currentTargetToken.isEmpty) { currentTargetLastShotTime = now currentTargetLocation = Some(target.Position) + cancelSelfReportedAutoFire() engageNewDetectedTarget(target) + true } else if ( currentTargetToken.contains(SourceEntry(target).unique) && - now - currentTargetLastShotTime < autoStats.map(_.missedShotCooldown).getOrElse(0L)) { + now - currentTargetLastShotTime < autoStats.map(_.cooldowns.missedShot).getOrElse(0L)) { currentTargetLastShotTime = now currentTargetLocation = Some(target.Position) + cancelSelfReportedAutoFire() + true + } else { + false } } @@ -207,10 +224,9 @@ trait AutomatedTurretBehavior { 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) + currentTargetSwitchTime = System.currentTimeMillis() AutomatedTurretObject.Target = target AutomatedTurretBehavior.startTracking(target, zoneid, AutomatedTurretObject.GUID, List(target.GUID)) AutomatedTurretBehavior.startShooting(target, zoneid, AutomatedTurretObject.Weapons.values.head.Equipment.get.GUID) @@ -280,30 +296,20 @@ trait AutomatedTurretBehavior { val turretPosition = AutomatedTurretObject.Position val turretGuid = AutomatedTurretObject.GUID val weaponGuid = AutomatedTurretObject.Weapons.values.head.Equipment.get.GUID - val radius = autoStats.get.targetTriggerRange - val validation = autoStats.get.targetValidation + val radius = autoStats.get.ranges.trigger + val validation = autoStats.get.checks.validation val faction = AutomatedTurretObject.Faction - //noinspection CollectHeadOption - val (targets, forValidation) = AutomatedTurretObject + val selectedTargets = AutomatedTurretObject .Targets .collect { case target if /*target.Faction != faction &&*/ AutomatedTurretBehavior.shapedDistanceCheckAgainstValue(autoStats, target.Position, turretPosition, radius, result = -1) && - validation.exists(func => func(target)) => + validation.exists(func => func(target))=> target } .sortBy(target => Vector3.DistanceSquared(target.Position, turretPosition)) - .flatMap { processForTestingDetectedTarget(_, turretGuid, weaponGuid) } - .unzip - //call an explicit stop for these targets - (for { - a <- forValidation - b <- previousTestedTargets - if SourceEntry(a).unique != SourceEntry(b).unique - } yield b) - .foreach(noLongerEngageTestedTarget) - previousTestedTargets = forValidation.toSet - targets.headOption + selectedTargets.foreach(processForTestingDetectedTarget(_, turretGuid, weaponGuid)) + selectedTargets.headOption } } @@ -363,7 +369,7 @@ trait AutomatedTurretBehavior { private def performDistanceCheck(): List[Target] = { //cull targets val pos = AutomatedTurretObject.Position - val range = autoStats.map(_.targetDetectionRange).getOrElse(0f) + val range = autoStats.map(_.ranges.detection).getOrElse(0f) val removedTargets = AutomatedTurretObject.Targets .collect { case t: InteractsWithZone @@ -388,10 +394,10 @@ trait AutomatedTurretBehavior { generalDecayCheck( target, now, - autoStats.map(_.targetEscapeRange).getOrElse(400f), - autoStats.map(_.targetSelectCooldown).getOrElse(3000L), - autoStats.map(_.missedShotCooldown).getOrElse(3000L), - autoStats.map(_.targetEliminationCooldown).getOrElse(0L) + autoStats.map(_.ranges.escape).getOrElse(400f), + autoStats.map(_.cooldowns.targetSelect).getOrElse(3000L), + autoStats.map(_.cooldowns.missedShot).getOrElse(3000L), + autoStats.map(_.cooldowns.targetElimination).getOrElse(0L) ) } .orElse { @@ -409,10 +415,11 @@ trait AutomatedTurretBehavior { * 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, + * no longer qualifies as a target due to an internal or external change, + * has 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. + * Apply appropriate cooldown to 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 */ @@ -425,11 +432,17 @@ trait AutomatedTurretBehavior { eliminationDelay: Long ): Option[Target] = { if (target.Destroyed) { - //if the target died while we were shooting at it + //if the target died or is no longer considered a valid target while we were shooting at it cancelSelfReportedAutoFire() noLongerEngageDetectedTarget(target) currentTargetLastShotTime = now + eliminationDelay None + } else if ((AutomatedTurretBehavior.commonBlanking ++ autoStats.map(_.checks.blanking).getOrElse(Nil)).exists(func => func(target))) { + //if the target, while being engaged, stops counting as a valid target + cancelSelfReportedAutoFire() + noLongerEngageDetectedTarget(target) + currentTargetLastShotTime = now + selectDelay + None } else if (AutomatedTurretBehavior.shapedDistanceCheckAgainstValue(autoStats, target.Position, AutomatedTurretObject.Position, escapeRange)) { //if the target made sufficient distance from the turret cancelSelfReportedAutoFire() @@ -469,7 +482,7 @@ trait AutomatedTurretBehavior { */ private def retimePeriodicTargetChecks(beforeSize: Int): Boolean = { if (beforeSize == 0 && AutomatedTurretObject.Targets.nonEmpty && autoStats.isDefined) { - val repeated = autoStats.map(_.detectionSpeed).getOrElse(0.seconds) + val repeated = autoStats.map(_.detectionSweepTime).getOrElse(1.seconds) retimePeriodicTargetChecks(repeated) true } else { @@ -509,9 +522,32 @@ trait AutomatedTurretBehavior { * It's like nothing ever happened. * @see `Actor.postStop` */ - def automaticTurretPostStop(): Unit = { + protected def automaticTurretPostStop(): Unit = { resetAlerts() AutomatedTurretObject.Targets.foreach { AutomatedTurretObject.RemoveTarget } + selfReportingCleanUp() + } + + /** + * Cleanup for the variables involved in self-reporting. + * Set them to zero. + */ + protected def selfReportingCleanUp(): Unit = { + shotsFired = 0 + targetsDestroyed = 0 + } + + /** + * The self-reporting mode for automatic turrets produces weapon fire data that should be sent to the database. + * The targets destroyed from self-reported fire are also logged to the database. + */ + protected def selfReportingDatabaseUpdate(): Unit = { + AutomatedTurretObject.TurretOwner match { + case p: PlayerSource => + val weaponId = AutomatedTurretObject.Weapons.values.head.Equipment.map(_.Definition.ObjectId).getOrElse(0) + ToDatabase.reportToolDischarge(p.CharId, EquipmentStat(weaponId, shotsFired, shotsFired, targetsDestroyed, 0)) + case _ => () + } } /** @@ -522,7 +558,11 @@ trait AutomatedTurretBehavior { * @return something the turret can potentially shoot at */ protected def attemptRetaliation(target: Target, cause: DamageResult): Option[Target] = { - if (automaticOperation && autoStats.exists(_.retaliatoryDuration > 0)) { + if ( + automaticOperation && + !currentTargetToken.contains(SourceEntry(target).unique) && + autoStats.exists(_.retaliatoryDelay > 0) + ) { AutomatedTurretBehavior.getAttackVectorFromCause(target.Zone, cause).collect { case attacker if attacker.Faction != target.Faction => performRetaliation(attacker) @@ -542,7 +582,11 @@ trait AutomatedTurretBehavior { private def performRetaliation(target: Target): Option[Target] = { AutomatedTurretObject.Target .collect { - case existingTarget if autoStats.exists(_.retaliationOverridesTarget) => + case existingTarget + if autoStats.exists { auto => + auto.retaliationOverridesTarget && + currentTargetSwitchTime + auto.retaliatoryDelay > System.currentTimeMillis() + } => cancelSelfReportedAutoFire() noLongerEngageDetectedTarget(existingTarget) engageNewDetectedTarget(target) @@ -565,7 +609,14 @@ trait AutomatedTurretBehavior { * @param target something the turret can potentially shoot at */ private def movementCancelSelfReportingFireConfirmShot(target: Target): Unit = { - normalConfirmShot(target) + currentTargetLastShotTime = System.currentTimeMillis() + shotsFired += 1 + target match { + case v: Damageable with Mountable + if v.Destroyed && v.Seats.values.exists(_.isOccupied) => + targetsDestroyed += 1 + case _ => () + } if (currentTargetLocation.exists(loc => Vector3.DistanceSquared(loc, target.Position) > 1f)) { cancelSelfReportedAutoFire() noLongerEngageDetectedTarget(target) @@ -575,7 +626,7 @@ trait AutomatedTurretBehavior { AutomatedTurretObject.Weapons.values.head.Equipment.get.GUID ) } else { - tryStartSelfReportedAutofire(target) + tryPerformSelfReportedAutofire(target) } } @@ -592,7 +643,9 @@ trait AutomatedTurretBehavior { private def trySelfReportedAutofireIfStationary(): Boolean = { AutomatedTurretObject.Target .collect { - case target if currentTargetLocation.exists(loc => Vector3.DistanceSquared(loc, target.Position) > 1f) => + case target + if currentTargetLocation.exists(loc => Vector3.DistanceSquared(loc, target.Position) > 1f) && + autoStats.exists(_.refireTime > 0.seconds) => trySelfReportedAutofireTest(target) } .getOrElse(false) @@ -606,8 +659,7 @@ trait AutomatedTurretBehavior { */ private def trySelfReportedAutofireTest(target: Target): Boolean = { if (selfReportedRefire.isCancelled) { - confirmShotFunc = movementCancelSelfReportingFireConfirmShot - target.Actor ! AffectedByAutomaticTurretFire.AiDamage(AutomatedTurretObject) + target.Actor ! AiDamage(AutomatedTurretObject) true } else { false @@ -621,14 +673,13 @@ trait AutomatedTurretBehavior { * @return `true`, if the self-reporting operation was initiated; * `false`, otherwise */ - private def tryStartSelfReportedAutofire(target: Target): Boolean = { + private def tryPerformSelfReportedAutofire(target: Target): Boolean = { if (selfReportedRefire.isCancelled) { - confirmShotFunc = movementCancelSelfReportingFireConfirmShot selfReportedRefire = context.system.scheduler.scheduleWithFixedDelay( 0.seconds, - autoStats.map(_.refireTime).getOrElse(1.second), + autoStats.map(_.refireTime).getOrElse(1.seconds), target.Actor, - AffectedByAutomaticTurretFire.AiDamage(AutomatedTurretObject) + AiDamage(AutomatedTurretObject) ) true } else { @@ -638,13 +689,13 @@ trait AutomatedTurretBehavior { /** * Stop directly communicating with a target to simulate weapons fire damage. + * Utilized as a p[art of the auto-fire reset process. * @return `true`, because we can not fail * @see `Default.Cancellable` */ private def cancelSelfReportedAutoFire(): Boolean = { selfReportedRefire.cancel() selfReportedRefire = Default.Cancellable - confirmShotFunc = normalConfirmShot true } } @@ -661,6 +712,11 @@ object AutomatedTurretBehavior { private case object PeriodicCheck + final val commonBlanking: List[PlanetSideGameObject => Boolean] = List( + EffectTarget.Validation.PlayerUndetectedByAutoTurret, + EffectTarget.Validation.VehicleUndetectedByAutoTurret + ) + /** * Are we tracking a `Vehicle` entity? * or, is it some other kind of entity? @@ -801,8 +857,8 @@ object AutomatedTurretBehavior { result: Int = 1 //by default, calculation > input ): Boolean = { val testRangeSq = range * range - if (stats.exists(_.cylindricalCheck)) { - val height = range + stats.map(_.cylindricalHeight).getOrElse(0f) + if (stats.exists(_.cylindrical)) { + val height = range + stats.map(_.cylindricalExtraHeight).getOrElse(0f) math.abs(positionA.z - positionB.z).compareTo(height) == result && Vector3.DistanceSquared(positionA.xy, positionB.xy).compareTo(testRangeSq) == result } else { diff --git a/src/main/scala/net/psforever/objects/serverobject/turret/AutomatedTurretDispatch.scala b/src/main/scala/net/psforever/objects/serverobject/turret/auto/AutomatedTurretDispatch.scala similarity index 96% rename from src/main/scala/net/psforever/objects/serverobject/turret/AutomatedTurretDispatch.scala rename to src/main/scala/net/psforever/objects/serverobject/turret/auto/AutomatedTurretDispatch.scala index dddaa2c1d..501cd7672 100644 --- a/src/main/scala/net/psforever/objects/serverobject/turret/AutomatedTurretDispatch.scala +++ b/src/main/scala/net/psforever/objects/serverobject/turret/auto/AutomatedTurretDispatch.scala @@ -1,8 +1,8 @@ // Copyright (c) 2024 PSForever -package net.psforever.objects.serverobject.turret +package net.psforever.objects.serverobject.turret.auto import akka.actor.ActorRef -import net.psforever.objects.serverobject.turret.AutomatedTurret.Target +import net.psforever.objects.serverobject.turret.auto.AutomatedTurret.Target import net.psforever.packet.PlanetSideGamePacket import net.psforever.packet.game.{ChangeFireStateMessage_Start, ChangeFireStateMessage_Stop, ObjectDetectedMessage} import net.psforever.services.Service diff --git a/src/main/scala/net/psforever/objects/serverobject/turret/auto/SelfReportingMessages.scala b/src/main/scala/net/psforever/objects/serverobject/turret/auto/SelfReportingMessages.scala new file mode 100644 index 000000000..7ac2f6c7a --- /dev/null +++ b/src/main/scala/net/psforever/objects/serverobject/turret/auto/SelfReportingMessages.scala @@ -0,0 +1,6 @@ +// Copyright (c) 2024 PSForever +package net.psforever.objects.serverobject.turret.auto + +private[auto] case class AiDamage(turret: AutomatedTurret) + +private[auto] case class SelfReportedConfirmShot(target: AutomatedTurret.Target) 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 d78c087ea..da1a05050 100644 --- a/src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala +++ b/src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala @@ -20,7 +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.serverobject.turret.auto.AffectedByAutomaticTurretFire import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource} import net.psforever.objects.vehicles._ import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult}