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
This commit is contained in:
Fate-JH 2024-03-02 23:16:10 -05:00 committed by GitHub
parent d049146b4f
commit ea77d4728f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
56 changed files with 2880 additions and 435 deletions

View file

@ -518,6 +518,9 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
case packet: LashMessage => case packet: LashMessage =>
sessionFuncs.shooting.handleLashHit(packet) sessionFuncs.shooting.handleLashHit(packet)
case packet: AIDamage =>
sessionFuncs.shooting.handleAIDamage(packet)
case packet: AvatarFirstTimeEventMessage => case packet: AvatarFirstTimeEventMessage =>
sessionFuncs.handleAvatarFirstTimeEvent(packet) sessionFuncs.handleAvatarFirstTimeEvent(packet)

View file

@ -4,6 +4,7 @@ package net.psforever.actors.session.support
import akka.actor.typed.scaladsl.adapter._ import akka.actor.typed.scaladsl.adapter._
import akka.actor.{ActorContext, ActorRef, Cancellable, typed} import akka.actor.{ActorContext, ActorRef, Cancellable, typed}
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource} import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource}
import net.psforever.objects.vital.etc.SuicideReason
import net.psforever.objects.zones.blockmap.{SectorGroup, SectorPopulation} import net.psforever.objects.zones.blockmap.{SectorGroup, SectorPopulation}
import scala.collection.mutable import scala.collection.mutable
@ -882,13 +883,7 @@ class SessionData(
case (None, _, _) => () case (None, _, _) => ()
case (Some(us: PlanetSideServerObject with Vitality with FactionAffinity), PlanetSideGUID(0), _) => case (Some(us: PlanetSideServerObject with Vitality with FactionAffinity), PlanetSideGUID(0), _) =>
if (collisionHistory.get(us.Actor) match { if (updateCollisionHistoryForTarget(us, curr)) {
case Some(lastCollision) if curr - lastCollision <= 1000L =>
false
case _ =>
collisionHistory.put(us.Actor, curr)
true
}) {
if (!bailProtectStatus) { if (!bailProtectStatus) {
handleDealingDamage( handleDealingDamage(
us, 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 ( case (
Some(us: PlanetSideServerObject with Vitality with FactionAffinity), _, Some(us: PlanetSideServerObject with Vitality with FactionAffinity), _,
Some(victim: PlanetSideServerObject with Vitality with FactionAffinity) Some(victim: PlanetSideServerObject with Vitality with FactionAffinity)
) => ) =>
if (collisionHistory.get(victim.Actor) match { if (updateCollisionHistoryForTarget(victim, curr)) {
case Some(lastCollision) if curr - lastCollision <= 1000L =>
false
case _ =>
collisionHistory.put(victim.Actor, curr)
true
}) {
val usSource = SourceEntry(us) val usSource = SourceEntry(us)
val victimSource = SourceEntry(victim) val victimSource = SourceEntry(victim)
//we take damage from the collision //we take damage from the collision
if (!bailProtectStatus) { if (!bailProtectStatus) {
handleDealingDamage( performCollisionWithSomethingDamage(us, usSource, ppos, victimSource, fallHeight, velocity - tv)
us,
DamageInteraction(
usSource,
CollisionWithReason(CollisionReason(velocity - tv, fallHeight, us.DamageModel), victimSource),
ppos
)
)
} }
//get dealt damage from our own collision (no protection) //get dealt damage from our own collision (no protection)
collisionHistory.put(us.Actor, curr) collisionHistory.put(us.Actor, curr)
handleDealingDamage( performCollisionWithSomethingDamage(victim, victimSource, tpos, usSource, fallHeight = 0f, tv - velocity)
victim,
DamageInteraction(
victimSource,
CollisionWithReason(CollisionReason(tv - velocity, 0, victim.DamageModel), usSource),
tpos
)
)
} }
case _ => () case _ => ()
@ -2202,7 +2183,7 @@ class SessionData(
/** /**
* Calculate the amount of damage to be dealt to an active `target` * 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. * and affect the `target` in a synchronized manner.
* The active `target` and the target of the `DamageResult` do not have be the same. * 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`, * 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 = { def failWithError(error: String): Unit = {
log.error(error) log.error(error)
middlewareActor ! MiddlewareActor.Teardown() middlewareActor ! MiddlewareActor.Teardown()

View file

@ -208,6 +208,7 @@ class SessionLocalHandlers(
continent.GUID(vehicleGuid) continent.GUID(vehicleGuid)
.collect { case vehicle: MountableWeapons => (vehicle, vehicle.PassengerInSeat(player)) } .collect { case vehicle: MountableWeapons => (vehicle, vehicle.PassengerInSeat(player)) }
.collect { case (vehicle, Some(seat_num)) => vehicle.WeaponControlledFromSeat(seat_num) } .collect { case (vehicle, Some(seat_num)) => vehicle.WeaponControlledFromSeat(seat_num) }
.getOrElse(Set.empty)
.collect { case weapon: Tool if weapon.GUID == weaponGuid => .collect { case weapon: Tool if weapon.GUID == weaponGuid =>
sendResponse(InventoryStateMessage(weapon.AmmoSlot.Box.GUID, weapon.GUID, weapon.Magazine)) sendResponse(InventoryStateMessage(weapon.AmmoSlot.Box.GUID, weapon.GUID, weapon.Magazine))
} }

View file

@ -2,7 +2,10 @@
package net.psforever.actors.session.support package net.psforever.actors.session.support
import akka.actor.{ActorContext, typed} 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.zones.Zoning
import net.psforever.objects.serverobject.turret.VanuSentry
import net.psforever.objects.zones.exp.ToDatabase import net.psforever.objects.zones.exp.ToDatabase
import scala.collection.mutable import scala.collection.mutable
@ -51,8 +54,7 @@ private[support] class WeaponAndProjectileOperations(
private[support] var shotsWhileDead: Int = 0 private[support] var shotsWhileDead: Int = 0
private val projectiles: Array[Option[Projectile]] = private val projectiles: Array[Option[Projectile]] =
Array.fill[Option[Projectile]](Projectile.rangeUID - Projectile.baseUID)(None) Array.fill[Option[Projectile]](Projectile.rangeUID - Projectile.baseUID)(None)
private var zoningOpt: Option[ZoningOperations] = None
def zoning: ZoningOperations = zoningOpt.orNull
/* packets */ /* packets */
def handleWeaponFire(pkt: WeaponFireMessage): Unit = { 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 */ /* support code */
def HandleWeaponFireOperations( def HandleWeaponFireOperations(
@ -519,11 +570,6 @@ private[support] class WeaponAndProjectileOperations(
) )
continent.Projectile ! ZoneProjectile.Add(player.GUID, qualityprojectile) 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 { } else {
log.warn( log.warn(
s"WeaponFireMessage: ${player.Name}'s ${tool.Definition.Name} projectile is too far from owner position at time of discharge ($distanceToOwner > $acceptableDistanceToOwner); suspect" 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 = { 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.VehicleEvents ! VehicleServiceMessage(
continent.id, continent.id,
VehicleAction.ChangeFireState_Start(player.GUID, itemGuid) VehicleAction.ChangeFireState_Start(player.GUID, itemGuid)
@ -1236,6 +1286,10 @@ private[support] class WeaponAndProjectileOperations(
} }
private def fireStateStopMountedMessages(itemGuid: PlanetSideGUID): Unit = { 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.VehicleEvents ! VehicleServiceMessage(
continent.id, continent.id,
VehicleAction.ChangeFireState_Stop(player.GUID, itemGuid) VehicleAction.ChangeFireState_Stop(player.GUID, itemGuid)
@ -1366,6 +1420,7 @@ private[support] class WeaponAndProjectileOperations(
addShotsToMap(shotsFired, weaponId, shots) addShotsToMap(shotsFired, weaponId, shots)
} }
//noinspection SameParameterValue
private def addShotsLanded(weaponId: Int, shots: Int): Unit = { private def addShotsLanded(weaponId: Int, shots: Int): Unit = {
addShotsToMap(shotsLanded, weaponId, shots) addShotsToMap(shotsLanded, weaponId, shots)
} }
@ -1405,6 +1460,44 @@ private[support] class WeaponAndProjectileOperations(
ToDatabase.reportToolDischarge(avatarId, EquipmentStat(weaponId, fired, landed, 0, 0)) 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 = { override protected[session] def stop(): Unit = {
if (player != null && player.HasGUID) { if (player != null && player.HasGUID) {
(prefire ++ shooting).foreach { guid => (prefire ++ shooting).foreach { guid =>

View file

@ -12,9 +12,10 @@ import net.psforever.objects.avatar.scoring.{CampaignStatistics, ScoreCard, Sess
import net.psforever.objects.inventory.InventoryItem import net.psforever.objects.inventory.InventoryItem
import net.psforever.objects.serverobject.mount.Seat import net.psforever.objects.serverobject.mount.Seat
import net.psforever.objects.serverobject.tube.SpawnTube 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.sourcing.{PlayerSource, SourceEntry, VehicleSource}
import net.psforever.objects.vital.{InGameHistory, IncarnationActivity, ReconstructionActivity, SpawningActivity} 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.collection.mutable
import scala.concurrent.duration._ 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 //sensor animation
normal normal
@ -555,6 +569,14 @@ class ZoningOperations(
) )
case _ => ; 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 //remote projectiles and radiation clouds
continent.Projectiles.foreach { projectile => continent.Projectiles.foreach { projectile =>

View file

@ -13,6 +13,8 @@ import akka.actor.typed.scaladsl.adapter._
import net.psforever.actors.zone.building.MajorFacilityLogic import net.psforever.actors.zone.building.MajorFacilityLogic
import net.psforever.objects.avatar.scoring.Kill import net.psforever.objects.avatar.scoring.Kill
import net.psforever.objects.serverobject.affinity.FactionAffinity 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.sourcing.SourceEntry
import net.psforever.objects.vital.{InGameActivity, InGameHistory} import net.psforever.objects.vital.{InGameActivity, InGameHistory}
import net.psforever.objects.zones.exp.{ExperienceCalculator, SupportExperienceCalculator} import net.psforever.objects.zones.exp.{ExperienceCalculator, SupportExperienceCalculator}
@ -96,14 +98,18 @@ class ZoneActor(context: ActorContext[ZoneActor.Command], zone: Zone)
case Success(buildings) => case Success(buildings) =>
buildings.foreach { building => buildings.foreach { building =>
zone.BuildingByMapId(building.localId) match { 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 //warp gates are controlled by game logic and are better off not restored via the database
case Some(b) => case Some(b) =>
if ((b.Faction = PlanetSideEmpire(building.factionId)) != PlanetSideEmpire.NEUTRAL) { if ((b.Faction = PlanetSideEmpire(building.factionId)) != PlanetSideEmpire.NEUTRAL) {
b.ForceDomeActive = MajorFacilityLogic.checkForceDomeStatus(b).getOrElse(false) 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 // TODO this happens during testing, need a way to not always persist during tests
} }
} }

View file

@ -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.structures.{AmenityDefinition, AutoRepairStats, BuildingDefinition, WarpGateDefinition}
import net.psforever.objects.serverobject.terminals.capture.CaptureTerminalDefinition import net.psforever.objects.serverobject.terminals.capture.CaptureTerminalDefinition
import net.psforever.objects.serverobject.terminals.implant.{ImplantTerminalDefinition, ImplantTerminalMechDefinition} 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.vehicles.{DestroyedVehicle, InternalTelepadDefinition, UtilityType, VehicleSubsystemEntry}
import net.psforever.objects.vital.base.DamageType import net.psforever.objects.vital.base.DamageType
import net.psforever.objects.vital.damage._ 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. * Using the definition for a `Vehicle` determine whether it can fly.
* Does not count the flying battleframe robotics vehicles. * Does not count the flying battleframe robotics vehicles.
@ -9059,6 +9073,24 @@ object GlobalDefinitions {
spitfire_turret.DeployTime = Duration.create(5000, "ms") spitfire_turret.DeployTime = Duration.create(5000, "ms")
spitfire_turret.Model = ComplexDeployableResolutions.calculate spitfire_turret.Model = ComplexDeployableResolutions.calculate
spitfire_turret.deployAnimation = DeployAnimation.Standard 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 { spitfire_turret.innateDamage = new DamageWithPosition {
CausesDamageType = DamageType.One CausesDamageType = DamageType.One
Damage0 = 200 Damage0 = 200
@ -9085,6 +9117,30 @@ object GlobalDefinitions {
spitfire_cloaked.DeployTime = Duration.create(5000, "ms") spitfire_cloaked.DeployTime = Duration.create(5000, "ms")
spitfire_cloaked.deployAnimation = DeployAnimation.Standard spitfire_cloaked.deployAnimation = DeployAnimation.Standard
spitfire_cloaked.Model = ComplexDeployableResolutions.calculate 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 { spitfire_cloaked.innateDamage = new DamageWithPosition {
CausesDamageType = DamageType.One CausesDamageType = DamageType.One
Damage0 = 50 Damage0 = 50
@ -9111,6 +9167,21 @@ object GlobalDefinitions {
spitfire_aa.DeployTime = Duration.create(5000, "ms") spitfire_aa.DeployTime = Duration.create(5000, "ms")
spitfire_aa.deployAnimation = DeployAnimation.Standard spitfire_aa.deployAnimation = DeployAnimation.Standard
spitfire_aa.Model = ComplexDeployableResolutions.calculate 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 { spitfire_aa.innateDamage = new DamageWithPosition {
CausesDamageType = DamageType.One CausesDamageType = DamageType.One
Damage0 = 200 Damage0 = 200
@ -9175,9 +9246,10 @@ object GlobalDefinitions {
portable_manned_turret.Damageable = true portable_manned_turret.Damageable = true
portable_manned_turret.Repairable = true portable_manned_turret.Repairable = true
portable_manned_turret.RepairIfDestroyed = false 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 -> new mutable.HashMap()
portable_manned_turret.WeaponPaths(1) += TurretUpgrade.None -> energy_gun 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 += 1 -> MountInfo(0)
portable_manned_turret.MountPoints += 2 -> MountInfo(0) portable_manned_turret.MountPoints += 2 -> MountInfo(0)
portable_manned_turret.ReserveAmmunition = true portable_manned_turret.ReserveAmmunition = true
@ -9209,6 +9281,7 @@ object GlobalDefinitions {
portable_manned_turret_nc.RepairIfDestroyed = false portable_manned_turret_nc.RepairIfDestroyed = false
portable_manned_turret_nc.WeaponPaths += 1 -> new mutable.HashMap() portable_manned_turret_nc.WeaponPaths += 1 -> new mutable.HashMap()
portable_manned_turret_nc.WeaponPaths(1) += TurretUpgrade.None -> energy_gun_nc 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.controlledWeapons(seat = 0, weapon = 1)
portable_manned_turret_nc.MountPoints += 1 -> MountInfo(0) portable_manned_turret_nc.MountPoints += 1 -> MountInfo(0)
portable_manned_turret_nc.MountPoints += 2 -> 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.RepairIfDestroyed = false
portable_manned_turret_tr.WeaponPaths += 1 -> new mutable.HashMap() portable_manned_turret_tr.WeaponPaths += 1 -> new mutable.HashMap()
portable_manned_turret_tr.WeaponPaths(1) += TurretUpgrade.None -> energy_gun_tr 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.controlledWeapons(seat = 0, weapon = 1)
portable_manned_turret_tr.MountPoints += 1 -> MountInfo(0) portable_manned_turret_tr.MountPoints += 1 -> MountInfo(0)
portable_manned_turret_tr.MountPoints += 2 -> 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.RepairIfDestroyed = false
portable_manned_turret_vs.WeaponPaths += 1 -> new mutable.HashMap() portable_manned_turret_vs.WeaponPaths += 1 -> new mutable.HashMap()
portable_manned_turret_vs.WeaponPaths(1) += TurretUpgrade.None -> energy_gun_vs 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.controlledWeapons(seat = 0, weapon = 1)
portable_manned_turret_vs.MountPoints += 1 -> MountInfo(0) portable_manned_turret_vs.MountPoints += 1 -> MountInfo(0)
portable_manned_turret_vs.MountPoints += 2 -> MountInfo(0) portable_manned_turret_vs.MountPoints += 2 -> MountInfo(0)
@ -9999,7 +10074,7 @@ object GlobalDefinitions {
manned_turret.Name = "manned_turret" manned_turret.Name = "manned_turret"
manned_turret.MaxHealth = 3600 manned_turret.MaxHealth = 3600
manned_turret.Damageable = true manned_turret.Damageable = true
manned_turret.DamageDisablesAt = 0 manned_turret.DamageDisablesAt = 1800
manned_turret.Repairable = true manned_turret.Repairable = true
manned_turret.autoRepair = AutoRepairStats(1.0909f, 10000, 1600, 0.05f) manned_turret.autoRepair = AutoRepairStats(1.0909f, 10000, 1600, 0.05f)
manned_turret.RepairIfDestroyed = true manned_turret.RepairIfDestroyed = true
@ -10007,11 +10082,32 @@ object GlobalDefinitions {
manned_turret.WeaponPaths(1) += TurretUpgrade.None -> phalanx_sgl_hevgatcan manned_turret.WeaponPaths(1) += TurretUpgrade.None -> phalanx_sgl_hevgatcan
manned_turret.WeaponPaths(1) += TurretUpgrade.AVCombo -> phalanx_avcombo manned_turret.WeaponPaths(1) += TurretUpgrade.AVCombo -> phalanx_avcombo
manned_turret.WeaponPaths(1) += TurretUpgrade.FlakCombo -> phalanx_flakcombo manned_turret.WeaponPaths(1) += TurretUpgrade.FlakCombo -> phalanx_flakcombo
manned_turret.Seats += 0 -> new SeatDefinition()
manned_turret.controlledWeapons(seat = 0, weapon = 1) manned_turret.controlledWeapons(seat = 0, weapon = 1)
manned_turret.MountPoints += 1 -> MountInfo(0) manned_turret.MountPoints += 1 -> MountInfo(0)
manned_turret.FactionLocked = true manned_turret.FactionLocked = true
manned_turret.ReserveAmmunition = false manned_turret.ReserveAmmunition = false
manned_turret.RadiationShielding = 0.5f 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 { manned_turret.innateDamage = new DamageWithPosition {
CausesDamageType = DamageType.One CausesDamageType = DamageType.One
Damage0 = 150 Damage0 = 150
@ -10031,6 +10127,7 @@ object GlobalDefinitions {
vanu_sentry_turret.RepairIfDestroyed = true vanu_sentry_turret.RepairIfDestroyed = true
vanu_sentry_turret.WeaponPaths += 1 -> new mutable.HashMap() vanu_sentry_turret.WeaponPaths += 1 -> new mutable.HashMap()
vanu_sentry_turret.WeaponPaths(1) += TurretUpgrade.None -> vanu_sentry_turret_weapon 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.controlledWeapons(seat = 0, weapon = 1)
vanu_sentry_turret.MountPoints += 1 -> MountInfo(0) vanu_sentry_turret.MountPoints += 1 -> MountInfo(0)
vanu_sentry_turret.MountPoints += 2 -> MountInfo(0) vanu_sentry_turret.MountPoints += 2 -> MountInfo(0)

View file

@ -1,7 +1,7 @@
// Copyright (c) 2019 PSForever // Copyright (c) 2019 PSForever
package net.psforever.objects package net.psforever.objects
import net.psforever.objects.sourcing.{PlayerSource, UniquePlayer} import net.psforever.objects.sourcing.UniquePlayer
import net.psforever.types.PlanetSideGUID import net.psforever.types.PlanetSideGUID
trait OwnableByPlayer { trait OwnableByPlayer {
@ -46,11 +46,11 @@ trait OwnableByPlayer {
def AssignOwnership(playerOpt: Option[Player]): OwnableByPlayer = { def AssignOwnership(playerOpt: Option[Player]): OwnableByPlayer = {
(originalOwnerName, playerOpt) match { (originalOwnerName, playerOpt) match {
case (None, Some(player)) => case (None, Some(player)) =>
owner = Some(PlayerSource(player).unique) owner = Some(UniquePlayer(player))
originalOwnerName = originalOwnerName.orElse { Some(player.Name) } originalOwnerName = originalOwnerName.orElse { Some(player.Name) }
OwnerGuid = player OwnerGuid = player
case (_, Some(player)) => case (_, Some(player)) =>
owner = Some(PlayerSource(player).unique) owner = Some(UniquePlayer(player))
OwnerGuid = player OwnerGuid = player
case (_, None) => case (_, None) =>
owner = None owner = None

View file

@ -3,7 +3,7 @@ package net.psforever.objects
import net.psforever.objects.avatar.{Avatar, LoadoutManager, SpecialCarry} import net.psforever.objects.avatar.{Avatar, LoadoutManager, SpecialCarry}
import net.psforever.objects.ballistics.InteractWithRadiationClouds 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.definition.{AvatarDefinition, ExoSuitDefinition, SpecialExoSuitDefinition}
import net.psforever.objects.equipment.{Equipment, EquipmentSize, EquipmentSlot, JammableUnit} import net.psforever.objects.equipment.{Equipment, EquipmentSize, EquipmentSlot, JammableUnit}
import net.psforever.objects.inventory.{Container, GridInventory, InventoryItem} import net.psforever.objects.inventory.{Container, GridInventory, InventoryItem}
@ -38,6 +38,7 @@ class Player(var avatar: Avatar)
with MountableEntity { with MountableEntity {
interaction(new InteractWithEnvironment()) interaction(new InteractWithEnvironment())
interaction(new InteractWithMinesUnlessSpectating(obj = this, range = 10)) interaction(new InteractWithMinesUnlessSpectating(obj = this, range = 10))
interaction(new InteractWithTurrets())
interaction(new InteractWithRadiationClouds(range = 10f, Some(this))) interaction(new InteractWithRadiationClouds(range = 10f, Some(this)))
private var backpack: Boolean = false private var backpack: Boolean = false

View file

@ -17,6 +17,7 @@ import net.psforever.services.Service
import net.psforever.services.local.{LocalAction, LocalServiceMessage} import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
import scala.annotation.unused
import scala.concurrent.duration._ import scala.concurrent.duration._
class SensorDeployable(cdef: SensorDeployableDefinition) extends Deployable(cdef) with Hackable with JammableUnit 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 Model = SimpleResolutions.calculate
Packet = new SmallDeployableConverter Packet = new SmallDeployableConverter
override def Initialize(obj: Deployable, context: ActorContext) = { override def Initialize(obj: Deployable, context: ActorContext): Unit = {
obj.Actor = obj.Actor =
context.actorOf(Props(classOf[SensorDeployableControl], obj), PlanetSideServerObject.UniqueActorName(obj)) context.actorOf(Props(classOf[SensorDeployableControl], obj), PlanetSideServerObject.UniqueActorName(obj))
} }
@ -45,10 +46,10 @@ class SensorDeployableControl(sensor: SensorDeployable)
with JammableBehavior with JammableBehavior
with DamageableEntity with DamageableEntity
with RepairableEntity { with RepairableEntity {
def DeployableObject = sensor def DeployableObject: SensorDeployable = sensor
def JammableObject = sensor def JammableObject: SensorDeployable = sensor
def DamageableObject = sensor def DamageableObject: SensorDeployable = sensor
def RepairableObject = sensor def RepairableObject: SensorDeployable = sensor
override def postStop(): Unit = { override def postStop(): Unit = {
super.postStop() super.postStop()
@ -64,7 +65,7 @@ class SensorDeployableControl(sensor: SensorDeployable)
case _ => ; 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 = { override protected def DestructionAwareness(target: Damageable.Target, cause: DamageResult): Unit = {
super.DestructionAwareness(target, cause) super.DestructionAwareness(target, cause)
@ -88,7 +89,7 @@ class SensorDeployableControl(sensor: SensorDeployable)
val zone = obj.Zone val zone = obj.Zone
zone.LocalEvents ! LocalServiceMessage( zone.LocalEvents ! LocalServiceMessage(
zone.id, 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) super.StartJammeredStatus(obj, dur)
case _ => ; case _ => ;
@ -113,7 +114,7 @@ class SensorDeployableControl(sensor: SensorDeployable)
val zone = sensor.Zone val zone = sensor.Zone
zone.LocalEvents ! LocalServiceMessage( zone.LocalEvents ! LocalServiceMessage(
zone.id, zone.id,
LocalAction.TriggerEffectInfo(Service.defaultPlayerGUID, "on", obj.GUID, true, 1000) LocalAction.TriggerEffectInfo(Service.defaultPlayerGUID, "on", obj.GUID, unk1=true, 1000)
) )
case _ => ; case _ => ;
} }
@ -125,7 +126,7 @@ class SensorDeployableControl(sensor: SensorDeployable)
val zone = sensor.Zone val zone = sensor.Zone
zone.LocalEvents ! LocalServiceMessage( zone.LocalEvents ! LocalServiceMessage(
zone.id, 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 val zone = target.Zone
zone.LocalEvents ! LocalServiceMessage( zone.LocalEvents ! LocalServiceMessage(
zone.id, 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 //position the explosion effect near the bulky area of the sensor stalk
val ang = target.Orientation val ang = target.Orientation

View file

@ -1,35 +1,56 @@
// Copyright (c) 2017 PSForever // Copyright (c) 2017 PSForever
package net.psforever.objects package net.psforever.objects
import akka.actor.{Actor, ActorContext, Props} import akka.actor.{Actor, ActorContext, ActorRef, Props}
import net.psforever.objects.ce.{Deployable, DeployableBehavior, DeployedItem} import net.psforever.objects.ce.{Deployable, DeployableBehavior, DeployedItem, InteractWithTurrets}
import net.psforever.objects.definition.DeployableDefinition import net.psforever.objects.definition.DeployableDefinition
import net.psforever.objects.definition.converter.SmallTurretConverter 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.guid.{GUIDTask, TaskWorkflow}
import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.affinity.FactionAffinityBehavior import net.psforever.objects.serverobject.affinity.FactionAffinityBehavior
import net.psforever.objects.serverobject.damage.Damageable.Target import net.psforever.objects.serverobject.damage.Damageable
import net.psforever.objects.serverobject.damage.DamageableWeaponTurret
import net.psforever.objects.serverobject.hackable.Hackable import net.psforever.objects.serverobject.hackable.Hackable
import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior} import net.psforever.objects.serverobject.mount.{InteractWithRadiationCloudsSeatedInEntity, Mountable}
import net.psforever.objects.serverobject.repair.RepairableWeaponTurret import net.psforever.objects.serverobject.turret.auto.AutomatedTurret.Target
import net.psforever.objects.serverobject.turret.{TurretDefinition, WeaponTurret} 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.damage.DamageCalculations
import net.psforever.objects.vital.interaction.DamageResult 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.vital.{SimpleResolutions, StandardVehicleResistance}
import net.psforever.objects.zones.InteractsWithZone
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
import net.psforever.types.PlanetSideGUID
import scala.concurrent.duration.FiniteDuration import scala.concurrent.duration.FiniteDuration
class TurretDeployable(tdef: TurretDeployableDefinition) class TurretDeployable(tdef: TurretDeployableDefinition)
extends Deployable(tdef) extends Deployable(tdef)
with AutomatedTurret
with WeaponTurret with WeaponTurret
with JammableUnit with JammableUnit
with InteractsWithZone
with StandardResistanceProfile
with Hackable { 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) class TurretDeployableDefinition(private val objectId: Int)
@ -46,7 +67,7 @@ class TurretDeployableDefinition(private val objectId: Int)
//override to clarify inheritance conflict //override to clarify inheritance conflict
override def MaxHealth_=(max: Int): Int = super[DeployableDefinition].MaxHealth_=(max) 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)) obj.Actor = context.actorOf(Props(classOf[TurretControl], obj), PlanetSideServerObject.UniqueActorName(obj))
} }
} }
@ -63,35 +84,92 @@ class TurretControl(turret: TurretDeployable)
extends Actor extends Actor
with DeployableBehavior with DeployableBehavior
with FactionAffinityBehavior.Check with FactionAffinityBehavior.Check
with JammableMountedWeapons //note: jammable status is reported as vehicle events, not local events with MountableTurretControl
with MountableBehavior with AutomatedTurretBehavior
with DamageableWeaponTurret with AffectedByAutomaticTurretFire {
with RepairableWeaponTurret { def TurretObject: TurretDeployable = turret
def DeployableObject = turret def DeployableObject: TurretDeployable = turret
def MountableObject = turret def MountableObject: TurretDeployable = turret
def JammableObject = turret def JammableObject: TurretDeployable = turret
def FactionObject = turret def FactionObject: TurretDeployable = turret
def DamageableObject = turret def DamageableObject: TurretDeployable = turret
def RepairableObject = turret def RepairableObject: TurretDeployable = turret
def AutomatedTurretObject: TurretDeployable = turret
def AffectedObject: TurretDeployable = turret
override def postStop(): Unit = { override def postStop(): Unit = {
super.postStop() super.postStop()
deployableBehaviorPostStop() deployableBehaviorPostStop()
damageableWeaponTurretPostStop() selfReportingDatabaseUpdate()
automaticTurretPostStop()
} }
def receive: Receive = def receive: Receive =
deployableBehavior commonBehavior
.orElse(deployableBehavior)
.orElse(checkBehavior) .orElse(checkBehavior)
.orElse(jammableBehavior)
.orElse(mountBehavior) .orElse(mountBehavior)
.orElse(dismountBehavior) .orElse(automatedTurretBehavior)
.orElse(takesDamage) .orElse(takeAutomatedDamage)
.orElse(canBeRepairedByNanoDispenser)
.orElse { .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( override protected def mountTest(
obj: PlanetSideServerObject with Mountable, obj: PlanetSideServerObject with Mountable,
seatNumber: Int, seatNumber: Int,
@ -99,7 +177,40 @@ class TurretControl(turret: TurretDeployable)
(!turret.Definition.FactionLocked || player.Faction == obj.Faction) && !obj.Destroyed (!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) super.DestructionAwareness(target, cause)
CancelJammeredSound(target) CancelJammeredSound(target)
CancelJammeredStatus(target) CancelJammeredStatus(target)
@ -107,22 +218,22 @@ class TurretControl(turret: TurretDeployable)
} }
override def deconstructDeployable(time: Option[FiniteDuration]) : Unit = { override def deconstructDeployable(time: Option[FiniteDuration]) : Unit = {
AutomaticOperation = false
val zone = turret.Zone val zone = turret.Zone
val seats = turret.Seats.values val seats = turret.Seats.values
//either we have no seats or no one gets to sit //either we have no seats or no one gets to sit
val retime = if (seats.count(_.isOccupied) > 0) { 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 val wasKickedByDriver = false
seats.foreach { seat => seats.foreach { seat =>
seat.occupant match { seat.occupant.collect {
case Some(tplayer) => case player: Player =>
seat.unmount(tplayer) seat.unmount(player)
tplayer.VehicleSeated = None player.VehicleSeated = None
zone.VehicleEvents ! VehicleServiceMessage( zone.VehicleEvents ! VehicleServiceMessage(
zone.id, 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) Some(time.getOrElse(Deployable.cleanup) + Deployable.cleanup)
@ -132,6 +243,11 @@ class TurretControl(turret: TurretDeployable)
super.deconstructDeployable(retime) super.deconstructDeployable(retime)
} }
override def finalizeDeployable(callback: ActorRef): Unit = {
super.finalizeDeployable(callback)
AutomaticOperation = true
}
override def unregisterDeployable(obj: Deployable): Unit = { override def unregisterDeployable(obj: Deployable): Unit = {
val zone = obj.Zone val zone = obj.Zone
TaskWorkflow.execute(GUIDTask.unregisterDeployableTurret(zone.GUID, turret)) TaskWorkflow.execute(GUIDTask.unregisterDeployableTurret(zone.GUID, turret))

View file

@ -1,7 +1,7 @@
// Copyright (c) 2017 PSForever // Copyright (c) 2017 PSForever
package net.psforever.objects 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.definition.{ToolDefinition, VehicleDefinition}
import net.psforever.objects.equipment.{Equipment, EquipmentSize, EquipmentSlot, JammableUnit} import net.psforever.objects.equipment.{Equipment, EquipmentSize, EquipmentSlot, JammableUnit}
import net.psforever.objects.inventory.{Container, GridInventory, InventoryItem, InventoryTile} import net.psforever.objects.inventory.{Container, GridInventory, InventoryItem, InventoryTile}
@ -92,6 +92,7 @@ class Vehicle(private val vehicleDef: VehicleDefinition)
with MountableEntity { with MountableEntity {
interaction(new InteractWithEnvironment()) interaction(new InteractWithEnvironment())
interaction(new InteractWithMines(range = 20)) interaction(new InteractWithMines(range = 20))
interaction(new InteractWithTurrets())
interaction(new InteractWithRadiationCloudsSeatedInVehicle(obj = this, range = 20)) interaction(new InteractWithRadiationCloudsSeatedInVehicle(obj = this, range = 20))
private var faction: PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL private var faction: PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL

View file

@ -224,6 +224,10 @@ case class Avatar(
false false
} }
override def hashCode(): Int = {
id
}
/** Avatar assertions /** Avatar assertions
* These protect against programming errors by asserting avatar properties have correct values * These protect against programming errors by asserting avatar properties have correct values
* They may or may not be disabled for live applications * They may or may not be disabled for live applications

View file

@ -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
}
}

View file

@ -9,7 +9,7 @@ import scala.util.{Success, Try}
class AmmoBoxConverter extends ObjectCreateConverter[AmmoBox] { class AmmoBoxConverter extends ObjectCreateConverter[AmmoBox] {
override def ConstructorData(obj: AmmoBox): Try[CommonFieldData] = { override def ConstructorData(obj: AmmoBox): Try[CommonFieldData] = {
Success(CommonFieldData()(false)) Success(CommonFieldData()(flag = false))
} }
override def DetailedConstructorData(obj: AmmoBox): Try[DetailedAmmoBoxData] = { override def DetailedConstructorData(obj: AmmoBox): Try[DetailedAmmoBoxData] = {
@ -19,9 +19,9 @@ class AmmoBoxConverter extends ObjectCreateConverter[AmmoBox] {
PlanetSideEmpire.NEUTRAL, PlanetSideEmpire.NEUTRAL,
bops = false, bops = false,
alternate = false, alternate = false,
true, v1 = true,
None, None,
false, jammered = false,
None, None,
None, None,
PlanetSideGUID(0) PlanetSideGUID(0)

View file

@ -21,9 +21,9 @@ class SmallTurretConverter extends ObjectCreateConverter[TurretDeployable]() {
obj.Faction, obj.Faction,
bops = false, bops = false,
alternate = false, alternate = false,
false, v1 = true,
None, None,
jammered = obj.Jammed, obj.Jammed,
Some(true), Some(true),
None, None,
obj.OwnerGuid match { obj.OwnerGuid match {
@ -45,9 +45,9 @@ class SmallTurretConverter extends ObjectCreateConverter[TurretDeployable]() {
obj.Faction, obj.Faction,
bops = false, bops = false,
alternate = true, alternate = true,
false, v1 = false,
None, None,
false, jammered = false,
Some(false), Some(false),
None, None,
PlanetSideGUID(0) PlanetSideGUID(0)

View file

@ -21,7 +21,7 @@ class ToolConverter extends ObjectCreateConverter[Tool]() {
obj.Faction, obj.Faction,
bops = false, bops = false,
alternate = false, alternate = false,
true, v1 = true,
None, None,
obj.Jammed, obj.Jammed,
None, None,
@ -47,7 +47,7 @@ class ToolConverter extends ObjectCreateConverter[Tool]() {
obj.Faction, obj.Faction,
bops = false, bops = false,
alternate = false, alternate = false,
true, v1 = true,
None, None,
obj.Jammed, obj.Jammed,
None, None,

View file

@ -32,10 +32,16 @@ trait WorldEntity {
def isMoving(test: Vector3): Boolean = WorldEntity.isMoving(Velocity, test) 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. * 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 * @param test the velocity to test against
* @return `true`, if we are moving; `false`, otherwise * @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) def isMoving(test: Float): Boolean = WorldEntity.isMoving(Velocity, test)
} }

View file

@ -2,9 +2,11 @@
package net.psforever.objects.equipment package net.psforever.objects.equipment
import net.psforever.objects._ import net.psforever.objects._
import net.psforever.objects.ce.DeployableCategory import net.psforever.objects.ce.{DeployableCategory, DeployedItem}
import net.psforever.objects.serverobject.turret.FacilityTurret import net.psforever.objects.serverobject.turret.{FacilityTurret, WeaponTurret}
import net.psforever.objects.vital.DamagingActivity 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) final case class TargetValidation(category: EffectTarget.Category.Value, test: EffectTarget.Validation.Value)
@ -21,6 +23,7 @@ object EffectTarget {
object Validation { object Validation {
type Value = PlanetSideGameObject => Boolean type Value = PlanetSideGameObject => Boolean
//noinspection ScalaUnusedSymbol
def Invalid(target: PlanetSideGameObject): Boolean = false def Invalid(target: PlanetSideGameObject): Boolean = false
def Medical(target: PlanetSideGameObject): Boolean = 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, * be a flight vehicle,
* must have some health already, but does not have all its health, * have some health already, but does not have all its health, and
* and can not have taken damage in the last five seconds. * have not taken damage in the last five seconds.
*/ */
def PadLanding(target: PlanetSideGameObject): Boolean = def PadLanding(target: PlanetSideGameObject): Boolean =
target match { target match {
@ -185,5 +188,269 @@ object EffectTarget {
case _ => case _ =>
false 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)
} }
} }

View file

@ -139,7 +139,7 @@ trait JammableBehavior {
* @param target the objects to be determined if affected by the source's jammering * @param target the objects to be determined if affected by the source's jammering
* @param cause the source of the "jammered" status * @param cause the source of the "jammered" status
*/ */
def TryJammerEffectActivate(target: Any, cause: DamageResult): Unit = def TryJammerEffectActivate(target: Any, cause: DamageResult): Unit = {
target match { target match {
case obj: PlanetSideServerObject => case obj: PlanetSideServerObject =>
val interaction = cause.interaction val interaction = cause.interaction
@ -157,8 +157,9 @@ trait JammableBehavior {
} }
case None => case None =>
} }
case _ => ; case _ => ()
} }
}
/** /**
* Activate a distinctive buzzing sound effect. * Activate a distinctive buzzing sound effect.

View file

@ -84,7 +84,7 @@ object GenericHackables {
HackState.Start HackState.Start
} else if (progress >= 100L) { } else if (progress >= 100L) {
HackState.Finished 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 // 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 HackState.Cancelled
} else { } else {

View file

@ -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()
}
}

View file

@ -65,7 +65,7 @@ trait MountableBehavior {
!obj.Destroyed !obj.Destroyed
} }
private def tryMount( protected def tryMount(
obj: PlanetSideServerObject with Mountable, obj: PlanetSideServerObject with Mountable,
seatNumber: Int, seatNumber: Int,
player: Player player: Player
@ -105,12 +105,12 @@ trait MountableBehavior {
): Boolean = { ): Boolean = {
obj.PassengerInSeat(user).contains(seatNumber) && obj.PassengerInSeat(user).contains(seatNumber) &&
(obj.Seats.get(seatNumber) match { (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 case _ => false
}) })
} }
private def tryDismount( protected def tryDismount(
obj: Mountable, obj: Mountable,
seatNumber: Int, seatNumber: Int,
user: Player, user: Player,

View file

@ -0,0 +1,5 @@
package net.psforever.objects.serverobject.mount
import net.psforever.objects.zones.ZoneInteractionType
case object RadiationInMountableInteraction extends ZoneInteractionType

View file

@ -3,7 +3,7 @@ package net.psforever.objects.serverobject.structures.participation
import net.psforever.objects.Player import net.psforever.objects.Player
import net.psforever.objects.avatar.scoring.Kill 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 net.psforever.types.{PlanetSideEmpire, Vector3}
import scala.collection.mutable import scala.collection.mutable
@ -143,7 +143,7 @@ object FacilityHackParticipation {
killTime <= end && killTime <= end &&
Vector3.DistanceSquared(centerXY, k.info.interaction.hitPos.xy) < distanceSq 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)
} }
} }

View file

@ -5,42 +5,46 @@ import net.psforever.objects.serverobject.mount.Mountable
import net.psforever.objects.serverobject.structures.Amenity import net.psforever.objects.serverobject.structures.Amenity
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
import scala.annotation.unused
/** /**
* The behaviours corresponding to an Amenity that is marked as being CaptureTerminalAware * The behaviours corresponding to an Amenity that is marked as being CaptureTerminalAware
* @see CaptureTerminalAware * @see CaptureTerminalAware
*/ */
trait CaptureTerminalAwareBehavior { trait CaptureTerminalAwareBehavior {
def CaptureTerminalAwareObject : Amenity with CaptureTerminalAware def CaptureTerminalAwareObject: Amenity with CaptureTerminalAware
val captureTerminalAwareBehaviour: Receive = { val captureTerminalAwareBehaviour: Receive = {
case CaptureTerminalAwareBehavior.TerminalStatusChanged(terminal, isResecured) => case CaptureTerminalAwareBehavior.TerminalStatusChanged(terminal, true) =>
isResecured match { captureTerminalIsResecured(terminal)
case true => ; // CC is resecured
case false => // CC is hacked
// Remove seated occupants for mountables
CaptureTerminalAwareObject match {
case mountable: Mountable =>
val guid = mountable.GUID case CaptureTerminalAwareBehavior.TerminalStatusChanged(terminal, _) =>
val zone = mountable.Zone captureTerminalIsHacked(terminal)
val zoneId = zone.id }
val events = zone.VehicleEvents
mountable.Seats.values.zipWithIndex.foreach { protected def captureTerminalIsResecured(@unused terminal: CaptureTerminal): Unit = { /* intentionally blank */ }
case (seat, seat_num) =>
seat.occupant match { protected def captureTerminalIsHacked(@unused terminal: CaptureTerminal): Unit = {
case Some(player) => // Remove seated occupants for mountables
seat.unmount(player) CaptureTerminalAwareObject match {
player.VehicleSeated = None case mountable: Mountable =>
if (player.HasGUID) { val guid = mountable.GUID
events ! VehicleServiceMessage(zoneId, VehicleAction.KickPassenger(player.GUID, seat_num, true, guid)) val zone = mountable.Zone
} val zoneId = zone.id
case None => ; val events = zone.VehicleEvents
} mountable.Seats.values.zipWithIndex.foreach {
} case (seat, seat_num) =>
case _ => 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 _ => ()
}
} }
} }

View file

@ -2,16 +2,35 @@
package net.psforever.objects.serverobject.turret package net.psforever.objects.serverobject.turret
import net.psforever.objects.equipment.JammableUnit 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.terminals.capture.CaptureTerminalAware
import net.psforever.objects.serverobject.turret.auto.AutomatedTurret
import net.psforever.objects.sourcing.SourceEntry
import net.psforever.types.Vector3 import net.psforever.types.Vector3
class FacilityTurret(tDef: FacilityTurretDefinition) class FacilityTurret(tDef: FacilityTurretDefinition)
extends Amenity extends Amenity
with WeaponTurret with AutomatedTurret
with JammableUnit with JammableUnit
with CaptureTerminalAware { 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 def Definition: FacilityTurretDefinition = tDef
} }
@ -27,9 +46,6 @@ object FacilityTurret {
new FacilityTurret(tDef) new FacilityTurret(tDef)
} }
final case class RechargeAmmo()
final case class WeaponDischarged()
import akka.actor.ActorContext import akka.actor.ActorContext
/** /**

View file

@ -1,190 +1,175 @@
// Copyright (c) 2017 PSForever // Copyright (c) 2017 PSForever
package net.psforever.objects.serverobject.turret package net.psforever.objects.serverobject.turret
import akka.actor.Cancellable import net.psforever.objects.{GlobalDefinitions, Player, Tool}
import net.psforever.objects.{Default, GlobalDefinitions, Player, Tool} import net.psforever.objects.equipment.Ammo
import net.psforever.objects.equipment.{Ammo, JammableMountedWeapons}
import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject}
import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior} import net.psforever.objects.serverobject.damage.Damageable
import net.psforever.objects.serverobject.affinity.FactionAffinityBehavior
import net.psforever.objects.serverobject.damage.{Damageable, DamageableWeaponTurret}
import net.psforever.objects.serverobject.hackable.GenericHackables import net.psforever.objects.serverobject.hackable.GenericHackables
import net.psforever.objects.serverobject.hackable.GenericHackables.getTurretUpgradeTime import net.psforever.objects.serverobject.mount.Mountable
import net.psforever.objects.serverobject.repair.{AmenityAutoRepair, RepairableWeaponTurret} import net.psforever.objects.serverobject.repair.AmenityAutoRepair
import net.psforever.objects.serverobject.structures.PoweredAmenityControl import net.psforever.objects.serverobject.structures.{Building, PoweredAmenityControl}
import net.psforever.objects.serverobject.terminals.capture.CaptureTerminalAwareBehavior 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.objects.vital.interaction.DamageResult
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.packet.game.ChangeFireModeMessage
import net.psforever.services.local.{LocalAction, LocalServiceMessage} import net.psforever.services.Service
import net.psforever.services.vehicle.support.TurretUpgrader
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
import net.psforever.types.{BailType, PlanetSideEmpire, PlanetSideGUID}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
/** /**
* An `Actor` that handles messages being dispatched to a specific `MannedTurret`.<br> * A control agency that handles messages being dispatched to a specific `FacilityTurret`.
* <br> * These turrets are attached specifically to surface-level facilities and field towers.
* Mounted turrets have only slightly different entry requirements than a normal vehicle * @param turret the `FacilityTurret` object being governed
* because they encompass both faction-specific facility turrets */
* and faction-blind cavern sentry turrets.
*
* @param turret the `MannedTurret` object being governed
*/
class FacilityTurretControl(turret: FacilityTurret) class FacilityTurretControl(turret: FacilityTurret)
extends PoweredAmenityControl extends PoweredAmenityControl
with FactionAffinityBehavior.Check
with MountableBehavior
with DamageableWeaponTurret
with RepairableWeaponTurret
with AmenityAutoRepair with AmenityAutoRepair
with JammableMountedWeapons with MountableTurretControl
with AutomatedTurretBehavior
with AffectedByAutomaticTurretFire
with CaptureTerminalAwareBehavior { with CaptureTerminalAwareBehavior {
def TurretObject: FacilityTurret = turret
def FactionObject: FacilityTurret = turret def FactionObject: FacilityTurret = turret
def MountableObject: FacilityTurret = turret def MountableObject: FacilityTurret = turret
def JammableObject: FacilityTurret = turret def JammableObject: FacilityTurret = turret
def DamageableObject: FacilityTurret = turret def DamageableObject: FacilityTurret = turret
def RepairableObject: FacilityTurret = turret def RepairableObject: FacilityTurret = turret
def AutoRepairObject: FacilityTurret = turret def AutoRepairObject: FacilityTurret = turret
def AutomatedTurretObject: FacilityTurret = turret
def CaptureTerminalAwareObject: FacilityTurret = turret def CaptureTerminalAwareObject: FacilityTurret = turret
def AffectedObject: FacilityTurret = turret
// Used for timing ammo recharge for vanu turrets in caves private var testToResetToDefaultFireMode: Boolean = false
var weaponAmmoRechargeTimer: Cancellable = Default.Cancellable
AutomaticOperation = true
override def postStop(): Unit = { override def postStop(): Unit = {
super.postStop() super.postStop()
damageableWeaponTurretPostStop() damageableWeaponTurretPostStop()
automaticTurretPostStop()
stopAutoRepair() stopAutoRepair()
} }
def commonBehavior: Receive = private val upgradeableTurret: Receive = {
checkBehavior case CommonMessages.Use(player, Some((item: Tool, upgradeValue: Int)))
.orElse(jammableBehavior) if player.Faction == TurretObject.Faction &&
.orElse(dismountBehavior) item.Definition == GlobalDefinitions.nano_dispenser && item.AmmoType == Ammo.upgrade_canister &&
.orElse(takesDamage) item.Magazine > 0 && TurretObject.Seats.values.forall(!_.isOccupied) =>
.orElse(canBeRepairedByNanoDispenser) TurretUpgrade.values.find(_.id == upgradeValue).foreach {
.orElse(autoRepairBehavior) case upgrade
.orElse(captureTerminalAwareBehaviour) 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 commonBehavior
.orElse(mountBehavior) .orElse(mountBehavior)
.orElse(upgradeableTurret)
.orElse { .orElse {
case CommonMessages.Use(player, Some((item: Tool, upgradeValue: Int))) case _ => ()
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 _ => ;
} }
def unpoweredStateLogic: Receive = override def unpoweredStateLogic: Receive =
commonBehavior commonBehavior
.orElse { .orElse {
case _ => ; case _ => ()
} }
override protected def mountTest( override protected def mountTest(
obj: PlanetSideServerObject with Mountable, obj: PlanetSideServerObject with Mountable,
seatNumber: Int, seatNumber: Int,
player: Player): Boolean = { player: Player): Boolean = {
(!turret.Definition.FactionLocked || player.Faction == obj.Faction) && !obj.Destroyed && !turret.isUpgrading || super.mountTest(obj, seatNumber, player) &&
System.currentTimeMillis() - getTurretUpgradeTime >= 1500L (!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 = { override protected def DamageAwareness(target: Damageable.Target, cause: DamageResult, amount: Any) : Unit = {
tryAutoRepair() 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) super.DamageAwareness(target, cause, amount)
} }
override protected def DestructionAwareness(target: Damageable.Target, cause: DamageResult): Unit = { override protected def DestructionAwareness(target: Damageable.Target, cause: DamageResult): Unit = {
tryAutoRepair()
super.DestructionAwareness(target, cause) super.DestructionAwareness(target, cause)
val zone = target.Zone tryAutoRepair()
val zoneId = zone.id AutomaticOperation = false
val events = zone.AvatarEvents selfReportingCleanUp()
val tguid = target.GUID
events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(tguid, 50, 1))
events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(tguid, 51, 1))
} }
override def PerformRepairs(target : Damageable.Target, amount : Int) : Int = { override def PerformRepairs(target : Damageable.Target, amount : Int) : Int = {
val newHealth = super.PerformRepairs(target, amount) 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() stopAutoRepair()
} }
newHealth 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 = { override def tryAutoRepair() : Boolean = {
isPowered && super.tryAutoRepair() isPowered && super.tryAutoRepair()
} }
def powerTurnOffCallback(): Unit = { def powerTurnOffCallback(): Unit = {
stopAutoRepair() stopAutoRepair()
AutomaticOperation = false
//kick all occupants //kick all occupants
val guid = turret.GUID val guid = TurretObject.GUID
val zone = turret.Zone val zone = TurretObject.Zone
val zoneId = zone.id val zoneId = zone.id
val events = zone.VehicleEvents val events = zone.VehicleEvents
turret.Seats.values.foreach(seat => TurretObject.Seats.values.foreach(seat =>
seat.occupant match { seat.occupant match {
case Some(player) => case Some(player) =>
seat.unmount(player) seat.unmount(player)
@ -192,12 +177,159 @@ class FacilityTurretControl(turret: FacilityTurret)
if (player.HasGUID) { if (player.HasGUID) {
events ! VehicleServiceMessage(zoneId, VehicleAction.KickPassenger(player.GUID, 4, unk2=false, guid)) events ! VehicleServiceMessage(zoneId, VehicleAction.KickPassenger(player.GUID, 4, unk2=false, guid))
} }
case None => ; case None => ()
} }
) )
} }
def powerTurnOnCallback(): Unit = { def powerTurnOnCallback(): Unit = {
tryAutoRepair() 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
}
} }
} }

View file

@ -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))
}
}

View file

@ -1,15 +1,72 @@
// Copyright (c) 2017 PSForever // Copyright (c) 2017 PSForever
package net.psforever.objects.serverobject.turret package net.psforever.objects.serverobject.turret
import net.psforever.objects.PlanetSideGameObject
import net.psforever.objects.definition.{ObjectDefinition, ToolDefinition, WithShields} import net.psforever.objects.definition.{ObjectDefinition, ToolDefinition, WithShields}
import net.psforever.objects.vehicles.{MountableWeaponsDefinition, Turrets} import net.psforever.objects.vehicles.{MountableWeaponsDefinition, Turrets}
import net.psforever.objects.vital.resistance.ResistanceProfileMutators import net.psforever.objects.vital.resistance.ResistanceProfileMutators
import net.psforever.objects.vital.resolution.DamageResistanceModel import net.psforever.objects.vital.resolution.DamageResistanceModel
import scala.collection.mutable 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 trait TurretDefinition
extends MountableWeaponsDefinition extends MountableWeaponsDefinition
@ -25,11 +82,12 @@ trait TurretDefinition
/** can only be mounted by owning faction when `true` */ /** can only be mounted by owning faction when `true` */
private var factionLocked: Boolean = true private var factionLocked: Boolean = true
/** creates internal ammunition reserves that can not become depleted /** creates internal ammunition reserves that can not become depleted */
* see `MannedTurret.TurretAmmoBox` for details
*/
private var hasReserveAmmunition: Boolean = false private var hasReserveAmmunition: Boolean = false
/** */
private var turretAutomation: Option[Automation] = None
def WeaponPaths: mutable.HashMap[Int, mutable.HashMap[TurretUpgrade.Value, ToolDefinition]] = weaponPaths def WeaponPaths: mutable.HashMap[Int, mutable.HashMap[TurretUpgrade.Value, ToolDefinition]] = weaponPaths
def FactionLocked: Boolean = factionLocked def FactionLocked: Boolean = factionLocked
@ -45,4 +103,15 @@ trait TurretDefinition
hasReserveAmmunition = reserved hasReserveAmmunition = reserved
ReserveAmmunition 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
}
} }

View file

@ -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
}
}

View file

@ -6,7 +6,7 @@ import net.psforever.objects.definition.{AmmoBoxDefinition, ToolDefinition}
import net.psforever.objects.equipment.EquipmentSlot import net.psforever.objects.equipment.EquipmentSlot
import net.psforever.objects.inventory.{Container, GridInventory} import net.psforever.objects.inventory.{Container, GridInventory}
import net.psforever.objects.serverobject.affinity.FactionAffinity 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 import net.psforever.objects.vehicles.MountableWeapons
trait WeaponTurret trait WeaponTurret
@ -14,10 +14,6 @@ trait WeaponTurret
with MountableWeapons with MountableWeapons
with Container { with Container {
_: PlanetSideGameObject => _: 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 /** may or may not have inaccessible inventory space
* see `ReserveAmmunition` in the definition * see `ReserveAmmunition` in the definition
*/ */
@ -84,25 +80,27 @@ trait WeaponTurret
} }
object WeaponTurret { object WeaponTurret {
/** /**
* Use the `*Definition` that was provided to this object to initialize its fields and settings. * Use the definition that was provided to this object to initialize its fields and settings.
* @see `{object}.LoadDefinition` * @see `WeaponTurret.LoadDefinition(WeaponTurret, TurretDefinition)`
* @param turret the `MannedTurret` being initialized * @param turret turret being initialized
*/ */
def LoadDefinition(turret: WeaponTurret): WeaponTurret = { def LoadDefinition(turret: WeaponTurret): WeaponTurret = {
LoadDefinition(turret, turret.Definition) LoadDefinition(turret, turret.Definition)
} }
/** /**
* Use the `*Definition` that was provided to this object to initialize its fields and settings. * Use the definition that was provided to this object to initialize its fields and settings.
* A default definition is provided to be used. * @see `WeaponTurret.LoadDefinition(WeaponTurret)`
* @see `{object}.LoadDefinition` * @param turret turret being initialized
* @param turret the `MannedTurret` being initialized * @param tdef object's specific definition
* @param tdef the object definition
*/ */
def LoadDefinition(turret: WeaponTurret, tdef: TurretDefinition): WeaponTurret = { def LoadDefinition(turret: WeaponTurret, tdef: TurretDefinition): WeaponTurret = {
import net.psforever.objects.equipment.EquipmentSize.BaseTurretWeapon 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 //create weapons; note the class
turret.weapons = tdef.WeaponPaths turret.weapons = tdef.WeaponPaths
.map({ .map({
@ -160,17 +158,18 @@ class TurretWeapon(
Upgrade 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. * The original quantity of ammunition does not change.
* @param adef ammunition definition * @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 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
} }

View file

@ -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())
}
}
}

View file

@ -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
}

View file

@ -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.<br>
* 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.<br>
* 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.<br>
* 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.<br>
* 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<br>
* 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.<br>
* Very important: do not return the owner of the entity that caused the damage;
* return the cause of the damage.<br>
* 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
}
}
}

View file

@ -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)

View file

@ -6,7 +6,6 @@ import net.psforever.objects.serverobject.hackable.Hackable
import net.psforever.objects.serverobject.hackable.Hackable.HackInfo import net.psforever.objects.serverobject.hackable.Hackable.HackInfo
import net.psforever.objects.serverobject.mount.Mountable import net.psforever.objects.serverobject.mount.Mountable
import net.psforever.objects.serverobject.structures.Amenity import net.psforever.objects.serverobject.structures.Amenity
import net.psforever.objects.sourcing
import net.psforever.objects.vital.resistance.ResistanceProfile import net.psforever.objects.vital.resistance.ResistanceProfile
import net.psforever.objects.vital.{Vitality, VitalityDefinition} import net.psforever.objects.vital.{Vitality, VitalityDefinition}
import net.psforever.types.{PlanetSideEmpire, Vector3} import net.psforever.types.{PlanetSideEmpire, Vector3}
@ -57,7 +56,7 @@ object AmenitySource {
Nil, Nil,
SourceEntry(obj.Owner), SourceEntry(obj.Owner),
hackData, hackData,
sourcing.UniqueAmenity(obj.Zone.Number, obj.GUID, obj.Position) UniqueAmenity(obj)
) )
amenity.copy(occupants = obj match { amenity.copy(occupants = obj match {
case o: Mountable => case o: Mountable =>

View file

@ -12,6 +12,12 @@ final case class UniqueBuilding(
building_guid: PlanetSideGUID building_guid: PlanetSideGUID
) extends SourceUniqueness ) extends SourceUniqueness
object UniqueBuilding {
def apply(obj: Building): UniqueBuilding = {
UniqueBuilding(obj.Zone.Number, obj.GUID)
}
}
final case class BuildingSource( final case class BuildingSource(
private val obj_def: BuildingDefinition, private val obj_def: BuildingDefinition,
Faction: PlanetSideEmpire.Value, Faction: PlanetSideEmpire.Value,
@ -35,7 +41,7 @@ object BuildingSource {
b.Position, b.Position,
b.Orientation, b.Orientation,
b.latticeConnectedFacilityBenefits(), b.latticeConnectedFacilityBenefits(),
UniqueBuilding(b.Zone.Number, b.GUID) UniqueBuilding(b)
) )
} }
} }

View file

@ -52,13 +52,7 @@ object DeployableSource {
obj.Position, obj.Position,
obj.Orientation, obj.Orientation,
occupants, occupants,
UniqueDeployable( UniqueDeployable(obj)
obj.History.headOption match {
case Some(entry) => entry.time
case None => 0L
},
obj.OriginalOwnerName.getOrElse("none")
)
) )
} }
} }

View file

@ -10,6 +10,12 @@ import net.psforever.types.{PlanetSideEmpire, Vector3}
final case class UniqueObject(objectId: Int) extends SourceUniqueness final case class UniqueObject(objectId: Int) extends SourceUniqueness
object UniqueObject {
def apply(obj: PlanetSideGameObject): UniqueObject = {
UniqueObject(obj.Definition.ObjectId)
}
}
final case class ObjectSource( final case class ObjectSource(
private val obj_def: ObjectDefinition, private val obj_def: ObjectDefinition,
Faction: PlanetSideEmpire.Value, Faction: PlanetSideEmpire.Value,
@ -44,7 +50,7 @@ object ObjectSource {
obj.Position, obj.Position,
obj.Orientation, obj.Orientation,
obj.Velocity, obj.Velocity,
UniqueObject(obj.Definition.ObjectId) UniqueObject(obj)
) )
} }
} }

View file

@ -16,6 +16,12 @@ final case class UniquePlayer(
faction: PlanetSideEmpire.Value faction: PlanetSideEmpire.Value
) extends SourceUniqueness ) extends SourceUniqueness
object UniquePlayer {
def apply(obj: Player): UniquePlayer = {
UniquePlayer(obj.CharId, obj.Name, obj.Sex, obj.Faction)
}
}
final case class PlayerSource( final case class PlayerSource(
Definition: AvatarDefinition, Definition: AvatarDefinition,
ExoSuit: ExoSuitType.Value, ExoSuit: ExoSuitType.Value,
@ -121,7 +127,6 @@ object PlayerSource {
*/ */
def inSeat(player: Player, source: SourceEntry, seatNumber: Int): PlayerSource = { def inSeat(player: Player, source: SourceEntry, seatNumber: Int): PlayerSource = {
val exosuit = player.ExoSuit val exosuit = player.ExoSuit
val faction = player.Faction
val avatar = player.avatar val avatar = player.avatar
PlayerSource( PlayerSource(
player.Definition, player.Definition,
@ -134,10 +139,10 @@ object PlayerSource {
player.Velocity, player.Velocity,
player.Crouching, player.Crouching,
player.Jumping, player.Jumping,
ExoSuitDefinition.Select(exosuit, faction), ExoSuitDefinition.Select(exosuit, player.Faction),
avatar.bep, avatar.bep,
progress = tokenLife, progress = tokenLife,
UniquePlayer(player.CharId, player.Name, player.Sex, faction) UniquePlayer(player)
) )
} }

View file

@ -11,8 +11,6 @@ import net.psforever.objects.vital.resistance.ResistanceProfile
import net.psforever.objects.{PlanetSideGameObject, Player, TurretDeployable, Vehicle} import net.psforever.objects.{PlanetSideGameObject, Player, TurretDeployable, Vehicle}
import net.psforever.types.{PlanetSideEmpire, Vector3} import net.psforever.types.{PlanetSideEmpire, Vector3}
trait SourceUniqueness
trait SourceEntry { trait SourceEntry {
def Name: String def Name: String
def Definition: ObjectDefinition with VitalityDefinition def Definition: ObjectDefinition with VitalityDefinition

View file

@ -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)
}
}
}

View file

@ -33,15 +33,9 @@ object TurretSource {
val position = obj.Position val position = obj.Position
val identifer = obj match { val identifer = obj match {
case o: TurretDeployable => case o: TurretDeployable =>
UniqueDeployable( UniqueDeployable(o)
o.History.headOption match {
case Some(entry) => entry.time
case None => 0L
},
o.OriginalOwnerName.getOrElse("none")
)
case o: FacilityTurret => case o: FacilityTurret =>
UniqueAmenity(o.Zone.Number, o.GUID, position) UniqueAmenity(o)
case o => case o =>
throw new IllegalArgumentException(s"was given ${o.Actor.toString()} when only wanted to model turrets") throw new IllegalArgumentException(s"was given ${o.Actor.toString()} when only wanted to model turrets")
} }

View file

@ -1,6 +1,7 @@
// Copyright (c) 2023 PSForever // Copyright (c) 2023 PSForever
package net.psforever.objects.sourcing package net.psforever.objects.sourcing
import net.psforever.objects.serverobject.structures.Amenity
import net.psforever.types.{PlanetSideGUID, Vector3} import net.psforever.types.{PlanetSideGUID, Vector3}
final case class UniqueAmenity( final case class UniqueAmenity(
@ -8,3 +9,9 @@ final case class UniqueAmenity(
guid: PlanetSideGUID, guid: PlanetSideGUID,
position: Vector3 position: Vector3
) extends SourceUniqueness ) extends SourceUniqueness
object UniqueAmenity {
def apply(obj: Amenity): UniqueAmenity = {
UniqueAmenity(obj.Zone.Number, obj.GUID, obj.Position)
}
}

View file

@ -1,7 +1,21 @@
// Copyright (c) 2023 PSForever // Copyright (c) 2023 PSForever
package net.psforever.objects.sourcing package net.psforever.objects.sourcing
import net.psforever.objects.ce.Deployable
final case class UniqueDeployable( final case class UniqueDeployable(
spawnTime: Long, spawnTime: Long,
originalOwnerName: String originalOwnerName: String
) extends SourceUniqueness ) 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")
)
}
}

View file

@ -8,6 +8,18 @@ import net.psforever.types.{DriveState, PlanetSideEmpire, Vector3}
final case class UniqueVehicle(spawnTime: Long, originalOwnerName: String) extends SourceUniqueness 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( final case class VehicleSource(
Definition: VehicleDefinition, Definition: VehicleDefinition,
Faction: PlanetSideEmpire.Value, Faction: PlanetSideEmpire.Value,
@ -46,13 +58,7 @@ object VehicleSource {
None, None,
Nil, Nil,
obj.Definition.asInstanceOf[ResistanceProfile], obj.Definition.asInstanceOf[ResistanceProfile],
UniqueVehicle( UniqueVehicle(obj)
obj.History.headOption match {
case Some(entry) => entry.time
case None => 0L
},
obj.OriginalOwnerName.getOrElse("none")
)
) )
//shallow information that references the existing source entry //shallow information that references the existing source entry
vehicle.copy( vehicle.copy(

View file

@ -2,17 +2,9 @@
package net.psforever.objects.vehicles package net.psforever.objects.vehicles
import net.psforever.objects.Vehicle import net.psforever.objects.Vehicle
import net.psforever.objects.ballistics.{Projectile, ProjectileQuality} import net.psforever.objects.serverobject.mount.{InteractWithRadiationCloudsSeatedInEntity, RadiationInMountableInteraction}
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.zones.blockmap.SectorPopulation import net.psforever.objects.zones.blockmap.SectorPopulation
import net.psforever.objects.zones.{InteractsWithZone, Zone, ZoneInteraction, ZoneInteractionType} import net.psforever.objects.zones.InteractsWithZone
import net.psforever.types.PlanetSideGUID
case object RadiationInVehicleInteraction extends ZoneInteractionType
/** /**
* This game entity may infrequently test whether it may interact with radiation cloud projectiles * 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( class InteractWithRadiationCloudsSeatedInVehicle(
private val obj: Vehicle, private val obj: Vehicle,
val range: Float override val range: Float
) extends ZoneInteraction { ) extends InteractWithRadiationCloudsSeatedInEntity(obj, range) {
/**
* 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
/** /**
* Drive into a radiation cloud and all the vehicle's occupants suffer the consequences. * 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 sector the portion of the block map being tested
* @param target the fixed element in this test * @param target the fixed element in this test
*/ */
override def interaction(sector: SectorPopulation, target: InteractsWithZone): Unit = { override def interaction(sector: SectorPopulation, target: InteractsWithZone): Unit = {
val position = target.Position super.interaction(sector, target)
//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 _ => ;
}
}
obj.CargoHolds obj.CargoHolds
.values .values
.collect { .collect {
case hold if hold.isOccupied => case hold if hold.isOccupied =>
val target = hold.occupant.get val target = hold.occupant.get
target.interaction().find { _.Type == RadiationInVehicleInteraction } match { target
case Some(func) => func.interaction(sector, target) .interaction()
case _ => ; .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()
} }
} }

View file

@ -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.damage.{AggravatedBehavior, DamageableVehicle}
import net.psforever.objects.serverobject.environment._ import net.psforever.objects.serverobject.environment._
import net.psforever.objects.serverobject.hackable.GenericHackables 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.repair.RepairableVehicle
import net.psforever.objects.serverobject.terminals.Terminal 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.sourcing.{PlayerSource, SourceEntry, VehicleSource}
import net.psforever.objects.vehicles._ import net.psforever.objects.vehicles._
import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult} import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult}
@ -57,7 +58,8 @@ class VehicleControl(vehicle: Vehicle)
with ContainableBehavior with ContainableBehavior
with AggravatedBehavior with AggravatedBehavior
with RespondsToZoneEnvironment with RespondsToZoneEnvironment
with CargoBehavior { with CargoBehavior
with AffectedByAutomaticTurretFire {
//make control actors belonging to utilities when making control actor belonging to vehicle //make control actors belonging to utilities when making control actor belonging to vehicle
vehicle.Utilities.foreach { case (_, util) => util.Setup } vehicle.Utilities.foreach { case (_, util) => util.Setup }
@ -70,6 +72,7 @@ class VehicleControl(vehicle: Vehicle)
def ContainerObject: Vehicle = vehicle def ContainerObject: Vehicle = vehicle
def InteractiveObject: Vehicle = vehicle def InteractiveObject: Vehicle = vehicle
def CargoObject: Vehicle = vehicle def CargoObject: Vehicle = vehicle
def AffectedObject: Vehicle = vehicle
SetInteraction(EnvironmentAttribute.Water, doInteractingWithWater) SetInteraction(EnvironmentAttribute.Water, doInteractingWithWater)
SetInteraction(EnvironmentAttribute.Lava, doInteractingWithLava) SetInteraction(EnvironmentAttribute.Lava, doInteractingWithLava)
@ -114,6 +117,7 @@ class VehicleControl(vehicle: Vehicle)
.orElse(containerBehavior) .orElse(containerBehavior)
.orElse(environmentBehavior) .orElse(environmentBehavior)
.orElse(cargoBehavior) .orElse(cargoBehavior)
.orElse(takeAutomatedDamage)
.orElse { .orElse {
case Vehicle.Ownership(None) => case Vehicle.Ownership(None) =>
LoseOwnership() LoseOwnership()
@ -243,7 +247,7 @@ class VehicleControl(vehicle: Vehicle)
commonEnabledBehavior commonEnabledBehavior
.orElse { .orElse {
case VehicleControl.RadiationTick => case VehicleControl.RadiationTick =>
vehicle.interaction().find { _.Type == RadiationInVehicleInteraction } match { vehicle.interaction().find { _.Type == RadiationInMountableInteraction } match {
case Some(func) => func.interaction(vehicle.getInteractionSector(), vehicle) case Some(func) => func.interaction(vehicle.getInteractionSector(), vehicle)
case _ => ; case _ => ;
} }

View file

@ -327,7 +327,7 @@ trait InGameHistory {
if (target eq this) { if (target eq this) {
None None
} else { } else {
val uniqueTarget = SourceEntry(target).unique val uniqueTarget = SourceUniqueness(target)
(target.GetContribution(), contributionInheritance.get(uniqueTarget)) match { (target.GetContribution(), contributionInheritance.get(uniqueTarget)) match {
case (Some(in), Some(curr)) => case (Some(in), Some(curr)) =>
val end = curr.end val end = curr.end
@ -395,6 +395,6 @@ object InGameHistory {
def ContributionFrom(target: PlanetSideGameObject with FactionAffinity with InGameHistory): Option[Contribution] = { def ContributionFrom(target: PlanetSideGameObject with FactionAffinity with InGameHistory): Option[Contribution] = {
target target
.GetContribution() .GetContribution()
.collect { case events => Contribution(SourceEntry(target).unique, events) } .collect { case events => Contribution(SourceUniqueness(target), events) }
} }
} }

View file

@ -411,7 +411,7 @@ object GamePacketOpcode extends Enumeration {
case 0x59 => noDecoder(UnknownMessage89) case 0x59 => noDecoder(UnknownMessage89)
case 0x5a => game.DelayedPathMountMsg.decode case 0x5a => game.DelayedPathMountMsg.decode
case 0x5b => game.OrbitalShuttleTimeMsg.decode case 0x5b => game.OrbitalShuttleTimeMsg.decode
case 0x5c => noDecoder(AIDamage) case 0x5c => game.AIDamage.decode
case 0x5d => game.DeployObjectMessage.decode case 0x5d => game.DeployObjectMessage.decode
case 0x5e => game.FavoritesRequest.decode case 0x5e => game.FavoritesRequest.decode
case 0x5f => noDecoder(FavoritesResponse) case 0x5f => noDecoder(FavoritesResponse)

View file

@ -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]
}

View file

@ -81,13 +81,13 @@ object CommonFieldData extends Marshallable[CommonFieldData] {
CommonFieldData(faction, false, false, false, None, false, None, None, PlanetSideGUID(0)) CommonFieldData(faction, false, false, false, None, false, None, None, PlanetSideGUID(0))
def apply(faction: PlanetSideEmpire.Value, unk: Int): CommonFieldData = 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 = 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 = 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( def apply(
faction: PlanetSideEmpire.Value, faction: PlanetSideEmpire.Value,

View file

@ -15,13 +15,14 @@ import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
import scala.concurrent.Future import scala.concurrent.Future
import scala.concurrent.duration._ import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global
class TurretUpgrader extends SupportActor[TurretUpgrader.Entry] { class TurretUpgrader extends SupportActor[TurretUpgrader.Entry] {
var task: Cancellable = Default.Cancellable var task: Cancellable = Default.Cancellable
var list: List[TurretUpgrader.Entry] = List() 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 = { def Test(entry1: TurretUpgrader.Entry, entry2: TurretUpgrader.Entry): Boolean = {
entry1.obj == entry2.obj && entry1.zone == entry2.zone && entry1.obj.GUID == entry2.obj.GUID 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 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) TurretUpgrader.Entry(obj, zone, upgrade, duration)
def InclusionTest(entry: TurretUpgrader.Entry): Boolean = entry.obj.isInstanceOf[FacilityTurret] def InclusionTest(entry: TurretUpgrader.Entry): Boolean = entry.obj.isInstanceOf[FacilityTurret]
@ -89,7 +90,6 @@ class TurretUpgrader extends SupportActor[TurretUpgrader.Entry] {
task.cancel() task.cancel()
if (list.nonEmpty) { if (list.nonEmpty) {
val short_timeout: FiniteDuration = math.max(1, list.head.duration - (now - list.head.time)).milliseconds 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()) 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 upgrade = entry.upgrade
val guid = zone.GUID val guid = zone.GUID
val turretGUID = target.GUID val turretGUID = target.GUID
target.setMiddleOfUpgrade(true)
//kick all occupying players for duration of conversion //kick all occupying players for duration of conversion
target.Seats.values target.Seats.values
.filter { _.isOccupied } .filter { _.isOccupied }
@ -160,7 +161,7 @@ class TurretUpgrader extends SupportActor[TurretUpgrader.Entry] {
if (tplayer.HasGUID) { if (tplayer.HasGUID) {
context.parent ! VehicleServiceMessage( context.parent ! VehicleServiceMessage(
zoneId, 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) } .filterNot { box => newBoxes.exists(_ eq box) }
.map(box => GUIDTask.unregisterEquipment(guid, box)) .map(box => GUIDTask.unregisterEquipment(guid, box))
.toList .toList
import scala.concurrent.ExecutionContext.Implicits.global
val newBoxesTask = TaskBundle( val newBoxesTask = TaskBundle(
new StraightforwardTask() { new StraightforwardTask() {
private val localFunc: () => Unit = FinishUpgradingTurret(entry) private val localFunc: () => Unit = FinishUpgradingTurret(entry)
@ -189,22 +189,25 @@ class TurretUpgrader extends SupportActor[TurretUpgrader.Entry] {
.map(box => GUIDTask.registerEquipment(guid, box)) .map(box => GUIDTask.registerEquipment(guid, box))
.toList .toList
) )
TaskWorkflow.execute(TaskBundle( val mainTask = TaskWorkflow.execute(TaskBundle(
new StraightforwardTask() { new StraightforwardTask() {
private val tasks = oldBoxesTask private val tasks = oldBoxesTask
def action(): Future[Any] = { def action(): Future[Any] = {
tasks.foreach { TaskWorkflow.execute } tasks.foreach(TaskWorkflow.execute)
Future(this) Future(this)
} }
}, },
newBoxesTask 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. * 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 * @return all of the internal ammunition objects
*/ */
def AllMountedWeaponMagazines(target: MountedWeapons): Iterable[AmmoBox] = { def AllMountedWeaponMagazines(target: MountedWeapons): Iterable[AmmoBox] = {
@ -224,11 +227,10 @@ class TurretUpgrader extends SupportActor[TurretUpgrader.Entry] {
val target = entry.obj.asInstanceOf[FacilityTurret] val target = entry.obj.asInstanceOf[FacilityTurret]
val zone = entry.zone val zone = entry.zone
trace(s"Wall turret finished ${target.Upgrade} upgrade") trace(s"Wall turret finished ${target.Upgrade} upgrade")
target.ConfirmUpgrade(entry.upgrade)
val targetGUID = target.GUID val targetGUID = target.GUID
if (target.Health > 0) { if (target.Health > 0) {
target.Weapons target.Weapons
.map({ case (index: Int, slot: EquipmentSlot) => (index, slot.Equipment) }) .map { case (index: Int, slot: EquipmentSlot) => (index, slot.Equipment) }
.collect { .collect {
case (index, Some(tool: Tool)) => case (index, Some(tool: Tool)) =>
context.parent ! VehicleServiceMessage( 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 Downgrade()
final case class UpgradeCompleted(targetGuid: PlanetSideGUID)
private def Similarity(entry1: TurretUpgrader.Entry, entry2: TurretUpgrader.Entry): Boolean = { private def Similarity(entry1: TurretUpgrader.Entry, entry2: TurretUpgrader.Entry): Boolean = {
entry1.obj == entry2.obj && entry1.zone == entry2.zone && entry1.obj.GUID == entry2.obj.GUID entry1.obj == entry2.obj && entry1.zone == entry2.zone && entry1.obj.GUID == entry2.obj.GUID
} }

View file

@ -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.capture.{CaptureTerminal, CaptureTerminalDefinition}
import net.psforever.objects.serverobject.terminals.implant.ImplantTerminalMech import net.psforever.objects.serverobject.terminals.implant.ImplantTerminalMech
import net.psforever.objects.serverobject.tube.SpawnTube 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.serverobject.zipline.ZipLinePath
import net.psforever.objects.sourcing.{DeployableSource, PlayerSource, TurretSource, VehicleSource} import net.psforever.objects.sourcing.{DeployableSource, PlayerSource, TurretSource, VehicleSource}
import net.psforever.objects.zones.{MapInfo, Zone, ZoneInfo, ZoneMap} import net.psforever.objects.zones.{MapInfo, Zone, ZoneInfo, ZoneMap}
@ -585,7 +585,7 @@ object Zones {
case _ => ; case _ => ;
} }
case "manned_turret" | "vanu_sentry_turret" => case "manned_turret" =>
zoneMap.addLocalObject( zoneMap.addLocalObject(
obj.guid, obj.guid,
FacilityTurret.Constructor( FacilityTurret.Constructor(
@ -596,6 +596,17 @@ object Zones {
) )
zoneMap.linkTurretToWeapon(obj.guid, turretWeaponGuid.getAndIncrement()) 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" => case "implant_terminal_mech" =>
zoneMap.addLocalObject( zoneMap.addLocalObject(
obj.guid, obj.guid,

View file

@ -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
}
}