radiator is turned off due to potential for server crashes; cerebus -> cerberus; turret kills name owner only when they are in the same zone; fewer chances for turrets to fire when they should not

This commit is contained in:
Fate-JH 2024-07-22 20:16:59 -04:00
parent a699c6c223
commit e5cde75e72
14 changed files with 110 additions and 81 deletions

View file

@ -76,6 +76,7 @@ add_property pulsar equiptime 600
add_property pulsar holstertime 600 add_property pulsar holstertime 600
add_property punisher equiptime 600 add_property punisher equiptime 600
add_property punisher holstertime 600 add_property punisher holstertime 600
add_property radiator allowed false
add_property r_shotgun equiptime 750 add_property r_shotgun equiptime 750
add_property r_shotgun holstertime 750 add_property r_shotgun holstertime 750
add_property remote_electronics_kit equiptime 500 add_property remote_electronics_kit equiptime 500

View file

@ -545,9 +545,9 @@ class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit
turret.Actor ! AutomatedTurretBehavior.ConfirmShot(target) turret.Actor ! AutomatedTurretBehavior.ConfirmShot(target)
Some(target) Some(target)
case turret: AutomatedTurret => case turret: AutomatedTurret with OwnableByPlayer =>
turret.Actor ! AutomatedTurretBehavior.ConfirmShot(target) turret.Actor ! AutomatedTurretBehavior.ConfirmShot(target)
HandleAIDamage(target, CompileAutomatedTurretDamageData(turret, turret.TurretOwner, projectileTypeId)) HandleAIDamage(target, CompileAutomatedTurretDamageData(turret, projectileTypeId))
Some(target) Some(target)
} }
} }
@ -558,9 +558,9 @@ class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit
case target: PlanetSideServerObject with FactionAffinity with Vitality => case target: PlanetSideServerObject with FactionAffinity with Vitality =>
sessionLogic.validObject(attackerGuid, decorator = "AIDamage/Attacker") sessionLogic.validObject(attackerGuid, decorator = "AIDamage/Attacker")
.collect { .collect {
case turret: AutomatedTurret if turret.Target.nonEmpty => case turret: AutomatedTurret with OwnableByPlayer if turret.Target.nonEmpty =>
//the turret must be shooting at something (else) first //the turret must be shooting at something (else) first
HandleAIDamage(target, CompileAutomatedTurretDamageData(turret, turret.TurretOwner, projectileTypeId)) HandleAIDamage(target, CompileAutomatedTurretDamageData(turret, projectileTypeId))
} }
Some(target) Some(target)
} }
@ -1268,14 +1268,17 @@ class WeaponAndProjectileLogic(val ops: WeaponAndProjectileOperations, implicit
} }
private def CompileAutomatedTurretDamageData( private def CompileAutomatedTurretDamageData(
turret: AutomatedTurret, turret: AutomatedTurret with OwnableByPlayer,
owner: SourceEntry,
projectileTypeId: Long projectileTypeId: Long
): Option[(AutomatedTurret, Tool, SourceEntry, ProjectileDefinition)] = { ): Option[(AutomatedTurret, Tool, SourceEntry, ProjectileDefinition)] = {
turret.Weapons turret.Weapons
.values .values
.flatMap { _.Equipment } .flatMap { _.Equipment }
.collect { case weapon: Tool => (turret, weapon, owner, weapon.Projectile) } .collect {
case weapon: Tool =>
val source = Deployables.AssignBlameTo(continent, turret.OwnerName, SourceEntry(turret))
(turret, weapon, source, weapon.Projectile)
}
.find { case (_, _, _, p) => p.ObjectId == projectileTypeId } .find { case (_, _, _, p) => p.ObjectId == projectileTypeId }
} }

View file

@ -6,6 +6,7 @@ import net.psforever.objects.avatar.{Avatar, Certification}
import scala.concurrent.duration._ import scala.concurrent.duration._
import net.psforever.objects.ce.{Deployable, DeployedItem} import net.psforever.objects.ce.{Deployable, DeployedItem}
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry}
import net.psforever.objects.zones.Zone import net.psforever.objects.zones.Zone
import net.psforever.packet.game._ import net.psforever.packet.game._
import net.psforever.types.PlanetSideGUID import net.psforever.types.PlanetSideGUID
@ -261,4 +262,48 @@ object Deployables {
} }
(sample intersect testDiff equals testDiff) && (sampleIntersect intersect testIntersect equals testIntersect) (sample intersect testDiff equals testDiff) && (sampleIntersect intersect testIntersect equals testIntersect)
} }
/**
* Find a player with a given name in this zone.
* The assumption is the player is the owner of a given deployable entity.
* If the player can not be found, the deployable entity can stand in as it's own owner.
* @param zone continent in which the player should be found;
* should be the same zone as the deployable, but not required
* @param nameOpt optional player's name
* @param deployableSource deployable entity
* @return discovered player as a reference
*/
def AssignBlameTo(zone: Zone, nameOpt: Option[String], deployableSource: SourceEntry): SourceEntry = {
zone
.Players
.find(a => nameOpt.contains(a.name))
.collect { a =>
val name = a.name
Deployables.AssignBlameToFrom(name, zone.LivePlayers)
.orElse(Deployables.AssignBlameToFrom(name, zone.Corpses))
.getOrElse {
val player = PlayerSource(name, deployableSource.Faction, deployableSource.Position) //might report minor inconsistencies, e.g., exo-suit type
player.copy(unique = player.unique.copy(charId = a.id), progress = a.scorecard.CurrentLife)
}
}
.getOrElse(deployableSource)
}
/**
* Find a player with a given name from this list of possible players.
* If the player is seated, attach a shallow copy of the mounting information.
* @param name player name
* @param blameList possible players in which to find the player name
* @return discovered player as a reference, or `None` if not found
*/
private def AssignBlameToFrom(name: String, blameList: List[Player]): Option[SourceEntry] = {
blameList
.find(_.Name.equals(name))
.map { player =>
PlayerSource
.mountableAndSeat(player)
.map { case (mount, seat) => PlayerSource.inSeat(player, mount, seat) }
.getOrElse { PlayerSource(player) }
}
}
} }

View file

@ -5,7 +5,7 @@ import akka.actor.{ActorContext, ActorRef, Props}
import net.psforever.objects.ce.{Deployable, DeployedItem} import net.psforever.objects.ce.{Deployable, DeployedItem}
import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.sourcing.{DeployableSource, PlayerSource, SourceEntry} import net.psforever.objects.sourcing.{DeployableSource, SourceEntry}
import net.psforever.objects.vital.Vitality import net.psforever.objects.vital.Vitality
import net.psforever.objects.vital.etc.TrippedMineReason import net.psforever.objects.vital.etc.TrippedMineReason
import net.psforever.objects.vital.interaction.DamageInteraction import net.psforever.objects.vital.interaction.DamageInteraction
@ -98,40 +98,8 @@ object MineDeployableControl {
private case class Triggered() private case class Triggered()
def trippedMineReason(mine: ExplosiveDeployable): TrippedMineReason = { def trippedMineReason(mine: ExplosiveDeployable): TrippedMineReason = {
lazy val deployableSource = DeployableSource(mine) val deployableSource = DeployableSource(mine)
val zone = mine.Zone val blame = Deployables.AssignBlameTo(mine.Zone, mine.OwnerName, deployableSource)
val ownerName = mine.OwnerName
val blame = zone
.Players
.find(a => ownerName.contains(a.name))
.collect { a =>
val name = a.name
assignBlameToFrom(name, zone.LivePlayers)
.orElse(assignBlameToFrom(name, zone.Corpses))
.getOrElse {
val player = PlayerSource(name, mine.Faction, mine.Position) //might report minor inconsistencies, e.g., exo-suit type
player.copy(unique = player.unique.copy(charId = a.id), progress = a.scorecard.CurrentLife)
}
}
.getOrElse(deployableSource)
TrippedMineReason(deployableSource, blame) TrippedMineReason(deployableSource, blame)
} }
/**
* Find a player with a given name from this list of possible players.
* If the player is seated, attach a shallow copy of the mounting information.
* @param name player name
* @param blameList possible players in which to find the player name
* @return discovered player as a reference, or `None` if not found
*/
private def assignBlameToFrom(name: String, blameList: List[Player]): Option[SourceEntry] = {
blameList
.find(_.Name.equals(name))
.map { player =>
PlayerSource
.mountableAndSeat(player)
.map { case (mount, seat) => PlayerSource.inSeat(player, mount, seat) }
.getOrElse { PlayerSource(player) }
}
}
} }

View file

@ -22,9 +22,9 @@ import scala.collection.mutable
* As deployables are added and removed, and tracked certifications are added and removed, * As deployables are added and removed, and tracked certifications are added and removed,
* these structures are updated to reflect proper count. * these structures are updated to reflect proper count.
* For example, the greatest number of spitfire turrets that can be placed is 15 (individual count) * For example, the greatest number of spitfire turrets that can be placed is 15 (individual count)
* and the greatest number of shadow turrets and cerebus turrets that can be placed is 5 each (individual counts) * and the greatest number of shadow turrets and cerberus turrets that can be placed is 5 each (individual counts)
* but the maximum number of small turrets that can be placed overall is only 15 (categorical count). * but the maximum number of small turrets that can be placed overall is only 15 (categorical count).
* Spitfire turrets, shadow turrets, and cerebus turrets are all included in the category of small turrets. * Spitfire turrets, shadow turrets, and cerberus turrets are all included in the category of small turrets.
*/ */
class DeployableToolbox { class DeployableToolbox {

View file

@ -114,7 +114,7 @@ object Deployable {
DeployedItem.jammer_mine.id -> DeployableIcon.DisruptorMine, DeployedItem.jammer_mine.id -> DeployableIcon.DisruptorMine,
DeployedItem.spitfire_turret.id -> DeployableIcon.SpitfireTurret, DeployedItem.spitfire_turret.id -> DeployableIcon.SpitfireTurret,
DeployedItem.spitfire_cloaked.id -> DeployableIcon.ShadowTurret, DeployedItem.spitfire_cloaked.id -> DeployableIcon.ShadowTurret,
DeployedItem.spitfire_aa.id -> DeployableIcon.CerebusTurret, DeployedItem.spitfire_aa.id -> DeployableIcon.cerberusTurret,
DeployedItem.motionalarmsensor.id -> DeployableIcon.MotionAlarmSensor, DeployedItem.motionalarmsensor.id -> DeployableIcon.MotionAlarmSensor,
DeployedItem.sensor_shield.id -> DeployableIcon.SensorDisruptor, DeployedItem.sensor_shield.id -> DeployableIcon.SensorDisruptor,
DeployedItem.tank_traps.id -> DeployableIcon.TRAP, DeployedItem.tank_traps.id -> DeployableIcon.TRAP,

View file

@ -10,7 +10,7 @@ object DeployedItem extends Enumeration {
final val jammer_mine = Value(420) //disruptor mine final val jammer_mine = Value(420) //disruptor mine
final val motionalarmsensor = Value(575) final val motionalarmsensor = Value(575)
final val sensor_shield = Value(752) //sensor disruptor final val sensor_shield = Value(752) //sensor disruptor
final val spitfire_aa = Value(819) //cerebus turret final val spitfire_aa = Value(819) //cerberus turret
final val spitfire_cloaked = Value(825) //shadow turret final val spitfire_cloaked = Value(825) //shadow turret
final val spitfire_turret = Value(826) final val spitfire_turret = Value(826)
final val tank_traps = Value(849) //trap final val tank_traps = Value(849) //trap

View file

@ -50,7 +50,7 @@ class UniqueNumberOps(
private val poolActors: Map[String, ActorRef] private val poolActors: Map[String, ActorRef]
) { ) {
/** The timeout used by all number pool `ask` messaging */ /** The timeout used by all number pool `ask` messaging */
private implicit val timeout = UniqueNumberOps.timeout private implicit val timeout: Timeout = UniqueNumberOps.timeout
/** /**
* The entry point for the entity GUID registration process. * The entry point for the entity GUID registration process.
@ -149,25 +149,26 @@ class UniqueNumberOps(
val localPool = pool val localPool = pool
val result = ask(pool, NumberPoolActor.GetAnyNumber())(timeout) val result = ask(pool, NumberPoolActor.GetAnyNumber())(timeout)
result.onComplete { result
case Success(NumberPoolActor.GiveNumber(number)) => .recover {
UniqueNumberOps.processRegisterResult( case ex: AskTimeoutException =>
localPromise, localPromise.failure(new RegisteringException(msg = s"did not register entity $localTarget in time", ex))
localTarget, }
localUns, .onComplete {
localPoolName, case Success(NumberPoolActor.GiveNumber(number)) =>
localPool, UniqueNumberOps.processRegisterResult(
number localPromise,
) localTarget,
case Success(NumberPoolActor.NoNumber(ex)) => localUns,
registrationProcessRetry(localPromise, ex, localTarget, localUns, localPools, localPoolName) localPoolName,
case msg => localPool,
UniqueNumberOps.log.warn(s"unexpected message during $localTarget's registration process - $msg") number
} )
result.recover { case Success(NumberPoolActor.NoNumber(ex)) =>
case ex: AskTimeoutException => registrationProcessRetry(localPromise, ex, localTarget, localUns, localPools, localPoolName)
localPromise.failure(new RegisteringException(msg = s"did not register entity $localTarget in time", ex)) case msg =>
} UniqueNumberOps.log.warn(s"unexpected message during $localTarget's registration process - $msg")
}
case None => case None =>
//do not log //do not log
@ -197,7 +198,7 @@ class UniqueNumberOps(
if (poolName.equals("generic")) { if (poolName.equals("generic")) {
promise.failure(new RegisteringException(msg = s"did not register entity $obj", exception)) promise.failure(new RegisteringException(msg = s"did not register entity $obj", exception))
} else { } else {
org.log4s.getLogger("UniqueNumberOps").warn(s"${exception.getLocalizedMessage()} - $poolName") UniqueNumberOps.log.warn(s"${exception.getLocalizedMessage()} - $poolName")
promise.completeWith(registrationProcess(obj, guid, pools, poolName = "generic")) promise.completeWith(registrationProcess(obj, guid, pools, poolName = "generic"))
} }
} }
@ -302,7 +303,7 @@ class UniqueNumberOps(
object UniqueNumberOps { object UniqueNumberOps {
private val log = org.log4s.getLogger private val log = org.log4s.getLogger
private implicit val timeout = Timeout(2.seconds) private implicit val timeout: Timeout = Timeout(4.seconds)
/** /**
* Final step of the object registration process. * Final step of the object registration process.
@ -431,7 +432,7 @@ class UniqueNumberSetup(
) extends Actor { ) extends Actor {
init() init()
final def receive: Receive = { case _ => ; } final def receive: Receive = { case _ => () }
def init(): UniqueNumberOps = { def init(): UniqueNumberOps = {
new UniqueNumberOps(hub, poolActorConversionFunc(context, hub)) new UniqueNumberOps(hub, poolActorConversionFunc(context, hub))

View file

@ -106,7 +106,7 @@ class FacilityTurretControl(turret: FacilityTurret)
override protected def tryMount(obj: PlanetSideServerObject with Mountable, seatNumber: Int, player: Player): Boolean = { override protected def tryMount(obj: PlanetSideServerObject with Mountable, seatNumber: Int, player: Player): Boolean = {
val originalAutoState = AutomaticOperation val originalAutoState = AutomaticOperation
AutomaticOperation = false //turn off AutomaticOperation = false //turn off
if (JammableObject.Jammed) { if (AutomaticOperationPossible && JammableObject.Jammed) {
val zone = TurretObject.Zone val zone = TurretObject.Zone
AutomatedTurretBehavior.stopTracking(zone, zone.id, TurretObject.GUID) //can not recover lost jamming aggro AutomatedTurretBehavior.stopTracking(zone, zone.id, TurretObject.GUID) //can not recover lost jamming aggro
} }
@ -220,6 +220,15 @@ class FacilityTurretControl(turret: FacilityTurret)
!TurretObject.isUpgrading !TurretObject.isUpgrading
} }
override def AutomaticOperationPossible: Boolean = {
super.AutomaticOperationPossible &&
(turret.Owner match {
case b: Building if b.CaptureTerminal.isEmpty => false
case b: Building => !b.CaptureTerminal.exists(_.Definition == GlobalDefinitions.secondary_capture)
case _ => false
})
}
private def primaryWeaponFireModeOnly(): Unit = { private def primaryWeaponFireModeOnly(): Unit = {
if (testToResetToDefaultFireMode) { if (testToResetToDefaultFireMode) {
val zone = TurretObject.Zone val zone = TurretObject.Zone
@ -295,7 +304,7 @@ class FacilityTurretControl(turret: FacilityTurret)
override def TryJammerEffectActivate(target: Any, cause: DamageResult): Unit = { override def TryJammerEffectActivate(target: Any, cause: DamageResult): Unit = {
super.TryJammerEffectActivate(target, cause) super.TryJammerEffectActivate(target, cause)
if (JammableObject.Jammed) { if (AutomaticOperationPossible && AutomaticOperation && JammableObject.Jammed) {
AutomaticOperation = false AutomaticOperation = false
if (!MountableObject.Seats.values.exists(_.isOccupied) && AutomatedTurretObject.Definition.AutoFire.exists(_.retaliatoryDelay > 0)) { if (!MountableObject.Seats.values.exists(_.isOccupied) && AutomatedTurretObject.Definition.AutoFire.exists(_.retaliatoryDelay > 0)) {
//look in direction of cause of jamming //look in direction of cause of jamming

View file

@ -80,6 +80,8 @@ trait AutomatedTurretBehavior {
Actor.emptyBehavior Actor.emptyBehavior
} }
def AutomaticOperationPossible: Boolean = autoStats.isDefined
def AutomaticOperation: Boolean = automaticOperation def AutomaticOperation: Boolean = automaticOperation
/** /**
@ -111,7 +113,7 @@ trait AutomatedTurretBehavior {
* @return `true`, if it would be possible for automated behavior to become operational; * @return `true`, if it would be possible for automated behavior to become operational;
* `false`, otherwise * `false`, otherwise
*/ */
protected def AutomaticOperationFunctionalityChecks: Boolean = { autoStats.isDefined } protected def AutomaticOperationFunctionalityChecks: Boolean = AutomaticOperationPossible
/** /**
* The last time weapons fire from the turret was confirmed by this control agency. * The last time weapons fire from the turret was confirmed by this control agency.

View file

@ -26,7 +26,7 @@ object DeploymentAction extends Enumeration {
object DeployableIcon extends Enumeration { object DeployableIcon extends Enumeration {
type Type = Value type Type = Value
val Boomer, HEMine, MotionAlarmSensor, SpitfireTurret, RouterTelepad, DisruptorMine, ShadowTurret, CerebusTurret, val Boomer, HEMine, MotionAlarmSensor, SpitfireTurret, RouterTelepad, DisruptorMine, ShadowTurret, cerberusTurret,
TRAP, AegisShieldGenerator, FieldTurret, SensorDisruptor = Value TRAP, AegisShieldGenerator, FieldTurret, SensorDisruptor = Value
implicit val codec: Codec[DeployableIcon.Value] = PacketHelpers.createEnumerationCodec(this, uint4L) implicit val codec: Codec[DeployableIcon.Value] = PacketHelpers.createEnumerationCodec(this, uint4L)

View file

@ -32,7 +32,7 @@ import scodec.codecs._
* `86 - max spitfire turrets`<br> * `86 - max spitfire turrets`<br>
* `87 - max motion sensors`<br> * `87 - max motion sensors`<br>
* `88 - max shadow turrets`<br> * `88 - max shadow turrets`<br>
* `89 - max cerebus turrets`<br> * `89 - max cerberus turrets`<br>
* `90 - max Aegis shield generators`<br> * `90 - max Aegis shield generators`<br>
* `91 - max TRAPs`<br> * `91 - max TRAPs`<br>
* `92 - max OMFTs`<br> * `92 - max OMFTs`<br>
@ -43,7 +43,7 @@ import scodec.codecs._
* `97 - spitfire turrets`<br> * `97 - spitfire turrets`<br>
* `98 - motion sensors`<br> * `98 - motion sensors`<br>
* `99 - shadow turrets`<br> * `99 - shadow turrets`<br>
* `100 - cerebus turrets`<br> * `100 - cerberus turrets`<br>
* `101 - Aegis shield generators`<br> * `101 - Aegis shield generators`<br>
* `102 - TRAPSs`<br> * `102 - TRAPSs`<br>
* `103 - OMFTs`<br> * `103 - OMFTs`<br>

View file

@ -138,14 +138,14 @@ class TurretDeployableTest extends Specification {
DeployedItem.portable_manned_turret_nc.id, DeployedItem.portable_manned_turret_nc.id,
DeployedItem.portable_manned_turret_vs.id DeployedItem.portable_manned_turret_vs.id
).foreach(id => { ).foreach(id => {
try { new TurretDeployableDefinition(id) } try { new TurretDeployableDefinition(id) { } }
catch { case _: Exception => ko } catch { case _: Exception => ko }
}) })
ok ok
} }
"define (invalid object)" in { "define (invalid object)" in {
new TurretDeployableDefinition(5) must throwA[NoSuchElementException] //wrong object id altogether new TurretDeployableDefinition(objectId = 5) { } must throwA[NoSuchElementException] //wrong object id altogether
} }
"construct" in { "construct" in {
@ -212,7 +212,7 @@ class DeployableMake extends Specification {
} }
} }
"construct a cerebus turret" in { "construct a cerberus turret" in {
val func = Deployables.Make(DeployedItem.spitfire_aa) val func = Deployables.Make(DeployedItem.spitfire_aa)
func() match { func() match {
case obj: TurretDeployable if obj.Definition == GlobalDefinitions.spitfire_aa => ok case obj: TurretDeployable if obj.Definition == GlobalDefinitions.spitfire_aa => ok

View file

@ -819,13 +819,13 @@ class DeployableToolboxTest extends Specification {
val obj = new DeployableToolbox val obj = new DeployableToolbox
obj.Initialize(Set(CombatEngineering)) obj.Initialize(Set(CombatEngineering))
val cerebus = new TurretDeployable(GlobalDefinitions.spitfire_aa) //cerebus turret val cerberus = new TurretDeployable(GlobalDefinitions.spitfire_aa) //cerberus turret
obj.Valid(cerebus) mustEqual false obj.Valid(cerberus) mustEqual false
obj.CountDeployable(DeployedItem.spitfire_aa).productIterator.toList mustEqual List(0, 0) obj.CountDeployable(DeployedItem.spitfire_aa).productIterator.toList mustEqual List(0, 0)
obj.UpdateMaxCounts(Set(CombatEngineering, AdvancedEngineering)) obj.UpdateMaxCounts(Set(CombatEngineering, AdvancedEngineering))
obj.Valid(cerebus) mustEqual true obj.Valid(cerberus) mustEqual true
obj.CountDeployable(DeployedItem.spitfire_aa).productIterator.toList mustEqual List(0, 5) obj.CountDeployable(DeployedItem.spitfire_aa).productIterator.toList mustEqual List(0, 5)
} }