diff --git a/src/main/scala/net/psforever/objects/Player.scala b/src/main/scala/net/psforever/objects/Player.scala index 9b7f55181..22b3904b3 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(range = 25f)) interaction(new InteractWithRadiationClouds(range = 10f, Some(this))) private var backpack: Boolean = false diff --git a/src/main/scala/net/psforever/objects/TurretDeployable.scala b/src/main/scala/net/psforever/objects/TurretDeployable.scala index 013092633..923e52532 100644 --- a/src/main/scala/net/psforever/objects/TurretDeployable.scala +++ b/src/main/scala/net/psforever/objects/TurretDeployable.scala @@ -14,7 +14,7 @@ import net.psforever.objects.serverobject.damage.DamageableWeaponTurret 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.turret.{AutomatedTurret, AutomatedTurretBehavior, TurretDefinition, WeaponTurret} import net.psforever.objects.vital.damage.DamageCalculations import net.psforever.objects.vital.interaction.DamageResult import net.psforever.objects.vital.{SimpleResolutions, StandardVehicleResistance} @@ -26,10 +26,11 @@ class TurretDeployable(tdef: TurretDeployableDefinition) extends Deployable(tdef) with WeaponTurret with JammableUnit - with Hackable { + with Hackable + with AutomatedTurret { WeaponTurret.LoadDefinition(this) - override def Definition = tdef + override def Definition: TurretDeployableDefinition = tdef } class TurretDeployableDefinition(private val objectId: Int) @@ -46,7 +47,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)) } } @@ -66,13 +67,15 @@ class TurretControl(turret: TurretDeployable) 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 RepairableWeaponTurret + with AutomatedTurretBehavior { + 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 override def postStop(): Unit = { super.postStop() @@ -88,8 +91,9 @@ class TurretControl(turret: TurretDeployable) .orElse(dismountBehavior) .orElse(takesDamage) .orElse(canBeRepairedByNanoDispenser) + .orElse(automatedTurretBehavior) .orElse { - case _ => ; + case _ => () } override protected def mountTest( @@ -122,7 +126,7 @@ class TurretControl(turret: TurretDeployable) zone.id, VehicleAction.KickPassenger(tplayer.GUID, 4, wasKickedByDriver, turret.GUID) ) - case None => ; + case None => () } } Some(time.getOrElse(Deployable.cleanup) + Deployable.cleanup) diff --git a/src/main/scala/net/psforever/objects/ce/InteractsWithTurrets.scala b/src/main/scala/net/psforever/objects/ce/InteractsWithTurrets.scala new file mode 100644 index 000000000..dea206d7f --- /dev/null +++ b/src/main/scala/net/psforever/objects/ce/InteractsWithTurrets.scala @@ -0,0 +1,62 @@ +// Copyright (c) 2021 PSForever +package net.psforever.objects.ce + +import net.psforever.objects.serverobject.PlanetSideServerObject +import net.psforever.objects.serverobject.turret.{AutomatedTurret, AutomatedTurretBehavior} +import net.psforever.objects.zones.blockmap.SectorPopulation +import net.psforever.objects.zones.{InteractsWithZone, ZoneInteraction, ZoneInteractionType} +import net.psforever.objects.sourcing.SourceEntry +import net.psforever.types.Vector3 + +case object TurretInteraction extends ZoneInteractionType + +/** + * ... + */ +class InteractWithTurrets(val range: Float) + extends ZoneInteraction { + def Type: TurretInteraction.type = TurretInteraction + + /** + * ... + */ + def interaction(sector: SectorPopulation, target: InteractsWithZone): Unit = { + target match { + case clarifiedTarget: AutomatedTurret.Target => + val posxy = clarifiedTarget.Position.xy + val unique = SourceEntry(clarifiedTarget).unique + val targets = getTurretTargets(sector, posxy).filter { turret => turret.Detected(unique).isEmpty } + targets.foreach { t => t.Actor ! AutomatedTurretBehavior.Alert(clarifiedTarget) } + case _ => () + } + } + + private def getTurretTargets( + sector: SectorPopulation, + position: Vector3 + ): List[PlanetSideServerObject with AutomatedTurret] = { + (sector + .deployableList + .collect { + case turret: AutomatedTurret => turret + } ++ sector + .amenityList + .collect { + case turret: AutomatedTurret => turret + }) + .filter { turret => Vector3.DistanceSquared(turret.Position.xy, position) < 625 } + } + + /** + * ... + * @param target na + */ + def resetInteraction(target: InteractsWithZone): Unit = { + getTurretTargets( + target.getInteractionSector(), + target.Position.xy + ).foreach { turret => + turret.Actor ! AutomatedTurretBehavior.ResetAlerts + } + } +} 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/serverobject/turret/AutomatedTurretBehavior.scala b/src/main/scala/net/psforever/objects/serverobject/turret/AutomatedTurretBehavior.scala new file mode 100644 index 000000000..d491e5a18 --- /dev/null +++ b/src/main/scala/net/psforever/objects/serverobject/turret/AutomatedTurretBehavior.scala @@ -0,0 +1,181 @@ +// Copyright (c) 2023 PSForever +package net.psforever.objects.serverobject.turret + +import akka.actor.{Actor, Cancellable} +import net.psforever.objects.definition.ObjectDefinition +import net.psforever.objects.{Default, Player} +import net.psforever.objects.serverobject.PlanetSideServerObject +import net.psforever.objects.sourcing.{SourceEntry, SourceUniqueness} +import net.psforever.objects.vital.Vitality +import net.psforever.packet.game.{ChangeFireStateMessage_Start, ObjectDetectedMessage} +import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} +import net.psforever.types.{PlanetSideGUID, Vector3} + +import scala.collection.mutable +import scala.concurrent.duration._ +import scala.concurrent.ExecutionContext.Implicits.global + +trait AutomatedTurret + extends PlanetSideServerObject + with WeaponTurret { + import AutomatedTurret.Target + private var targets: List[Target] = List[Target]() + + def Targets: List[Target] = targets + + def Detected(target: Target): Option[Target] = { + val unique = SourceEntry(target).unique + targets.find(SourceEntry(_).unique == unique) + } + + def Detected(target: SourceUniqueness): Option[Target] = { + targets.find(SourceEntry(_).unique == target) + } + + def AddTarget(target: Target): Unit = { + targets = targets :+ target + } + + def RemoveTarget(target: Target): Unit = { + val unique = SourceEntry(target).unique + targets = targets.filterNot(SourceEntry(_).unique == unique) + } + + def Clear(): List[Target] = { + val oldTargets = targets + targets = Nil + oldTargets + } + + def Definition: ObjectDefinition with TurretDefinition +} + +object AutomatedTurret { + type Target = PlanetSideServerObject with Vitality +} + +trait AutomatedTurretBehavior { + _: Actor => + import AutomatedTurret.Target + import AutomatedTurretBehavior.TurretTargetEntry + + private val targets: mutable.HashMap[SourceUniqueness, TurretTargetEntry] = + mutable.HashMap[SourceUniqueness, TurretTargetEntry]() + + private var targetDistanceCheck: Cancellable = Default.Cancellable + + def AutomatedTurretObject: AutomatedTurret + + val automatedTurretBehavior: Actor.Receive = { + case AutomatedTurretBehavior.Alert(target) => + val targets = AutomatedTurretObject.Targets + val size = targets.size + AutomatedTurretObject.Detected(target) + .orElse { + AutomatedTurretObject.AddTarget(target) + retimeDistanceCheck(size) + Some(target) + } + .foreach { newDetectedTarget } + + case AutomatedTurretBehavior.Unalert(target) => + val targets = AutomatedTurretObject.Targets + val size = targets.size + AutomatedTurretObject.Detected(target) + .collect { out => + AutomatedTurretObject.RemoveTarget(target) + testDistanceCheckQualifications(size) + out + } + .foreach { noLongerDetectedTarget } + + case AutomatedTurretBehavior.ResetAlerts => + AutomatedTurretObject.Clear().foreach { noLongerDetectedTarget } + testDistanceCheckQualifications(beforeSize = 1) + + case AutomatedTurretBehavior.PeriodicDistanceCheck => + performPeriodicDistanceCheck() + } + + private def newDetectedTarget(target: Target): Unit = { + target match { + case target: Player => + target.Zone.AvatarEvents ! AvatarServiceMessage( + target.Name, + AvatarAction.SendResponse(PlanetSideGUID(0), ObjectDetectedMessage(AutomatedTurretObject.GUID, AutomatedTurretObject.GUID, 0, List(target.GUID))) + ) + target.Zone.AvatarEvents ! AvatarServiceMessage( + target.Name, + AvatarAction.SendResponse(PlanetSideGUID(0), ChangeFireStateMessage_Start(AutomatedTurretObject.Weapons.values.head.Equipment.get.GUID)) + ) + case _ => () + } + } + + private def noLongerDetectedTarget(target: Target): Unit = { + target match { + case target: Player => + target.Zone.AvatarEvents ! AvatarServiceMessage( + target.Name, + AvatarAction.SendResponse(PlanetSideGUID(0), ObjectDetectedMessage(AutomatedTurretObject.GUID, AutomatedTurretObject.GUID, 0, List(PlanetSideGUID(0)))) + ) + case _ => () + } + } + + private def testDistanceCheckQualifications(beforeSize: Int): Unit = { + if (beforeSize > 0 && AutomatedTurretObject.Targets.isEmpty) { + targetDistanceCheck.cancel() + } + } + + private def retimeDistanceCheck(beforeSize: Int): Unit = { + if (beforeSize == 0 && AutomatedTurretObject.Targets.nonEmpty) { + targetDistanceCheck = context.system.scheduler.scheduleAtFixedRate( + 1.second, + 1.second, + self, + AutomatedTurretBehavior.PeriodicDistanceCheck + ) + } + } + + private def performPeriodicDistanceCheck(): List[Target] = { + val pos = AutomatedTurretObject.Position + val earlyTargets = AutomatedTurretObject.Targets + val earlySize = earlyTargets.size + val removedTargets = earlyTargets + .collect { + case t if Vector3.DistanceSquared(t.Position, pos) > 625 => + AutomatedTurretObject.RemoveTarget(t) + t + } + removedTargets.foreach { noLongerDetectedTarget } + testDistanceCheckQualifications(earlySize) + removedTargets + } +} + +object AutomatedTurretBehavior { + import AutomatedTurret.Target + final case class Alert(target: Target) + + final case class Unalert(target: Target) + + final case object ResetAlerts + + private case object PeriodicDistanceCheck + + final case class TurretTargetEntry(target: Target, regard: RegardTargetAs.TurretOpinion) + + object RegardTargetAs { + trait TurretOpinion + + final case object Friendly extends TurretOpinion + final case object Testing extends TurretOpinion + final case object Unreachable extends TurretOpinion + final case object Blocked extends TurretOpinion + final case object Invalid extends TurretOpinion + final case object Attack extends TurretOpinion + } +} 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,