diff --git a/src/main/scala/net/psforever/actors/session/SessionActor.scala b/src/main/scala/net/psforever/actors/session/SessionActor.scala
index 0f26f1179..a3ee36e54 100644
--- a/src/main/scala/net/psforever/actors/session/SessionActor.scala
+++ b/src/main/scala/net/psforever/actors/session/SessionActor.scala
@@ -518,6 +518,9 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
case packet: LashMessage =>
sessionFuncs.shooting.handleLashHit(packet)
+ case packet: AIDamage =>
+ sessionFuncs.shooting.handleAIDamage(packet)
+
case packet: AvatarFirstTimeEventMessage =>
sessionFuncs.handleAvatarFirstTimeEvent(packet)
diff --git a/src/main/scala/net/psforever/actors/session/support/SessionData.scala b/src/main/scala/net/psforever/actors/session/support/SessionData.scala
index a4ca457d1..9eba63b82 100644
--- a/src/main/scala/net/psforever/actors/session/support/SessionData.scala
+++ b/src/main/scala/net/psforever/actors/session/support/SessionData.scala
@@ -4,6 +4,7 @@ package net.psforever.actors.session.support
import akka.actor.typed.scaladsl.adapter._
import akka.actor.{ActorContext, ActorRef, Cancellable, typed}
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource}
+import net.psforever.objects.vital.etc.SuicideReason
import net.psforever.objects.zones.blockmap.{SectorGroup, SectorPopulation}
import scala.collection.mutable
@@ -882,13 +883,7 @@ class SessionData(
case (None, _, _) => ()
case (Some(us: PlanetSideServerObject with Vitality with FactionAffinity), PlanetSideGUID(0), _) =>
- if (collisionHistory.get(us.Actor) match {
- case Some(lastCollision) if curr - lastCollision <= 1000L =>
- false
- case _ =>
- collisionHistory.put(us.Actor, curr)
- true
- }) {
+ if (updateCollisionHistoryForTarget(us, curr)) {
if (!bailProtectStatus) {
handleDealingDamage(
us,
@@ -901,40 +896,26 @@ class SessionData(
}
}
+ case (Some(us: Vehicle), _, Some(victim: SensorDeployable)) =>
+ collisionBetweenVehicleAndFragileDeployable(us, ppos, victim, tpos, velocity - tv, fallHeight, curr)
+
+ case (Some(us: Vehicle), _, Some(victim: TurretDeployable)) if victim.Seats.isEmpty =>
+ collisionBetweenVehicleAndFragileDeployable(us, ppos, victim, tpos, velocity - tv, fallHeight, curr)
+
case (
Some(us: PlanetSideServerObject with Vitality with FactionAffinity), _,
Some(victim: PlanetSideServerObject with Vitality with FactionAffinity)
) =>
- if (collisionHistory.get(victim.Actor) match {
- case Some(lastCollision) if curr - lastCollision <= 1000L =>
- false
- case _ =>
- collisionHistory.put(victim.Actor, curr)
- true
- }) {
+ if (updateCollisionHistoryForTarget(victim, curr)) {
val usSource = SourceEntry(us)
val victimSource = SourceEntry(victim)
//we take damage from the collision
if (!bailProtectStatus) {
- handleDealingDamage(
- us,
- DamageInteraction(
- usSource,
- CollisionWithReason(CollisionReason(velocity - tv, fallHeight, us.DamageModel), victimSource),
- ppos
- )
- )
+ performCollisionWithSomethingDamage(us, usSource, ppos, victimSource, fallHeight, velocity - tv)
}
//get dealt damage from our own collision (no protection)
collisionHistory.put(us.Actor, curr)
- handleDealingDamage(
- victim,
- DamageInteraction(
- victimSource,
- CollisionWithReason(CollisionReason(tv - velocity, 0, victim.DamageModel), usSource),
- tpos
- )
- )
+ performCollisionWithSomethingDamage(victim, victimSource, tpos, usSource, fallHeight = 0f, tv - velocity)
}
case _ => ()
@@ -2202,7 +2183,7 @@ class SessionData(
/**
* Calculate the amount of damage to be dealt to an active `target`
- * using the information reconstructed from a `Resolvedprojectile`
+ * using the information reconstructed from a `ResolvedProjectile`
* and affect the `target` in a synchronized manner.
* The active `target` and the target of the `DamageResult` do not have be the same.
* While the "tell" for being able to sustain damage is an entity of type `Vitality`,
@@ -2836,6 +2817,59 @@ class SessionData(
}
}
+ private def updateCollisionHistoryForTarget(
+ target: PlanetSideServerObject with Vitality with FactionAffinity,
+ curr: Long
+ ): Boolean = {
+ collisionHistory.get(target.Actor) match {
+ case Some(lastCollision) if curr - lastCollision <= 1000L =>
+ false
+ case _ =>
+ collisionHistory.put(target.Actor, curr)
+ true
+ }
+ }
+
+ private def collisionBetweenVehicleAndFragileDeployable(
+ vehicle: Vehicle,
+ vehiclePosition: Vector3,
+ smallDeployable: Deployable,
+ smallDeployablePosition: Vector3,
+ velocity: Vector3,
+ fallHeight: Float,
+ collisionTime: Long
+ ): Unit = {
+ if (updateCollisionHistoryForTarget(smallDeployable, collisionTime)) {
+ val smallDeployableSource = SourceEntry(smallDeployable)
+ //vehicle takes damage from the collision (ignore bail protection in this case)
+ performCollisionWithSomethingDamage(vehicle, SourceEntry(vehicle), vehiclePosition, smallDeployableSource, fallHeight, velocity)
+ //deployable gets absolutely destroyed
+ collisionHistory.put(vehicle.Actor, collisionTime)
+ handleDealingDamage(
+ smallDeployable,
+ DamageInteraction(smallDeployableSource, SuicideReason(), smallDeployablePosition)
+ )
+ }
+ }
+
+ private def performCollisionWithSomethingDamage(
+ target: PlanetSideServerObject with Vitality with FactionAffinity,
+ targetSource: SourceEntry,
+ targetPosition: Vector3,
+ victimSource: SourceEntry,
+ fallHeight: Float,
+ velocity: Vector3
+ ): Unit = {
+ handleDealingDamage(
+ target,
+ DamageInteraction(
+ targetSource,
+ CollisionWithReason(CollisionReason(velocity, fallHeight, target.DamageModel), victimSource),
+ targetPosition
+ )
+ )
+ }
+
def failWithError(error: String): Unit = {
log.error(error)
middlewareActor ! MiddlewareActor.Teardown()
diff --git a/src/main/scala/net/psforever/actors/session/support/SessionLocalHandlers.scala b/src/main/scala/net/psforever/actors/session/support/SessionLocalHandlers.scala
index 6d78bd875..f0dc4f03f 100644
--- a/src/main/scala/net/psforever/actors/session/support/SessionLocalHandlers.scala
+++ b/src/main/scala/net/psforever/actors/session/support/SessionLocalHandlers.scala
@@ -208,6 +208,7 @@ class SessionLocalHandlers(
continent.GUID(vehicleGuid)
.collect { case vehicle: MountableWeapons => (vehicle, vehicle.PassengerInSeat(player)) }
.collect { case (vehicle, Some(seat_num)) => vehicle.WeaponControlledFromSeat(seat_num) }
+ .getOrElse(Set.empty)
.collect { case weapon: Tool if weapon.GUID == weaponGuid =>
sendResponse(InventoryStateMessage(weapon.AmmoSlot.Box.GUID, weapon.GUID, weapon.Magazine))
}
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 2e7c8484b..b6cdd5f30 100644
--- a/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala
+++ b/src/main/scala/net/psforever/actors/session/support/WeaponAndProjectileOperations.scala
@@ -2,7 +2,10 @@
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.VanuSentry
import net.psforever.objects.zones.exp.ToDatabase
import scala.collection.mutable
@@ -51,8 +54,7 @@ private[support] class WeaponAndProjectileOperations(
private[support] var shotsWhileDead: Int = 0
private val projectiles: Array[Option[Projectile]] =
Array.fill[Option[Projectile]](Projectile.rangeUID - Projectile.baseUID)(None)
- private var zoningOpt: Option[ZoningOperations] = None
- def zoning: ZoningOperations = zoningOpt.orNull
+
/* packets */
def handleWeaponFire(pkt: WeaponFireMessage): Unit = {
@@ -430,6 +432,55 @@ private[support] class WeaponAndProjectileOperations(
}
}
+ def handleAIDamage(pkt: AIDamage): Unit = {
+ val AIDamage(targetGuid, attackerGuid, projectileTypeId, _, _) = pkt
+ (continent.GUID(player.VehicleSeated) match {
+ case Some(tobj: PlanetSideServerObject with FactionAffinity with Vitality with OwnableByPlayer)
+ if tobj.GUID == targetGuid &&
+ tobj.OwnerGuid.contains(player.GUID) =>
+ //deployable turrets
+ Some(tobj)
+ case Some(tobj: PlanetSideServerObject with FactionAffinity with Vitality with Mountable)
+ if tobj.GUID == targetGuid &&
+ tobj.Seats.values.flatMap(_.occupants.map(_.GUID)).toSeq.contains(player.GUID) =>
+ //facility turrets, etc.
+ Some(tobj)
+ case _
+ if player.GUID == targetGuid =>
+ //player avatars
+ Some(player)
+ case _ =>
+ None
+ }).collect {
+ case target: AutomatedTurret.Target =>
+ sessionData.validObject(attackerGuid, decorator = "AIDamage/AutomatedTurret")
+ .collect {
+ case turret: AutomatedTurret if turret.Target.isEmpty =>
+ turret.Actor ! AutomatedTurretBehavior.ConfirmShot(target)
+ Some(target)
+
+ case turret: AutomatedTurret =>
+ turret.Actor ! AutomatedTurretBehavior.ConfirmShot(target)
+ HandleAIDamage(target, CompileAutomatedTurretDamageData(turret, turret.TurretOwner, projectileTypeId))
+ Some(target)
+ }
+ }
+ .orElse {
+ //occasionally, something that is not technically a turret's natural target may be attacked
+ sessionData.validObject(targetGuid, decorator = "AIDamage/Target")
+ .collect {
+ case target: PlanetSideServerObject with FactionAffinity with Vitality =>
+ sessionData.validObject(attackerGuid, decorator = "AIDamage/Attacker")
+ .collect {
+ case turret: AutomatedTurret if turret.Target.nonEmpty =>
+ //the turret must be shooting at something (else) first
+ HandleAIDamage(target, CompileAutomatedTurretDamageData(turret, turret.TurretOwner, projectileTypeId))
+ }
+ Some(target)
+ }
+ }
+ }
+
/* support code */
def HandleWeaponFireOperations(
@@ -519,11 +570,6 @@ private[support] class WeaponAndProjectileOperations(
)
continent.Projectile ! ZoneProjectile.Add(player.GUID, qualityprojectile)
}
- obj match {
- case turret: FacilityTurret if turret.Definition == GlobalDefinitions.vanu_sentry_turret =>
- turret.Actor ! FacilityTurret.WeaponDischarged()
- case _ => ()
- }
} else {
log.warn(
s"WeaponFireMessage: ${player.Name}'s ${tool.Definition.Name} projectile is too far from owner position at time of discharge ($distanceToOwner > $acceptableDistanceToOwner); suspect"
@@ -1174,6 +1220,10 @@ private[support] class WeaponAndProjectileOperations(
}
private def fireStateStartMountedMessages(itemGuid: PlanetSideGUID): Unit = {
+ sessionData.findContainedEquipment()._1.collect {
+ case turret: FacilityTurret if continent.map.cavern =>
+ turret.Actor ! VanuSentry.ChangeFireStart
+ }
continent.VehicleEvents ! VehicleServiceMessage(
continent.id,
VehicleAction.ChangeFireState_Start(player.GUID, itemGuid)
@@ -1236,6 +1286,10 @@ private[support] class WeaponAndProjectileOperations(
}
private def fireStateStopMountedMessages(itemGuid: PlanetSideGUID): Unit = {
+ sessionData.findContainedEquipment()._1.collect {
+ case turret: FacilityTurret if continent.map.cavern =>
+ turret.Actor ! VanuSentry.ChangeFireStop
+ }
continent.VehicleEvents ! VehicleServiceMessage(
continent.id,
VehicleAction.ChangeFireState_Stop(player.GUID, itemGuid)
@@ -1366,6 +1420,7 @@ private[support] class WeaponAndProjectileOperations(
addShotsToMap(shotsFired, weaponId, shots)
}
+ //noinspection SameParameterValue
private def addShotsLanded(weaponId: Int, shots: Int): Unit = {
addShotsToMap(shotsLanded, weaponId, shots)
}
@@ -1405,6 +1460,44 @@ private[support] class WeaponAndProjectileOperations(
ToDatabase.reportToolDischarge(avatarId, EquipmentStat(weaponId, fired, landed, 0, 0))
}
+ private def CompileAutomatedTurretDamageData(
+ turret: AutomatedTurret,
+ owner: SourceEntry,
+ projectileTypeId: Long
+ ): Option[(AutomatedTurret, Tool, SourceEntry, ProjectileDefinition)] = {
+ turret.Weapons
+ .values
+ .flatMap { _.Equipment }
+ .collect { case weapon: Tool => (turret, weapon, owner, weapon.Projectile) }
+ .find { case (_, _, _, p) => p.ObjectId == projectileTypeId }
+ }
+
+ private def HandleAIDamage(
+ target: PlanetSideServerObject with FactionAffinity with Vitality,
+ results: Option[(AutomatedTurret, Tool, SourceEntry, ProjectileDefinition)]
+ ): Unit = {
+ results.collect {
+ case (obj, tool, owner, projectileInfo) =>
+ val angle = Vector3.Unit(target.Position - obj.Position)
+ val proj = new Projectile(
+ projectileInfo,
+ tool.Definition,
+ tool.FireMode,
+ None,
+ owner,
+ obj.Definition.ObjectId,
+ obj.Position + Vector3.z(value = 1f),
+ angle,
+ Some(angle * projectileInfo.FinalVelocity)
+ )
+ val hitPos = target.Position + Vector3.z(value = 1f)
+ ResolveProjectileInteraction(proj, DamageResolution.Hit, target, hitPos).collect { resprojectile =>
+ addShotsLanded(resprojectile.cause.attribution, shots = 1)
+ sessionData.handleDealingDamage(target, resprojectile)
+ }
+ }
+ }
+
override protected[session] def stop(): Unit = {
if (player != null && player.HasGUID) {
(prefire ++ shooting).foreach { guid =>
diff --git a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala
index 7c49124c2..f61853fe6 100644
--- a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala
+++ b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala
@@ -12,9 +12,10 @@ import net.psforever.objects.avatar.scoring.{CampaignStatistics, ScoreCard, Sess
import net.psforever.objects.inventory.InventoryItem
import net.psforever.objects.serverobject.mount.Seat
import net.psforever.objects.serverobject.tube.SpawnTube
+import net.psforever.objects.serverobject.turret.auto.AutomatedTurret
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource}
import net.psforever.objects.vital.{InGameHistory, IncarnationActivity, ReconstructionActivity, SpawningActivity}
-import net.psforever.packet.game.{CampaignStatistic, MailMessage, SessionStatistic}
+import net.psforever.packet.game.{CampaignStatistic, ChangeFireStateMessage_Start, MailMessage, ObjectDetectedMessage, SessionStatistic}
import scala.collection.mutable
import scala.concurrent.duration._
@@ -259,6 +260,19 @@ class ZoningOperations(
)
}
}
+ //auto turret behavior
+ (obj match {
+ case turret: AutomatedTurret with JammableUnit => turret.Target
+ case _ => None
+ }).collect {
+ target =>
+ val guid = obj.GUID
+ val turret = obj.asInstanceOf[AutomatedTurret]
+ sendResponse(ObjectDetectedMessage(guid, guid, 0, List(target.GUID)))
+ if (!obj.asInstanceOf[JammableUnit].Jammed) {
+ sendResponse(ChangeFireStateMessage_Start(turret.Weapons.values.head.Equipment.get.GUID))
+ }
+ }
})
//sensor animation
normal
@@ -555,6 +569,14 @@ class ZoningOperations(
)
case _ => ;
}
+ turret.Target.collect {
+ target =>
+ val guid = turret.GUID
+ sendResponse(ObjectDetectedMessage(guid, guid, 0, List(target.GUID)))
+ if (!turret.Jammed) {
+ sendResponse(ChangeFireStateMessage_Start(turret.Weapons.values.head.Equipment.get.GUID))
+ }
+ }
}
//remote projectiles and radiation clouds
continent.Projectiles.foreach { projectile =>
diff --git a/src/main/scala/net/psforever/actors/zone/ZoneActor.scala b/src/main/scala/net/psforever/actors/zone/ZoneActor.scala
index 43ee1ffa4..87ae03190 100644
--- a/src/main/scala/net/psforever/actors/zone/ZoneActor.scala
+++ b/src/main/scala/net/psforever/actors/zone/ZoneActor.scala
@@ -13,6 +13,8 @@ import akka.actor.typed.scaladsl.adapter._
import net.psforever.actors.zone.building.MajorFacilityLogic
import net.psforever.objects.avatar.scoring.Kill
import net.psforever.objects.serverobject.affinity.FactionAffinity
+import net.psforever.objects.serverobject.terminals.capture.CaptureTerminalAwareBehavior
+import net.psforever.objects.serverobject.turret.FacilityTurret
import net.psforever.objects.sourcing.SourceEntry
import net.psforever.objects.vital.{InGameActivity, InGameHistory}
import net.psforever.objects.zones.exp.{ExperienceCalculator, SupportExperienceCalculator}
@@ -96,14 +98,18 @@ class ZoneActor(context: ActorContext[ZoneActor.Command], zone: Zone)
case Success(buildings) =>
buildings.foreach { building =>
zone.BuildingByMapId(building.localId) match {
- case Some(_: WarpGate) => ;
+ case Some(_: WarpGate) => ()
//warp gates are controlled by game logic and are better off not restored via the database
case Some(b) =>
if ((b.Faction = PlanetSideEmpire(building.factionId)) != PlanetSideEmpire.NEUTRAL) {
b.ForceDomeActive = MajorFacilityLogic.checkForceDomeStatus(b).getOrElse(false)
- b.Neighbours.getOrElse(Nil).foreach { _.Actor ! BuildingActor.AlertToFactionChange(b) }
+ b.Neighbours.getOrElse(Nil).foreach(_.Actor ! BuildingActor.AlertToFactionChange(b))
+ b.CaptureTerminal.collect { terminal =>
+ val msg = CaptureTerminalAwareBehavior.TerminalStatusChanged(terminal, isResecured = true)
+ b.Amenities.collect { case turret: FacilityTurret => turret.Actor ! msg }
+ }
}
- case None => ;
+ case None => ()
// TODO this happens during testing, need a way to not always persist during tests
}
}
diff --git a/src/main/scala/net/psforever/objects/GlobalDefinitions.scala b/src/main/scala/net/psforever/objects/GlobalDefinitions.scala
index ae8797c09..094419264 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.{FacilityTurretDefinition, TurretUpgrade}
+import net.psforever.objects.serverobject.turret.{AutoChecks, AutoCooldowns, AutoRanges, Automation, FacilityTurretDefinition, TurretUpgrade}
import net.psforever.objects.vehicles.{DestroyedVehicle, InternalTelepadDefinition, UtilityType, VehicleSubsystemEntry}
import net.psforever.objects.vital.base.DamageType
import net.psforever.objects.vital.damage._
@@ -1913,6 +1913,20 @@ object GlobalDefinitions {
}
}
+ /**
+ * Using the definition for a `Vehicle` determine whether it is an all-terrain vehicle type.
+ * @param vdef the `VehicleDefinition` of the vehicle
+ * @return `true`, if it is; `false`, otherwise
+ */
+ def isAtvVehicle(vdef: VehicleDefinition): Boolean = {
+ vdef match {
+ case `quadassault` | `fury` | `quadstealth` =>
+ true
+ case _ =>
+ false
+ }
+ }
+
/**
* Using the definition for a `Vehicle` determine whether it can fly.
* Does not count the flying battleframe robotics vehicles.
@@ -9059,6 +9073,24 @@ object GlobalDefinitions {
spitfire_turret.DeployTime = Duration.create(5000, "ms")
spitfire_turret.Model = ComplexDeployableResolutions.calculate
spitfire_turret.deployAnimation = DeployAnimation.Standard
+ spitfire_turret.AutoFire = Automation(
+ AutoRanges(
+ detection = 75f,
+ trigger = 50f,
+ escape = 50f
+ ),
+ AutoChecks(
+ validation = List(
+ EffectTarget.Validation.SmallRoboticsTurretValidatePlayerTarget,
+ EffectTarget.Validation.SmallRoboticsTurretValidateMaxTarget,
+ EffectTarget.Validation.SmallRoboticsTurretValidateGroundVehicleTarget,
+ EffectTarget.Validation.SmallRoboticsTurretValidateAircraftTarget,
+ EffectTarget.Validation.AutoTurretValidateMountableEntityTarget
+ )
+ ),
+ retaliatoryDelay = 2000L, //8000L
+ refireTime = 200.milliseconds //150.milliseconds
+ )
spitfire_turret.innateDamage = new DamageWithPosition {
CausesDamageType = DamageType.One
Damage0 = 200
@@ -9085,6 +9117,30 @@ object GlobalDefinitions {
spitfire_cloaked.DeployTime = Duration.create(5000, "ms")
spitfire_cloaked.deployAnimation = DeployAnimation.Standard
spitfire_cloaked.Model = ComplexDeployableResolutions.calculate
+ spitfire_cloaked.AutoFire = Automation(
+ AutoRanges(
+ detection = 75f,
+ trigger = 50f,
+ escape = 75f
+ ),
+ AutoChecks(
+ validation = List(
+ EffectTarget.Validation.SmallRoboticsTurretValidatePlayerTarget,
+ EffectTarget.Validation.SmallRoboticsTurretValidateMaxTarget,
+ EffectTarget.Validation.SmallRoboticsTurretValidateGroundVehicleTarget,
+ EffectTarget.Validation.SmallRoboticsTurretValidateAircraftTarget,
+ EffectTarget.Validation.AutoTurretValidateMountableEntityTarget
+ )
+ ),
+ cooldowns = AutoCooldowns(
+ targetSelect = 0L,
+ missedShot = 0L
+ ),
+ detectionSweepTime = 500.milliseconds,
+ retaliatoryDelay = 1L, //8000L
+ retaliationOverridesTarget = false,
+ refireTime = 200.milliseconds //150.milliseconds
+ )
spitfire_cloaked.innateDamage = new DamageWithPosition {
CausesDamageType = DamageType.One
Damage0 = 50
@@ -9111,6 +9167,21 @@ object GlobalDefinitions {
spitfire_aa.DeployTime = Duration.create(5000, "ms")
spitfire_aa.deployAnimation = DeployAnimation.Standard
spitfire_aa.Model = ComplexDeployableResolutions.calculate
+ spitfire_aa.AutoFire = Automation(
+ AutoRanges(
+ detection = 125f,
+ trigger = 100f,
+ escape = 200f
+ ),
+ AutoChecks(
+ validation = List(EffectTarget.Validation.SmallRoboticsTurretValidateAircraftTarget)
+ ),
+ retaliatoryDelay = 2000L, //8000L
+ retaliationOverridesTarget = false,
+ refireTime = 0.seconds, //300.milliseconds
+ cylindrical = true,
+ cylindricalExtraHeight = 50f
+ )
spitfire_aa.innateDamage = new DamageWithPosition {
CausesDamageType = DamageType.One
Damage0 = 200
@@ -9175,9 +9246,10 @@ object GlobalDefinitions {
portable_manned_turret.Damageable = true
portable_manned_turret.Repairable = true
portable_manned_turret.RepairIfDestroyed = false
- portable_manned_turret.controlledWeapons(seat = 0, weapon = 1)
portable_manned_turret.WeaponPaths += 1 -> new mutable.HashMap()
portable_manned_turret.WeaponPaths(1) += TurretUpgrade.None -> energy_gun
+ portable_manned_turret.Seats += 0 -> new SeatDefinition()
+ portable_manned_turret.controlledWeapons(seat = 0, weapon = 1)
portable_manned_turret.MountPoints += 1 -> MountInfo(0)
portable_manned_turret.MountPoints += 2 -> MountInfo(0)
portable_manned_turret.ReserveAmmunition = true
@@ -9209,6 +9281,7 @@ object GlobalDefinitions {
portable_manned_turret_nc.RepairIfDestroyed = false
portable_manned_turret_nc.WeaponPaths += 1 -> new mutable.HashMap()
portable_manned_turret_nc.WeaponPaths(1) += TurretUpgrade.None -> energy_gun_nc
+ portable_manned_turret_nc.Seats += 0 -> new SeatDefinition()
portable_manned_turret_nc.controlledWeapons(seat = 0, weapon = 1)
portable_manned_turret_nc.MountPoints += 1 -> MountInfo(0)
portable_manned_turret_nc.MountPoints += 2 -> MountInfo(0)
@@ -9240,6 +9313,7 @@ object GlobalDefinitions {
portable_manned_turret_tr.RepairIfDestroyed = false
portable_manned_turret_tr.WeaponPaths += 1 -> new mutable.HashMap()
portable_manned_turret_tr.WeaponPaths(1) += TurretUpgrade.None -> energy_gun_tr
+ portable_manned_turret_tr.Seats += 0 -> new SeatDefinition()
portable_manned_turret_tr.controlledWeapons(seat = 0, weapon = 1)
portable_manned_turret_tr.MountPoints += 1 -> MountInfo(0)
portable_manned_turret_tr.MountPoints += 2 -> MountInfo(0)
@@ -9271,6 +9345,7 @@ object GlobalDefinitions {
portable_manned_turret_vs.RepairIfDestroyed = false
portable_manned_turret_vs.WeaponPaths += 1 -> new mutable.HashMap()
portable_manned_turret_vs.WeaponPaths(1) += TurretUpgrade.None -> energy_gun_vs
+ portable_manned_turret_vs.Seats += 0 -> new SeatDefinition()
portable_manned_turret_vs.controlledWeapons(seat = 0, weapon = 1)
portable_manned_turret_vs.MountPoints += 1 -> MountInfo(0)
portable_manned_turret_vs.MountPoints += 2 -> MountInfo(0)
@@ -9999,7 +10074,7 @@ object GlobalDefinitions {
manned_turret.Name = "manned_turret"
manned_turret.MaxHealth = 3600
manned_turret.Damageable = true
- manned_turret.DamageDisablesAt = 0
+ manned_turret.DamageDisablesAt = 1800
manned_turret.Repairable = true
manned_turret.autoRepair = AutoRepairStats(1.0909f, 10000, 1600, 0.05f)
manned_turret.RepairIfDestroyed = true
@@ -10007,11 +10082,32 @@ object GlobalDefinitions {
manned_turret.WeaponPaths(1) += TurretUpgrade.None -> phalanx_sgl_hevgatcan
manned_turret.WeaponPaths(1) += TurretUpgrade.AVCombo -> phalanx_avcombo
manned_turret.WeaponPaths(1) += TurretUpgrade.FlakCombo -> phalanx_flakcombo
+ manned_turret.Seats += 0 -> new SeatDefinition()
manned_turret.controlledWeapons(seat = 0, weapon = 1)
manned_turret.MountPoints += 1 -> MountInfo(0)
manned_turret.FactionLocked = true
manned_turret.ReserveAmmunition = false
manned_turret.RadiationShielding = 0.5f
+ manned_turret.AutoFire = Automation(
+ AutoRanges(
+ detection = 125f,
+ trigger = 100f,
+ escape = 200f
+ ),
+ AutoChecks(
+ validation = List(
+ EffectTarget.Validation.FacilityTurretValidateMaxTarget,
+ EffectTarget.Validation.FacilityTurretValidateGroundVehicleTarget,
+ EffectTarget.Validation.FacilityTurretValidateAircraftTarget,
+ EffectTarget.Validation.AutoTurretValidateMountableEntityTarget
+ )
+ ),
+ retaliatoryDelay = 4000L, //8000L
+ cylindrical = true,
+ cylindricalExtraHeight = 50f,
+ detectionSweepTime = 2.seconds,
+ refireTime = 362.milliseconds //312.milliseconds
+ )
manned_turret.innateDamage = new DamageWithPosition {
CausesDamageType = DamageType.One
Damage0 = 150
@@ -10031,6 +10127,7 @@ object GlobalDefinitions {
vanu_sentry_turret.RepairIfDestroyed = true
vanu_sentry_turret.WeaponPaths += 1 -> new mutable.HashMap()
vanu_sentry_turret.WeaponPaths(1) += TurretUpgrade.None -> vanu_sentry_turret_weapon
+ vanu_sentry_turret.Seats += 0 -> new SeatDefinition()
vanu_sentry_turret.controlledWeapons(seat = 0, weapon = 1)
vanu_sentry_turret.MountPoints += 1 -> MountInfo(0)
vanu_sentry_turret.MountPoints += 2 -> MountInfo(0)
diff --git a/src/main/scala/net/psforever/objects/OwnableByPlayer.scala b/src/main/scala/net/psforever/objects/OwnableByPlayer.scala
index a83c908e4..2653dca35 100644
--- a/src/main/scala/net/psforever/objects/OwnableByPlayer.scala
+++ b/src/main/scala/net/psforever/objects/OwnableByPlayer.scala
@@ -1,7 +1,7 @@
// Copyright (c) 2019 PSForever
package net.psforever.objects
-import net.psforever.objects.sourcing.{PlayerSource, UniquePlayer}
+import net.psforever.objects.sourcing.UniquePlayer
import net.psforever.types.PlanetSideGUID
trait OwnableByPlayer {
@@ -46,11 +46,11 @@ trait OwnableByPlayer {
def AssignOwnership(playerOpt: Option[Player]): OwnableByPlayer = {
(originalOwnerName, playerOpt) match {
case (None, Some(player)) =>
- owner = Some(PlayerSource(player).unique)
+ owner = Some(UniquePlayer(player))
originalOwnerName = originalOwnerName.orElse { Some(player.Name) }
OwnerGuid = player
case (_, Some(player)) =>
- owner = Some(PlayerSource(player).unique)
+ owner = Some(UniquePlayer(player))
OwnerGuid = player
case (_, None) =>
owner = None
diff --git a/src/main/scala/net/psforever/objects/Player.scala b/src/main/scala/net/psforever/objects/Player.scala
index 9b7f55181..52b65fcc0 100644
--- a/src/main/scala/net/psforever/objects/Player.scala
+++ b/src/main/scala/net/psforever/objects/Player.scala
@@ -3,7 +3,7 @@ package net.psforever.objects
import net.psforever.objects.avatar.{Avatar, LoadoutManager, SpecialCarry}
import net.psforever.objects.ballistics.InteractWithRadiationClouds
-import net.psforever.objects.ce.{Deployable, InteractWithMines}
+import net.psforever.objects.ce.{Deployable, InteractWithMines, InteractWithTurrets}
import net.psforever.objects.definition.{AvatarDefinition, ExoSuitDefinition, SpecialExoSuitDefinition}
import net.psforever.objects.equipment.{Equipment, EquipmentSize, EquipmentSlot, JammableUnit}
import net.psforever.objects.inventory.{Container, GridInventory, InventoryItem}
@@ -38,6 +38,7 @@ class Player(var avatar: Avatar)
with MountableEntity {
interaction(new InteractWithEnvironment())
interaction(new InteractWithMinesUnlessSpectating(obj = this, range = 10))
+ interaction(new InteractWithTurrets())
interaction(new InteractWithRadiationClouds(range = 10f, Some(this)))
private var backpack: Boolean = false
diff --git a/src/main/scala/net/psforever/objects/SensorDeployable.scala b/src/main/scala/net/psforever/objects/SensorDeployable.scala
index 2b4fe67ad..cd08bedb3 100644
--- a/src/main/scala/net/psforever/objects/SensorDeployable.scala
+++ b/src/main/scala/net/psforever/objects/SensorDeployable.scala
@@ -17,6 +17,7 @@ import net.psforever.services.Service
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
+import scala.annotation.unused
import scala.concurrent.duration._
class SensorDeployable(cdef: SensorDeployableDefinition) extends Deployable(cdef) with Hackable with JammableUnit
@@ -27,7 +28,7 @@ class SensorDeployableDefinition(private val objectId: Int) extends DeployableDe
Model = SimpleResolutions.calculate
Packet = new SmallDeployableConverter
- override def Initialize(obj: Deployable, context: ActorContext) = {
+ override def Initialize(obj: Deployable, context: ActorContext): Unit = {
obj.Actor =
context.actorOf(Props(classOf[SensorDeployableControl], obj), PlanetSideServerObject.UniqueActorName(obj))
}
@@ -45,10 +46,10 @@ class SensorDeployableControl(sensor: SensorDeployable)
with JammableBehavior
with DamageableEntity
with RepairableEntity {
- def DeployableObject = sensor
- def JammableObject = sensor
- def DamageableObject = sensor
- def RepairableObject = sensor
+ def DeployableObject: SensorDeployable = sensor
+ def JammableObject: SensorDeployable = sensor
+ def DamageableObject: SensorDeployable = sensor
+ def RepairableObject: SensorDeployable = sensor
override def postStop(): Unit = {
super.postStop()
@@ -64,7 +65,7 @@ class SensorDeployableControl(sensor: SensorDeployable)
case _ => ;
}
- override protected def DamageLog(msg: String): Unit = {}
+ override protected def DamageLog(@unused msg: String): Unit = {}
override protected def DestructionAwareness(target: Damageable.Target, cause: DamageResult): Unit = {
super.DestructionAwareness(target, cause)
@@ -88,7 +89,7 @@ class SensorDeployableControl(sensor: SensorDeployable)
val zone = obj.Zone
zone.LocalEvents ! LocalServiceMessage(
zone.id,
- LocalAction.TriggerEffectInfo(Service.defaultPlayerGUID, "on", obj.GUID, false, 1000)
+ LocalAction.TriggerEffectInfo(Service.defaultPlayerGUID, "on", obj.GUID, unk1=false, 1000)
)
super.StartJammeredStatus(obj, dur)
case _ => ;
@@ -113,7 +114,7 @@ class SensorDeployableControl(sensor: SensorDeployable)
val zone = sensor.Zone
zone.LocalEvents ! LocalServiceMessage(
zone.id,
- LocalAction.TriggerEffectInfo(Service.defaultPlayerGUID, "on", obj.GUID, true, 1000)
+ LocalAction.TriggerEffectInfo(Service.defaultPlayerGUID, "on", obj.GUID, unk1=true, 1000)
)
case _ => ;
}
@@ -125,7 +126,7 @@ class SensorDeployableControl(sensor: SensorDeployable)
val zone = sensor.Zone
zone.LocalEvents ! LocalServiceMessage(
zone.id,
- LocalAction.TriggerEffectInfo(Service.defaultPlayerGUID, "on", sensor.GUID, true, 1000)
+ LocalAction.TriggerEffectInfo(Service.defaultPlayerGUID, "on", sensor.GUID, unk1=true, 1000)
)
}
}
@@ -142,7 +143,7 @@ object SensorDeployableControl {
val zone = target.Zone
zone.LocalEvents ! LocalServiceMessage(
zone.id,
- LocalAction.TriggerEffectInfo(Service.defaultPlayerGUID, "on", target.GUID, false, 1000)
+ LocalAction.TriggerEffectInfo(Service.defaultPlayerGUID, "on", target.GUID, unk1=false, 1000)
)
//position the explosion effect near the bulky area of the sensor stalk
val ang = target.Orientation
diff --git a/src/main/scala/net/psforever/objects/TurretDeployable.scala b/src/main/scala/net/psforever/objects/TurretDeployable.scala
index 013092633..18ed2fe37 100644
--- a/src/main/scala/net/psforever/objects/TurretDeployable.scala
+++ b/src/main/scala/net/psforever/objects/TurretDeployable.scala
@@ -1,35 +1,56 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects
-import akka.actor.{Actor, ActorContext, Props}
-import net.psforever.objects.ce.{Deployable, DeployableBehavior, DeployedItem}
+import akka.actor.{Actor, ActorContext, ActorRef, Props}
+import net.psforever.objects.ce.{Deployable, DeployableBehavior, DeployedItem, InteractWithTurrets}
import net.psforever.objects.definition.DeployableDefinition
import net.psforever.objects.definition.converter.SmallTurretConverter
-import net.psforever.objects.equipment.{JammableMountedWeapons, JammableUnit}
+import net.psforever.objects.equipment.JammableUnit
import net.psforever.objects.guid.{GUIDTask, TaskWorkflow}
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.affinity.FactionAffinityBehavior
-import net.psforever.objects.serverobject.damage.Damageable.Target
-import net.psforever.objects.serverobject.damage.DamageableWeaponTurret
+import net.psforever.objects.serverobject.damage.Damageable
import net.psforever.objects.serverobject.hackable.Hackable
-import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior}
-import net.psforever.objects.serverobject.repair.RepairableWeaponTurret
-import net.psforever.objects.serverobject.turret.{TurretDefinition, WeaponTurret}
+import net.psforever.objects.serverobject.mount.{InteractWithRadiationCloudsSeatedInEntity, Mountable}
+import net.psforever.objects.serverobject.turret.auto.AutomatedTurret.Target
+import net.psforever.objects.serverobject.turret.auto.{AffectedByAutomaticTurretFire, 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.resistance.StandardResistanceProfile
import net.psforever.objects.vital.{SimpleResolutions, StandardVehicleResistance}
+import net.psforever.objects.zones.InteractsWithZone
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
+import net.psforever.types.PlanetSideGUID
import scala.concurrent.duration.FiniteDuration
class TurretDeployable(tdef: TurretDeployableDefinition)
- extends Deployable(tdef)
+ extends Deployable(tdef)
+ with AutomatedTurret
with WeaponTurret
with JammableUnit
+ with InteractsWithZone
+ with StandardResistanceProfile
with Hackable {
- WeaponTurret.LoadDefinition(this)
+ if (tdef.Seats.nonEmpty) {
+ interaction(new InteractWithTurrets())
+ interaction(new InteractWithRadiationCloudsSeatedInEntity(obj = this, range = 100f))
+ }
+ WeaponTurret.LoadDefinition(turret = this)
- override def Definition = tdef
+ def TurretOwner: SourceEntry = {
+ Seats
+ .values
+ .headOption
+ .flatMap(_.occupant)
+ .map(p => PlayerSource.inSeat(PlayerSource(p), SourceEntry(this), seatNumber=0))
+ .orElse(Owners.map(PlayerSource(_, Position)))
+ .getOrElse(SourceEntry(this))
+ }
+
+ override def Definition: TurretDeployableDefinition = tdef
}
class TurretDeployableDefinition(private val objectId: Int)
@@ -46,7 +67,7 @@ class TurretDeployableDefinition(private val objectId: Int)
//override to clarify inheritance conflict
override def MaxHealth_=(max: Int): Int = super[DeployableDefinition].MaxHealth_=(max)
- override def Initialize(obj: Deployable, context: ActorContext) = {
+ override def Initialize(obj: Deployable, context: ActorContext): Unit = {
obj.Actor = context.actorOf(Props(classOf[TurretControl], obj), PlanetSideServerObject.UniqueActorName(obj))
}
}
@@ -63,35 +84,92 @@ class TurretControl(turret: TurretDeployable)
extends Actor
with DeployableBehavior
with FactionAffinityBehavior.Check
- with JammableMountedWeapons //note: jammable status is reported as vehicle events, not local events
- with MountableBehavior
- with DamageableWeaponTurret
- with RepairableWeaponTurret {
- def DeployableObject = turret
- def MountableObject = turret
- def JammableObject = turret
- def FactionObject = turret
- def DamageableObject = turret
- def RepairableObject = turret
+ with MountableTurretControl
+ with AutomatedTurretBehavior
+ with AffectedByAutomaticTurretFire {
+ def TurretObject: TurretDeployable = turret
+ def DeployableObject: TurretDeployable = turret
+ def MountableObject: TurretDeployable = turret
+ def JammableObject: TurretDeployable = turret
+ def FactionObject: TurretDeployable = turret
+ def DamageableObject: TurretDeployable = turret
+ def RepairableObject: TurretDeployable = turret
+ def AutomatedTurretObject: TurretDeployable = turret
+ def AffectedObject: TurretDeployable = turret
override def postStop(): Unit = {
super.postStop()
deployableBehaviorPostStop()
- damageableWeaponTurretPostStop()
+ selfReportingDatabaseUpdate()
+ automaticTurretPostStop()
}
def receive: Receive =
- deployableBehavior
+ commonBehavior
+ .orElse(deployableBehavior)
.orElse(checkBehavior)
- .orElse(jammableBehavior)
.orElse(mountBehavior)
- .orElse(dismountBehavior)
- .orElse(takesDamage)
- .orElse(canBeRepairedByNanoDispenser)
+ .orElse(automatedTurretBehavior)
+ .orElse(takeAutomatedDamage)
.orElse {
- case _ => ;
+ case _ => ()
}
+ protected def engageNewDetectedTarget(
+ target: AutomatedTurret.Target,
+ channel: String,
+ turretGuid: PlanetSideGUID,
+ weaponGuid: PlanetSideGUID
+ ): Unit = {
+ val zone = target.Zone
+ AutomatedTurretBehavior.startTracking(zone, channel, turretGuid, List(target.GUID))
+ AutomatedTurretBehavior.startShooting(zone, channel, weaponGuid)
+ }
+
+ protected def noLongerEngageTarget(
+ target: AutomatedTurret.Target,
+ channel: String,
+ turretGuid: PlanetSideGUID,
+ weaponGuid: PlanetSideGUID
+ ): Option[AutomatedTurret.Target] = {
+ val zone = target.Zone
+ AutomatedTurretBehavior.stopTracking(zone, channel, turretGuid)
+ AutomatedTurretBehavior.stopShooting(zone, channel, weaponGuid)
+ None
+ }
+
+ protected def testNewDetected(
+ target: AutomatedTurret.Target,
+ channel: String,
+ turretGuid: PlanetSideGUID,
+ weaponGuid: PlanetSideGUID
+ ): Unit = {
+ val zone = target.Zone
+ AutomatedTurretBehavior.startTracking(zone, channel, turretGuid, List(target.GUID))
+ AutomatedTurretBehavior.startShooting(zone, channel, weaponGuid)
+ AutomatedTurretBehavior.stopShooting(zone, channel, weaponGuid)
+ }
+
+ protected def testKnownDetected(
+ target: AutomatedTurret.Target,
+ channel: String,
+ turretGuid: PlanetSideGUID,
+ weaponGuid: PlanetSideGUID
+ ): Unit = {
+ val zone = target.Zone
+ AutomatedTurretBehavior.startShooting(zone, channel, weaponGuid)
+ AutomatedTurretBehavior.stopShooting(zone, channel, weaponGuid)
+ }
+
+ override protected def suspendTargetTesting(
+ target: Target,
+ channel: String,
+ turretGuid: PlanetSideGUID,
+ weaponGuid: PlanetSideGUID
+ ): Unit = {
+ AutomatedTurretBehavior.stopTracking(target.Zone, channel, turretGuid)
+ }
+
override protected def mountTest(
obj: PlanetSideServerObject with Mountable,
seatNumber: Int,
@@ -99,7 +177,40 @@ class TurretControl(turret: TurretDeployable)
(!turret.Definition.FactionLocked || player.Faction == obj.Faction) && !obj.Destroyed
}
- override protected def DestructionAwareness(target: Target, cause: DamageResult): Unit = {
+ override def TryJammerEffectActivate(target: Any, cause: DamageResult): Unit = {
+ val startsUnjammed = !JammableObject.Jammed
+ super.TryJammerEffectActivate(target, cause)
+ if (JammableObject.Jammed && AutomatedTurretObject.Definition.AutoFire.exists(_.retaliatoryDelay > 0)) {
+ if (startsUnjammed) {
+ AutomaticOperation = false
+ }
+ //look in direction of cause of jamming
+ val zone = JammableObject.Zone
+ AutomatedTurretBehavior.getAttackVectorFromCause(zone, cause).foreach { attacker =>
+ AutomatedTurretBehavior.startTracking(zone, zone.id, AutomatedTurretObject.GUID, List(attacker.GUID))
+ }
+ }
+ }
+
+ override def CancelJammeredStatus(target: Any): Unit = {
+ val startsJammed = JammableObject.Jammed
+ super.CancelJammeredStatus(target)
+ if (startsJammed && AutomaticOperation_=(state = true)) {
+ val zone = TurretObject.Zone
+ AutomatedTurretBehavior.stopTracking(zone, zone.id, TurretObject.GUID)
+ }
+ }
+
+ override protected def DamageAwareness(target: Damageable.Target, cause: DamageResult, amount: Any): Unit = {
+ amount match {
+ case 0 => ()
+ case _ => attemptRetaliation(target, cause)
+ }
+ super.DamageAwareness(target, cause, amount)
+ }
+
+ override protected def DestructionAwareness(target: Damageable.Target, cause: DamageResult): Unit = {
+ AutomaticOperation = false
super.DestructionAwareness(target, cause)
CancelJammeredSound(target)
CancelJammeredStatus(target)
@@ -107,22 +218,22 @@ class TurretControl(turret: TurretDeployable)
}
override def deconstructDeployable(time: Option[FiniteDuration]) : Unit = {
+ AutomaticOperation = false
val zone = turret.Zone
val seats = turret.Seats.values
//either we have no seats or no one gets to sit
val retime = if (seats.count(_.isOccupied) > 0) {
- //unlike with vehicles, it's possible to request deconstruction of one's own field turret while seated in it
+ //it's possible to request deconstruction of one's own field turret while seated in it
val wasKickedByDriver = false
seats.foreach { seat =>
- seat.occupant match {
- case Some(tplayer) =>
- seat.unmount(tplayer)
- tplayer.VehicleSeated = None
+ seat.occupant.collect {
+ case player: Player =>
+ seat.unmount(player)
+ player.VehicleSeated = None
zone.VehicleEvents ! VehicleServiceMessage(
zone.id,
- VehicleAction.KickPassenger(tplayer.GUID, 4, wasKickedByDriver, turret.GUID)
+ VehicleAction.KickPassenger(player.GUID, 4, wasKickedByDriver, turret.GUID)
)
- case None => ;
}
}
Some(time.getOrElse(Deployable.cleanup) + Deployable.cleanup)
@@ -132,6 +243,11 @@ class TurretControl(turret: TurretDeployable)
super.deconstructDeployable(retime)
}
+ override def finalizeDeployable(callback: ActorRef): Unit = {
+ super.finalizeDeployable(callback)
+ AutomaticOperation = true
+ }
+
override def unregisterDeployable(obj: Deployable): Unit = {
val zone = obj.Zone
TaskWorkflow.execute(GUIDTask.unregisterDeployableTurret(zone.GUID, turret))
diff --git a/src/main/scala/net/psforever/objects/Vehicle.scala b/src/main/scala/net/psforever/objects/Vehicle.scala
index 962e19cff..d6bbd19d1 100644
--- a/src/main/scala/net/psforever/objects/Vehicle.scala
+++ b/src/main/scala/net/psforever/objects/Vehicle.scala
@@ -1,7 +1,7 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects
-import net.psforever.objects.ce.InteractWithMines
+import net.psforever.objects.ce.{InteractWithMines, InteractWithTurrets}
import net.psforever.objects.definition.{ToolDefinition, VehicleDefinition}
import net.psforever.objects.equipment.{Equipment, EquipmentSize, EquipmentSlot, JammableUnit}
import net.psforever.objects.inventory.{Container, GridInventory, InventoryItem, InventoryTile}
@@ -92,6 +92,7 @@ class Vehicle(private val vehicleDef: VehicleDefinition)
with MountableEntity {
interaction(new InteractWithEnvironment())
interaction(new InteractWithMines(range = 20))
+ interaction(new InteractWithTurrets())
interaction(new InteractWithRadiationCloudsSeatedInVehicle(obj = this, range = 20))
private var faction: PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL
diff --git a/src/main/scala/net/psforever/objects/avatar/Avatar.scala b/src/main/scala/net/psforever/objects/avatar/Avatar.scala
index 4b00b9c42..166137c2d 100644
--- a/src/main/scala/net/psforever/objects/avatar/Avatar.scala
+++ b/src/main/scala/net/psforever/objects/avatar/Avatar.scala
@@ -224,6 +224,10 @@ case class Avatar(
false
}
+ override def hashCode(): Int = {
+ id
+ }
+
/** Avatar assertions
* These protect against programming errors by asserting avatar properties have correct values
* They may or may not be disabled for live applications
diff --git a/src/main/scala/net/psforever/objects/ce/InteractWithTurrets.scala b/src/main/scala/net/psforever/objects/ce/InteractWithTurrets.scala
new file mode 100644
index 000000000..b6c772eca
--- /dev/null
+++ b/src/main/scala/net/psforever/objects/ce/InteractWithTurrets.scala
@@ -0,0 +1,86 @@
+// Copyright (c) 2021 PSForever
+package net.psforever.objects.ce
+
+import net.psforever.objects.GlobalDefinitions
+import net.psforever.objects.serverobject.PlanetSideServerObject
+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.SourceUniqueness
+import net.psforever.types.Vector3
+
+case object TurretInteraction extends ZoneInteractionType
+
+/**
+ * ...
+ */
+class InteractWithTurrets()
+ extends ZoneInteraction {
+ def range: Float = InteractWithTurrets.Range
+
+ def Type: TurretInteraction.type = TurretInteraction
+
+ /**
+ * ...
+ */
+ def interaction(sector: SectorPopulation, target: InteractsWithZone): Unit = {
+ target match {
+ case clarifiedTarget: AutomatedTurret.Target =>
+ val pos = clarifiedTarget.Position
+ val unique = SourceUniqueness(clarifiedTarget)
+ val targets = getTurretTargets(sector, pos).filter { turret => turret.Definition.AutoFire.nonEmpty && turret.Detected(unique).isEmpty }
+ targets.foreach { t => t.Actor ! AutomatedTurretBehavior.Alert(clarifiedTarget) }
+ case _ => ()
+ }
+ }
+
+ private def getTurretTargets(
+ sector: SectorPopulation,
+ position: Vector3
+ ): Iterable[PlanetSideServerObject with AutomatedTurret] = {
+ val list: Iterable[AutomatedTurret] = sector
+ .deployableList
+ .collect {
+ case turret: AutomatedTurret => turret
+ } ++ sector
+ .amenityList
+ .collect {
+ case turret: AutomatedTurret => turret
+ }
+ list.collect {
+ case turret: AutomatedTurret
+ if {
+ val stats = turret.Definition.AutoFire
+ stats.nonEmpty &&
+ AutomatedTurretBehavior.shapedDistanceCheckAgainstValue(stats, turret.Position, position, range, result = -1)
+ } => turret
+ }
+ }
+
+ /**
+ * ...
+ * @param target na
+ */
+ def resetInteraction(target: InteractsWithZone): Unit = {
+ getTurretTargets(
+ target.getInteractionSector(),
+ target.Position.xy
+ ).foreach { turret =>
+ turret.Actor ! AutomatedTurretBehavior.Reset
+ }
+ }
+}
+
+object InteractWithTurrets {
+ private lazy val Range: Float = {
+ Seq(
+ GlobalDefinitions.spitfire_turret,
+ GlobalDefinitions.spitfire_cloaked,
+ GlobalDefinitions.spitfire_aa,
+ GlobalDefinitions.manned_turret
+ )
+ .flatMap(_.AutoFire)
+ .map(_.ranges.detection)
+ .max
+ }
+}
diff --git a/src/main/scala/net/psforever/objects/definition/converter/AmmoBoxConverter.scala b/src/main/scala/net/psforever/objects/definition/converter/AmmoBoxConverter.scala
index d2351f5e3..4e2af370c 100644
--- a/src/main/scala/net/psforever/objects/definition/converter/AmmoBoxConverter.scala
+++ b/src/main/scala/net/psforever/objects/definition/converter/AmmoBoxConverter.scala
@@ -9,7 +9,7 @@ import scala.util.{Success, Try}
class AmmoBoxConverter extends ObjectCreateConverter[AmmoBox] {
override def ConstructorData(obj: AmmoBox): Try[CommonFieldData] = {
- Success(CommonFieldData()(false))
+ Success(CommonFieldData()(flag = false))
}
override def DetailedConstructorData(obj: AmmoBox): Try[DetailedAmmoBoxData] = {
@@ -19,9 +19,9 @@ class AmmoBoxConverter extends ObjectCreateConverter[AmmoBox] {
PlanetSideEmpire.NEUTRAL,
bops = false,
alternate = false,
- true,
+ v1 = true,
None,
- false,
+ jammered = false,
None,
None,
PlanetSideGUID(0)
diff --git a/src/main/scala/net/psforever/objects/definition/converter/SmallTurretConverter.scala b/src/main/scala/net/psforever/objects/definition/converter/SmallTurretConverter.scala
index 13ee2745f..8bbb5249a 100644
--- a/src/main/scala/net/psforever/objects/definition/converter/SmallTurretConverter.scala
+++ b/src/main/scala/net/psforever/objects/definition/converter/SmallTurretConverter.scala
@@ -21,9 +21,9 @@ class SmallTurretConverter extends ObjectCreateConverter[TurretDeployable]() {
obj.Faction,
bops = false,
alternate = false,
- false,
+ v1 = true,
None,
- jammered = obj.Jammed,
+ obj.Jammed,
Some(true),
None,
obj.OwnerGuid match {
@@ -45,9 +45,9 @@ class SmallTurretConverter extends ObjectCreateConverter[TurretDeployable]() {
obj.Faction,
bops = false,
alternate = true,
- false,
+ v1 = false,
None,
- false,
+ jammered = false,
Some(false),
None,
PlanetSideGUID(0)
diff --git a/src/main/scala/net/psforever/objects/definition/converter/ToolConverter.scala b/src/main/scala/net/psforever/objects/definition/converter/ToolConverter.scala
index 6ebb3ee61..74405a5cb 100644
--- a/src/main/scala/net/psforever/objects/definition/converter/ToolConverter.scala
+++ b/src/main/scala/net/psforever/objects/definition/converter/ToolConverter.scala
@@ -21,7 +21,7 @@ class ToolConverter extends ObjectCreateConverter[Tool]() {
obj.Faction,
bops = false,
alternate = false,
- true,
+ v1 = true,
None,
obj.Jammed,
None,
@@ -47,7 +47,7 @@ class ToolConverter extends ObjectCreateConverter[Tool]() {
obj.Faction,
bops = false,
alternate = false,
- true,
+ v1 = true,
None,
obj.Jammed,
None,
diff --git a/src/main/scala/net/psforever/objects/entity/WorldEntity.scala b/src/main/scala/net/psforever/objects/entity/WorldEntity.scala
index 15b9a8759..1ca70cddf 100644
--- a/src/main/scala/net/psforever/objects/entity/WorldEntity.scala
+++ b/src/main/scala/net/psforever/objects/entity/WorldEntity.scala
@@ -32,10 +32,16 @@ trait WorldEntity {
def isMoving(test: Vector3): Boolean = WorldEntity.isMoving(Velocity, test)
/**
- * This object is not considered moving unless it is moving at least as fast as a certain velocity.
- * @param test the (squared) velocity to test against
- * @return `true`, if we are moving; `false`, otherwise
- */
+ * This object is not considered moving unless it is moving at least as fast as a certain velocity.
+ * @param test the velocity to test against
+ * @return `true`, if we are moving; `false`, otherwise
+ */
+ def isMoving(test: Double): Boolean = WorldEntity.isMoving(Velocity, (test * test).toFloat)
+ /**
+ * This object is not considered moving unless it is moving at least as fast as a certain velocity.
+ * @param test the (squared) velocity to test against
+ * @return `true`, if we are moving; `false`, otherwise
+ */
def isMoving(test: Float): Boolean = WorldEntity.isMoving(Velocity, test)
}
diff --git a/src/main/scala/net/psforever/objects/equipment/EffectTarget.scala b/src/main/scala/net/psforever/objects/equipment/EffectTarget.scala
index 0b119f3e6..101b1cc5a 100644
--- a/src/main/scala/net/psforever/objects/equipment/EffectTarget.scala
+++ b/src/main/scala/net/psforever/objects/equipment/EffectTarget.scala
@@ -2,9 +2,11 @@
package net.psforever.objects.equipment
import net.psforever.objects._
-import net.psforever.objects.ce.DeployableCategory
-import net.psforever.objects.serverobject.turret.FacilityTurret
-import net.psforever.objects.vital.DamagingActivity
+import net.psforever.objects.ce.{DeployableCategory, DeployedItem}
+import net.psforever.objects.serverobject.turret.{FacilityTurret, WeaponTurret}
+import net.psforever.objects.vital.{DamagingActivity, InGameHistory, Vitality}
+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)
@@ -21,6 +23,7 @@ object EffectTarget {
object Validation {
type Value = PlanetSideGameObject => Boolean
+ //noinspection ScalaUnusedSymbol
def Invalid(target: PlanetSideGameObject): Boolean = false
def Medical(target: PlanetSideGameObject): Boolean =
@@ -72,10 +75,10 @@ object EffectTarget {
}
/**
- * To repair at this landing pad, the vehicle:
+ * To repair at this landing pad, the vehicle must:
* be a flight vehicle,
- * must have some health already, but does not have all its health,
- * and can not have taken damage in the last five seconds.
+ * have some health already, but does not have all its health, and
+ * have not taken damage in the last five seconds.
*/
def PadLanding(target: PlanetSideGameObject): Boolean =
target match {
@@ -185,5 +188,269 @@ object EffectTarget {
case _ =>
false
}
+
+ def SmallRoboticsTurretValidatePlayerTarget(target: PlanetSideGameObject): Boolean =
+ target match {
+ case p: Player
+ if p.ExoSuit != ExoSuitType.MAX && p.VehicleSeated.isEmpty =>
+ val now = System.currentTimeMillis()
+ val pos = p.Position
+ val faction = p.Faction
+ val sector = p.Zone.blockMap.sector(pos, range = 51f)
+ //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 cloakedByInfiltrationSuit = p.ExoSuit == ExoSuitType.Infiltration && p.Cloaked
+ lazy val silentRunActive = p.avatar.implants.flatten.find(a => a.definition.implantType == ImplantType.SilentRun).exists(_.active)
+ lazy val movingFast = p.isMoving(test = 15.5d)
+ lazy val isCrouched = p.Crouching
+ lazy val isMoving = p.isMoving(test = 1d)
+ lazy val isJumping = p.Jumping
+ if (radarCloakedAms(sector, pos) || radarCloakedAegis(sector, pos)) false
+ else if (entityTookDamage(p, now) || usedEquipment) true
+ else if (radarCloakedSensor(sector, pos, faction) || silentRunActive) false
+ else if (radarEnhancedInterlink(sector, pos, faction)) true
+ else if (radarEnhancedSensor(sector, pos, faction)) !isCrouched && isMoving
+ else if (cloakedByInfiltrationSuit) isJumping || movingFast
+ else isJumping || movingFast
+ case _ =>
+ false
+ }
+
+ def SmallRoboticsTurretValidateMaxTarget(target: PlanetSideGameObject): Boolean =
+ target match {
+ case p: Player
+ if p.ExoSuit == ExoSuitType.MAX && p.VehicleSeated.isEmpty =>
+ val now = System.currentTimeMillis()
+ val pos = p.Position
+ val faction = p.Faction
+ val sector = p.Zone.blockMap.sector(pos, range = 51f)
+ lazy val usedEquipment = p.Holsters().flatMap(_.Equipment)
+ .collect { case t: Tool => now - t.LastDischarge }
+ .exists(_ < 2000L)
+ lazy val isMoving = p.isMoving(test = 1d)
+ if (radarCloakedAms(sector, pos) || radarCloakedAegis(sector, pos)) false
+ else if (entityTookDamage(p, now) || usedEquipment) true
+ else if (radarCloakedSensor(sector, pos, faction)) false
+ else if (radarEnhancedInterlink(sector, pos, faction)) true
+ else isMoving
+ case _ =>
+ false
+ }
+
+ def SmallRoboticsTurretValidateGroundVehicleTarget(target: PlanetSideGameObject): Boolean =
+ target match {
+ case v: Vehicle
+ if !GlobalDefinitions.isFlightVehicle(v.Definition) && v.MountedIn.isEmpty && v.Seats.values.exists(_.isOccupied) =>
+ val now = System.currentTimeMillis()
+ val vdef = v.Definition
+ val pos = v.Position
+ lazy val sector = v.Zone.blockMap.sector(pos, range = 51f)
+ lazy val usedEquipment = v.Weapons.values.flatMap(_.Equipment)
+ .collect { case t: Tool => now - t.LastDischarge }
+ .exists(_ < 2000L)
+ if (
+ (vdef == GlobalDefinitions.ams && v.DeploymentState == DriveState.Deployed) ||
+ radarCloakedAms(sector, pos) || radarCloakedAegis(sector, pos)
+ ) false
+ else !v.Cloaked && v.isMoving(test = 1d) || entityTookDamage(v, now) || usedEquipment
+ case _ =>
+ false
+ }
+
+ def SmallRoboticsTurretValidateAircraftTarget(target: PlanetSideGameObject): Boolean =
+ target match {
+ case v: Vehicle
+ if GlobalDefinitions.isFlightVehicle(v.Definition) && v.Seats.values.exists(_.isOccupied) =>
+ val now = System.currentTimeMillis()
+ val pos = v.Position
+ val sector = v.Zone.blockMap.sector(pos, range = 51f)
+ lazy val usedEquipment = v.Weapons.values.flatMap(_.Equipment)
+ .collect { case t: Tool => now - t.LastDischarge }
+ .exists(_ < 2000L)
+ if (radarCloakedAms(sector, pos) || radarCloakedAegis(sector, pos)) false
+ else !v.Cloaked && (v.isFlying || v.isMoving(test = 1d)) || entityTookDamage(v, now) || usedEquipment
+ case _ =>
+ false
+ }
+
+ def FacilityTurretValidateMaxTarget(target: PlanetSideGameObject): Boolean =
+ target match {
+ case p: Player
+ if p.ExoSuit == ExoSuitType.MAX && p.VehicleSeated.isEmpty =>
+ val now = System.currentTimeMillis()
+ val pos = p.Position
+ val faction = p.Faction
+ val sector = p.Zone.blockMap.sector(p.Position, range = 51f)
+ lazy val usedEquipment = p.Holsters().flatMap(_.Equipment)
+ .collect { case t: Tool => now - t.LastDischarge }
+ .exists(_ < 2000L)
+ if (radarCloakedAms(sector, pos) || radarCloakedAegis(sector, pos)) false
+ else if (radarCloakedSensor(sector, pos, faction)) entityTookDamage(p, now) || usedEquipment
+ else if (radarEnhancedInterlink(sector, pos, faction)) true
+ else p.isMoving(test = 15.5d)
+ case _ =>
+ false
+ }
+
+ def FacilityTurretValidateGroundVehicleTarget(target: PlanetSideGameObject): Boolean =
+ target match {
+ case v: Vehicle
+ if !GlobalDefinitions.isFlightVehicle(v.Definition) && v.MountedIn.isEmpty && v.Seats.values.exists(_.isOccupied) =>
+ val now = System.currentTimeMillis()
+ val vdef = v.Definition
+ val pos = v.Position
+ lazy val sector = v.Zone.blockMap.sector(pos, range = 51f)
+ lazy val usedEquipment = v.Weapons.values.flatMap(_.Equipment)
+ .collect { case t: Tool => now - t.LastDischarge }
+ .exists(_ < 2000L)
+ if (
+ (vdef == GlobalDefinitions.ams && v.DeploymentState == DriveState.Deployed) ||
+ vdef == GlobalDefinitions.two_man_assault_buggy ||
+ GlobalDefinitions.isAtvVehicle(vdef) || //todo should all ATV types get carte blanche treatment?
+ radarCloakedAms(sector, pos) ||
+ radarCloakedAegis(sector, pos)
+ ) false
+ else v.isMoving(test = 1d) || entityTookDamage(v, now) || usedEquipment
+ case _ =>
+ false
+ }
+
+ def FacilityTurretValidateAircraftTarget(target: PlanetSideGameObject): Boolean =
+ target match {
+ case v: Vehicle
+ if GlobalDefinitions.isFlightVehicle(v.Definition) && v.Seats.values.exists(_.isOccupied) =>
+ val now = System.currentTimeMillis()
+ val pos = v.Position
+ lazy val sector = v.Zone.blockMap.sector(pos, range = 51f)
+ lazy val usedEquipment = v.Weapons.values.flatMap(_.Equipment)
+ .collect { case t: Tool => now - t.LastDischarge }
+ .exists(_ < 2000L)
+ // from the perspective of a mosquito, at 5th gauge, forward velocity is 59~60
+ lazy val movingFast = Vector3.MagnitudeSquared(v.Velocity.getOrElse(Vector3.Zero).xy) > 3721f //61
+ lazy val isMoving = v.isMoving(test = 1d)
+ if (v.Cloaked || radarCloakedAms(sector, pos) || radarCloakedAegis(sector, pos)) false
+ else if (v.Definition == GlobalDefinitions.mosquito) movingFast
+ else v.isFlying && (isMoving || entityTookDamage(v, now) || usedEquipment)
+ case _ =>
+ false
+ }
+
+ def AutoTurretValidateMountableEntityTarget(target: PlanetSideGameObject): Boolean =
+ target match {
+ case _: Vehicle =>
+ false //strict vehicles are handled by other validations
+ case t: WeaponTurret with Vitality =>
+ t.Seats.values.exists(_.isOccupied)
+ case _ =>
+ false
+ }
+
+ def AutoTurretBlankPlayerTarget(target: PlanetSideGameObject): Boolean =
+ target match {
+ case p: Player =>
+ val pos = p.Position
+ lazy val sector = p.Zone.blockMap.sector(p.Position, range = 51f)
+ p.VehicleSeated.nonEmpty || radarCloakedAms(sector, pos) || radarCloakedAegis(sector, pos)
+ case _ =>
+ false
+ }
+
+ def AutoTurretBlankVehicleTarget(target: PlanetSideGameObject): Boolean =
+ target match {
+ case v: Vehicle =>
+ val pos = v.Position
+ lazy val sector = v.Zone.blockMap.sector(pos, range = 51f)
+ (v.Definition == GlobalDefinitions.ams && v.DeploymentState == DriveState.Deployed) ||
+ v.MountedIn.nonEmpty ||
+ v.Cloaked ||
+ radarCloakedAms(sector, pos) ||
+ radarCloakedAegis(sector, pos)
+ 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) < 169f //12+1m
+ }.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) < 121f //10+1m
+ }.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) < 961f //30+1m
+ }.contains(true)
+ }
+
+ private def entityTookDamage(
+ obj: InGameHistory,
+ now: Long = System.currentTimeMillis(),
+ interval: Long = 2000L
+ ): Boolean = {
+ obj.VitalsHistory()
+ .findLast(_.isInstanceOf[DamagingActivity])
+ .exists(dam => now - dam.time < interval)
}
}
diff --git a/src/main/scala/net/psforever/objects/equipment/JammingUnit.scala b/src/main/scala/net/psforever/objects/equipment/JammingUnit.scala
index ac33cae0d..84443d560 100644
--- a/src/main/scala/net/psforever/objects/equipment/JammingUnit.scala
+++ b/src/main/scala/net/psforever/objects/equipment/JammingUnit.scala
@@ -139,7 +139,7 @@ trait JammableBehavior {
* @param target the objects to be determined if affected by the source's jammering
* @param cause the source of the "jammered" status
*/
- def TryJammerEffectActivate(target: Any, cause: DamageResult): Unit =
+ def TryJammerEffectActivate(target: Any, cause: DamageResult): Unit = {
target match {
case obj: PlanetSideServerObject =>
val interaction = cause.interaction
@@ -157,8 +157,9 @@ trait JammableBehavior {
}
case None =>
}
- case _ => ;
+ case _ => ()
}
+ }
/**
* Activate a distinctive buzzing sound effect.
diff --git a/src/main/scala/net/psforever/objects/serverobject/hackable/GenericHackables.scala b/src/main/scala/net/psforever/objects/serverobject/hackable/GenericHackables.scala
index 1f6807692..5565bf13e 100644
--- a/src/main/scala/net/psforever/objects/serverobject/hackable/GenericHackables.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/hackable/GenericHackables.scala
@@ -84,7 +84,7 @@ object GenericHackables {
HackState.Start
} else if (progress >= 100L) {
HackState.Finished
- } else if (target.isMoving(1f)) {
+ } else if (target.isMoving(test = 1f)) {
// If the object is moving (more than slightly to account for things like magriders rotating, or the last velocity reported being the magrider dipping down on dismount) then cancel the hack
HackState.Cancelled
} else {
diff --git a/src/main/scala/net/psforever/objects/serverobject/mount/InteractWithRadiationCloudsSeatedInEntity.scala b/src/main/scala/net/psforever/objects/serverobject/mount/InteractWithRadiationCloudsSeatedInEntity.scala
new file mode 100644
index 000000000..ac87785c9
--- /dev/null
+++ b/src/main/scala/net/psforever/objects/serverobject/mount/InteractWithRadiationCloudsSeatedInEntity.scala
@@ -0,0 +1,99 @@
+// Copyright (c) 2024 PSForever
+package net.psforever.objects.serverobject.mount
+
+import net.psforever.objects.ballistics.{Projectile, ProjectileQuality}
+import net.psforever.objects.sourcing.SourceEntry
+import net.psforever.objects.vital.Vitality
+import net.psforever.objects.vital.base.{DamageResolution, DamageType}
+import net.psforever.objects.vital.etc.RadiationReason
+import net.psforever.objects.vital.interaction.DamageInteraction
+import net.psforever.objects.vital.resistance.StandardResistanceProfile
+import net.psforever.objects.zones.blockmap.SectorPopulation
+import net.psforever.objects.zones.{InteractsWithZone, Zone, ZoneInteraction}
+import net.psforever.types.PlanetSideGUID
+
+/**
+ * This game entity may infrequently test whether it may interact with radiation cloud projectiles
+ * that may be emitted in the game environment for a limited amount of time.
+ * Since the entity in question is a vehicle, the occupants of the vehicle get tested their interaction.
+ */
+class InteractWithRadiationCloudsSeatedInEntity(
+ private val obj: Mountable with StandardResistanceProfile,
+ val range: Float
+ ) extends ZoneInteraction {
+ /**
+ * radiation clouds that, though detected, are skipped from affecting the target;
+ * in between interaction tests, a memory of the clouds that were tested last are retained and
+ * are excluded from being tested this next time;
+ * clouds that are detected a second time are cleared from the list and are available to be tested next time
+ */
+ private var skipTargets: List[PlanetSideGUID] = List()
+
+ def Type: RadiationInMountableInteraction.type = RadiationInMountableInteraction
+
+ /**
+ * Drive into a radiation cloud and all the vehicle's occupants suffer the consequences.
+ * @param sector the portion of the block map being tested
+ * @param target the fixed element in this test
+ */
+ override def interaction(sector: SectorPopulation, target: InteractsWithZone): Unit = {
+ val position = target.Position
+ //collect all projectiles in sector/range
+ val projectiles = sector
+ .projectileList
+ .filter { cloud =>
+ val definition = cloud.Definition
+ definition.radiation_cloud &&
+ definition.AllDamageTypes.contains(DamageType.Radiation) &&
+ {
+ val radius = definition.DamageRadius
+ Zone.distanceCheck(target, cloud, radius * radius)
+ }
+ }
+ .distinct
+ val notSkipped = projectiles.filterNot { t => skipTargets.contains(t.GUID) }
+ skipTargets = notSkipped.map { _.GUID }
+ if (notSkipped.nonEmpty) {
+ (
+ //isolate one of each type of projectile
+ notSkipped
+ .foldLeft(Nil: List[Projectile]) {
+ (acc, next) => if (acc.exists { _.profile == next.profile }) acc else next :: acc
+ },
+ obj.Seats
+ .values
+ .collect { case seat => seat.occupant }
+ .flatten
+ ) match {
+ case (uniqueProjectiles, targets) if uniqueProjectiles.nonEmpty && targets.nonEmpty =>
+ val shielding = obj.RadiationShielding
+ targets.foreach { t =>
+ uniqueProjectiles.foreach { p =>
+ t.Actor ! Vitality.Damage(
+ DamageInteraction(
+ SourceEntry(t),
+ RadiationReason(
+ ProjectileQuality.modifiers(p, DamageResolution.Radiation, t, t.Position, None),
+ t.DamageModel,
+ shielding
+ ),
+ position
+ ).calculate()
+ )
+ }
+ }
+ case _ => ()
+ }
+ }
+ }
+
+ /**
+ * Any radiation clouds blocked from being tested should be cleared.
+ * All that can be done is blanking our retained previous effect targets.
+ * @param target the fixed element in this test
+ */
+ def resetInteraction(target: InteractsWithZone): Unit = {
+ skipTargets = List()
+ }
+}
+
diff --git a/src/main/scala/net/psforever/objects/serverobject/mount/MountableBehavior.scala b/src/main/scala/net/psforever/objects/serverobject/mount/MountableBehavior.scala
index 0fb697d23..1c60938c6 100644
--- a/src/main/scala/net/psforever/objects/serverobject/mount/MountableBehavior.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/mount/MountableBehavior.scala
@@ -65,7 +65,7 @@ trait MountableBehavior {
!obj.Destroyed
}
- private def tryMount(
+ protected def tryMount(
obj: PlanetSideServerObject with Mountable,
seatNumber: Int,
player: Player
@@ -105,12 +105,12 @@ trait MountableBehavior {
): Boolean = {
obj.PassengerInSeat(user).contains(seatNumber) &&
(obj.Seats.get(seatNumber) match {
- case Some(seat) => seat.bailable || !obj.isMoving(test = 1)
+ case Some(seat) => seat.bailable || !obj.isMoving(test = 1f)
case _ => false
})
}
- private def tryDismount(
+ protected def tryDismount(
obj: Mountable,
seatNumber: Int,
user: Player,
diff --git a/src/main/scala/net/psforever/objects/serverobject/mount/RadiationInMountableInteraction.scala b/src/main/scala/net/psforever/objects/serverobject/mount/RadiationInMountableInteraction.scala
new file mode 100644
index 000000000..4a6093735
--- /dev/null
+++ b/src/main/scala/net/psforever/objects/serverobject/mount/RadiationInMountableInteraction.scala
@@ -0,0 +1,5 @@
+package net.psforever.objects.serverobject.mount
+
+import net.psforever.objects.zones.ZoneInteractionType
+
+case object RadiationInMountableInteraction extends ZoneInteractionType
diff --git a/src/main/scala/net/psforever/objects/serverobject/structures/participation/FacilityHackParticipation.scala b/src/main/scala/net/psforever/objects/serverobject/structures/participation/FacilityHackParticipation.scala
index 88a8a82c9..f85211860 100644
--- a/src/main/scala/net/psforever/objects/serverobject/structures/participation/FacilityHackParticipation.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/structures/participation/FacilityHackParticipation.scala
@@ -3,7 +3,7 @@ package net.psforever.objects.serverobject.structures.participation
import net.psforever.objects.Player
import net.psforever.objects.avatar.scoring.Kill
-import net.psforever.objects.sourcing.{PlayerSource, UniquePlayer}
+import net.psforever.objects.sourcing.UniquePlayer
import net.psforever.types.{PlanetSideEmpire, Vector3}
import scala.collection.mutable
@@ -143,7 +143,7 @@ object FacilityHackParticipation {
killTime <= end &&
Vector3.DistanceSquared(centerXY, k.info.interaction.hitPos.xy) < distanceSq
}
- (PlayerSource(p).unique, math.min(d, duration).toFloat / duration.toFloat, killList)
+ (UniquePlayer(p), math.min(d, duration).toFloat / duration.toFloat, killList)
}
}
diff --git a/src/main/scala/net/psforever/objects/serverobject/terminals/capture/CaptureTerminalAwareBehavior.scala b/src/main/scala/net/psforever/objects/serverobject/terminals/capture/CaptureTerminalAwareBehavior.scala
index 3d638504d..f12a2e3b5 100644
--- a/src/main/scala/net/psforever/objects/serverobject/terminals/capture/CaptureTerminalAwareBehavior.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/terminals/capture/CaptureTerminalAwareBehavior.scala
@@ -5,42 +5,46 @@ import net.psforever.objects.serverobject.mount.Mountable
import net.psforever.objects.serverobject.structures.Amenity
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
+import scala.annotation.unused
+
/**
* The behaviours corresponding to an Amenity that is marked as being CaptureTerminalAware
* @see CaptureTerminalAware
*/
trait CaptureTerminalAwareBehavior {
- def CaptureTerminalAwareObject : Amenity with CaptureTerminalAware
+ def CaptureTerminalAwareObject: Amenity with CaptureTerminalAware
val captureTerminalAwareBehaviour: Receive = {
- case CaptureTerminalAwareBehavior.TerminalStatusChanged(terminal, isResecured) =>
- isResecured match {
- case true => ; // CC is resecured
- case false => // CC is hacked
- // Remove seated occupants for mountables
- CaptureTerminalAwareObject match {
- case mountable: Mountable =>
+ case CaptureTerminalAwareBehavior.TerminalStatusChanged(terminal, true) =>
+ captureTerminalIsResecured(terminal)
- val guid = mountable.GUID
- val zone = mountable.Zone
- val zoneId = zone.id
- val events = zone.VehicleEvents
+ case CaptureTerminalAwareBehavior.TerminalStatusChanged(terminal, _) =>
+ captureTerminalIsHacked(terminal)
+ }
- mountable.Seats.values.zipWithIndex.foreach {
- case (seat, seat_num) =>
- seat.occupant match {
- case Some(player) =>
- seat.unmount(player)
- player.VehicleSeated = None
- if (player.HasGUID) {
- events ! VehicleServiceMessage(zoneId, VehicleAction.KickPassenger(player.GUID, seat_num, true, guid))
- }
- case None => ;
- }
- }
- case _ =>
- }
- }
+ protected def captureTerminalIsResecured(@unused terminal: CaptureTerminal): Unit = { /* intentionally blank */ }
+
+ protected def captureTerminalIsHacked(@unused terminal: CaptureTerminal): Unit = {
+ // Remove seated occupants for mountables
+ CaptureTerminalAwareObject match {
+ case mountable: Mountable =>
+ val guid = mountable.GUID
+ val zone = mountable.Zone
+ val zoneId = zone.id
+ val events = zone.VehicleEvents
+ mountable.Seats.values.zipWithIndex.foreach {
+ case (seat, seat_num) =>
+ seat.occupant.collect {
+ case player =>
+ seat.unmount(player)
+ player.VehicleSeated = None
+ if (player.HasGUID) {
+ events ! VehicleServiceMessage(zoneId, VehicleAction.KickPassenger(player.GUID, seat_num, unk2=true, guid))
+ }
+ }
+ }
+ case _ => ()
+ }
}
}
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 ae056a587..5ff5bd246 100644
--- a/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurret.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurret.scala
@@ -2,16 +2,35 @@
package net.psforever.objects.serverobject.turret
import net.psforever.objects.equipment.JammableUnit
-import net.psforever.objects.serverobject.structures.Amenity
+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
class FacilityTurret(tDef: FacilityTurretDefinition)
extends Amenity
- with WeaponTurret
+ 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) {
+ Building.NoBuilding
+ } else {
+ super.Owner
+ }
+ }
def Definition: FacilityTurretDefinition = tDef
}
@@ -27,9 +46,6 @@ object FacilityTurret {
new FacilityTurret(tDef)
}
- final case class RechargeAmmo()
- final case class WeaponDischarged()
-
import akka.actor.ActorContext
/**
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 b0e58c59d..433356f63 100644
--- a/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala
@@ -1,190 +1,175 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.serverobject.turret
-import akka.actor.Cancellable
-import net.psforever.objects.{Default, GlobalDefinitions, Player, Tool}
-import net.psforever.objects.equipment.{Ammo, JammableMountedWeapons}
+import net.psforever.objects.{GlobalDefinitions, Player, Tool}
+import net.psforever.objects.equipment.Ammo
import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject}
-import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior}
-import net.psforever.objects.serverobject.affinity.FactionAffinityBehavior
-import net.psforever.objects.serverobject.damage.{Damageable, DamageableWeaponTurret}
+import net.psforever.objects.serverobject.damage.Damageable
import net.psforever.objects.serverobject.hackable.GenericHackables
-import net.psforever.objects.serverobject.hackable.GenericHackables.getTurretUpgradeTime
-import net.psforever.objects.serverobject.repair.{AmenityAutoRepair, RepairableWeaponTurret}
-import net.psforever.objects.serverobject.structures.PoweredAmenityControl
-import net.psforever.objects.serverobject.terminals.capture.CaptureTerminalAwareBehavior
+import net.psforever.objects.serverobject.mount.Mountable
+import net.psforever.objects.serverobject.repair.AmenityAutoRepair
+import net.psforever.objects.serverobject.structures.{Building, PoweredAmenityControl}
+import net.psforever.objects.serverobject.terminals.capture.{CaptureTerminal, CaptureTerminalAwareBehavior}
+import net.psforever.objects.serverobject.turret.auto.AutomatedTurret.Target
+import net.psforever.objects.serverobject.turret.auto.{AffectedByAutomaticTurretFire, AutomatedTurret, AutomatedTurretBehavior}
import net.psforever.objects.vital.interaction.DamageResult
-import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
-import net.psforever.services.local.{LocalAction, LocalServiceMessage}
+import net.psforever.packet.game.ChangeFireModeMessage
+import net.psforever.services.Service
+import net.psforever.services.vehicle.support.TurretUpgrader
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
-
-import scala.concurrent.ExecutionContext.Implicits.global
-import scala.concurrent.duration._
+import net.psforever.types.{BailType, PlanetSideEmpire, PlanetSideGUID}
/**
- * An `Actor` that handles messages being dispatched to a specific `MannedTurret`.
- *
- * Mounted turrets have only slightly different entry requirements than a normal vehicle
- * because they encompass both faction-specific facility turrets
- * and faction-blind cavern sentry turrets.
- *
- * @param turret the `MannedTurret` object being governed
- */
+ * A control agency that handles messages being dispatched to a specific `FacilityTurret`.
+ * These turrets are attached specifically to surface-level facilities and field towers.
+ * @param turret the `FacilityTurret` object being governed
+ */
class FacilityTurretControl(turret: FacilityTurret)
- extends PoweredAmenityControl
- with FactionAffinityBehavior.Check
- with MountableBehavior
- with DamageableWeaponTurret
- with RepairableWeaponTurret
+ extends PoweredAmenityControl
with AmenityAutoRepair
- with JammableMountedWeapons
+ with MountableTurretControl
+ with AutomatedTurretBehavior
+ with AffectedByAutomaticTurretFire
with CaptureTerminalAwareBehavior {
+ def TurretObject: FacilityTurret = turret
def FactionObject: FacilityTurret = turret
def MountableObject: FacilityTurret = turret
def JammableObject: FacilityTurret = turret
def DamageableObject: FacilityTurret = turret
def RepairableObject: FacilityTurret = turret
def AutoRepairObject: FacilityTurret = turret
+ def AutomatedTurretObject: FacilityTurret = turret
def CaptureTerminalAwareObject: FacilityTurret = turret
+ def AffectedObject: FacilityTurret = turret
- // Used for timing ammo recharge for vanu turrets in caves
- var weaponAmmoRechargeTimer: Cancellable = Default.Cancellable
+ private var testToResetToDefaultFireMode: Boolean = false
+
+ AutomaticOperation = true
override def postStop(): Unit = {
super.postStop()
damageableWeaponTurretPostStop()
+ automaticTurretPostStop()
stopAutoRepair()
}
- def commonBehavior: Receive =
- checkBehavior
- .orElse(jammableBehavior)
- .orElse(dismountBehavior)
- .orElse(takesDamage)
- .orElse(canBeRepairedByNanoDispenser)
- .orElse(autoRepairBehavior)
- .orElse(captureTerminalAwareBehaviour)
+ private val upgradeableTurret: Receive = {
+ case CommonMessages.Use(player, Some((item: Tool, upgradeValue: Int)))
+ if player.Faction == TurretObject.Faction &&
+ item.Definition == GlobalDefinitions.nano_dispenser && item.AmmoType == Ammo.upgrade_canister &&
+ item.Magazine > 0 && TurretObject.Seats.values.forall(!_.isOccupied) =>
+ TurretUpgrade.values.find(_.id == upgradeValue).foreach {
+ case upgrade
+ if TurretObject.Upgrade != upgrade && TurretObject.Definition.WeaponPaths.values
+ .flatMap(_.keySet)
+ .exists(_ == upgrade) =>
+ AutomaticOperation = false
+ sender() ! CommonMessages.Progress(
+ 1.25f,
+ WeaponTurrets.FinishUpgradingMannedTurret(TurretObject, player, item, upgrade),
+ GenericHackables.TurretUpgradingTickAction(progressType = 2, player, TurretObject, item.GUID)
+ )
+ }
+ case TurretUpgrader.UpgradeCompleted(_) =>
+ CurrentTargetLastShotReported = System.currentTimeMillis() + 2000L
+ AutomaticOperation = true
+ }
- def poweredStateLogic: Receive =
+ override def commonBehavior: Receive = super.commonBehavior
+ .orElse(automatedTurretBehavior)
+ .orElse(takeAutomatedDamage)
+ .orElse(autoRepairBehavior)
+ .orElse(captureTerminalAwareBehaviour)
+
+ override def poweredStateLogic: Receive =
commonBehavior
.orElse(mountBehavior)
+ .orElse(upgradeableTurret)
.orElse {
- case CommonMessages.Use(player, Some((item: Tool, upgradeValue: Int)))
- if player.Faction == turret.Faction &&
- item.Definition == GlobalDefinitions.nano_dispenser && item.AmmoType == Ammo.upgrade_canister &&
- item.Magazine > 0 && turret.Seats.values.forall(!_.isOccupied) =>
- TurretUpgrade.values.find(_.id == upgradeValue) match {
- case Some(upgrade)
- if turret.Upgrade != upgrade && turret.Definition.WeaponPaths.values
- .flatMap(_.keySet)
- .exists(_ == upgrade) =>
- turret.setMiddleOfUpgrade(true)
- sender() ! CommonMessages.Progress(
- 1.25f,
- WeaponTurrets.FinishUpgradingMannedTurret(turret, player, item, upgrade),
- GenericHackables.TurretUpgradingTickAction(progressType = 2, player, turret, item.GUID)
- )
- case _ => ;
- }
-
- case FacilityTurret.WeaponDischarged() =>
- if (weaponAmmoRechargeTimer != Default.Cancellable) {
- weaponAmmoRechargeTimer.cancel()
- }
-
- weaponAmmoRechargeTimer = context.system.scheduler.scheduleWithFixedDelay(
- 3 seconds,
- 200 milliseconds,
- self,
- FacilityTurret.RechargeAmmo()
- )
-
- case FacilityTurret.RechargeAmmo() =>
- turret.ControlledWeapon(wepNumber = 1).foreach {
- case weapon: Tool =>
- // recharge when last shot fired 3s delay, +1, 200ms interval
- if (weapon.Magazine < weapon.MaxMagazine && System.currentTimeMillis() - weapon.LastDischarge > 3000L) {
- weapon.Magazine += 1
- val seat = turret.Seat(0).get
- seat.occupant match {
- case Some(player : Player) =>
- turret.Zone.LocalEvents ! LocalServiceMessage(
- turret.Zone.id,
- LocalAction.RechargeVehicleWeapon(player.GUID, turret.GUID, weapon.GUID)
- )
- case _ => ;
- }
- }
- else if (weapon.Magazine == weapon.MaxMagazine && weaponAmmoRechargeTimer != Default.Cancellable) {
- weaponAmmoRechargeTimer.cancel()
- weaponAmmoRechargeTimer = Default.Cancellable
- }
- case _ => ;
- }
-
- case _ => ;
+ case _ => ()
}
- def unpoweredStateLogic: Receive =
+ override def unpoweredStateLogic: Receive =
commonBehavior
.orElse {
- case _ => ;
+ case _ => ()
}
override protected def mountTest(
obj: PlanetSideServerObject with Mountable,
seatNumber: Int,
player: Player): Boolean = {
- (!turret.Definition.FactionLocked || player.Faction == obj.Faction) && !obj.Destroyed && !turret.isUpgrading ||
- System.currentTimeMillis() - getTurretUpgradeTime >= 1500L
+ super.mountTest(obj, seatNumber, player) &&
+ (!TurretObject.isUpgrading || System.currentTimeMillis() - GenericHackables.getTurretUpgradeTime >= 1500L)
+ }
+
+ override protected def tryMount(obj: PlanetSideServerObject with Mountable, seatNumber: Int, player: Player): Boolean = {
+ AutomaticOperation = false //turn off
+ if (!super.tryMount(obj, seatNumber, player)) {
+ AutomaticOperation = true //revert?
+ false
+ } else {
+ true
+ }
+ }
+
+ override protected def tryDismount(obj: Mountable, seatNumber: Int, player: Player, bailType: BailType.Value): Boolean = {
+ AutomaticOperation = AutomaticOperationFunctionalityChecksExceptMounting //turn on, if can turn on
+ if (!super.tryDismount(obj, seatNumber, player, bailType)) {
+ AutomaticOperation = false //revert
+ false
+ } else {
+ CurrentTargetLastShotReported = System.currentTimeMillis() + 4000L
+ true
+ }
}
override protected def DamageAwareness(target: Damageable.Target, cause: DamageResult, amount: Any) : Unit = {
tryAutoRepair()
+ if (AutomaticOperation) {
+ if (TurretObject.Health < TurretObject.Definition.DamageDisablesAt) {
+ AutomaticOperation = false
+ } else {
+ amount match {
+ case 0 => ()
+ case _ => attemptRetaliation(target, cause)
+ }
+ }
+ }
super.DamageAwareness(target, cause, amount)
}
override protected def DestructionAwareness(target: Damageable.Target, cause: DamageResult): Unit = {
- tryAutoRepair()
super.DestructionAwareness(target, cause)
- val zone = target.Zone
- val zoneId = zone.id
- val events = zone.AvatarEvents
- val tguid = target.GUID
- events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(tguid, 50, 1))
- events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(tguid, 51, 1))
+ tryAutoRepair()
+ AutomaticOperation = false
+ selfReportingCleanUp()
}
override def PerformRepairs(target : Damageable.Target, amount : Int) : Int = {
val newHealth = super.PerformRepairs(target, amount)
- if(newHealth == target.Definition.MaxHealth) {
+ if (!AutomaticOperation && newHealth > target.Definition.DamageDisablesAt) {
+ AutomaticOperation = true
+ }
+ if (newHealth == target.Definition.MaxHealth) {
stopAutoRepair()
}
newHealth
}
- override def Restoration(obj: Damageable.Target): Unit = {
- super.Restoration(obj)
- val zone = turret.Zone
- val zoneId = zone.id
- val events = zone.AvatarEvents
- val tguid = turret.GUID
- events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(tguid, 50, 0))
- events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(tguid, 51, 0))
- }
-
override def tryAutoRepair() : Boolean = {
isPowered && super.tryAutoRepair()
}
def powerTurnOffCallback(): Unit = {
stopAutoRepair()
+ AutomaticOperation = false
//kick all occupants
- val guid = turret.GUID
- val zone = turret.Zone
+ val guid = TurretObject.GUID
+ val zone = TurretObject.Zone
val zoneId = zone.id
val events = zone.VehicleEvents
- turret.Seats.values.foreach(seat =>
+ TurretObject.Seats.values.foreach(seat =>
seat.occupant match {
case Some(player) =>
seat.unmount(player)
@@ -192,12 +177,159 @@ class FacilityTurretControl(turret: FacilityTurret)
if (player.HasGUID) {
events ! VehicleServiceMessage(zoneId, VehicleAction.KickPassenger(player.GUID, 4, unk2=false, guid))
}
- case None => ;
+ case None => ()
}
)
}
def powerTurnOnCallback(): Unit = {
tryAutoRepair()
+ AutomaticOperation = true
+ }
+
+ override def AutomaticOperation_=(state: Boolean): Boolean = {
+ val result = super.AutomaticOperation_=(state)
+ testToResetToDefaultFireMode = result && TurretObject.Definition.AutoFire.exists(_.revertToDefaultFireMode)
+ result
+ }
+
+ override protected def AutomaticOperationFunctionalityChecks: Boolean = {
+ AutomaticOperationFunctionalityChecksExceptMounting &&
+ !TurretObject.Seats.values.exists(_.isOccupied)
+ }
+
+ private def AutomaticOperationFunctionalityChecksExceptMounting: Boolean = {
+ AutomaticOperationFunctionalityChecksExceptMountingAndHacking &&
+ (TurretObject.Owner match {
+ case b: Building => !b.CaptureTerminalIsHacked
+ case _ => false
+ })
+ }
+
+ private def AutomaticOperationFunctionalityChecksExceptMountingAndHacking: Boolean = {
+ super.AutomaticOperationFunctionalityChecks &&
+ isPowered &&
+ TurretObject.Owner.Faction != PlanetSideEmpire.NEUTRAL &&
+ !JammableObject.Jammed &&
+ TurretObject.Health >= TurretObject.Definition.DamageDisablesAt &&
+ !TurretObject.isUpgrading
+ }
+
+ private def primaryWeaponFireModeOnly(): Unit = {
+ if (testToResetToDefaultFireMode) {
+ val zone = TurretObject.Zone
+ val zoneid = zone.id
+ val events = zone.VehicleEvents
+ TurretObject.Weapons.values
+ .flatMap(_.Equipment)
+ .collect { case weapon: Tool if weapon.FireModeIndex > 0 =>
+ weapon.FireModeIndex = 0
+ events ! VehicleServiceMessage(
+ zoneid,
+ VehicleAction.SendResponse(Service.defaultPlayerGUID, ChangeFireModeMessage(weapon.GUID, 0))
+ )
+ }
+ }
+ testToResetToDefaultFireMode = false
+ }
+
+ override protected def trySelectNewTarget(): Option[AutomatedTurret.Target] = {
+ primaryWeaponFireModeOnly()
+ super.trySelectNewTarget()
+ }
+
+ protected def engageNewDetectedTarget(
+ target: Target,
+ channel: String,
+ turretGuid: PlanetSideGUID,
+ weaponGuid: PlanetSideGUID
+ ): Unit = {
+ val zone = target.Zone
+ primaryWeaponFireModeOnly()
+ AutomatedTurretBehavior.startTracking(zone, channel, turretGuid, List(target.GUID))
+ AutomatedTurretBehavior.startShooting(zone, channel, weaponGuid)
+ }
+
+ protected def noLongerEngageTarget(
+ target: Target,
+ channel: String,
+ turretGuid: PlanetSideGUID,
+ weaponGuid: PlanetSideGUID
+ ): Option[Target] = {
+ val zone = target.Zone
+ AutomatedTurretBehavior.stopTracking(zone, channel, turretGuid)
+ AutomatedTurretBehavior.stopShooting(zone, channel, weaponGuid)
+ None
+ }
+
+ protected def testNewDetected(
+ target: Target,
+ channel: String,
+ turretGuid: PlanetSideGUID,
+ weaponGuid: PlanetSideGUID
+ ): Unit = {
+ val zone = target.Zone
+ AutomatedTurretBehavior.startTracking(zone, channel, turretGuid, List(target.GUID))
+ AutomatedTurretBehavior.startShooting(zone, channel, weaponGuid)
+ AutomatedTurretBehavior.stopShooting(zone, channel, weaponGuid)
+ AutomatedTurretBehavior.stopTracking(zone, channel, turretGuid)
+ }
+
+ protected def testKnownDetected(
+ target: Target,
+ channel: String,
+ turretGuid: PlanetSideGUID,
+ weaponGuid: PlanetSideGUID
+ ): Unit = {
+ val zone = target.Zone
+ AutomatedTurretBehavior.startTracking(zone, channel, turretGuid, List(target.GUID))
+ AutomatedTurretBehavior.startShooting(zone, channel, weaponGuid)
+ AutomatedTurretBehavior.stopShooting(zone, channel, weaponGuid)
+ AutomatedTurretBehavior.stopTracking(zone, channel, turretGuid)
+ }
+
+ override def TryJammerEffectActivate(target: Any, cause: DamageResult): Unit = {
+ val startsUnjammed = !JammableObject.Jammed
+ super.TryJammerEffectActivate(target, cause)
+ if (JammableObject.Jammed && AutomatedTurretObject.Definition.AutoFire.exists(_.retaliatoryDelay > 0)) {
+ if (startsUnjammed) {
+ AutomaticOperation = false
+ }
+ //look in direction of cause of jamming
+ val zone = JammableObject.Zone
+ AutomatedTurretBehavior.getAttackVectorFromCause(zone, cause).foreach { attacker =>
+ AutomatedTurretBehavior.startTracking(zone, zone.id, JammableObject.GUID, List(attacker.GUID))
+ }
+ }
+ }
+
+ override def CancelJammeredStatus(target: Any): Unit = {
+ val startsJammed = JammableObject.Jammed
+ super.CancelJammeredStatus(target)
+ if (startsJammed && AutomaticOperation_=(state = true)) {
+ val zone = TurretObject.Zone
+ AutomatedTurretBehavior.stopTracking(zone, zone.id, TurretObject.GUID)
+ }
+ }
+
+ override protected def captureTerminalIsResecured(terminal: CaptureTerminal): Unit = {
+ captureTerminalChanges(terminal, super.captureTerminalIsResecured, actionDelays = 2000L)
+ }
+
+ override protected def captureTerminalIsHacked(terminal: CaptureTerminal): Unit = {
+ captureTerminalChanges(terminal, super.captureTerminalIsHacked, actionDelays = 3000L)
+ }
+
+ private def captureTerminalChanges(
+ terminal: CaptureTerminal,
+ changeFunc: CaptureTerminal=>Unit,
+ actionDelays: Long
+ ): Unit = {
+ AutomaticOperation = false
+ changeFunc(terminal)
+ if (AutomaticOperationFunctionalityChecks) {
+ CurrentTargetLastShotReported = System.currentTimeMillis() + actionDelays
+ AutomaticOperation = true
+ }
}
}
diff --git a/src/main/scala/net/psforever/objects/serverobject/turret/MountableTurretControl.scala b/src/main/scala/net/psforever/objects/serverobject/turret/MountableTurretControl.scala
new file mode 100644
index 000000000..046bccb95
--- /dev/null
+++ b/src/main/scala/net/psforever/objects/serverobject/turret/MountableTurretControl.scala
@@ -0,0 +1,72 @@
+// Copyright (c) 2023 PSForever
+package net.psforever.objects.serverobject.turret
+
+import akka.actor.Actor
+import net.psforever.objects.Player
+import net.psforever.objects.equipment.JammableMountedWeapons
+import net.psforever.objects.serverobject.PlanetSideServerObject
+import net.psforever.objects.serverobject.affinity.FactionAffinityBehavior
+import net.psforever.objects.serverobject.damage.{Damageable, DamageableWeaponTurret}
+import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior}
+import net.psforever.objects.serverobject.repair.RepairableWeaponTurret
+import net.psforever.objects.vital.interaction.DamageResult
+import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
+
+trait MountableTurretControl
+ extends Actor
+ with FactionAffinityBehavior.Check
+ with MountableBehavior
+ with DamageableWeaponTurret
+ with RepairableWeaponTurret
+ with JammableMountedWeapons { /* note: jammable status is reported as vehicle events, not local events */
+ def TurretObject: PlanetSideServerObject with WeaponTurret with Mountable
+
+ override def postStop(): Unit = {
+ super.postStop()
+ damageableWeaponTurretPostStop()
+ }
+
+ /** commonBehavior does not implement mountingBehavior; please do so when implementing */
+ def commonBehavior: Receive =
+ checkBehavior
+ .orElse(jammableBehavior)
+ .orElse(dismountBehavior)
+ .orElse(takesDamage)
+ .orElse(canBeRepairedByNanoDispenser)
+
+ override protected def mountTest(
+ obj: PlanetSideServerObject with Mountable,
+ seatNumber: Int,
+ player: Player): Boolean = {
+ (!TurretObject.Definition.FactionLocked || player.Faction == obj.Faction) && !obj.Destroyed
+ }
+
+ /**
+ * An override for `Restoration`, best for facility turrets.
+ * @param obj the entity being restored
+ */
+ override def Restoration(obj: Damageable.Target): Unit = {
+ super.Restoration(obj)
+ val zone = TurretObject.Zone
+ val zoneId = zone.id
+ val events = zone.AvatarEvents
+ val tguid = TurretObject.GUID
+ events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(tguid, 50, 0))
+ events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(tguid, 51, 0))
+ }
+
+ /**
+ * An override for `DamageAwareness`, best for facility turrets.
+ * @param target the entity being destroyed
+ * @param cause historical information about the damage
+ */
+ override protected def DestructionAwareness(target: Damageable.Target, cause: DamageResult): Unit = {
+ super.DestructionAwareness(target, cause)
+ val zone = target.Zone
+ val zoneId = zone.id
+ val events = zone.AvatarEvents
+ val tguid = target.GUID
+ events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(tguid, 50, 1))
+ events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(tguid, 51, 1))
+ }
+}
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 ecb84ac5e..17916f5b1 100644
--- a/src/main/scala/net/psforever/objects/serverobject/turret/TurretDefinition.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/turret/TurretDefinition.scala
@@ -1,15 +1,72 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.serverobject.turret
+import net.psforever.objects.PlanetSideGameObject
import net.psforever.objects.definition.{ObjectDefinition, ToolDefinition, WithShields}
import net.psforever.objects.vehicles.{MountableWeaponsDefinition, Turrets}
import net.psforever.objects.vital.resistance.ResistanceProfileMutators
import net.psforever.objects.vital.resolution.DamageResistanceModel
import scala.collection.mutable
+import scala.concurrent.duration._
+
+final case class Automation(
+ 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,
+ /** 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(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 `MannedTurret`.
+ * The definition for any `WeaponTurret`.
*/
trait TurretDefinition
extends MountableWeaponsDefinition
@@ -25,11 +82,12 @@ trait TurretDefinition
/** can only be mounted by owning faction when `true` */
private var factionLocked: Boolean = true
- /** creates internal ammunition reserves that can not become depleted
- * see `MannedTurret.TurretAmmoBox` for details
- */
+ /** creates internal ammunition reserves that can not become depleted */
private var hasReserveAmmunition: Boolean = false
+ /** */
+ private var turretAutomation: Option[Automation] = None
+
def WeaponPaths: mutable.HashMap[Int, mutable.HashMap[TurretUpgrade.Value, ToolDefinition]] = weaponPaths
def FactionLocked: Boolean = factionLocked
@@ -45,4 +103,15 @@ trait TurretDefinition
hasReserveAmmunition = reserved
ReserveAmmunition
}
+
+ def AutoFire: Option[Automation] = turretAutomation
+
+ def AutoFire_=(auto: Automation): Option[Automation] = {
+ AutoFire_=(Some(auto))
+ }
+
+ def AutoFire_=(auto: Option[Automation]): Option[Automation] = {
+ turretAutomation = auto
+ turretAutomation
+ }
}
diff --git a/src/main/scala/net/psforever/objects/serverobject/turret/VanuSentryControl.scala b/src/main/scala/net/psforever/objects/serverobject/turret/VanuSentryControl.scala
new file mode 100644
index 000000000..4a6cb991c
--- /dev/null
+++ b/src/main/scala/net/psforever/objects/serverobject/turret/VanuSentryControl.scala
@@ -0,0 +1,96 @@
+// Copyright (c) 2023 PSForever
+package net.psforever.objects.serverobject.turret
+
+import akka.actor.Cancellable
+import net.psforever.objects.serverobject.ServerObjectControl
+import net.psforever.objects.{Default, Player, Tool}
+import net.psforever.services.local.{LocalAction, LocalServiceMessage}
+import net.psforever.types.Vector3
+
+import scala.concurrent.ExecutionContext.Implicits.global
+import scala.concurrent.duration._
+
+/**
+ * A control agency that handles messages being dispatched to a specific `FacilityTurret`.
+ * These turrets are installed tangential to cavern facilities but are independent of the facility.
+ * @param turret the `FacilityTurret` object being governed
+ */
+class VanuSentryControl(turret: FacilityTurret)
+ extends ServerObjectControl
+ with MountableTurretControl {
+ def TurretObject: FacilityTurret = turret
+ def FactionObject: FacilityTurret = turret
+ def MountableObject: FacilityTurret = turret
+ def JammableObject: FacilityTurret = turret
+ def DamageableObject: FacilityTurret = turret
+ def RepairableObject: FacilityTurret = turret
+
+ // Used for timing ammo recharge for vanu turrets in caves
+ private var weaponAmmoRechargeTimer: Cancellable = Default.Cancellable
+
+ private val weaponAmmoRecharge: Receive = {
+ case VanuSentry.ChangeFireStart =>
+ weaponAmmoRechargeTimer.cancel()
+ weaponAmmoRechargeTimer = Default.Cancellable
+
+ case VanuSentry.ChangeFireStop =>
+ weaponAmmoRechargeTimer.cancel()
+ weaponAmmoRechargeTimer = context.system.scheduler.scheduleWithFixedDelay(
+ 3 seconds,
+ 200 milliseconds,
+ self,
+ VanuSentry.RechargeAmmo
+ )
+
+ case VanuSentry.RechargeAmmo =>
+ TurretObject.ControlledWeapon(wepNumber = 1).collect {
+ case weapon: Tool =>
+ // recharge when last shot fired 3s delay, +1, 200ms interval
+ if (weapon.Magazine < weapon.MaxMagazine && System.currentTimeMillis() - weapon.LastDischarge > 3000L) {
+ weapon.Magazine += 1
+ val seat = TurretObject.Seat(0).get
+ seat.occupant.collect {
+ case player: Player =>
+ TurretObject.Zone.LocalEvents ! LocalServiceMessage(
+ TurretObject.Zone.id,
+ LocalAction.RechargeVehicleWeapon(player.GUID, TurretObject.GUID, weapon.GUID)
+ )
+ }
+ }
+ else if (weapon.Magazine == weapon.MaxMagazine && weaponAmmoRechargeTimer != Default.Cancellable) {
+ weaponAmmoRechargeTimer.cancel()
+ weaponAmmoRechargeTimer = Default.Cancellable
+ }
+ }
+ }
+
+ override def postStop(): Unit = {
+ super.postStop()
+ weaponAmmoRechargeTimer.cancel()
+ }
+
+ def receive: Receive =
+ commonBehavior
+ .orElse(mountBehavior)
+ .orElse(weaponAmmoRecharge)
+ .orElse {
+ case _ => ()
+ }
+
+ override def parseAttribute(attribute: Int, value: Long, other: Option[Any]): Unit = { /*intentionally blank*/ }
+}
+
+object VanuSentry {
+ final case object RechargeAmmo
+ final case object ChangeFireStart
+ final case object ChangeFireStop
+
+ import akka.actor.ActorContext
+ def Constructor(pos: Vector3, tdef: FacilityTurretDefinition)(id: Int, context: ActorContext): FacilityTurret = {
+ import akka.actor.Props
+ val obj = FacilityTurret(tdef)
+ obj.Position = pos
+ obj.Actor = context.actorOf(Props(classOf[VanuSentryControl], obj), s"${tdef.Name}_$id")
+ obj
+ }
+}
diff --git a/src/main/scala/net/psforever/objects/serverobject/turret/WeaponTurret.scala b/src/main/scala/net/psforever/objects/serverobject/turret/WeaponTurret.scala
index 274a409ea..b93ddd25f 100644
--- a/src/main/scala/net/psforever/objects/serverobject/turret/WeaponTurret.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/turret/WeaponTurret.scala
@@ -6,7 +6,7 @@ import net.psforever.objects.definition.{AmmoBoxDefinition, ToolDefinition}
import net.psforever.objects.equipment.EquipmentSlot
import net.psforever.objects.inventory.{Container, GridInventory}
import net.psforever.objects.serverobject.affinity.FactionAffinity
-import net.psforever.objects.serverobject.mount.{SeatDefinition, Seat => Chair}
+import net.psforever.objects.serverobject.mount.{Seat => Chair}
import net.psforever.objects.vehicles.MountableWeapons
trait WeaponTurret
@@ -14,10 +14,6 @@ trait WeaponTurret
with MountableWeapons
with Container {
_: PlanetSideGameObject =>
-
- /** manned turrets have just one mount; this is just standard interface */
- seats = Map(0 -> new Chair(new SeatDefinition()))
-
/** may or may not have inaccessible inventory space
* see `ReserveAmmunition` in the definition
*/
@@ -84,25 +80,27 @@ trait WeaponTurret
}
object WeaponTurret {
-
/**
- * Use the `*Definition` that was provided to this object to initialize its fields and settings.
- * @see `{object}.LoadDefinition`
- * @param turret the `MannedTurret` being initialized
+ * Use the definition that was provided to this object to initialize its fields and settings.
+ * @see `WeaponTurret.LoadDefinition(WeaponTurret, TurretDefinition)`
+ * @param turret turret being initialized
*/
def LoadDefinition(turret: WeaponTurret): WeaponTurret = {
LoadDefinition(turret, turret.Definition)
}
/**
- * Use the `*Definition` that was provided to this object to initialize its fields and settings.
- * A default definition is provided to be used.
- * @see `{object}.LoadDefinition`
- * @param turret the `MannedTurret` being initialized
- * @param tdef the object definition
+ * Use the definition that was provided to this object to initialize its fields and settings.
+ * @see `WeaponTurret.LoadDefinition(WeaponTurret)`
+ * @param turret turret being initialized
+ * @param tdef object's specific definition
*/
def LoadDefinition(turret: WeaponTurret, tdef: TurretDefinition): WeaponTurret = {
import net.psforever.objects.equipment.EquipmentSize.BaseTurretWeapon
+ //create seats, if any
+ turret.seats = tdef.Seats.map {
+ case (num, definition) => num -> new Chair(definition)
+ }.toMap
//create weapons; note the class
turret.weapons = tdef.WeaponPaths
.map({
@@ -160,17 +158,18 @@ class TurretWeapon(
Upgrade
}
- override def Definition = udefs(Upgrade)
+ override def Definition: ToolDefinition = udefs(Upgrade)
}
/**
- * A special type of ammunition box contained within a `MannedTurret` for the purposes of infinite reloads.
+ * A special type of ammunition box contained for the purposes of infinite reloads.
* The original quantity of ammunition does not change.
* @param adef ammunition definition
*/
-class TurretAmmoBox(private val adef: AmmoBoxDefinition) extends AmmoBox(adef, Some(65535)) {
+class TurretAmmoBox(private val adef: AmmoBoxDefinition)
+ extends AmmoBox(adef, Some(65535)) {
import net.psforever.objects.inventory.InventoryTile
- override def Tile = InventoryTile.Tile11
+ override def Tile: InventoryTile = InventoryTile.Tile11
- override def Capacity_=(toCapacity: Int) = Capacity
+ override def Capacity_=(toCapacity: Int): Int = Capacity
}
diff --git a/src/main/scala/net/psforever/objects/serverobject/turret/auto/AffectedByAutomaticTurretFire.scala b/src/main/scala/net/psforever/objects/serverobject/turret/auto/AffectedByAutomaticTurretFire.scala
new file mode 100644
index 000000000..7485c3a47
--- /dev/null
+++ b/src/main/scala/net/psforever/objects/serverobject/turret/auto/AffectedByAutomaticTurretFire.scala
@@ -0,0 +1,66 @@
+// Copyright (c) 2024 PSForever
+package net.psforever.objects.serverobject.turret.auto
+
+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 AiDamage(turret) =>
+ performAutomatedDamage(turret)
+ }
+
+ protected def performAutomatedDamage(turret: AutomatedTurret): Unit = {
+ val target = AffectedObject
+ if (!(target.Destroyed || target.isMoving(test = 1f))) {
+ 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 ! SelfReportedConfirmShot(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())
+ }
+ }
+}
diff --git a/src/main/scala/net/psforever/objects/serverobject/turret/auto/AutomatedTurret.scala b/src/main/scala/net/psforever/objects/serverobject/turret/auto/AutomatedTurret.scala
new file mode 100644
index 000000000..88fa250c8
--- /dev/null
+++ b/src/main/scala/net/psforever/objects/serverobject/turret/auto/AutomatedTurret.scala
@@ -0,0 +1,70 @@
+// Copyright (c) 2024 PSForever
+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
+
+trait AutomatedTurret
+ extends PlanetSideServerObject
+ with WeaponTurret {
+ import AutomatedTurret.Target
+ private var currentTarget: Option[Target] = None
+
+ 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
+
+ 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 = SourceUniqueness(target)
+ targets.find(SourceUniqueness(_) == unique)
+ }
+
+ def Detected(target: SourceUniqueness): Option[Target] = {
+ targets.find(SourceUniqueness(_) == target)
+ }
+
+ def AddTarget(target: Target): Unit = {
+ targets = targets :+ target
+ }
+
+ def RemoveTarget(target: Target): Unit = {
+ val unique = SourceUniqueness(target)
+ targets = targets.filterNot(SourceUniqueness(_) == 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/auto/AutomatedTurretBehavior.scala b/src/main/scala/net/psforever/objects/serverobject/turret/auto/AutomatedTurretBehavior.scala
new file mode 100644
index 000000000..bdb818afd
--- /dev/null
+++ b/src/main/scala/net/psforever/objects/serverobject/turret/auto/AutomatedTurretBehavior.scala
@@ -0,0 +1,998 @@
+// Copyright (c) 2024 PSForever
+package net.psforever.objects.serverobject.turret.auto
+
+import akka.actor.{Actor, Cancellable}
+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.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}
+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.types.{PlanetSideGUID, Vector3}
+
+import scala.concurrent.ExecutionContext.Implicits.global
+import scala.concurrent.duration._
+
+trait AutomatedTurretBehavior {
+ _: Actor with DamageableEntity =>
+ import AutomatedTurret.Target
+ /** 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 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
+ /** 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
+ /** targets that have been the subject of test shots just recently;
+ * emptied when switching from the test shot cycle to actually selecting a target */
+ private var ongoingTestedTargets: Seq[Target] = Seq[Target]()
+
+ /** timer managing the trailing target qualifications self test
+ * where the source will shoot directly at some target
+ * expecting a response in return */
+ private var selfReportedRefire: Cancellable = Default.Cancellable
+ /** 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;
+ * this may duplicate information processed during some other database update call */
+ private var targetsDestroyed: Int = 0
+
+ def AutomatedTurretObject: AutomatedTurret
+
+ val automatedTurretBehavior: Actor.Receive = if (autoStats.isDefined) {
+ case AutomatedTurretBehavior.Alert(target) =>
+ bringAttentionToTarget(target)
+
+ case AutomatedTurretBehavior.ConfirmShot(target, _) =>
+ normalConfirmShot(target)
+
+ case SelfReportedConfirmShot(target) =>
+ movementCancelSelfReportingFireConfirmShot(target)
+
+ case AutomatedTurretBehavior.Unalert(target) =>
+ disregardTarget(target)
+
+ case AutomatedTurretBehavior.Reset =>
+ resetAlerts()
+
+ case AutomatedTurretBehavior.PeriodicCheck =>
+ performPeriodicTargetValidation()
+ } else {
+ Actor.emptyBehavior
+ }
+
+ 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
+ val newState = state && AutomaticOperationFunctionalityChecks
+ automaticOperation = newState
+ if (!previousState && newState) {
+ trySelectNewTarget()
+ } else if (previousState && !newState) {
+ ongoingTestedTargets = Seq()
+ cancelSelfReportedAutoFire()
+ AutomatedTurretObject.Target.foreach(noLongerEngageDetectedTarget)
+ }
+ newState
+ }
+
+ /**
+ * 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 = { autoStats.isDefined }
+
+ /**
+ * 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 = {
+ 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
+ AutomatedTurretObject.Detected(target)
+ .orElse {
+ AutomatedTurretObject.AddTarget(target)
+ retimePeriodicTargetChecks(size)
+ Some(target)
+ }
+ }
+
+ /**
+ * 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
+ AutomatedTurretObject.Detected(target)
+ .collect { out =>
+ AutomatedTurretObject.RemoveTarget(target)
+ testTargetQualificationsForOngoingChecks(size)
+ out
+ }
+ .flatMap {
+ noLongerDetectTargetIfCurrent
+ }
+ }
+
+ /**
+ * 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()
+ currentTargetToken = None
+ currentTargetLocation = None
+ ongoingTestedTargets = Seq()
+ }
+
+ /* 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
+ * @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): Boolean = {
+ val now = System.currentTimeMillis()
+ if (
+ currentTargetToken.isEmpty &&
+ target.Faction != AutomatedTurretObject.Faction
+ ) {
+ currentTargetLastShotTime = now
+ currentTargetLocation = Some(target.Position)
+ ongoingTestedTargets = Seq()
+ cancelSelfReportedAutoFire()
+ engageNewDetectedTarget(target)
+ true
+ } else if (
+ currentTargetToken.contains(SourceUniqueness(target)) &&
+ now - currentTargetLastShotTime < autoStats.map(_.cooldowns.missedShot).getOrElse(0L)) {
+ currentTargetLastShotTime = now
+ currentTargetLocation = Some(target.Position)
+ cancelSelfReportedAutoFire()
+ true
+ } else {
+ false
+ }
+ }
+
+ /**
+ * 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
+ */
+ private def engageNewDetectedTarget(target: Target): Unit = {
+ val zone = target.Zone
+ val zoneid = zone.id
+ currentTargetToken = Some(SourceUniqueness(target))
+ currentTargetLocation = Some(target.Position)
+ currentTargetSwitchTime = System.currentTimeMillis()
+ AutomatedTurretObject.Target = target
+ engageNewDetectedTarget(
+ target,
+ zoneid,
+ AutomatedTurretObject.GUID,
+ AutomatedTurretObject.Weapons.values.head.Equipment.get.GUID
+ )
+ }
+
+ /**
+ * 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.
+ * For implementing behavior.
+ * Must be implemented.
+ * @param target something the turret can potentially shoot at
+ * @param channel scope of the message
+ * @param turretGuid turret
+ * @param weaponGuid turret's weapon
+ */
+ protected def engageNewDetectedTarget(target: Target, channel: String, turretGuid: PlanetSideGUID, weaponGuid: PlanetSideGUID): Unit
+
+ /**
+ * 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(SourceUniqueness(target))) {
+ 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
+ */
+ private def noLongerEngageDetectedTarget(target: Target): Option[Target] = {
+ AutomatedTurretObject.Target = None
+ currentTargetToken = None
+ currentTargetLocation = None
+ noLongerEngageTarget(
+ target,
+ target.Zone.id,
+ AutomatedTurretObject.GUID,
+ AutomatedTurretObject.Weapons.values.head.Equipment.get.GUID
+ )
+ None
+ }
+
+ /**
+ * Stop pointing the business end of the turret's weapon at a provided target.
+ * Stop shooting at the target.
+ * For implementing behavior.
+ * Must be implemented.
+ * @param target something the turret can potentially shoot at
+ * @param channel scope of the message
+ * @param turretGuid turret
+ * @param weaponGuid turret's weapon
+ * @return something the turret was potentially shooting at
+ */
+ protected def noLongerEngageTarget(target: Target, channel: String, turretGuid: PlanetSideGUID, weaponGuid: PlanetSideGUID): Option[Target]
+
+ /**
+ * 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;
+ * it doesn't really matter which something is returned but, rather, if anything is returned
+ */
+ protected def trySelectNewTarget(): Option[Target] = {
+ AutomatedTurretObject.Target.orElse {
+ val turretPosition = AutomatedTurretObject.Position
+ val turretGuid = AutomatedTurretObject.GUID
+ val weaponGuid = AutomatedTurretObject.Weapons.values.head.Equipment.get.GUID
+ val radius = autoStats.get.ranges.trigger
+ val validation = autoStats.get.checks.validation
+ val disqualifiers = autoStats.get.checks.blanking
+ val faction = AutomatedTurretObject.Faction
+ //current targets
+ val selectedTargets = AutomatedTurretObject
+ .Targets
+ .collect { case target
+ if !target.Destroyed &&
+ target.Faction != faction &&
+ AutomatedTurretBehavior.shapedDistanceCheckAgainstValue(autoStats, target.Position, turretPosition, radius, result = -1) &&
+ validation.exists(func => func(target)) &&
+ disqualifiers.takeWhile(func => func(target)).isEmpty =>
+ target
+ }
+ //sort targets into categories
+ val (previousTargets, newTargets, staleTargets) = {
+ val previouslyTestedTokens = ongoingTestedTargets.map(target => SourceUniqueness(target))
+ val (previous_targets, new_targets) = selectedTargets.partition(target => previouslyTestedTokens.contains(SourceUniqueness(target)))
+ val previousTargetTokens = previous_targets.map(target => (SourceUniqueness(target), target))
+ val stale_targets = {
+ for {
+ (token, target) <- previousTargetTokens
+ if !previouslyTestedTokens.contains(token)
+ } yield target
+ }
+ (previous_targets, new_targets, stale_targets)
+ }
+ //associate with proper functionality and perform callbacks
+ val newTargetsFunc: Iterable[(Target, (Target, String, PlanetSideGUID, PlanetSideGUID) => Unit)] =
+ newTargets.map(target => (target, testNewDetected))
+ val previousTargetsFunc: Iterable[(Target, (Target, String, PlanetSideGUID, PlanetSideGUID) => Unit)] =
+ previousTargets.map(target => (target, testKnownDetected))
+ ongoingTestedTargets = (newTargetsFunc ++ previousTargetsFunc)
+ .toSeq
+ .sortBy { case (target, _) => Vector3.DistanceSquared(target.Position, turretPosition) }
+ .flatMap { case (target, func) => processForTestingTarget(target, turretGuid, weaponGuid, func) }
+ .map { case (target, _) => target }
+ staleTargets.foreach(target => processForTestingTarget(target, turretGuid, weaponGuid, suspendTargetTesting))
+ selectedTargets.headOption
+ }
+ }
+
+ /**
+ * 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
+ * @param processFunc na
+ * @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 processForTestingTarget(
+ target: Target,
+ turretGuid: PlanetSideGUID,
+ weaponGuid: PlanetSideGUID,
+ processFunc: (Target, String, PlanetSideGUID, PlanetSideGUID)=>Unit
+ ): Option[(Target, Target)] = {
+ target match {
+ case target: Player =>
+ processFunc(target, target.Name, turretGuid, weaponGuid)
+ Some((target, target))
+ case target: Mountable =>
+ target.Seats.values
+ .flatMap(_.occupants)
+ .collectFirst { passenger =>
+ processFunc(target, passenger.Name, turretGuid, weaponGuid)
+ (target, passenger)
+ }
+ case _ =>
+ None
+ }
+ }
+
+ /**
+ * 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.
+ * For implementing behavior.
+ * Must be implemented.
+ * @param target something the turret can potentially shoot at
+ * @param channel scope of the message
+ * @param turretGuid turret
+ * @param weaponGuid turret's weapon
+ */
+ protected def testNewDetected(target: Target, channel: String, turretGuid: PlanetSideGUID, weaponGuid: PlanetSideGUID): Unit
+
+ /**
+ * 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.
+ * For implementing behavior.
+ * Must be implemented.
+ * @param target something the turret can potentially shoot at
+ * @param channel scope of the message
+ * @param turretGuid not used
+ * @param weaponGuid turret's weapon
+ */
+ protected def testKnownDetected(target: Target, channel: String, turretGuid: PlanetSideGUID, weaponGuid: PlanetSideGUID): Unit
+
+ /**
+ * na
+ * For overriding behavior.
+ * @param target something the turret can potentially shoot at
+ * @param channel scope of the message
+ * @param turretGuid not used
+ * @param weaponGuid turret's weapon
+ */
+ protected def suspendTargetTesting(
+ target: Target,
+ channel: String,
+ turretGuid: PlanetSideGUID,
+ weaponGuid: PlanetSideGUID
+ ): Unit = { /*do nothing*/ }
+
+ /**
+ * 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()
+ performCurrentTargetDecayCheck()
+ testTargetQualificationsForOngoingChecks(size)
+ 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
+ val range = autoStats.map(_.ranges.detection).getOrElse(0f)
+ val removedTargets = AutomatedTurretObject.Targets
+ .collect {
+ case t: InteractsWithZone
+ if t.Destroyed || AutomatedTurretBehavior.shapedDistanceCheckAgainstValue(autoStats, t.Position, pos, range) =>
+ AutomatedTurretObject.RemoveTarget(t)
+ t
+ }
+ 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()
+ AutomatedTurretObject.Target
+ .collect { target =>
+ //test target
+ generalDecayCheck(
+ target,
+ now,
+ 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 {
+ //no target; unless we are deactivated or have any unfinished delays, search for new target
+ //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,
+ * 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 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
+ */
+ private def generalDecayCheck(
+ target: Target,
+ now: Long,
+ escapeRange: Float,
+ selectDelay: Long,
+ cooldownDelay: Long,
+ eliminationDelay: Long
+ ): Option[Target] = {
+ if (target.Destroyed) {
+ //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()
+ noLongerEngageDetectedTarget(target)
+ currentTargetLastShotTime = now + cooldownDelay
+ None
+ }
+ else if ({
+ target match {
+ case mount: Mountable => !mount.Seats.values.exists(_.isOccupied)
+ case _ => false
+ }
+ }) {
+ //certain targets can go "unresponsive" even though they should still be reachable, otherwise the target is mia
+ trySelfReportedAutofireIfStationary()
+ noLongerEngageDetectedTarget(target)
+ currentTargetLastShotTime = now + selectDelay
+ None
+ } else if (now - currentTargetLastShotTime >= cooldownDelay) {
+ //if the target goes mia through lack of response
+ noLongerEngageDetectedTarget(target)
+ currentTargetLastShotTime = now + selectDelay
+ None
+ } else {
+ //continue shooting
+ Some(target)
+ }
+ }
+
+ /**
+ * If there are no available targets,
+ * and no current target,
+ * stop the evaluation of available targets.
+ * @param beforeListSize 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 testTargetQualificationsForOngoingChecks(beforeListSize: Int): Boolean = {
+ beforeListSize > 0 &&
+ AutomatedTurretObject.Targets.isEmpty &&
+ AutomatedTurretObject.Target.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(_.detectionSweepTime).getOrElse(1.seconds)
+ retimePeriodicTargetChecks(repeated)
+ true
+ } else {
+ false
+ }
+ }
+
+ /**
+ * 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(
+ 0.seconds,
+ repeated,
+ self,
+ AutomatedTurretBehavior.PeriodicCheck
+ )
+ }
+
+ /**
+ * 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 = {
+ ongoingTestedTargets = Seq()
+ periodicValidationTest.cancel()
+ 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`
+ */
+ protected def automaticTurretPostStop(): Unit = {
+ resetAlerts()
+ AutomatedTurretObject.Targets.foreach { AutomatedTurretObject.RemoveTarget }
+ selfReportingCleanUp()
+ }
+
+ /* Retaliation behavior */
+
+ /**
+ * 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] = {
+ val unique = SourceUniqueness(target)
+ if (
+ automaticOperation &&
+ !currentTargetToken.contains(unique) &&
+ autoStats.exists(_.retaliatoryDelay > 0)
+ ) {
+ AutomatedTurretBehavior.getAttackVectorFromCause(target.Zone, cause).collect {
+ case attacker
+ if attacker.Faction != target.Faction &&
+ performRetaliation(attacker).nonEmpty &&
+ currentTargetToken.contains(unique) =>
+ if (periodicValidationTest.isCancelled) {
+ //timer may need to be started, for example if damaged by things outside of detection perimeter
+ retimePeriodicTargetChecks(autoStats.map(_.detectionSweepTime).getOrElse(1.seconds))
+ }
+ attacker
+ }
+ } else {
+ None
+ }
+ }
+
+ /**
+ * 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 existingTarget
+ if autoStats.exists { auto =>
+ auto.retaliationOverridesTarget &&
+ currentTargetSwitchTime + auto.retaliatoryDelay > System.currentTimeMillis() &&
+ auto.checks.blanking.takeWhile(func => func(target)).isEmpty
+ } =>
+ //conditions necessary for overriding the current target
+ cancelSelfReportedAutoFire()
+ noLongerEngageDetectedTarget(existingTarget)
+ engageNewDetectedTarget(target)
+ target
+
+ case existingTarget =>
+ //stay with the current target
+ existingTarget
+ }
+ .orElse {
+ //no current target
+ if (autoStats.exists(_.checks.blanking.takeWhile(func => func(target)).isEmpty)) {
+ engageNewDetectedTarget(target)
+ Some(target)
+ } else {
+ None
+ }
+ }
+ }
+
+ /* 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 = {
+ currentTargetLastShotTime = System.currentTimeMillis()
+ shotsFired += 1
+ target match {
+ case v: Mountable
+ if v.Destroyed && !v.Seats.values.exists(_.isOccupied) =>
+ targetsDestroyed += 1
+ case _ => ()
+ }
+ AutomatedTurretObject.Target
+ .collect { oldTarget =>
+ if (currentTargetToken.contains(SourceUniqueness(oldTarget))) {
+ //target already being handled
+ if (oldTarget.Destroyed || currentTargetLocation.exists(loc => Vector3.DistanceSquared(loc, oldTarget.Position) > 1f)) {
+ //stop (destroyed, or movement disqualification)
+ cancelSelfReportedAutoFire()
+ noLongerEngageDetectedTarget(oldTarget)
+ processForTestingTarget(
+ oldTarget,
+ AutomatedTurretObject.GUID,
+ AutomatedTurretObject.Weapons.values.head.Equipment.get.GUID,
+ testNewDetected
+ )
+ }
+ } else {
+ //stop (wrong target)
+ cancelSelfReportedAutoFire()
+ }
+ }
+ .orElse {
+ //start new target
+ engageNewDetectedTarget(target)
+ tryPerformSelfReportedAutofire(target)
+ None
+ }
+ }
+
+ /**
+ * 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) &&
+ autoStats.exists(_.refireTime > 0.seconds) =>
+ 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) {
+ target.Actor ! 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 tryPerformSelfReportedAutofire(target: Target): Boolean = {
+ if (selfReportedRefire.isCancelled) {
+ selfReportedRefire = context.system.scheduler.scheduleWithFixedDelay(
+ 0.seconds,
+ autoStats.map(_.refireTime).getOrElse(1.seconds),
+ target.Actor,
+ AiDamage(AutomatedTurretObject)
+ )
+ true
+ } else {
+ false
+ }
+ }
+
+ /**
+ * 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
+ true
+ }
+
+ /**
+ * 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))
+ selfReportingCleanUp()
+ case _ => ()
+ }
+ }
+}
+
+object AutomatedTurretBehavior {
+ import AutomatedTurret.Target
+ final case class Alert(target: Target)
+
+ final case class Unalert(target: Target)
+
+ final case class ConfirmShot(target: Target, reporter: Option[SourceEntry] = None)
+
+ final case object Reset
+
+ private case object PeriodicCheck
+
+ private val commonBlanking: List[PlanetSideGameObject => Boolean] = List(
+ EffectTarget.Validation.AutoTurretBlankPlayerTarget,
+ EffectTarget.Validation.AutoTurretBlankVehicleTarget
+ )
+
+ private val noTargets: List[PlanetSideGUID] = List(Service.defaultPlayerGUID)
+
+ /**
+ * Are we tracking a target entity?
+ * @param zone the region in which the messages will be dispatched
+ * @param channel scope of the message
+ * @param turretGuid turret
+ * @param list target's globally unique identifier, in list form
+ */
+ def startTracking(zone: Zone, channel: String, turretGuid: PlanetSideGUID, list: List[PlanetSideGUID]): Unit = {
+ zone.LocalEvents ! LocalServiceMessage(
+ channel,
+ LocalAction.SendResponse(ObjectDetectedMessage(turretGuid, turretGuid, 0, list))
+ )
+ }
+
+ /**
+ * Are we no longer tracking a target entity?
+ * @param zone the region in which the messages will be dispatched
+ * @param channel scope of the message
+ * @param turretGuid turret
+ */
+ def stopTracking(zone: Zone, channel: String, turretGuid: PlanetSideGUID): Unit = {
+ zone.LocalEvents ! LocalServiceMessage(
+ channel,
+ LocalAction.SendResponse(ObjectDetectedMessage(turretGuid, turretGuid, 0, noTargets))
+ )
+ }
+
+ /**
+ * Are we shooting a weapon?
+ * @param zone the region in which the messages will be dispatched
+ * @param channel scope of the message
+ * @param weaponGuid turret's weapon
+ */
+ def startShooting(zone: Zone, channel: String, weaponGuid: PlanetSideGUID): Unit = {
+ zone.LocalEvents ! LocalServiceMessage(
+ channel,
+ LocalAction.SendResponse(ChangeFireStateMessage_Start(weaponGuid))
+ )
+ }
+
+ /**
+ * Are we no longer shooting a weapon?
+ * @param zone the region in which the messages will be dispatched
+ * @param channel scope of the message
+ * @param weaponGuid turret's weapon
+ */
+ def stopShooting(zone: Zone, channel: String, weaponGuid: PlanetSideGUID): Unit = {
+ zone.LocalEvents ! LocalServiceMessage(
+ channel,
+ LocalAction.SendResponse(ChangeFireStateMessage_Stop(weaponGuid))
+ )
+ }
+
+ /**
+ * 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
+ .adversarial
+ .collect { adversarial =>
+ adversarial.attacker match {
+ case p: PlayerSource =>
+ p.seatedIn
+ .map { _._1.unique }
+ .collect {
+ case v: UniqueVehicle => zone.Vehicles.find(SourceUniqueness(_) == v)
+ case a: UniqueAmenity => zone.GUID(a.guid)
+ case d: UniqueDeployable => zone.DeployableList.find(SourceUniqueness(_) == d)
+ }
+ .flatten
+ .orElse {
+ val name = p.Name
+ zone.LivePlayers.find(_.Name.equals(name))
+ }
+ case o =>
+ o.unique match {
+ case v: UniqueVehicle => zone.Vehicles.find(SourceUniqueness(_) == v)
+ case a: UniqueAmenity => zone.GUID(a.guid)
+ case d: UniqueDeployable => zone.DeployableList.find(SourceUniqueness(_) == d)
+ case _ => None
+ }
+ }
+ }
+ .flatten
+ .collect {
+ case out: PlanetSideServerObject with Vitality => out
+ }
+ }
+
+ /**
+ * 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 if the actual result of the comparison matches its anticipation `result`
+ */
+ def shapedDistanceCheckAgainstValue(
+ stats: Option[Automation],
+ positionA: Vector3,
+ positionB: Vector3,
+ range: Float,
+ result: Int = 1 //by default, calculation > input
+ ): Boolean = {
+ val testRangeSq = range * range
+ if (stats.exists(_.cylindrical)) {
+ val height = range + stats.map(_.cylindricalExtraHeight).getOrElse(0f)
+ (if (positionA.z > positionB.z) positionA.z - positionB.z else positionB.z - positionA.z).compareTo(height) == result &&
+ Vector3.DistanceSquared(positionA.xy, positionB.xy).compareTo(testRangeSq) == result
+ } else {
+ Vector3.DistanceSquared(positionA, positionB).compareTo(testRangeSq) == result
+ }
+ }
+}
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/sourcing/AmenitySource.scala b/src/main/scala/net/psforever/objects/sourcing/AmenitySource.scala
index 23893df77..8b201ae5c 100644
--- a/src/main/scala/net/psforever/objects/sourcing/AmenitySource.scala
+++ b/src/main/scala/net/psforever/objects/sourcing/AmenitySource.scala
@@ -6,7 +6,6 @@ import net.psforever.objects.serverobject.hackable.Hackable
import net.psforever.objects.serverobject.hackable.Hackable.HackInfo
import net.psforever.objects.serverobject.mount.Mountable
import net.psforever.objects.serverobject.structures.Amenity
-import net.psforever.objects.sourcing
import net.psforever.objects.vital.resistance.ResistanceProfile
import net.psforever.objects.vital.{Vitality, VitalityDefinition}
import net.psforever.types.{PlanetSideEmpire, Vector3}
@@ -57,7 +56,7 @@ object AmenitySource {
Nil,
SourceEntry(obj.Owner),
hackData,
- sourcing.UniqueAmenity(obj.Zone.Number, obj.GUID, obj.Position)
+ UniqueAmenity(obj)
)
amenity.copy(occupants = obj match {
case o: Mountable =>
diff --git a/src/main/scala/net/psforever/objects/sourcing/BuildingSource.scala b/src/main/scala/net/psforever/objects/sourcing/BuildingSource.scala
index 92445d6dc..ce7142db8 100644
--- a/src/main/scala/net/psforever/objects/sourcing/BuildingSource.scala
+++ b/src/main/scala/net/psforever/objects/sourcing/BuildingSource.scala
@@ -12,6 +12,12 @@ final case class UniqueBuilding(
building_guid: PlanetSideGUID
) extends SourceUniqueness
+object UniqueBuilding {
+ def apply(obj: Building): UniqueBuilding = {
+ UniqueBuilding(obj.Zone.Number, obj.GUID)
+ }
+}
+
final case class BuildingSource(
private val obj_def: BuildingDefinition,
Faction: PlanetSideEmpire.Value,
@@ -35,7 +41,7 @@ object BuildingSource {
b.Position,
b.Orientation,
b.latticeConnectedFacilityBenefits(),
- UniqueBuilding(b.Zone.Number, b.GUID)
+ UniqueBuilding(b)
)
}
}
diff --git a/src/main/scala/net/psforever/objects/sourcing/DeployableSource.scala b/src/main/scala/net/psforever/objects/sourcing/DeployableSource.scala
index 62dd62470..e7ca29882 100644
--- a/src/main/scala/net/psforever/objects/sourcing/DeployableSource.scala
+++ b/src/main/scala/net/psforever/objects/sourcing/DeployableSource.scala
@@ -52,13 +52,7 @@ object DeployableSource {
obj.Position,
obj.Orientation,
occupants,
- UniqueDeployable(
- obj.History.headOption match {
- case Some(entry) => entry.time
- case None => 0L
- },
- obj.OriginalOwnerName.getOrElse("none")
- )
+ UniqueDeployable(obj)
)
}
}
diff --git a/src/main/scala/net/psforever/objects/sourcing/ObjectSource.scala b/src/main/scala/net/psforever/objects/sourcing/ObjectSource.scala
index 48624c3df..5b303c2a9 100644
--- a/src/main/scala/net/psforever/objects/sourcing/ObjectSource.scala
+++ b/src/main/scala/net/psforever/objects/sourcing/ObjectSource.scala
@@ -10,6 +10,12 @@ import net.psforever.types.{PlanetSideEmpire, Vector3}
final case class UniqueObject(objectId: Int) extends SourceUniqueness
+object UniqueObject {
+ def apply(obj: PlanetSideGameObject): UniqueObject = {
+ UniqueObject(obj.Definition.ObjectId)
+ }
+}
+
final case class ObjectSource(
private val obj_def: ObjectDefinition,
Faction: PlanetSideEmpire.Value,
@@ -44,7 +50,7 @@ object ObjectSource {
obj.Position,
obj.Orientation,
obj.Velocity,
- UniqueObject(obj.Definition.ObjectId)
+ UniqueObject(obj)
)
}
}
diff --git a/src/main/scala/net/psforever/objects/sourcing/PlayerSource.scala b/src/main/scala/net/psforever/objects/sourcing/PlayerSource.scala
index a291d7b8d..c7405e041 100644
--- a/src/main/scala/net/psforever/objects/sourcing/PlayerSource.scala
+++ b/src/main/scala/net/psforever/objects/sourcing/PlayerSource.scala
@@ -16,6 +16,12 @@ final case class UniquePlayer(
faction: PlanetSideEmpire.Value
) extends SourceUniqueness
+object UniquePlayer {
+ def apply(obj: Player): UniquePlayer = {
+ UniquePlayer(obj.CharId, obj.Name, obj.Sex, obj.Faction)
+ }
+}
+
final case class PlayerSource(
Definition: AvatarDefinition,
ExoSuit: ExoSuitType.Value,
@@ -121,7 +127,6 @@ object PlayerSource {
*/
def inSeat(player: Player, source: SourceEntry, seatNumber: Int): PlayerSource = {
val exosuit = player.ExoSuit
- val faction = player.Faction
val avatar = player.avatar
PlayerSource(
player.Definition,
@@ -134,10 +139,10 @@ object PlayerSource {
player.Velocity,
player.Crouching,
player.Jumping,
- ExoSuitDefinition.Select(exosuit, faction),
+ ExoSuitDefinition.Select(exosuit, player.Faction),
avatar.bep,
progress = tokenLife,
- UniquePlayer(player.CharId, player.Name, player.Sex, faction)
+ UniquePlayer(player)
)
}
diff --git a/src/main/scala/net/psforever/objects/sourcing/SourceEntry.scala b/src/main/scala/net/psforever/objects/sourcing/SourceEntry.scala
index aae15dca5..81b20bf40 100644
--- a/src/main/scala/net/psforever/objects/sourcing/SourceEntry.scala
+++ b/src/main/scala/net/psforever/objects/sourcing/SourceEntry.scala
@@ -11,8 +11,6 @@ import net.psforever.objects.vital.resistance.ResistanceProfile
import net.psforever.objects.{PlanetSideGameObject, Player, TurretDeployable, Vehicle}
import net.psforever.types.{PlanetSideEmpire, Vector3}
-trait SourceUniqueness
-
trait SourceEntry {
def Name: String
def Definition: ObjectDefinition with VitalityDefinition
diff --git a/src/main/scala/net/psforever/objects/sourcing/SourceUniqueness.scala b/src/main/scala/net/psforever/objects/sourcing/SourceUniqueness.scala
new file mode 100644
index 000000000..0e8534678
--- /dev/null
+++ b/src/main/scala/net/psforever/objects/sourcing/SourceUniqueness.scala
@@ -0,0 +1,25 @@
+// Copyright (c) 2024 PSForever
+package net.psforever.objects.sourcing
+
+import net.psforever.objects.ce.Deployable
+import net.psforever.objects.{PlanetSideGameObject, Player, TurretDeployable, Vehicle}
+import net.psforever.objects.serverobject.affinity.FactionAffinity
+import net.psforever.objects.serverobject.structures.{Amenity, Building}
+import net.psforever.objects.serverobject.turret.FacilityTurret
+
+trait SourceUniqueness
+
+object SourceUniqueness {
+ def apply(target: PlanetSideGameObject with FactionAffinity): SourceUniqueness = {
+ target match {
+ case obj: Player => UniquePlayer(obj)
+ case obj: Vehicle => UniqueVehicle(obj)
+ case obj: FacilityTurret => UniqueAmenity(obj)
+ case obj: Amenity => UniqueAmenity(obj)
+ case obj: TurretDeployable => UniqueDeployable(obj)
+ case obj: Deployable => UniqueDeployable(obj)
+ case obj: Building => UniqueBuilding(obj)
+ case _ => UniqueObject(target)
+ }
+ }
+}
diff --git a/src/main/scala/net/psforever/objects/sourcing/TurretSource.scala b/src/main/scala/net/psforever/objects/sourcing/TurretSource.scala
index a1df7df61..1261cb281 100644
--- a/src/main/scala/net/psforever/objects/sourcing/TurretSource.scala
+++ b/src/main/scala/net/psforever/objects/sourcing/TurretSource.scala
@@ -33,15 +33,9 @@ object TurretSource {
val position = obj.Position
val identifer = obj match {
case o: TurretDeployable =>
- UniqueDeployable(
- o.History.headOption match {
- case Some(entry) => entry.time
- case None => 0L
- },
- o.OriginalOwnerName.getOrElse("none")
- )
+ UniqueDeployable(o)
case o: FacilityTurret =>
- UniqueAmenity(o.Zone.Number, o.GUID, position)
+ UniqueAmenity(o)
case o =>
throw new IllegalArgumentException(s"was given ${o.Actor.toString()} when only wanted to model turrets")
}
diff --git a/src/main/scala/net/psforever/objects/sourcing/UniqueAmenity.scala b/src/main/scala/net/psforever/objects/sourcing/UniqueAmenity.scala
index b5a27df1a..54ae79a15 100644
--- a/src/main/scala/net/psforever/objects/sourcing/UniqueAmenity.scala
+++ b/src/main/scala/net/psforever/objects/sourcing/UniqueAmenity.scala
@@ -1,6 +1,7 @@
// Copyright (c) 2023 PSForever
package net.psforever.objects.sourcing
+import net.psforever.objects.serverobject.structures.Amenity
import net.psforever.types.{PlanetSideGUID, Vector3}
final case class UniqueAmenity(
@@ -8,3 +9,9 @@ final case class UniqueAmenity(
guid: PlanetSideGUID,
position: Vector3
) extends SourceUniqueness
+
+object UniqueAmenity {
+ def apply(obj: Amenity): UniqueAmenity = {
+ UniqueAmenity(obj.Zone.Number, obj.GUID, obj.Position)
+ }
+}
diff --git a/src/main/scala/net/psforever/objects/sourcing/UniqueDeployable.scala b/src/main/scala/net/psforever/objects/sourcing/UniqueDeployable.scala
index f8e6ba220..dcf7a5aa1 100644
--- a/src/main/scala/net/psforever/objects/sourcing/UniqueDeployable.scala
+++ b/src/main/scala/net/psforever/objects/sourcing/UniqueDeployable.scala
@@ -1,7 +1,21 @@
// Copyright (c) 2023 PSForever
package net.psforever.objects.sourcing
+import net.psforever.objects.ce.Deployable
+
final case class UniqueDeployable(
spawnTime: Long,
originalOwnerName: String
) extends SourceUniqueness
+
+object UniqueDeployable {
+ def apply(obj: Deployable): UniqueDeployable = {
+ UniqueDeployable(
+ obj.History.headOption match {
+ case Some(entry) => entry.time
+ case None => 0L
+ },
+ obj.OriginalOwnerName.getOrElse("none")
+ )
+ }
+}
diff --git a/src/main/scala/net/psforever/objects/sourcing/VehicleSource.scala b/src/main/scala/net/psforever/objects/sourcing/VehicleSource.scala
index 7ff45cfd4..25f60b710 100644
--- a/src/main/scala/net/psforever/objects/sourcing/VehicleSource.scala
+++ b/src/main/scala/net/psforever/objects/sourcing/VehicleSource.scala
@@ -8,6 +8,18 @@ import net.psforever.types.{DriveState, PlanetSideEmpire, Vector3}
final case class UniqueVehicle(spawnTime: Long, originalOwnerName: String) extends SourceUniqueness
+object UniqueVehicle {
+ def apply(obj: Vehicle): UniqueVehicle = {
+ UniqueVehicle(
+ obj.History.headOption match {
+ case Some(entry) => entry.time
+ case None => 0L
+ },
+ obj.OriginalOwnerName.getOrElse("none")
+ )
+ }
+}
+
final case class VehicleSource(
Definition: VehicleDefinition,
Faction: PlanetSideEmpire.Value,
@@ -46,13 +58,7 @@ object VehicleSource {
None,
Nil,
obj.Definition.asInstanceOf[ResistanceProfile],
- UniqueVehicle(
- obj.History.headOption match {
- case Some(entry) => entry.time
- case None => 0L
- },
- obj.OriginalOwnerName.getOrElse("none")
- )
+ UniqueVehicle(obj)
)
//shallow information that references the existing source entry
vehicle.copy(
diff --git a/src/main/scala/net/psforever/objects/vehicles/InteractWithRadiationCloudsSeatedInVehicle.scala b/src/main/scala/net/psforever/objects/vehicles/InteractWithRadiationCloudsSeatedInVehicle.scala
index 763fed3d6..fd5fdcd25 100644
--- a/src/main/scala/net/psforever/objects/vehicles/InteractWithRadiationCloudsSeatedInVehicle.scala
+++ b/src/main/scala/net/psforever/objects/vehicles/InteractWithRadiationCloudsSeatedInVehicle.scala
@@ -2,17 +2,9 @@
package net.psforever.objects.vehicles
import net.psforever.objects.Vehicle
-import net.psforever.objects.ballistics.{Projectile, ProjectileQuality}
-import net.psforever.objects.sourcing.SourceEntry
-import net.psforever.objects.vital.Vitality
-import net.psforever.objects.vital.base.{DamageResolution, DamageType}
-import net.psforever.objects.vital.etc.RadiationReason
-import net.psforever.objects.vital.interaction.DamageInteraction
+import net.psforever.objects.serverobject.mount.{InteractWithRadiationCloudsSeatedInEntity, RadiationInMountableInteraction}
import net.psforever.objects.zones.blockmap.SectorPopulation
-import net.psforever.objects.zones.{InteractsWithZone, Zone, ZoneInteraction, ZoneInteractionType}
-import net.psforever.types.PlanetSideGUID
-
-case object RadiationInVehicleInteraction extends ZoneInteractionType
+import net.psforever.objects.zones.InteractsWithZone
/**
* This game entity may infrequently test whether it may interact with radiation cloud projectiles
@@ -21,90 +13,24 @@ case object RadiationInVehicleInteraction extends ZoneInteractionType
*/
class InteractWithRadiationCloudsSeatedInVehicle(
private val obj: Vehicle,
- val range: Float
- ) extends ZoneInteraction {
- /**
- * radiation clouds that, though detected, are skipped from affecting the target;
- * in between interaction tests, a memory of the clouds that were tested last are retained and
- * are excluded from being tested this next time;
- * clouds that are detected a second time are cleared from the list and are available to be tested next time
- */
- private var skipTargets: List[PlanetSideGUID] = List()
-
- def Type = RadiationInVehicleInteraction
-
+ override val range: Float
+ ) extends InteractWithRadiationCloudsSeatedInEntity(obj, range) {
/**
* Drive into a radiation cloud and all the vehicle's occupants suffer the consequences.
* @param sector the portion of the block map being tested
* @param target the fixed element in this test
*/
override def interaction(sector: SectorPopulation, target: InteractsWithZone): Unit = {
- val position = target.Position
- //collect all projectiles in sector/range
- val projectiles = sector
- .projectileList
- .filter { cloud =>
- val definition = cloud.Definition
- definition.radiation_cloud &&
- definition.AllDamageTypes.contains(DamageType.Radiation) &&
- {
- val radius = definition.DamageRadius
- Zone.distanceCheck(target, cloud, radius * radius)
- }
- }
- .distinct
- val notSkipped = projectiles.filterNot { t => skipTargets.contains(t.GUID) }
- skipTargets = notSkipped.map { _.GUID }
- if (notSkipped.nonEmpty) {
- (
- //isolate one of each type of projectile
- notSkipped
- .foldLeft(Nil: List[Projectile]) {
- (acc, next) => if (acc.exists { _.profile == next.profile }) acc else next :: acc
- },
- obj.Seats
- .values
- .collect { case seat => seat.occupant }
- .flatten
- ) match {
- case (uniqueProjectiles, targets) if uniqueProjectiles.nonEmpty && targets.nonEmpty =>
- val shielding = obj.Definition.RadiationShielding
- targets.foreach { t =>
- uniqueProjectiles.foreach { p =>
- t.Actor ! Vitality.Damage(
- DamageInteraction(
- SourceEntry(t),
- RadiationReason(
- ProjectileQuality.modifiers(p, DamageResolution.Radiation, t, t.Position, None),
- t.DamageModel,
- shielding
- ),
- position
- ).calculate()
- )
- }
- }
- case _ => ;
- }
- }
+ super.interaction(sector, target)
obj.CargoHolds
.values
.collect {
case hold if hold.isOccupied =>
val target = hold.occupant.get
- target.interaction().find { _.Type == RadiationInVehicleInteraction } match {
- case Some(func) => func.interaction(sector, target)
- case _ => ;
+ target
+ .interaction()
+ .find(_.Type == RadiationInMountableInteraction)
+ .foreach(func => func.interaction(sector, target))
}
- }
- }
-
- /**
- * Any radiation clouds blocked from being tested should be cleared.
- * All that can be done is blanking our retained previous effect targets.
- * @param target the fixed element in this test
- */
- def resetInteraction(target: InteractsWithZone): Unit = {
- skipTargets = List()
}
}
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..8670b8894 100644
--- a/src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala
+++ b/src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala
@@ -17,9 +17,10 @@ import net.psforever.objects.serverobject.damage.Damageable.Target
import net.psforever.objects.serverobject.damage.{AggravatedBehavior, DamageableVehicle}
import net.psforever.objects.serverobject.environment._
import net.psforever.objects.serverobject.hackable.GenericHackables
-import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior}
+import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior, RadiationInMountableInteraction}
import net.psforever.objects.serverobject.repair.RepairableVehicle
import net.psforever.objects.serverobject.terminals.Terminal
+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}
@@ -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()
@@ -243,7 +247,7 @@ class VehicleControl(vehicle: Vehicle)
commonEnabledBehavior
.orElse {
case VehicleControl.RadiationTick =>
- vehicle.interaction().find { _.Type == RadiationInVehicleInteraction } match {
+ vehicle.interaction().find { _.Type == RadiationInMountableInteraction } match {
case Some(func) => func.interaction(vehicle.getInteractionSector(), vehicle)
case _ => ;
}
diff --git a/src/main/scala/net/psforever/objects/vital/InGameHistory.scala b/src/main/scala/net/psforever/objects/vital/InGameHistory.scala
index d62990974..359cfe2f8 100644
--- a/src/main/scala/net/psforever/objects/vital/InGameHistory.scala
+++ b/src/main/scala/net/psforever/objects/vital/InGameHistory.scala
@@ -327,7 +327,7 @@ trait InGameHistory {
if (target eq this) {
None
} else {
- val uniqueTarget = SourceEntry(target).unique
+ val uniqueTarget = SourceUniqueness(target)
(target.GetContribution(), contributionInheritance.get(uniqueTarget)) match {
case (Some(in), Some(curr)) =>
val end = curr.end
@@ -395,6 +395,6 @@ object InGameHistory {
def ContributionFrom(target: PlanetSideGameObject with FactionAffinity with InGameHistory): Option[Contribution] = {
target
.GetContribution()
- .collect { case events => Contribution(SourceEntry(target).unique, events) }
+ .collect { case events => Contribution(SourceUniqueness(target), events) }
}
}
diff --git a/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
index 384458bcb..291928c6c 100644
--- a/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
+++ b/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
@@ -411,7 +411,7 @@ object GamePacketOpcode extends Enumeration {
case 0x59 => noDecoder(UnknownMessage89)
case 0x5a => game.DelayedPathMountMsg.decode
case 0x5b => game.OrbitalShuttleTimeMsg.decode
- case 0x5c => noDecoder(AIDamage)
+ case 0x5c => game.AIDamage.decode
case 0x5d => game.DeployObjectMessage.decode
case 0x5e => game.FavoritesRequest.decode
case 0x5f => noDecoder(FavoritesResponse)
diff --git a/src/main/scala/net/psforever/packet/game/AIDamage.scala b/src/main/scala/net/psforever/packet/game/AIDamage.scala
new file mode 100644
index 000000000..f2d97479b
--- /dev/null
+++ b/src/main/scala/net/psforever/packet/game/AIDamage.scala
@@ -0,0 +1,32 @@
+// Copyright (c) 2023 PSForever
+package net.psforever.packet.game
+
+import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
+import net.psforever.types.PlanetSideGUID
+import scodec.Codec
+import scodec.codecs._
+
+/**
+ * ...
+ */
+final case class AIDamage(
+ target_guid: PlanetSideGUID,
+ attacker_guid: PlanetSideGUID,
+ projectile_type: Long,
+ unk1: Long,
+ unk2: Long
+ ) extends PlanetSideGamePacket {
+ type Packet = ActionResultMessage
+ def opcode = GamePacketOpcode.AIDamage
+ def encode = AIDamage.encode(this)
+}
+
+object AIDamage extends Marshallable[AIDamage] {
+ implicit val codec: Codec[AIDamage] = (
+ ("target_guid" | PlanetSideGUID.codec) ::
+ ("attacker_guid" | PlanetSideGUID.codec) ::
+ ("projectile_type" | ulongL(bits = 32)) ::
+ ("unk1" | ulongL(bits = 32)) ::
+ ("unk2" | ulongL(bits = 32))
+ ).as[AIDamage]
+}
diff --git a/src/main/scala/net/psforever/packet/game/objectcreate/CommonFieldData.scala b/src/main/scala/net/psforever/packet/game/objectcreate/CommonFieldData.scala
index f1ca73535..60e76d39e 100644
--- a/src/main/scala/net/psforever/packet/game/objectcreate/CommonFieldData.scala
+++ b/src/main/scala/net/psforever/packet/game/objectcreate/CommonFieldData.scala
@@ -81,13 +81,13 @@ object CommonFieldData extends Marshallable[CommonFieldData] {
CommonFieldData(faction, false, false, false, None, false, None, None, PlanetSideGUID(0))
def apply(faction: PlanetSideEmpire.Value, unk: Int): CommonFieldData =
- CommonFieldData(faction, false, false, unk > 1, None, unk % 1 == 1, None, None, PlanetSideGUID(0))
+ CommonFieldData(faction, false, false, unk > 1, None, unk > 0, None, None, PlanetSideGUID(0))
def apply(faction: PlanetSideEmpire.Value, unk: Int, player_guid: PlanetSideGUID): CommonFieldData =
- CommonFieldData(faction, false, false, unk > 1, None, unk % 1 == 1, None, None, player_guid)
+ CommonFieldData(faction, false, false, unk > 1, None, unk > 0, None, None, player_guid)
def apply(faction: PlanetSideEmpire.Value, destroyed: Boolean, unk: Int): CommonFieldData =
- CommonFieldData(faction, false, destroyed, unk > 1, None, unk % 1 == 1, None, None, PlanetSideGUID(0))
+ CommonFieldData(faction, false, destroyed, unk > 1, None, unk > 0, None, None, PlanetSideGUID(0))
def apply(
faction: PlanetSideEmpire.Value,
diff --git a/src/main/scala/net/psforever/services/vehicle/support/TurretUpgrader.scala b/src/main/scala/net/psforever/services/vehicle/support/TurretUpgrader.scala
index eb064d72b..bd6e02866 100644
--- a/src/main/scala/net/psforever/services/vehicle/support/TurretUpgrader.scala
+++ b/src/main/scala/net/psforever/services/vehicle/support/TurretUpgrader.scala
@@ -15,13 +15,14 @@ import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
import scala.concurrent.Future
import scala.concurrent.duration._
+import scala.concurrent.ExecutionContext.Implicits.global
class TurretUpgrader extends SupportActor[TurretUpgrader.Entry] {
var task: Cancellable = Default.Cancellable
var list: List[TurretUpgrader.Entry] = List()
- val sameEntryComparator = new SimilarityComparator[TurretUpgrader.Entry]() {
+ val sameEntryComparator: SimilarityComparator[TurretUpgrader.Entry] = new SimilarityComparator[TurretUpgrader.Entry]() {
def Test(entry1: TurretUpgrader.Entry, entry2: TurretUpgrader.Entry): Boolean = {
entry1.obj == entry2.obj && entry1.zone == entry2.zone && entry1.obj.GUID == entry2.obj.GUID
}
@@ -41,7 +42,7 @@ class TurretUpgrader extends SupportActor[TurretUpgrader.Entry] {
list = Nil
}
- def CreateEntry(obj: PlanetSideGameObject, zone: Zone, upgrade: TurretUpgrade.Value, duration: Long) =
+ def CreateEntry(obj: PlanetSideGameObject, zone: Zone, upgrade: TurretUpgrade.Value, duration: Long): TurretUpgrader.Entry =
TurretUpgrader.Entry(obj, zone, upgrade, duration)
def InclusionTest(entry: TurretUpgrader.Entry): Boolean = entry.obj.isInstanceOf[FacilityTurret]
@@ -89,7 +90,6 @@ class TurretUpgrader extends SupportActor[TurretUpgrader.Entry] {
task.cancel()
if (list.nonEmpty) {
val short_timeout: FiniteDuration = math.max(1, list.head.duration - (now - list.head.time)).milliseconds
- import scala.concurrent.ExecutionContext.Implicits.global
task = context.system.scheduler.scheduleOnce(short_timeout, self, TurretUpgrader.Downgrade())
}
}
@@ -150,6 +150,7 @@ class TurretUpgrader extends SupportActor[TurretUpgrader.Entry] {
val upgrade = entry.upgrade
val guid = zone.GUID
val turretGUID = target.GUID
+ target.setMiddleOfUpgrade(true)
//kick all occupying players for duration of conversion
target.Seats.values
.filter { _.isOccupied }
@@ -160,7 +161,7 @@ class TurretUpgrader extends SupportActor[TurretUpgrader.Entry] {
if (tplayer.HasGUID) {
context.parent ! VehicleServiceMessage(
zoneId,
- VehicleAction.KickPassenger(tplayer.GUID, 4, false, turretGUID)
+ VehicleAction.KickPassenger(tplayer.GUID, 4, unk2=false, turretGUID)
)
}
})
@@ -174,7 +175,6 @@ class TurretUpgrader extends SupportActor[TurretUpgrader.Entry] {
.filterNot { box => newBoxes.exists(_ eq box) }
.map(box => GUIDTask.unregisterEquipment(guid, box))
.toList
- import scala.concurrent.ExecutionContext.Implicits.global
val newBoxesTask = TaskBundle(
new StraightforwardTask() {
private val localFunc: () => Unit = FinishUpgradingTurret(entry)
@@ -189,22 +189,25 @@ class TurretUpgrader extends SupportActor[TurretUpgrader.Entry] {
.map(box => GUIDTask.registerEquipment(guid, box))
.toList
)
- TaskWorkflow.execute(TaskBundle(
+ val mainTask = TaskWorkflow.execute(TaskBundle(
new StraightforwardTask() {
private val tasks = oldBoxesTask
def action(): Future[Any] = {
- tasks.foreach { TaskWorkflow.execute }
+ tasks.foreach(TaskWorkflow.execute)
Future(this)
}
},
newBoxesTask
))
+ mainTask.recoverWith {
+ case _: Exception => Finalize(target, upgrade); Future(true)
+ }
}
/**
* From an object that has mounted weapons, parse all of the internal ammunition loaded into all of the weapons.
- * @param target the object with mounted weaponry
+ * @param target entity with mounted weaponry
* @return all of the internal ammunition objects
*/
def AllMountedWeaponMagazines(target: MountedWeapons): Iterable[AmmoBox] = {
@@ -224,11 +227,10 @@ class TurretUpgrader extends SupportActor[TurretUpgrader.Entry] {
val target = entry.obj.asInstanceOf[FacilityTurret]
val zone = entry.zone
trace(s"Wall turret finished ${target.Upgrade} upgrade")
- target.ConfirmUpgrade(entry.upgrade)
val targetGUID = target.GUID
if (target.Health > 0) {
target.Weapons
- .map({ case (index: Int, slot: EquipmentSlot) => (index, slot.Equipment) })
+ .map { case (index: Int, slot: EquipmentSlot) => (index, slot.Equipment) }
.collect {
case (index, Some(tool: Tool)) =>
context.parent ! VehicleServiceMessage(
@@ -237,6 +239,17 @@ class TurretUpgrader extends SupportActor[TurretUpgrader.Entry] {
)
}
}
+ Finalize(target, entry.upgrade)
+ }
+
+ /**
+ * Dispatch messages to report on the completion of this effort.
+ * @param target the object with mounted weaponry
+ * @param upgrade the path of the turret's progression
+ */
+ def Finalize(target: FacilityTurret, upgrade: TurretUpgrade.Value): Unit = {
+ target.ConfirmUpgrade(upgrade)
+ target.Actor ! TurretUpgrader.UpgradeCompleted(target.GUID)
}
}
@@ -263,6 +276,8 @@ object TurretUpgrader extends SupportActorCaseConversions {
final case class Downgrade()
+ final case class UpgradeCompleted(targetGuid: PlanetSideGUID)
+
private def Similarity(entry1: TurretUpgrader.Entry, entry2: TurretUpgrader.Entry): Boolean = {
entry1.obj == entry2.obj && entry1.zone == entry2.zone && entry1.obj.GUID == entry2.obj.GUID
}
diff --git a/src/main/scala/net/psforever/zones/Zones.scala b/src/main/scala/net/psforever/zones/Zones.scala
index a0a15f8d8..e82d0f861 100644
--- a/src/main/scala/net/psforever/zones/Zones.scala
+++ b/src/main/scala/net/psforever/zones/Zones.scala
@@ -23,7 +23,7 @@ import net.psforever.objects.serverobject.structures.{Building, BuildingDefiniti
import net.psforever.objects.serverobject.terminals.capture.{CaptureTerminal, CaptureTerminalDefinition}
import net.psforever.objects.serverobject.terminals.implant.ImplantTerminalMech
import net.psforever.objects.serverobject.tube.SpawnTube
-import net.psforever.objects.serverobject.turret.{FacilityTurret, FacilityTurretDefinition}
+import net.psforever.objects.serverobject.turret.{FacilityTurret, FacilityTurretDefinition, VanuSentry}
import net.psforever.objects.serverobject.zipline.ZipLinePath
import net.psforever.objects.sourcing.{DeployableSource, PlayerSource, TurretSource, VehicleSource}
import net.psforever.objects.zones.{MapInfo, Zone, ZoneInfo, ZoneMap}
@@ -585,7 +585,7 @@ object Zones {
case _ => ;
}
- case "manned_turret" | "vanu_sentry_turret" =>
+ case "manned_turret" =>
zoneMap.addLocalObject(
obj.guid,
FacilityTurret.Constructor(
@@ -596,6 +596,17 @@ object Zones {
)
zoneMap.linkTurretToWeapon(obj.guid, turretWeaponGuid.getAndIncrement())
+ case "vanu_sentry_turret" =>
+ zoneMap.addLocalObject(
+ obj.guid,
+ VanuSentry.Constructor(
+ obj.position,
+ obj.objectDefinition.asInstanceOf[FacilityTurretDefinition]
+ ),
+ owningBuildingGuid = ownerGuid
+ )
+ zoneMap.linkTurretToWeapon(obj.guid, turretWeaponGuid.getAndIncrement())
+
case "implant_terminal_mech" =>
zoneMap.addLocalObject(
obj.guid,
diff --git a/src/test/scala/game/AIDamageTest.scala b/src/test/scala/game/AIDamageTest.scala
new file mode 100644
index 000000000..5b9af0874
--- /dev/null
+++ b/src/test/scala/game/AIDamageTest.scala
@@ -0,0 +1,32 @@
+// Copyright (c) 2023 PSForever
+package game
+
+import org.specs2.mutable._
+import net.psforever.packet._
+import net.psforever.packet.game._
+import net.psforever.types.PlanetSideGUID
+import scodec.bits._
+
+class AIDamageTest extends Specification {
+ val string1 = hex"5c de10 89e8 38030000 00000000 04020000"
+
+ "decode" in {
+ PacketCoding.decodePacket(string1).require match {
+ case AIDamage(target_guid, attacker_guid, projectile_type, unk1, unk2) =>
+ target_guid mustEqual PlanetSideGUID(4318)
+ attacker_guid mustEqual PlanetSideGUID(59529)
+ projectile_type mustEqual 824L
+ unk1 mustEqual 0L
+ unk2 mustEqual 516L
+ case _ =>
+ ko
+ }
+ }
+
+ "encode" in {
+ val msg = AIDamage(PlanetSideGUID(4318), PlanetSideGUID(59529), 824L, 0L, 516L)
+ val pkt = PacketCoding.encodePacket(msg).require.toByteVector
+
+ pkt mustEqual string1
+ }
+}