From fc66b43cb5cc6dd84c41146e25defbdb499d63b6 Mon Sep 17 00:00:00 2001 From: Jakob Gillich Date: Fri, 16 Feb 2024 22:08:16 +0100 Subject: [PATCH 1/8] fix logo url --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f6a74e315..ed08b7071 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # PSForever Server [![Build Status](https://travis-ci.org/psforever/PSF-LoginServer.svg?branch=master)](https://travis-ci.com/psforever/PSF-LoginServer) [![Code coverage](https://codecov.io/gh/psforever/PSF-LoginServer/coverage.svg?branch=master)](https://codecov.io/gh/psforever/PSF-LoginServer/) [![Documentation](https://img.shields.io/badge/documentation-master-lightgrey)](https://psforever.github.io/docs/master/index.html) - + Welcome to the recreated login and world servers for PlanetSide 1. We are a community of players and developers who took it upon ourselves to preserve PlanetSide 1's unique gameplay and history _forever_. From 3556a17f3d3211309417b152f2b088ce18d19662 Mon Sep 17 00:00:00 2001 From: ScrawnyRonnie Date: Sat, 2 Mar 2024 07:29:18 -0500 Subject: [PATCH 2/8] access granted --- src/main/resources/zonemaps/map09.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/resources/zonemaps/map09.json b/src/main/resources/zonemaps/map09.json index 0067cc6fa..93d6b648c 100644 --- a/src/main/resources/zonemaps/map09.json +++ b/src/main/resources/zonemaps/map09.json @@ -12566,7 +12566,7 @@ "AbsY": 4508.811, "AbsZ": 62.3072777, "Yaw": 0.0, - "GUID": 1006, + "GUID": 1007, "MapID": null, "IsChildObject": true }, @@ -12579,7 +12579,7 @@ "AbsY": 4508.811, "AbsZ": 82.30728, "Yaw": 0.0, - "GUID": 1007, + "GUID": 1008, "MapID": null, "IsChildObject": true }, @@ -12592,7 +12592,7 @@ "AbsY": 4159.456, "AbsZ": 46.40945, "Yaw": 5.0, - "GUID": 1008, + "GUID": 1006, "MapID": null, "IsChildObject": true }, From f2c486d6f6ef8dca391cc221eca7c8f5b9bb29ca Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Sat, 2 Mar 2024 23:08:59 -0500 Subject: [PATCH 3/8] initial debug draw packet; data for the tests are entirely fabricated as we have no instances of this packet in the wild (#1170) --- .../psforever/packet/GamePacketOpcode.scala | 2 +- .../packet/game/DebugDrawMessage.scala | 32 +++++++++++++++++ .../scala/game/DebugDrawMessageTest.scala | 36 +++++++++++++++++++ 3 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 src/main/scala/net/psforever/packet/game/DebugDrawMessage.scala create mode 100644 src/test/scala/game/DebugDrawMessageTest.scala diff --git a/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/src/main/scala/net/psforever/packet/GamePacketOpcode.scala index a2beebc0e..384458bcb 100644 --- a/src/main/scala/net/psforever/packet/GamePacketOpcode.scala +++ b/src/main/scala/net/psforever/packet/GamePacketOpcode.scala @@ -487,7 +487,7 @@ object GamePacketOpcode extends Enumeration { case 0x99 => noDecoder(EmpireIncentivesMessage) case 0x9a => game.InvalidTerrainMessage.decode case 0x9b => noDecoder(SyncMessage) - case 0x9c => noDecoder(DebugDrawMessage) + case 0x9c => game.DebugDrawMessage.decode case 0x9d => noDecoder(SoulMarkMessage) case 0x9e => noDecoder(UplinkPositionEvent) case 0x9f => game.HotSpotUpdateMessage.decode diff --git a/src/main/scala/net/psforever/packet/game/DebugDrawMessage.scala b/src/main/scala/net/psforever/packet/game/DebugDrawMessage.scala new file mode 100644 index 000000000..309ec88e5 --- /dev/null +++ b/src/main/scala/net/psforever/packet/game/DebugDrawMessage.scala @@ -0,0 +1,32 @@ +// Copyright (c) 2024 PSForever +package net.psforever.packet.game + +import net.psforever.packet.GamePacketOpcode.Type +import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket} +import net.psforever.types.Vector3 +import scodec.bits.BitVector +import scodec.{Attempt, Codec} +import scodec.codecs._ + +final case class DebugDrawMessage( + unk1: Int, + unk2: Long, + unk3: Long, + unk4: Long, + unk5: List[Vector3] + ) + extends PlanetSideGamePacket { + type Packet = DebugDrawMessage + def opcode: Type = GamePacketOpcode.DebugDrawMessage + def encode: Attempt[BitVector] = DebugDrawMessage.encode(this) +} + +object DebugDrawMessage extends Marshallable[DebugDrawMessage] { + implicit val codec: Codec[DebugDrawMessage] = ( + ("unk1" | uint(bits = 3)) :: + ("unk2" | ulongL(bits = 32)) :: + ("unk3" | ulongL(bits = 32)) :: + ("unk4" | ulongL(bits = 32)) :: + ("unk5" | listOfN(uint2, Vector3.codec_pos)) + ).as[DebugDrawMessage] +} diff --git a/src/test/scala/game/DebugDrawMessageTest.scala b/src/test/scala/game/DebugDrawMessageTest.scala new file mode 100644 index 000000000..c554ef953 --- /dev/null +++ b/src/test/scala/game/DebugDrawMessageTest.scala @@ -0,0 +1,36 @@ +// Copyright (c) 2024 PSForever +package game + +import net.psforever.packet.PacketCoding +import net.psforever.packet.game.DebugDrawMessage +import net.psforever.types.Vector3 +import org.specs2.mutable.Specification +import scodec.bits._ + +class DebugDrawMessageTest extends Specification { + val string = hex"9c2040000000600000008000001c0010000186000800c8000f04008807d00016080578" + + "decode" in { + PacketCoding.decodePacket(string).require match { + case DebugDrawMessage(u1, u2, u3, u4, u5) => + u1 mustEqual 1 + u2 mustEqual 2L + u3 mustEqual 3L + u4 mustEqual 4L + u5 mustEqual List( + Vector3(5,6,7), + Vector3(50,60,70), + Vector3(500,600,700) + ) + case _ => + ko + } + } + + "encode" in { + val msg = DebugDrawMessage(1, 2L, 3L, 4L, List(Vector3(5,6,7), Vector3(50,60,70), Vector3(500,600,700))) + val pkt = PacketCoding.encodePacket(msg).require.toByteVector + + pkt mustEqual string + } +} From 9ed39c6e2fe90573cbccf4cc60d52010875c7034 Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Sat, 2 Mar 2024 23:09:15 -0500 Subject: [PATCH 4/8] driver as the killer, not the vehicle (#1171) --- src/main/scala/net/psforever/objects/SpecialEmp.scala | 4 ++-- .../objects/vehicles/control/VehicleCapacitance.scala | 4 ++-- .../net/psforever/objects/vital/etc/EmpReason.scala | 10 +++++++--- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/main/scala/net/psforever/objects/SpecialEmp.scala b/src/main/scala/net/psforever/objects/SpecialEmp.scala index 17da3b464..5578690af 100644 --- a/src/main/scala/net/psforever/objects/SpecialEmp.scala +++ b/src/main/scala/net/psforever/objects/SpecialEmp.scala @@ -118,8 +118,8 @@ object SpecialEmp { case _ => OwnerGuid_=(Some(owner.GUID)) } Position = position - def Faction = faction - def Definition = proxy_definition + def Faction: PlanetSideEmpire.Value = faction + def Definition: ObjectDefinition with VitalityDefinition = proxy_definition }) } diff --git a/src/main/scala/net/psforever/objects/vehicles/control/VehicleCapacitance.scala b/src/main/scala/net/psforever/objects/vehicles/control/VehicleCapacitance.scala index bb2ca9374..2b84932a9 100644 --- a/src/main/scala/net/psforever/objects/vehicles/control/VehicleCapacitance.scala +++ b/src/main/scala/net/psforever/objects/vehicles/control/VehicleCapacitance.scala @@ -1,7 +1,7 @@ // Copyright (c) 2021 PSForever package net.psforever.objects.vehicles.control -import akka.actor.Actor +import akka.actor.{Actor, Cancellable} import net.psforever.objects._ import net.psforever.services.Service import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} @@ -16,7 +16,7 @@ trait VehicleCapacitance { _: Actor => def CapacitanceObject: Vehicle - protected var capacitor = Default.Cancellable + protected var capacitor: Cancellable = Default.Cancellable startCapacitorTimer() diff --git a/src/main/scala/net/psforever/objects/vital/etc/EmpReason.scala b/src/main/scala/net/psforever/objects/vital/etc/EmpReason.scala index 7269fab5d..f62b5a135 100644 --- a/src/main/scala/net/psforever/objects/vital/etc/EmpReason.scala +++ b/src/main/scala/net/psforever/objects/vital/etc/EmpReason.scala @@ -1,9 +1,9 @@ // Copyright (c) 2021 PSForever package net.psforever.objects.vital.etc -import net.psforever.objects.PlanetSideGameObject +import net.psforever.objects.{PlanetSideGameObject, Vehicle} import net.psforever.objects.serverobject.affinity.FactionAffinity -import net.psforever.objects.sourcing.SourceEntry +import net.psforever.objects.sourcing.{SourceEntry, VehicleSource} import net.psforever.objects.vital.Vitality import net.psforever.objects.vital.base.{DamageReason, DamageResolution} import net.psforever.objects.vital.prop.DamageWithPosition @@ -41,6 +41,10 @@ object EmpReason { source: DamageWithPosition, target: PlanetSideGameObject with Vitality ): EmpReason = { - EmpReason(SourceEntry(owner), source, target.DamageModel, owner.Definition.ObjectId) + val ownerSource = owner match { + case v: Vehicle => VehicleSource(v).occupants.head + case _ => SourceEntry(owner) + } + EmpReason(ownerSource, source, target.DamageModel, owner.Definition.ObjectId) } } From 34ac1e526672e1589ae4c82648e210a9b0205fa7 Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Sat, 2 Mar 2024 23:10:52 -0500 Subject: [PATCH 5/8] Loadout Item Issues (#1172) * issue where contents of inventory did not match loadout specifications of the exo-suit to which the player was switching * accidentally using the loadout change set armor in exo-suit change set armor; reverted --- .../support/SessionAvatarHandlers.scala | 1 + .../scala/net/psforever/objects/Players.scala | 33 ++- .../objects/avatar/PlayerControl.scala | 205 ++++++++---------- .../objects/inventory/InventoryItem.scala | 2 + 4 files changed, 111 insertions(+), 130 deletions(-) diff --git a/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala b/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala index 26276cbe1..b3aefad4d 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionAvatarHandlers.scala @@ -350,6 +350,7 @@ class SessionAvatarHandlers( sendResponse(ObjectDeleteMessage(objGuid, unk1=0)) TaskWorkflow.execute(GUIDTask.unregisterEquipment(continent.GUID, obj)) } + drops.foreach(item => sendResponse(ObjectDeleteMessage(item.obj.GUID, unk1=0))) //redraw if (maxhand) { TaskWorkflow.execute(HoldNewEquipmentUp(player)( diff --git a/src/main/scala/net/psforever/objects/Players.scala b/src/main/scala/net/psforever/objects/Players.scala index 9a0993ef1..ae57870cc 100644 --- a/src/main/scala/net/psforever/objects/Players.scala +++ b/src/main/scala/net/psforever/objects/Players.scala @@ -110,25 +110,34 @@ object Players { * For any slots that are not yet occupied by an item, search through the `List` and find an item that fits in that slot. * Add that item to the slot and remove it from the list. * @param iter the `Iterator` of `EquipmentSlot`s - * @param list a `List` of all `Equipment` that is not yet assigned to a holster slot or an inventory slot - * @return the `List` of all `Equipment` not yet assigned to a holster slot or an inventory slot + * @param list a list of all `Equipment` not assigned to a new slot + * @param slotNum current slot index associated with the value extracted from `iter` param + * @param placedList a list of all `Equipment` reassigned to a slot + * @return two lists: + * all `Equipment` reassigned to a slot, and + * all `Equipment` not assigned to a new slot */ - @tailrec def fillEmptyHolsters(iter: Iterator[EquipmentSlot], list: List[InventoryItem]): List[InventoryItem] = { + @tailrec def fillEmptyHolsters( + iter: Iterator[EquipmentSlot], + list: List[InventoryItem], + slotNum: Int = 0, + placedList: List[InventoryItem] = Nil + ): (List[InventoryItem], List[InventoryItem]) = { if (!iter.hasNext) { - list + (placedList, list) } else { val slot = iter.next() if (slot.Equipment.isEmpty) { - list.find(item => item.obj.Size == slot.Size) match { - case Some(obj) => - val index = list.indexOf(obj) - slot.Equipment = obj.obj - fillEmptyHolsters(iter, list.take(index) ++ list.drop(index + 1)) - case None => - fillEmptyHolsters(iter, list) + list.indexWhere(item => item.obj.Size == slot.Size) match { + case -1 => + fillEmptyHolsters(iter, list, slotNum + 1, placedList) + case index => + val entry = list(index) + entry.start = slotNum + fillEmptyHolsters(iter, list.take(index) ++ list.drop(index + 1), slotNum + 1, placedList :+ entry) } } else { - fillEmptyHolsters(iter, list) + fillEmptyHolsters(iter, list, slotNum + 1, placedList) } } } diff --git a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala index 73e20dd69..002475f49 100644 --- a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala +++ b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala @@ -381,26 +381,8 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm log.info(s"${player.Name} wants to change equipment loadout to their option #${msg.unk1 + 1}") val originalSuit = player.ExoSuit val originalSubtype = Loadout.DetermineSubtype(player) - //sanitize exo-suit for change val dropPred = ContainableBehavior.DropPredicate(player) - val oldHolsters = Players.clearHolsters(player.Holsters().iterator) - val dropHolsters = oldHolsters.filter(dropPred) - val oldInventory = player.Inventory.Clear() - val dropInventory = oldInventory.filter(dropPred) - val toDeleteOrDrop : List[InventoryItem] = (player.FreeHand.Equipment match { - case Some(obj) => - val out = InventoryItem(obj, -1) - player.FreeHand.Equipment = None - if (dropPred(out)) { - List(out) - } else { - Nil - } - case _ => - Nil - }) ++ dropHolsters ++ dropInventory - //a loadout with a prohibited exo-suit type will result in the fallback exo-suit type - //imposed 5min delay on mechanized exo-suit switches + //determine player's next exo-suit val (nextSuit, nextSubtype) = { lazy val fallbackSuit = if (Players.CertificationToUseExoSuit(player, originalSuit, originalSubtype)) { //TODO will we ever need to check for the cooldown status of an original non-MAX exo-suit? @@ -430,20 +412,30 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm fallbackSuit } } - if (nextSuit == ExoSuitType.MAX) { - player.ResistArmMotion(PlayerControl.maxRestriction) - } else { - player.ResistArmMotion(Player.neverRestrict) + //sanitize current exo-suit for change + val (dropHolsters, oldHolsters) = Players.clearHolsters(player.Holsters().iterator).partition(dropPred) + val (dropInventory, oldInventory) = player.Inventory.Clear().partition(dropPred) + val (dropHand, deleteHand) = player.FreeHand.Equipment match { + case Some(obj) => + val out = InventoryItem(obj, -1) + player.FreeHand.Equipment = None + if (dropPred(out)) { + (List(out), Nil) + } else { + (Nil, List(out)) + } + case _ => + (Nil, Nil) } - //sanitize (incoming) inventory - //TODO equipment permissions; these loops may be expanded upon in future - val curatedHolsters = for { + //these dropped items exist and must be accounted for + val itemsToDrop = dropHand ++ dropHolsters ++ dropInventory + val newHolsters = for { item <- holsters //id = item.obj.Definition.ObjectId //lastTime = player.GetLastUsedTime(id) if true } yield item - val curatedInventory = for { + val newInventory = for { item <- inventory //id = item.obj.Definition.ObjectId //lastTime = player.GetLastUsedTime(id) @@ -461,60 +453,55 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm player.Armor = originalArmor } } - //ensure arm is down, even if it needs to go back up - if (player.DrawnSlot != Player.HandsDownSlot) { - player.DrawnSlot = Player.HandsDownSlot - } - //a change due to exo-suit permissions mismatch will result in (more) items being re-arranged and/or dropped - //dropped items are not registered and can just be forgotten - val (afterHolsters, afterInventory) = if (nextSuit == exosuit) { - ( - //melee slot preservation for MAX - if (nextSuit == ExoSuitType.MAX) { - holsters.filter(_.start == 4) - } else { - curatedHolsters.filterNot(dropPred) - }, - curatedInventory.filterNot(dropPred) - ) + val (afterHolsters, afterInventory) = if (exosuit == nextSuit) { + //proposed loadout inventory matched the projected exo-suit selection + if (nextSuit == ExoSuitType.MAX) { + //loadout for a MAX + player.ResistArmMotion(PlayerControl.maxRestriction) + player.DrawnSlot = Player.HandsDownSlot + (newHolsters.filter(_.start == 4), newInventory.filterNot(dropPred)) + } else { + //loadout for a vanilla exo-suit + player.ResistArmMotion(Player.neverRestrict) + (newHolsters.filterNot(dropPred), newInventory.filterNot(dropPred)) + } } else { - //our exo-suit type was hijacked by changing permissions; we shouldn't even be able to use that loadout(!) - //holsters - val leftoversForInventory = Players.fillEmptyHolsters( + //proposed loadout conforms to a different inventory layout than the projected exo-suit + player.ResistArmMotion(Player.neverRestrict) + //holsters (matching holsters will be inserted, the rest will deposited into the inventory) + val (finalHolsters, leftoversForInventory) = Players.fillEmptyHolsters( player.Holsters().iterator, - (curatedHolsters ++ curatedInventory).filterNot(dropPred) + (newHolsters.filterNot(_.obj.Size == EquipmentSize.Max) ++ newInventory).filterNot(dropPred) ) - val finalHolsters = player.HolsterItems() - //inventory + //inventory (items will be placed to accommodate the change, or dropped) val (finalInventory, _) = GridInventory.recoverInventory(leftoversForInventory, player.Inventory) (finalHolsters, finalInventory) } (afterHolsters ++ afterInventory).foreach { entry => entry.obj.Faction = player.Faction } - afterHolsters.foreach { + afterHolsters.collect { case InventoryItem(citem: ConstructionItem, _) => Deployables.initializeConstructionItem(player.avatar.certifications, citem) - case _ => ; } - toDeleteOrDrop.foreach { entry => entry.obj.Faction = PlanetSideEmpire.NEUTRAL } //deactivate non-passive implants avatarActor ! AvatarActor.DeactivateActiveImplants() - player.Zone.AvatarEvents ! AvatarServiceMessage( - player.Zone.id, + val zone = player.Zone + zone.AvatarEvents ! AvatarServiceMessage( + zone.id, AvatarAction.ChangeLoadout( player.GUID, toArmor, nextSuit, nextSubtype, player.LastDrawnSlot, - exosuit == ExoSuitType.MAX, + nextSuit == ExoSuitType.MAX, oldHolsters.map { case InventoryItem(obj, _) => (obj, obj.GUID) }, afterHolsters, - oldInventory.map { case InventoryItem(obj, _) => (obj, obj.GUID) }, + (oldInventory ++ deleteHand).map { case InventoryItem(obj, _) => (obj, obj.GUID) }, afterInventory, - toDeleteOrDrop + itemsToDrop ) ) - player.Zone.AvatarEvents ! AvatarServiceMessage( + zone.AvatarEvents ! AvatarServiceMessage( player.Name, AvatarAction.TerminalOrderResult(msg.terminal_guid, msg.transaction_type, result=true) ) @@ -617,30 +604,31 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm } def setExoSuit(exosuit: ExoSuitType.Value, subtype: Int): Boolean = { - var toDelete : List[InventoryItem] = Nil + val willBecomeMax = exosuit == ExoSuitType.MAX val originalSuit = player.ExoSuit val originalSubtype = Loadout.DetermineSubtype(player) - val requestToChangeArmor = originalSuit != exosuit || originalSubtype != subtype - val allowedToChangeArmor = Players.CertificationToUseExoSuit(player, exosuit, subtype) && - (if (exosuit == ExoSuitType.MAX) { + val changeSuit = originalSuit != exosuit + val changeSubtype = originalSubtype != subtype + val doChangeArmor = (changeSuit || changeSubtype) && + Players.CertificationToUseExoSuit(player, exosuit, subtype) && + (if (willBecomeMax) { val weapon = GlobalDefinitions.MAXArms(subtype, player.Faction) - player.avatar.purchaseCooldown(weapon) match { - case Some(_) => - false - case None => + player.avatar.purchaseCooldown(weapon) + .collect(_ => false) + .getOrElse { avatarActor ! AvatarActor.UpdatePurchaseTime(weapon) true - } + } } else { true }) - if (requestToChangeArmor && allowedToChangeArmor) { + if (doChangeArmor) { log.info(s"${player.Name} wants to change to a different exo-suit - $exosuit") val beforeHolsters = Players.clearHolsters(player.Holsters().iterator) val beforeInventory = player.Inventory.Clear() - //change suit + //update suit internally val originalArmor = player.Armor - player.ExoSuit = exosuit //changes the value of MaxArmor to reflect the new exo-suit + player.ExoSuit = exosuit val toMaxArmor = player.MaxArmor val toArmor = toMaxArmor if (originalSuit != exosuit || originalArmor != toMaxArmor) { @@ -651,50 +639,32 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm if (player.DrawnSlot != Player.HandsDownSlot) { player.DrawnSlot = Player.HandsDownSlot } - val normalHolsters = if (originalSuit == ExoSuitType.MAX) { - val (maxWeapons, normalWeapons) = beforeHolsters.partition(elem => elem.obj.Size == EquipmentSize.Max) - toDelete ++= maxWeapons - normalWeapons - } - else { - beforeHolsters - } - //populate holsters - val (afterHolsters, finalInventory) = if (exosuit == ExoSuitType.MAX) { - ( - normalHolsters, - Players.fillEmptyHolsters(List(player.Slot(4)).iterator, normalHolsters) ++ beforeInventory - ) - } - else if (originalSuit == exosuit) { //note - this will rarely be the situation - (normalHolsters, Players.fillEmptyHolsters(player.Holsters().iterator, normalHolsters)) - } - else { - val (afterHolsters, toInventory) = - normalHolsters.partition(elem => elem.obj.Size == player.Slot(elem.start).Size) - afterHolsters.foreach({ elem => player.Slot(elem.start).Equipment = elem.obj }) - val remainder = Players.fillEmptyHolsters(player.Holsters().iterator, toInventory ++ beforeInventory) - ( - player.HolsterItems(), - remainder - ) - } - //put items back into inventory - val (stow, drop) = if (originalSuit == exosuit) { - (finalInventory, Nil) - } - else { - val (a, b) = GridInventory.recoverInventory(finalInventory, player.Inventory) - ( - a, - b.map { - InventoryItem(_, -1) - } - ) - } - stow.foreach { elem => - player.Inventory.InsertQuickly(elem.start, elem.obj) + val (toDelete, toDrop, afterHolsters, afterInventory) = if (originalSuit == ExoSuitType.MAX) { + //was max + val (delete, insert) = beforeHolsters.partition(elem => elem.obj.Size == EquipmentSize.Max) + if (willBecomeMax) { + //changing to a different kind(?) of max + (delete, Nil, insert, beforeInventory) + } else { + //changing to a vanilla exo-suit + val (newHolsters, unplacedHolsters) = Players.fillEmptyHolsters(player.Holsters().iterator, insert ++ beforeInventory) + val (inventory, unplacedInventory) = GridInventory.recoverInventory(unplacedHolsters, player.Inventory) + (delete, unplacedInventory.map(InventoryItem(_, -1)), newHolsters, inventory) + } + } else if (willBecomeMax) { + //will be max, drop everything but melee slot + val (melee, other) = beforeHolsters.partition(elem => elem.obj.Size == EquipmentSize.Melee) + val (inventory, unplacedInventory) = GridInventory.recoverInventory(beforeInventory ++ other, player.Inventory) + (Nil, unplacedInventory.map(InventoryItem(_, -1)), melee, inventory) + } else { + //was not a max nor will become a max; vanilla exo-suit to a vanilla-exo-suit + val (insert, unplacedHolsters) = Players.fillEmptyHolsters(player.Holsters().iterator, beforeHolsters ++ beforeInventory) + val (inventory, unplacedInventory) = GridInventory.recoverInventory(unplacedHolsters, player.Inventory) + (Nil, unplacedInventory.map(InventoryItem(_, -1)), insert, inventory) } + //insert + afterHolsters.foreach(elem => player.Slot(elem.start).Equipment = elem.obj) + afterInventory.foreach(elem => player.Inventory.InsertQuickly(elem.start, elem.obj)) //deactivate non-passive implants avatarActor ! AvatarActor.DeactivateActiveImplants() player.Zone.AvatarEvents ! AvatarServiceMessage( @@ -705,18 +675,17 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm exosuit, subtype, player.LastDrawnSlot, - exosuit == ExoSuitType.MAX && requestToChangeArmor, + willBecomeMax, beforeHolsters.map { case InventoryItem(obj, _) => (obj, obj.GUID) }, afterHolsters, beforeInventory.map { case InventoryItem(obj, _) => (obj, obj.GUID) }, - stow, - drop, + afterInventory, + toDrop, toDelete.map { case InventoryItem(obj, _) => (obj, obj.GUID) } ) ) true - } - else { + } else { false } } diff --git a/src/main/scala/net/psforever/objects/inventory/InventoryItem.scala b/src/main/scala/net/psforever/objects/inventory/InventoryItem.scala index 60dd86c6c..ee7f722be 100644 --- a/src/main/scala/net/psforever/objects/inventory/InventoryItem.scala +++ b/src/main/scala/net/psforever/objects/inventory/InventoryItem.scala @@ -14,6 +14,8 @@ import net.psforever.types.PlanetSideGUID class InventoryItem(val obj: Equipment, var start: Int = 0) { //TODO eventually move this object from storing the item directly to just storing its GUID? def GUID: PlanetSideGUID = obj.GUID + + override def toString: String = s"InventoryItem(${obj.Definition.Name}-$start)" } object InventoryItem { From 44f1560a94fdf3bb92f96a11209fc9078c9b44f3 Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Sat, 2 Mar 2024 23:12:28 -0500 Subject: [PATCH 6/8] Battle Island Facility Names (#1173) * alias the internal facility names to their continental map names (battle islands) * capturebase, but easier to follow * sraosha(whitespace) is no longer with (whitespace) --- .../psforever/actors/session/ChatActor.scala | 351 +++++++++++------- .../session/support/ZoningOperations.scala | 1 - .../psforever/objects/zones/ZoneInfo.scala | 65 +++- 3 files changed, 257 insertions(+), 160 deletions(-) diff --git a/src/main/scala/net/psforever/actors/session/ChatActor.scala b/src/main/scala/net/psforever/actors/session/ChatActor.scala index 61aae9c29..bc6e96e26 100644 --- a/src/main/scala/net/psforever/actors/session/ChatActor.scala +++ b/src/main/scala/net/psforever/actors/session/ChatActor.scala @@ -7,9 +7,11 @@ import akka.actor.typed.scaladsl.{ActorContext, Behaviors, StashBuffer} import akka.actor.typed.scaladsl.adapter._ import net.psforever.actors.zone.ZoneActor import net.psforever.objects.sourcing.PlayerSource +import net.psforever.objects.zones.ZoneInfo import net.psforever.services.local.{LocalAction, LocalServiceMessage} -import scala.collection.mutable +import scala.annotation.unused +import scala.collection.{Seq, mutable} import scala.concurrent.ExecutionContextExecutor import scala.concurrent.duration._ // @@ -204,7 +206,8 @@ class ChatActor( Behaviors .receiveMessagePartial[Command] { case SetSession(newSession) => - active(newSession, chatService,cluster) + this.session = Some(newSession) + active(newSession, chatService, cluster) case JoinChannel(channel) => chatService ! ChatService.JoinChannel(chatServiceAdapter, session, channel) @@ -398,133 +401,114 @@ class ChatActor( case (U_CMT_ZONEROTATE, _, contents) if gmCommandAllowed => cluster ! InterstellarClusterService.CavernRotation(CavernRotationService.HurryNextRotation) - /** Messages starting with ! are custom chat commands */ - case (_, _, contents) if contents.startsWith("!") && - customCommandMessages(message, session, chatService, cluster, gmCommandAllowed) => ; - case (CMT_CAPTUREBASE, _, contents) if gmCommandAllowed => - val args = contents.split(" ").filter(_ != "") - val (faction, factionPos): (PlanetSideEmpire.Value, Option[Int]) = args.zipWithIndex - .map { case (factionName, pos) => (factionName.toLowerCase, pos) } - .flatMap { - case ("tr", pos) => Some(PlanetSideEmpire.TR, pos) - case ("nc", pos) => Some(PlanetSideEmpire.NC, pos) - case ("vs", pos) => Some(PlanetSideEmpire.VS, pos) - case ("none", pos) => Some(PlanetSideEmpire.NEUTRAL, pos) - case ("bo", pos) => Some(PlanetSideEmpire.NEUTRAL, pos) - case ("neutral", pos) => Some(PlanetSideEmpire.NEUTRAL, pos) - case _ => None - } - .headOption match { - case Some((isFaction, pos)) => (isFaction, Some(pos)) - case None => (session.player.Faction, None) - } - val (buildingsOption, buildingPos): (Option[Seq[Building]], Option[Int]) = args.zipWithIndex.flatMap { - case (_, pos) if factionPos.isDefined && factionPos.get == pos => None - case ("all", pos) => - Some( - Some( - session.zone.Buildings - .filter { - case (_, building) => building.CaptureTerminal.isDefined - } - .values - .toSeq - ), - Some(pos) - ) - case (name: String, pos) => - session.zone.Buildings.find { - case (_, building) => name.equalsIgnoreCase(building.Name) && building.CaptureTerminal.isDefined - } match { - case Some((_, building)) => Some(Some(Seq(building)), Some(pos)) - case None => - try { - // check if we have a timer - name.toInt + val buffer = contents.split(" ").filterNot(_ == "").take(3) + //walk through the param buffer + val (foundFacilities, foundFacilitiesTag, factionBuffer) = firstParam(session, buffer, captureBaseParamFacilities) + val (foundFaction, foundFactionTag, timerBuffer) = firstParam(session, factionBuffer, captureBaseParamFaction) + val (foundTimer, foundTimerTag, _) = firstParam(session, timerBuffer, captureBaseParamTimer) + //resolve issues with the initial params + var facilityError: Int = 0 + var factionError: Boolean = false + var timerError: Boolean = false + var usageMessage: Boolean = false + val resolvedFacilities = foundFacilities + .orElse { + if (foundFacilitiesTag.nonEmpty) { + if (foundFaction.isEmpty) { + /* /capturebase OR /capturebase */ + //malformed facility tag error + facilityError = 2 + None + } else if (!foundFacilitiesTag.contains("curr")) { //did we do this next check already + /* /capturebase , potentially */ + val buildings = captureBaseCurrSoi(session) + if (buildings.nonEmpty) { + //convert facilities to faction + Some(buildings.toSeq) + } else { + //no facilities error + facilityError = 1 None - } catch { - case _: Throwable => - Some(None, Some(pos)) } - } - }.headOption match { - case Some((buildings, pos)) => (buildings, pos) - case None => (None, None) - } - val (timerOption, timerPos): (Option[Int], Option[Int]) = args.zipWithIndex.flatMap { - case (_, pos) - if factionPos.isDefined && factionPos.get == pos || buildingPos.isDefined && buildingPos.get == pos => - None - case (timer: String, pos) => - try { - val t = timer.toInt // TODO what is the timer format supposed to be? - Some(Some(t), Some(pos)) - } catch { - case _: Throwable => - Some(None, Some(pos)) - } - }.headOption match { - case Some((timer, posOption)) => (timer, posOption) - case None => (None, None) - } - - (factionPos, buildingPos, timerPos, buildingsOption, timerOption) match { - case // [[|none []] - (Some(0), None, Some(1), None, Some(_)) | (Some(0), None, None, None, None) | - (None, None, None, None, None) | - // [ [|none [timer]]] - (None | Some(1), Some(0), None, Some(_), None) | (Some(1), Some(0), Some(2), Some(_), Some(_)) | - // [all [|none]] - (Some(1) | None, Some(0), None, Some(_), None) => - val buildings: Seq[Building] = buildingsOption.getOrElse( - session.zone.Buildings.values.filter { building => - building.PlayersInSOI.exists { soiPlayer => - session.player.CharId == soiPlayer.CharId - } - }.toSeq - ) - buildings foreach { building => - // TODO implement timer - - val terminal = building.CaptureTerminal.get - - building.Actor ! BuildingActor.SetFaction(faction) - building.Actor ! BuildingActor.AmenityStateChange(terminal, Some(false)) - - // clear any previous hack via "resecure" - if (building.CaptureTerminalIsHacked) { - building.Zone.LocalEvents ! LocalServiceMessage(terminal.Zone.id,LocalAction.ResecureCaptureTerminal(terminal, PlayerSource.Nobody)) + } else { + //no facilities error + facilityError = 1 + None } - - // push any updates this might cause to clients - building.Zone.actor ! ZoneActor.ZoneMapUpdate() + } else { + //no params; post command usage reminder + usageMessage = true + None } - - case (_, Some(0), _, None, _) => - sessionActor ! SessionActor.SendResponse( - ChatMsg( - UNK_229, - wideContents=true, - "", - s"\\#FF4040ERROR - \'${args(0)}\' is not a valid building name.", + } + val resolvedFaction = foundFaction + .orElse { + if (resolvedFacilities.nonEmpty) { + /* /capturebase OR /capturebase */ + if (foundFactionTag.isEmpty || foundTimer.nonEmpty) { + //convert facilities to OUR PLAYER'S faction + Some(session.player.Faction) + } else { + //malformed faction tag error + factionError = true None - ) - ) - case (Some(0), _, Some(1), _, None) | (Some(1), Some(0), Some(2), _, None) => - sessionActor ! SessionActor.SendResponse( - ChatMsg( - UNK_229, - wideContents=true, - "", - s"\\#FF4040ERROR - \'${args(timerPos.get)}\' is not a valid timer value.", - None - ) - ) + } + } else { + //incorrect params; already posted an error message + None + } + } + val resolvedTimer = foundTimer + .orElse { + //todo stop command execution? post command usage reminder? + if (resolvedFaction.nonEmpty && foundTimerTag.nonEmpty) { + /* /capturebase */ + //malformed timer tag error + timerError = true + None + } else { + //eh + Some(1) + } + } + //evaluate results + (resolvedFacilities, resolvedFaction, resolvedTimer) match { + case (Some(buildings), Some(faction), Some(_)) => + buildings.foreach { building => + //TODO implement timer + val terminal = building.CaptureTerminal.get + val zone = building.Zone + val zoneActor = zone.actor + val buildingActor = building.Actor + //clear any previous hack + if (building.CaptureTerminalIsHacked) { + zone.LocalEvents ! LocalServiceMessage( + zone.id, + LocalAction.ResecureCaptureTerminal(terminal, PlayerSource.Nobody) + ) + } + //push any updates this might cause + zoneActor ! ZoneActor.ZoneMapUpdate() + //convert faction affiliation + buildingActor ! BuildingActor.SetFaction(faction) + buildingActor ! BuildingActor.AmenityStateChange(terminal, Some(false)) + //push for map updates again + zoneActor ! ZoneActor.ZoneMapUpdate() + } case _ => - sessionActor ! SessionActor.SendResponse( - message.copy(messageType = UNK_229, contents = "@CMT_CAPTUREBASE_usage") - ) + if (usageMessage) { + sessionActor ! SessionActor.SendResponse( + message.copy(messageType = UNK_229, contents = "@CMT_CAPTUREBASE_usage") + ) + } else { + val msg = if (facilityError == 1) { "can not contextually determine building target" } + else if (facilityError == 2) { s"\'${foundFacilitiesTag.get}\' is not a valid building name" } + else if (factionError) { s"\'${foundFactionTag.get}\' is not a valid faction designation" } + else if (timerError) { s"\'${foundTimerTag.get}\' is not a valid timer value" } + else { "malformed params; check usage" } + sessionActor ! SessionActor.SendResponse(ChatMsg(UNK_229, wideContents=true, "", s"\\#FF4040ERROR - $msg", None)) + } } case (CMT_GMBROADCAST | CMT_GMBROADCAST_NC | CMT_GMBROADCAST_VS | CMT_GMBROADCAST_TR, _, _) @@ -549,26 +533,6 @@ class ChatActor( ChatChannel.Default() ) - case (_, "tr", contents) => - sessionActor ! SessionActor.SendResponse( - ZonePopulationUpdateMessage(4, 414, 138, contents.toInt, 138, contents.toInt / 2, 138, 0, 138, 0) - ) - - case (_, "nc", contents) => - sessionActor ! SessionActor.SendResponse( - ZonePopulationUpdateMessage(4, 414, 138, 0, 138, contents.toInt, 138, contents.toInt / 3, 138, 0) - ) - - case (_, "vs", contents) => - sessionActor ! SessionActor.SendResponse( - ZonePopulationUpdateMessage(4, 414, 138, contents.toInt * 2, 138, 0, 138, contents.toInt, 138, 0) - ) - - case (_, "bo", contents) => - sessionActor ! SessionActor.SendResponse( - ZonePopulationUpdateMessage(4, 414, 138, 0, 138, 0, 138, 0, 138, contents.toInt) - ) - case (CMT_OPEN, _, _) if !session.player.silenced => chatService ! ChatService.Message( session, @@ -915,6 +879,30 @@ class ChatActor( ) } + case (_, "tr", contents) => + sessionActor ! SessionActor.SendResponse( + ZonePopulationUpdateMessage(4, 414, 138, contents.toInt, 138, contents.toInt / 2, 138, 0, 138, 0) + ) + + case (_, "nc", contents) => + sessionActor ! SessionActor.SendResponse( + ZonePopulationUpdateMessage(4, 414, 138, 0, 138, contents.toInt, 138, contents.toInt / 3, 138, 0) + ) + + case (_, "vs", contents) => + sessionActor ! SessionActor.SendResponse( + ZonePopulationUpdateMessage(4, 414, 138, contents.toInt * 2, 138, 0, 138, contents.toInt, 138, 0) + ) + + case (_, "bo", contents) => + sessionActor ! SessionActor.SendResponse( + ZonePopulationUpdateMessage(4, 414, 138, 0, 138, 0, 138, 0, 138, contents.toInt) + ) + + /** Messages starting with ! are custom chat commands */ + case (_, _, contents) if contents.startsWith("!") && + customCommandMessages(message, session, chatService, cluster, gmCommandAllowed) => ; + case _ => log.warn(s"Unhandled chat message $message") } @@ -1386,4 +1374,83 @@ class ChatActor( false } } + + private def captureBaseParamFacilities(session: Session, token: Option[String]): Option[Seq[Building]] = { + token.collect { + case "curr" => + val list = captureBaseCurrSoi(session) + if (list.nonEmpty) { + Some(list.toSeq) + } else { + None + } + case "all" => + val list = session.zone.Buildings.values.filter(_.CaptureTerminal.isDefined) + if (list.nonEmpty) { + Some(list.toSeq) + } else { + None + } + case name => + val trueName = ZoneInfo + .values + .find(_.id.equals(session.zone.id)) + .flatMap { info => + info.aliases + .facilities + .collectFirst { case (key, internalName) if key.equalsIgnoreCase(name) => internalName } + } + .getOrElse(name) + session.zone.Buildings + .values + .find { + building => trueName.equalsIgnoreCase(building.Name) && building.CaptureTerminal.isDefined + } + .map(b => Seq(b)) + } + .flatten + } + + private def captureBaseCurrSoi(session: Session): Iterable[Building] = { + val charId = session.player.CharId + session.zone.Buildings.values.filter { building => + building.PlayersInSOI.exists(_.CharId == charId) + } + } + + private def captureBaseParamFaction(@unused session: Session, token: Option[String]): Option[PlanetSideEmpire.Value] = { + token.collect { + case faction => + faction.toLowerCase() match { + case "tr" => Some(PlanetSideEmpire.TR) + case "nc" => Some(PlanetSideEmpire.NC) + case "vs" => Some(PlanetSideEmpire.VS) + case "none" => Some(PlanetSideEmpire.NEUTRAL) + case "bo" => Some(PlanetSideEmpire.NEUTRAL) + case "neutral" => Some(PlanetSideEmpire.NEUTRAL) + case _ => None + } + }.flatten + } + + private def captureBaseParamTimer(@unused session: Session, token: Option[String]): Option[Int] = { + token.collect { + case n if n.forall(Character.isDigit) => n.toInt + } + } + + private def firstParam[T]( + session: Session, + buffer: Array[String], + func: (Session, Option[String])=>Option[T] + ): (Option[T], Option[String], Array[String]) = { + val tokenOpt = buffer.headOption + val valueOpt = func(session, tokenOpt) + val outBuffer = if (valueOpt.nonEmpty) { + buffer.drop(1) + } else { + buffer + } + (valueOpt, tokenOpt, outBuffer) + } } 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 17731a01d..7c49124c2 100644 --- a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala +++ b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala @@ -656,7 +656,6 @@ class ZoningOperations( spawn.handleNewPlayerLoaded(player) } else { //alive but doesn't have a GUID; probably logging in? - session = session.copy(zone = Zone.Nowhere) context.self ! ICS.ZoneResponse(Some(player.Zone)) } } else { diff --git a/src/main/scala/net/psforever/objects/zones/ZoneInfo.scala b/src/main/scala/net/psforever/objects/zones/ZoneInfo.scala index 54cb1c9f2..f9e289d5d 100644 --- a/src/main/scala/net/psforever/objects/zones/ZoneInfo.scala +++ b/src/main/scala/net/psforever/objects/zones/ZoneInfo.scala @@ -2,15 +2,21 @@ package net.psforever.objects.zones import enumeratum.values.{IntEnum, IntEnumEntry} +final case class AliasLookup( + zone: Seq[String] = Seq(), + facilities: Map[String, String] = Map() + ) + sealed abstract class ZoneInfo( val value: Int, val name: String, val id: String, val map: MapInfo, - val aliases: Seq[String] = Seq() + val aliases: AliasLookup = ZoneInfo.defaultAliases, ) extends IntEnumEntry {} case object ZoneInfo extends IntEnum[ZoneInfo] { + private val defaultAliases = AliasLookup(Nil, Map.empty[String, String]) case object Solsar extends ZoneInfo( @@ -98,7 +104,7 @@ case object ZoneInfo extends IntEnum[ZoneInfo] { name = "NC Sanctuary", id = "home1", map = MapInfo.Map11, - aliases = Seq("nc-sanctuary") + aliases = AliasLookup(zone = Seq("nc-sanctuary")) ) case object TrSanctuary @@ -107,7 +113,7 @@ case object ZoneInfo extends IntEnum[ZoneInfo] { name = "TR Sanctuary", id = "home2", map = MapInfo.Map12, - aliases = Seq("tr-sanctuary") + aliases = AliasLookup(zone = Seq("tr-sanctuary")) ) case object VsSanctuary @@ -116,7 +122,7 @@ case object ZoneInfo extends IntEnum[ZoneInfo] { name = "VS Sanctuary", id = "home3", map = MapInfo.Map13, - aliases = Seq("vs-sanctuary") + aliases = AliasLookup(zone = Seq("vs-sanctuary")) ) case object tzshtr @@ -124,7 +130,8 @@ case object ZoneInfo extends IntEnum[ZoneInfo] { value = 14, name = "tzshtr", id = "tzshtr", - map = MapInfo.Map14 + map = MapInfo.Map14, + aliases = AliasLookup(zone = Seq("tr-shooting")) ) case object tzdrtr @@ -132,7 +139,8 @@ case object ZoneInfo extends IntEnum[ZoneInfo] { value = 15, name = "tzdrtr", id = "tzdrtr", - map = MapInfo.Map15 + map = MapInfo.Map15, + aliases = AliasLookup(zone = Seq("tr-driving")) ) case object tzcotr @@ -148,7 +156,8 @@ case object ZoneInfo extends IntEnum[ZoneInfo] { value = 17, name = "tzshnc", id = "tzshnc", - map = MapInfo.Map14 + map = MapInfo.Map14, + aliases = AliasLookup(zone = Seq("nc-shooting")) ) case object tzdrnc @@ -156,7 +165,8 @@ case object ZoneInfo extends IntEnum[ZoneInfo] { value = 18, name = "tzdrnc", id = "tzdrnc", - map = MapInfo.Map15 + map = MapInfo.Map15, + aliases = AliasLookup(zone = Seq("nc-driving")) ) case object tzconc @@ -172,7 +182,8 @@ case object ZoneInfo extends IntEnum[ZoneInfo] { value = 20, name = "tzshvs", id = "tzshvs", - map = MapInfo.Map14 + map = MapInfo.Map14, + aliases = AliasLookup(zone = Seq("vs-shooting")) ) case object tzdrvs @@ -180,7 +191,8 @@ case object ZoneInfo extends IntEnum[ZoneInfo] { value = 21, name = "tzdrvs", id = "tzdrvs", - map = MapInfo.Map15 + map = MapInfo.Map15, + aliases = AliasLookup(zone = Seq("vs-driving")) ) case object tzcovs @@ -244,7 +256,12 @@ case object ZoneInfo extends IntEnum[ZoneInfo] { value = 29, name = "Extinction", id = "i1", - map = MapInfo.Map99 + map = MapInfo.Map99, + aliases = AliasLookup(facilities = Map( + ("Mithra", "Blue_Base"), + ("Yazata", "Red_Base"), + ("Hvar", "Indigo_Base") + )) ) case object Ascension @@ -252,7 +269,12 @@ case object ZoneInfo extends IntEnum[ZoneInfo] { value = 30, name = "Ascension", id = "i2", - map = MapInfo.Map98 + map = MapInfo.Map98, + aliases = AliasLookup(facilities = Map( + ("Zal", "Base_Alpha"), + ("Rashnu", "Base_Bravo"), + ("Sraosha", "Base_Charlie") + )) ) case object Desolation @@ -260,7 +282,12 @@ case object ZoneInfo extends IntEnum[ZoneInfo] { value = 31, name = "Desolation", id = "i3", - map = MapInfo.Map97 + map = MapInfo.Map97, + aliases = AliasLookup(facilities = Map( + ("Dahaka", "Red_Base_97"), + ("Jamshid", "Blue_Base_97"), + ("Izha", "Indigo_Base_97") + )) ) case object Nexus @@ -268,16 +295,20 @@ case object ZoneInfo extends IntEnum[ZoneInfo] { value = 32, name = "Nexus", id = "i4", - map = MapInfo.Map96 + map = MapInfo.Map96, + aliases = AliasLookup(facilities = Map( + ("Atar", "Nexus_base") + )) ) val values: IndexedSeq[ZoneInfo] = findValues def findName(name: String): ZoneInfo = findNameOpt(name).get - def findNameOpt(name: String): Option[ZoneInfo] = + def findNameOpt(name: String): Option[ZoneInfo] = { + val lowerName = name.toLowerCase() values.find(v => - v.name.toLowerCase() == name.toLowerCase() || v.aliases.map(_.toLowerCase()).contains(name.toLowerCase()) + v.name.toLowerCase().equals(lowerName) || v.aliases.zone.map(_.toLowerCase()).contains(lowerName) ) - + } } From d049146b4f3e6be9e2ff34feb497fb43c19f3fb3 Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Sat, 2 Mar 2024 23:12:45 -0500 Subject: [PATCH 7/8] jammered mines explode again (he mines were exploding for incorrect reasons) (#1175) --- .../scala/net/psforever/objects/ExplosiveDeployable.scala | 6 +++--- .../psforever/objects/vital/etc/TrippedMineReason.scala | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala b/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala index 201d39d8d..a99d72c15 100644 --- a/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala +++ b/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala @@ -54,7 +54,7 @@ class ExplosiveDeployableDefinition(private val objectId: Int) DetonateOnJamming } - override def Initialize(obj: Deployable, context: ActorContext) = { + override def Initialize(obj: Deployable, context: ActorContext): Unit = { obj.Actor = context.actorOf(Props(classOf[MineDeployableControl], obj), PlanetSideServerObject.UniqueActorName(obj)) } @@ -70,8 +70,8 @@ abstract class ExplosiveDeployableControl(mine: ExplosiveDeployable) extends Actor with DeployableBehavior with Damageable { - def DeployableObject = mine - def DamageableObject = mine + def DeployableObject: ExplosiveDeployable = mine + def DamageableObject: ExplosiveDeployable = mine override def postStop(): Unit = { super.postStop() diff --git a/src/main/scala/net/psforever/objects/vital/etc/TrippedMineReason.scala b/src/main/scala/net/psforever/objects/vital/etc/TrippedMineReason.scala index 3f4595c9d..0cae9c7e1 100644 --- a/src/main/scala/net/psforever/objects/vital/etc/TrippedMineReason.scala +++ b/src/main/scala/net/psforever/objects/vital/etc/TrippedMineReason.scala @@ -17,19 +17,19 @@ import net.psforever.objects.vital.resolution.DamageAndResistance final case class TrippedMineReason(mine: DeployableSource, owner: SourceEntry) extends DamageReason { - def source: DamageProperties = mine.Definition.innateDamage.getOrElse(TrippedMineReason.triggered) + def source: DamageProperties = TrippedMineReason.triggered def resolution: DamageResolution.Value = DamageResolution.Resolved def same(test: DamageReason): Boolean = test match { - case trip: TrippedMineReason => mine == trip.mine && mine.OwnerName == trip.mine.OwnerName - case _ => false + case trip: TrippedMineReason => mine.unique == trip.mine.unique && owner.unique == owner.unique + case _ => false } /** lay the blame on the player who laid this mine, if possible */ def adversary: Option[SourceEntry] = Some(owner) - override def damageModel : DamageAndResistance = mine.Definition + override def damageModel: DamageAndResistance = mine.Definition override def attribution: Int = mine.Definition.ObjectId } From ea77d4728f32b64474dc6952a5283b1b8b7934eb Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Sat, 2 Mar 2024 23:16:10 -0500 Subject: [PATCH 8/8] Turret Automation (#1166) * zone interaction for turret discovery, players only so far; minor field value change for small turret data; automated turret target recognition; grammatical and linter fixes * initial AIDamage packet and tests; wrote handling code for the AIDamage packet that transforms it into actionable projectile damage * thoroughly reorganized code in behavior; added code for turret-specific interactions for deployable construction, deployable destruction, jamming, and for weaponfire retribution; killing is currently disable for testing turnaround * introduced definition properties to configure auto fire; interspersed properties into relevant files; non-squared velocity check for isMoving * separated overworld facility turrets from cavern facility turrets, called vanu sentry turrets; tightened functionality inheritance between turret deployables and facility turrets; corrected issue where recharging vehicle weapons didn't work * adjust mounting code to betterhandle automation with the facility turrets; basic operation of automation has also been changed, adding a variety of ranges to test against, and cylindrical distance checks * attempted cleanup of previous test fire condition; division of turret callbacks between generic targets and vehicle targets; facility turret stops automatic fire when being mounted and resumes automatic mode when being dismounted * self-reported firing mode for targets that go stationary and then use a 'clever trick' to avoid taking damage while in full exposure to the automated turret; documentation on the automated turret operations (it needs it!) * making specific target validation conditions for different auto turrets, also target blanking, and clarification of how the self-reporting mode cleansup after itself; wrote function documentation to make it all make sense (it doesn't) * secondary queue that keeps track of the previous test shot subjects when none have been tested, allowing for a packet to be skipped during subsequent test shots * reactivating turret deployable destruction; clarifying the validation and clearing conditions for different kinds of auto turrets; extending self-reporting auto turret behavior to other auto-turrets * overhaul of the auto turret target selection process; conditions for MAX detection; rewired self-reporting to address the its issue a bit more specifically; ATDispatch is no longer useless as differences between facility turrets and deployable turrets have been identified, shifting the method to implementing and overriding in subclass control agencies * turret detection methods accounting for specific targets and considerations such as silent running; various turret interactions with other turrets and radiation clouds; proper management of retaliation and jamming; facility turrets have play in the lifecycle in the power structure and capture mechanics of the facility * uniqueness can be generated without having to having to go through source entries; made certain turret upgrading cooperates with turret automation; other targets for turret misaimed aggression; turrets sychronize better on zone load; target validation and blankinghas changed again * starting the target validation timer when dealing with retaliation if it should come from beyond the maximum detection range * stop assuming mountable turrets have places to mount; AMS and AEGIS blocking detection of vehicles; deployable sensors and small robotics turrets are allergic to vehicles * AEGIS and AMS cloak bubbles more proactive --- .../actors/session/SessionActor.scala | 3 + .../actors/session/support/SessionData.scala | 96 +- .../support/SessionLocalHandlers.scala | 1 + .../WeaponAndProjectileOperations.scala | 107 +- .../session/support/ZoningOperations.scala | 24 +- .../net/psforever/actors/zone/ZoneActor.scala | 12 +- .../psforever/objects/GlobalDefinitions.scala | 103 +- .../psforever/objects/OwnableByPlayer.scala | 6 +- .../scala/net/psforever/objects/Player.scala | 3 +- .../psforever/objects/SensorDeployable.scala | 21 +- .../psforever/objects/TurretDeployable.scala | 190 +++- .../scala/net/psforever/objects/Vehicle.scala | 3 +- .../net/psforever/objects/avatar/Avatar.scala | 4 + .../objects/ce/InteractWithTurrets.scala | 86 ++ .../converter/AmmoBoxConverter.scala | 6 +- .../converter/SmallTurretConverter.scala | 8 +- .../definition/converter/ToolConverter.scala | 4 +- .../objects/entity/WorldEntity.scala | 14 +- .../objects/equipment/EffectTarget.scala | 279 ++++- .../objects/equipment/JammingUnit.scala | 5 +- .../hackable/GenericHackables.scala | 2 +- ...actWithRadiationCloudsSeatedInEntity.scala | 99 ++ .../mount/MountableBehavior.scala | 6 +- .../RadiationInMountableInteraction.scala | 5 + .../FacilityHackParticipation.scala | 4 +- .../CaptureTerminalAwareBehavior.scala | 58 +- .../serverobject/turret/FacilityTurret.scala | 28 +- .../turret/FacilityTurretControl.scala | 372 ++++--- .../turret/MountableTurretControl.scala | 72 ++ .../turret/TurretDefinition.scala | 77 +- .../turret/VanuSentryControl.scala | 96 ++ .../serverobject/turret/WeaponTurret.scala | 37 +- .../auto/AffectedByAutomaticTurretFire.scala | 66 ++ .../turret/auto/AutomatedTurret.scala | 70 ++ .../turret/auto/AutomatedTurretBehavior.scala | 998 ++++++++++++++++++ .../turret/auto/SelfReportingMessages.scala | 6 + .../objects/sourcing/AmenitySource.scala | 3 +- .../objects/sourcing/BuildingSource.scala | 8 +- .../objects/sourcing/DeployableSource.scala | 8 +- .../objects/sourcing/ObjectSource.scala | 8 +- .../objects/sourcing/PlayerSource.scala | 11 +- .../objects/sourcing/SourceEntry.scala | 2 - .../objects/sourcing/SourceUniqueness.scala | 25 + .../objects/sourcing/TurretSource.scala | 10 +- .../objects/sourcing/UniqueAmenity.scala | 7 + .../objects/sourcing/UniqueDeployable.scala | 14 + .../objects/sourcing/VehicleSource.scala | 20 +- ...ctWithRadiationCloudsSeatedInVehicle.scala | 92 +- .../vehicles/control/VehicleControl.scala | 10 +- .../objects/vital/InGameHistory.scala | 4 +- .../psforever/packet/GamePacketOpcode.scala | 2 +- .../net/psforever/packet/game/AIDamage.scala | 32 + .../game/objectcreate/CommonFieldData.scala | 6 +- .../vehicle/support/TurretUpgrader.scala | 35 +- .../scala/net/psforever/zones/Zones.scala | 15 +- src/test/scala/game/AIDamageTest.scala | 32 + 56 files changed, 2880 insertions(+), 435 deletions(-) create mode 100644 src/main/scala/net/psforever/objects/ce/InteractWithTurrets.scala create mode 100644 src/main/scala/net/psforever/objects/serverobject/mount/InteractWithRadiationCloudsSeatedInEntity.scala create mode 100644 src/main/scala/net/psforever/objects/serverobject/mount/RadiationInMountableInteraction.scala create mode 100644 src/main/scala/net/psforever/objects/serverobject/turret/MountableTurretControl.scala create mode 100644 src/main/scala/net/psforever/objects/serverobject/turret/VanuSentryControl.scala create mode 100644 src/main/scala/net/psforever/objects/serverobject/turret/auto/AffectedByAutomaticTurretFire.scala create mode 100644 src/main/scala/net/psforever/objects/serverobject/turret/auto/AutomatedTurret.scala create mode 100644 src/main/scala/net/psforever/objects/serverobject/turret/auto/AutomatedTurretBehavior.scala create mode 100644 src/main/scala/net/psforever/objects/serverobject/turret/auto/SelfReportingMessages.scala create mode 100644 src/main/scala/net/psforever/objects/sourcing/SourceUniqueness.scala create mode 100644 src/main/scala/net/psforever/packet/game/AIDamage.scala create mode 100644 src/test/scala/game/AIDamageTest.scala 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 + } +}