Merge branch 'master' into environment-redo

This commit is contained in:
Fate-JH 2024-03-02 23:39:22 -05:00 committed by GitHub
commit 76e48c8bba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
71 changed files with 3342 additions and 747 deletions

View file

@ -1,6 +1,6 @@
# PSForever Server [![Build Status](https://travis-ci.org/psforever/PSF-LoginServer.svg?branch=master)](https://travis-ci.com/psforever/PSF-LoginServer) [![Code coverage](https://codecov.io/gh/psforever/PSF-LoginServer/coverage.svg?branch=master)](https://codecov.io/gh/psforever/PSF-LoginServer/) [![Documentation](https://img.shields.io/badge/documentation-master-lightgrey)](https://psforever.github.io/docs/master/index.html)
<img src="https://psforever.net/index_files/logo_crop.png" align="left" title="PSForever" width="120">
<img src="https://www.psforever.net/res/logo.png" align="left" title="PSForever" width="120">
Welcome to the recreated login and world servers for PlanetSide 1. We are a community of players and developers who took
it upon ourselves to preserve PlanetSide 1's unique gameplay and history _forever_.

View file

@ -12566,7 +12566,7 @@
"AbsY": 4508.811,
"AbsZ": 62.3072777,
"Yaw": 0.0,
"GUID": 1006,
"GUID": 1007,
"MapID": null,
"IsChildObject": true
},
@ -12579,7 +12579,7 @@
"AbsY": 4508.811,
"AbsZ": 82.30728,
"Yaw": 0.0,
"GUID": 1007,
"GUID": 1008,
"MapID": null,
"IsChildObject": true
},
@ -12592,7 +12592,7 @@
"AbsY": 4159.456,
"AbsZ": 46.40945,
"Yaw": 5.0,
"GUID": 1008,
"GUID": 1006,
"MapID": null,
"IsChildObject": true
},

View file

@ -7,9 +7,11 @@ import akka.actor.typed.scaladsl.{ActorContext, Behaviors, StashBuffer}
import akka.actor.typed.scaladsl.adapter._
import net.psforever.actors.zone.ZoneActor
import net.psforever.objects.sourcing.PlayerSource
import net.psforever.objects.zones.ZoneInfo
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import scala.collection.mutable
import scala.annotation.unused
import scala.collection.{Seq, mutable}
import scala.concurrent.ExecutionContextExecutor
import scala.concurrent.duration._
//
@ -204,7 +206,8 @@ class ChatActor(
Behaviors
.receiveMessagePartial[Command] {
case SetSession(newSession) =>
active(newSession, chatService,cluster)
this.session = Some(newSession)
active(newSession, chatService, cluster)
case JoinChannel(channel) =>
chatService ! ChatService.JoinChannel(chatServiceAdapter, session, channel)
@ -398,133 +401,114 @@ class ChatActor(
case (U_CMT_ZONEROTATE, _, contents) if gmCommandAllowed =>
cluster ! InterstellarClusterService.CavernRotation(CavernRotationService.HurryNextRotation)
/** Messages starting with ! are custom chat commands */
case (_, _, contents) if contents.startsWith("!") &&
customCommandMessages(message, session, chatService, cluster, gmCommandAllowed) => ;
case (CMT_CAPTUREBASE, _, contents) if gmCommandAllowed =>
val args = contents.split(" ").filter(_ != "")
val (faction, factionPos): (PlanetSideEmpire.Value, Option[Int]) = args.zipWithIndex
.map { case (factionName, pos) => (factionName.toLowerCase, pos) }
.flatMap {
case ("tr", pos) => Some(PlanetSideEmpire.TR, pos)
case ("nc", pos) => Some(PlanetSideEmpire.NC, pos)
case ("vs", pos) => Some(PlanetSideEmpire.VS, pos)
case ("none", pos) => Some(PlanetSideEmpire.NEUTRAL, pos)
case ("bo", pos) => Some(PlanetSideEmpire.NEUTRAL, pos)
case ("neutral", pos) => Some(PlanetSideEmpire.NEUTRAL, pos)
case _ => None
}
.headOption match {
case Some((isFaction, pos)) => (isFaction, Some(pos))
case None => (session.player.Faction, None)
}
val (buildingsOption, buildingPos): (Option[Seq[Building]], Option[Int]) = args.zipWithIndex.flatMap {
case (_, pos) if factionPos.isDefined && factionPos.get == pos => None
case ("all", pos) =>
Some(
Some(
session.zone.Buildings
.filter {
case (_, building) => building.CaptureTerminal.isDefined
}
.values
.toSeq
),
Some(pos)
)
case (name: String, pos) =>
session.zone.Buildings.find {
case (_, building) => name.equalsIgnoreCase(building.Name) && building.CaptureTerminal.isDefined
} match {
case Some((_, building)) => Some(Some(Seq(building)), Some(pos))
case None =>
try {
// check if we have a timer
name.toInt
val buffer = contents.split(" ").filterNot(_ == "").take(3)
//walk through the param buffer
val (foundFacilities, foundFacilitiesTag, factionBuffer) = firstParam(session, buffer, captureBaseParamFacilities)
val (foundFaction, foundFactionTag, timerBuffer) = firstParam(session, factionBuffer, captureBaseParamFaction)
val (foundTimer, foundTimerTag, _) = firstParam(session, timerBuffer, captureBaseParamTimer)
//resolve issues with the initial params
var facilityError: Int = 0
var factionError: Boolean = false
var timerError: Boolean = false
var usageMessage: Boolean = false
val resolvedFacilities = foundFacilities
.orElse {
if (foundFacilitiesTag.nonEmpty) {
if (foundFaction.isEmpty) {
/* /capturebase <bad_facility> OR /capturebase <bad_facility> <no_faction> */
//malformed facility tag error
facilityError = 2
None
} else if (!foundFacilitiesTag.contains("curr")) { //did we do this next check already
/* /capturebase <faction>, potentially */
val buildings = captureBaseCurrSoi(session)
if (buildings.nonEmpty) {
//convert facilities to faction
Some(buildings.toSeq)
} else {
//no facilities error
facilityError = 1
None
} catch {
case _: Throwable =>
Some(None, Some(pos))
}
}
}.headOption match {
case Some((buildings, pos)) => (buildings, pos)
case None => (None, None)
}
val (timerOption, timerPos): (Option[Int], Option[Int]) = args.zipWithIndex.flatMap {
case (_, pos)
if factionPos.isDefined && factionPos.get == pos || buildingPos.isDefined && buildingPos.get == pos =>
None
case (timer: String, pos) =>
try {
val t = timer.toInt // TODO what is the timer format supposed to be?
Some(Some(t), Some(pos))
} catch {
case _: Throwable =>
Some(None, Some(pos))
}
}.headOption match {
case Some((timer, posOption)) => (timer, posOption)
case None => (None, None)
}
(factionPos, buildingPos, timerPos, buildingsOption, timerOption) match {
case // [[<empire>|none [<timer>]]
(Some(0), None, Some(1), None, Some(_)) | (Some(0), None, None, None, None) |
(None, None, None, None, None) |
// [<building name> [<empire>|none [timer]]]
(None | Some(1), Some(0), None, Some(_), None) | (Some(1), Some(0), Some(2), Some(_), Some(_)) |
// [all [<empire>|none]]
(Some(1) | None, Some(0), None, Some(_), None) =>
val buildings: Seq[Building] = buildingsOption.getOrElse(
session.zone.Buildings.values.filter { building =>
building.PlayersInSOI.exists { soiPlayer =>
session.player.CharId == soiPlayer.CharId
}
}.toSeq
)
buildings foreach { building =>
// TODO implement timer
val terminal = building.CaptureTerminal.get
building.Actor ! BuildingActor.SetFaction(faction)
building.Actor ! BuildingActor.AmenityStateChange(terminal, Some(false))
// clear any previous hack via "resecure"
if (building.CaptureTerminalIsHacked) {
building.Zone.LocalEvents ! LocalServiceMessage(terminal.Zone.id,LocalAction.ResecureCaptureTerminal(terminal, PlayerSource.Nobody))
} else {
//no facilities error
facilityError = 1
None
}
// push any updates this might cause to clients
building.Zone.actor ! ZoneActor.ZoneMapUpdate()
} else {
//no params; post command usage reminder
usageMessage = true
None
}
case (_, Some(0), _, None, _) =>
sessionActor ! SessionActor.SendResponse(
ChatMsg(
UNK_229,
wideContents=true,
"",
s"\\#FF4040ERROR - \'${args(0)}\' is not a valid building name.",
}
val resolvedFaction = foundFaction
.orElse {
if (resolvedFacilities.nonEmpty) {
/* /capturebase <facility> OR /capturebase <facility> <timer> */
if (foundFactionTag.isEmpty || foundTimer.nonEmpty) {
//convert facilities to OUR PLAYER'S faction
Some(session.player.Faction)
} else {
//malformed faction tag error
factionError = true
None
)
)
case (Some(0), _, Some(1), _, None) | (Some(1), Some(0), Some(2), _, None) =>
sessionActor ! SessionActor.SendResponse(
ChatMsg(
UNK_229,
wideContents=true,
"",
s"\\#FF4040ERROR - \'${args(timerPos.get)}\' is not a valid timer value.",
None
)
)
}
} else {
//incorrect params; already posted an error message
None
}
}
val resolvedTimer = foundTimer
.orElse {
//todo stop command execution? post command usage reminder?
if (resolvedFaction.nonEmpty && foundTimerTag.nonEmpty) {
/* /capturebase <?> <?> <bad_timer> */
//malformed timer tag error
timerError = true
None
} else {
//eh
Some(1)
}
}
//evaluate results
(resolvedFacilities, resolvedFaction, resolvedTimer) match {
case (Some(buildings), Some(faction), Some(_)) =>
buildings.foreach { building =>
//TODO implement timer
val terminal = building.CaptureTerminal.get
val zone = building.Zone
val zoneActor = zone.actor
val buildingActor = building.Actor
//clear any previous hack
if (building.CaptureTerminalIsHacked) {
zone.LocalEvents ! LocalServiceMessage(
zone.id,
LocalAction.ResecureCaptureTerminal(terminal, PlayerSource.Nobody)
)
}
//push any updates this might cause
zoneActor ! ZoneActor.ZoneMapUpdate()
//convert faction affiliation
buildingActor ! BuildingActor.SetFaction(faction)
buildingActor ! BuildingActor.AmenityStateChange(terminal, Some(false))
//push for map updates again
zoneActor ! ZoneActor.ZoneMapUpdate()
}
case _ =>
sessionActor ! SessionActor.SendResponse(
message.copy(messageType = UNK_229, contents = "@CMT_CAPTUREBASE_usage")
)
if (usageMessage) {
sessionActor ! SessionActor.SendResponse(
message.copy(messageType = UNK_229, contents = "@CMT_CAPTUREBASE_usage")
)
} else {
val msg = if (facilityError == 1) { "can not contextually determine building target" }
else if (facilityError == 2) { s"\'${foundFacilitiesTag.get}\' is not a valid building name" }
else if (factionError) { s"\'${foundFactionTag.get}\' is not a valid faction designation" }
else if (timerError) { s"\'${foundTimerTag.get}\' is not a valid timer value" }
else { "malformed params; check usage" }
sessionActor ! SessionActor.SendResponse(ChatMsg(UNK_229, wideContents=true, "", s"\\#FF4040ERROR - $msg", None))
}
}
case (CMT_GMBROADCAST | CMT_GMBROADCAST_NC | CMT_GMBROADCAST_VS | CMT_GMBROADCAST_TR, _, _)
@ -549,26 +533,6 @@ class ChatActor(
ChatChannel.Default()
)
case (_, "tr", contents) =>
sessionActor ! SessionActor.SendResponse(
ZonePopulationUpdateMessage(4, 414, 138, contents.toInt, 138, contents.toInt / 2, 138, 0, 138, 0)
)
case (_, "nc", contents) =>
sessionActor ! SessionActor.SendResponse(
ZonePopulationUpdateMessage(4, 414, 138, 0, 138, contents.toInt, 138, contents.toInt / 3, 138, 0)
)
case (_, "vs", contents) =>
sessionActor ! SessionActor.SendResponse(
ZonePopulationUpdateMessage(4, 414, 138, contents.toInt * 2, 138, 0, 138, contents.toInt, 138, 0)
)
case (_, "bo", contents) =>
sessionActor ! SessionActor.SendResponse(
ZonePopulationUpdateMessage(4, 414, 138, 0, 138, 0, 138, 0, 138, contents.toInt)
)
case (CMT_OPEN, _, _) if !session.player.silenced =>
chatService ! ChatService.Message(
session,
@ -915,6 +879,30 @@ class ChatActor(
)
}
case (_, "tr", contents) =>
sessionActor ! SessionActor.SendResponse(
ZonePopulationUpdateMessage(4, 414, 138, contents.toInt, 138, contents.toInt / 2, 138, 0, 138, 0)
)
case (_, "nc", contents) =>
sessionActor ! SessionActor.SendResponse(
ZonePopulationUpdateMessage(4, 414, 138, 0, 138, contents.toInt, 138, contents.toInt / 3, 138, 0)
)
case (_, "vs", contents) =>
sessionActor ! SessionActor.SendResponse(
ZonePopulationUpdateMessage(4, 414, 138, contents.toInt * 2, 138, 0, 138, contents.toInt, 138, 0)
)
case (_, "bo", contents) =>
sessionActor ! SessionActor.SendResponse(
ZonePopulationUpdateMessage(4, 414, 138, 0, 138, 0, 138, 0, 138, contents.toInt)
)
/** Messages starting with ! are custom chat commands */
case (_, _, contents) if contents.startsWith("!") &&
customCommandMessages(message, session, chatService, cluster, gmCommandAllowed) => ;
case _ =>
log.warn(s"Unhandled chat message $message")
}
@ -1386,4 +1374,83 @@ class ChatActor(
false
}
}
private def captureBaseParamFacilities(session: Session, token: Option[String]): Option[Seq[Building]] = {
token.collect {
case "curr" =>
val list = captureBaseCurrSoi(session)
if (list.nonEmpty) {
Some(list.toSeq)
} else {
None
}
case "all" =>
val list = session.zone.Buildings.values.filter(_.CaptureTerminal.isDefined)
if (list.nonEmpty) {
Some(list.toSeq)
} else {
None
}
case name =>
val trueName = ZoneInfo
.values
.find(_.id.equals(session.zone.id))
.flatMap { info =>
info.aliases
.facilities
.collectFirst { case (key, internalName) if key.equalsIgnoreCase(name) => internalName }
}
.getOrElse(name)
session.zone.Buildings
.values
.find {
building => trueName.equalsIgnoreCase(building.Name) && building.CaptureTerminal.isDefined
}
.map(b => Seq(b))
}
.flatten
}
private def captureBaseCurrSoi(session: Session): Iterable[Building] = {
val charId = session.player.CharId
session.zone.Buildings.values.filter { building =>
building.PlayersInSOI.exists(_.CharId == charId)
}
}
private def captureBaseParamFaction(@unused session: Session, token: Option[String]): Option[PlanetSideEmpire.Value] = {
token.collect {
case faction =>
faction.toLowerCase() match {
case "tr" => Some(PlanetSideEmpire.TR)
case "nc" => Some(PlanetSideEmpire.NC)
case "vs" => Some(PlanetSideEmpire.VS)
case "none" => Some(PlanetSideEmpire.NEUTRAL)
case "bo" => Some(PlanetSideEmpire.NEUTRAL)
case "neutral" => Some(PlanetSideEmpire.NEUTRAL)
case _ => None
}
}.flatten
}
private def captureBaseParamTimer(@unused session: Session, token: Option[String]): Option[Int] = {
token.collect {
case n if n.forall(Character.isDigit) => n.toInt
}
}
private def firstParam[T](
session: Session,
buffer: Array[String],
func: (Session, Option[String])=>Option[T]
): (Option[T], Option[String], Array[String]) = {
val tokenOpt = buffer.headOption
val valueOpt = func(session, tokenOpt)
val outBuffer = if (valueOpt.nonEmpty) {
buffer.drop(1)
} else {
buffer
}
(valueOpt, tokenOpt, outBuffer)
}
}

View file

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

View file

@ -350,6 +350,7 @@ class SessionAvatarHandlers(
sendResponse(ObjectDeleteMessage(objGuid, unk1=0))
TaskWorkflow.execute(GUIDTask.unregisterEquipment(continent.GUID, obj))
}
drops.foreach(item => sendResponse(ObjectDeleteMessage(item.obj.GUID, unk1=0)))
//redraw
if (maxhand) {
TaskWorkflow.execute(HoldNewEquipmentUp(player)(

View file

@ -4,6 +4,7 @@ package net.psforever.actors.session.support
import akka.actor.typed.scaladsl.adapter._
import akka.actor.{ActorContext, ActorRef, Cancellable, typed}
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource}
import net.psforever.objects.vital.etc.SuicideReason
import net.psforever.objects.zones.blockmap.{SectorGroup, SectorPopulation}
import scala.collection.mutable
@ -882,13 +883,7 @@ class SessionData(
case (None, _, _) => ()
case (Some(us: PlanetSideServerObject with Vitality with FactionAffinity), PlanetSideGUID(0), _) =>
if (collisionHistory.get(us.Actor) match {
case Some(lastCollision) if curr - lastCollision <= 1000L =>
false
case _ =>
collisionHistory.put(us.Actor, curr)
true
}) {
if (updateCollisionHistoryForTarget(us, curr)) {
if (!bailProtectStatus) {
handleDealingDamage(
us,
@ -901,40 +896,26 @@ class SessionData(
}
}
case (Some(us: Vehicle), _, Some(victim: SensorDeployable)) =>
collisionBetweenVehicleAndFragileDeployable(us, ppos, victim, tpos, velocity - tv, fallHeight, curr)
case (Some(us: Vehicle), _, Some(victim: TurretDeployable)) if victim.Seats.isEmpty =>
collisionBetweenVehicleAndFragileDeployable(us, ppos, victim, tpos, velocity - tv, fallHeight, curr)
case (
Some(us: PlanetSideServerObject with Vitality with FactionAffinity), _,
Some(victim: PlanetSideServerObject with Vitality with FactionAffinity)
) =>
if (collisionHistory.get(victim.Actor) match {
case Some(lastCollision) if curr - lastCollision <= 1000L =>
false
case _ =>
collisionHistory.put(victim.Actor, curr)
true
}) {
if (updateCollisionHistoryForTarget(victim, curr)) {
val usSource = SourceEntry(us)
val victimSource = SourceEntry(victim)
//we take damage from the collision
if (!bailProtectStatus) {
handleDealingDamage(
us,
DamageInteraction(
usSource,
CollisionWithReason(CollisionReason(velocity - tv, fallHeight, us.DamageModel), victimSource),
ppos
)
)
performCollisionWithSomethingDamage(us, usSource, ppos, victimSource, fallHeight, velocity - tv)
}
//get dealt damage from our own collision (no protection)
collisionHistory.put(us.Actor, curr)
handleDealingDamage(
victim,
DamageInteraction(
victimSource,
CollisionWithReason(CollisionReason(tv - velocity, 0, victim.DamageModel), usSource),
tpos
)
)
performCollisionWithSomethingDamage(victim, victimSource, tpos, usSource, fallHeight = 0f, tv - velocity)
}
case _ => ()
@ -2202,7 +2183,7 @@ class SessionData(
/**
* Calculate the amount of damage to be dealt to an active `target`
* using the information reconstructed from a `Resolvedprojectile`
* using the information reconstructed from a `ResolvedProjectile`
* and affect the `target` in a synchronized manner.
* The active `target` and the target of the `DamageResult` do not have be the same.
* While the "tell" for being able to sustain damage is an entity of type `Vitality`,
@ -2836,6 +2817,59 @@ class SessionData(
}
}
private def updateCollisionHistoryForTarget(
target: PlanetSideServerObject with Vitality with FactionAffinity,
curr: Long
): Boolean = {
collisionHistory.get(target.Actor) match {
case Some(lastCollision) if curr - lastCollision <= 1000L =>
false
case _ =>
collisionHistory.put(target.Actor, curr)
true
}
}
private def collisionBetweenVehicleAndFragileDeployable(
vehicle: Vehicle,
vehiclePosition: Vector3,
smallDeployable: Deployable,
smallDeployablePosition: Vector3,
velocity: Vector3,
fallHeight: Float,
collisionTime: Long
): Unit = {
if (updateCollisionHistoryForTarget(smallDeployable, collisionTime)) {
val smallDeployableSource = SourceEntry(smallDeployable)
//vehicle takes damage from the collision (ignore bail protection in this case)
performCollisionWithSomethingDamage(vehicle, SourceEntry(vehicle), vehiclePosition, smallDeployableSource, fallHeight, velocity)
//deployable gets absolutely destroyed
collisionHistory.put(vehicle.Actor, collisionTime)
handleDealingDamage(
smallDeployable,
DamageInteraction(smallDeployableSource, SuicideReason(), smallDeployablePosition)
)
}
}
private def performCollisionWithSomethingDamage(
target: PlanetSideServerObject with Vitality with FactionAffinity,
targetSource: SourceEntry,
targetPosition: Vector3,
victimSource: SourceEntry,
fallHeight: Float,
velocity: Vector3
): Unit = {
handleDealingDamage(
target,
DamageInteraction(
targetSource,
CollisionWithReason(CollisionReason(velocity, fallHeight, target.DamageModel), victimSource),
targetPosition
)
)
}
def failWithError(error: String): Unit = {
log.error(error)
middlewareActor ! MiddlewareActor.Teardown()

View file

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

View file

@ -2,7 +2,10 @@
package net.psforever.actors.session.support
import akka.actor.{ActorContext, typed}
import net.psforever.objects.definition.ProjectileDefinition
import net.psforever.objects.serverobject.turret.auto.{AutomatedTurret, AutomatedTurretBehavior}
import net.psforever.objects.zones.Zoning
import net.psforever.objects.serverobject.turret.VanuSentry
import net.psforever.objects.zones.exp.ToDatabase
import scala.collection.mutable
@ -51,8 +54,7 @@ private[support] class WeaponAndProjectileOperations(
private[support] var shotsWhileDead: Int = 0
private val projectiles: Array[Option[Projectile]] =
Array.fill[Option[Projectile]](Projectile.rangeUID - Projectile.baseUID)(None)
private var zoningOpt: Option[ZoningOperations] = None
def zoning: ZoningOperations = zoningOpt.orNull
/* packets */
def handleWeaponFire(pkt: WeaponFireMessage): Unit = {
@ -430,6 +432,55 @@ private[support] class WeaponAndProjectileOperations(
}
}
def handleAIDamage(pkt: AIDamage): Unit = {
val AIDamage(targetGuid, attackerGuid, projectileTypeId, _, _) = pkt
(continent.GUID(player.VehicleSeated) match {
case Some(tobj: PlanetSideServerObject with FactionAffinity with Vitality with OwnableByPlayer)
if tobj.GUID == targetGuid &&
tobj.OwnerGuid.contains(player.GUID) =>
//deployable turrets
Some(tobj)
case Some(tobj: PlanetSideServerObject with FactionAffinity with Vitality with Mountable)
if tobj.GUID == targetGuid &&
tobj.Seats.values.flatMap(_.occupants.map(_.GUID)).toSeq.contains(player.GUID) =>
//facility turrets, etc.
Some(tobj)
case _
if player.GUID == targetGuid =>
//player avatars
Some(player)
case _ =>
None
}).collect {
case target: AutomatedTurret.Target =>
sessionData.validObject(attackerGuid, decorator = "AIDamage/AutomatedTurret")
.collect {
case turret: AutomatedTurret if turret.Target.isEmpty =>
turret.Actor ! AutomatedTurretBehavior.ConfirmShot(target)
Some(target)
case turret: AutomatedTurret =>
turret.Actor ! AutomatedTurretBehavior.ConfirmShot(target)
HandleAIDamage(target, CompileAutomatedTurretDamageData(turret, turret.TurretOwner, projectileTypeId))
Some(target)
}
}
.orElse {
//occasionally, something that is not technically a turret's natural target may be attacked
sessionData.validObject(targetGuid, decorator = "AIDamage/Target")
.collect {
case target: PlanetSideServerObject with FactionAffinity with Vitality =>
sessionData.validObject(attackerGuid, decorator = "AIDamage/Attacker")
.collect {
case turret: AutomatedTurret if turret.Target.nonEmpty =>
//the turret must be shooting at something (else) first
HandleAIDamage(target, CompileAutomatedTurretDamageData(turret, turret.TurretOwner, projectileTypeId))
}
Some(target)
}
}
}
/* support code */
def HandleWeaponFireOperations(
@ -519,11 +570,6 @@ private[support] class WeaponAndProjectileOperations(
)
continent.Projectile ! ZoneProjectile.Add(player.GUID, qualityprojectile)
}
obj match {
case turret: FacilityTurret if turret.Definition == GlobalDefinitions.vanu_sentry_turret =>
turret.Actor ! FacilityTurret.WeaponDischarged()
case _ => ()
}
} else {
log.warn(
s"WeaponFireMessage: ${player.Name}'s ${tool.Definition.Name} projectile is too far from owner position at time of discharge ($distanceToOwner > $acceptableDistanceToOwner); suspect"
@ -1174,6 +1220,10 @@ private[support] class WeaponAndProjectileOperations(
}
private def fireStateStartMountedMessages(itemGuid: PlanetSideGUID): Unit = {
sessionData.findContainedEquipment()._1.collect {
case turret: FacilityTurret if continent.map.cavern =>
turret.Actor ! VanuSentry.ChangeFireStart
}
continent.VehicleEvents ! VehicleServiceMessage(
continent.id,
VehicleAction.ChangeFireState_Start(player.GUID, itemGuid)
@ -1236,6 +1286,10 @@ private[support] class WeaponAndProjectileOperations(
}
private def fireStateStopMountedMessages(itemGuid: PlanetSideGUID): Unit = {
sessionData.findContainedEquipment()._1.collect {
case turret: FacilityTurret if continent.map.cavern =>
turret.Actor ! VanuSentry.ChangeFireStop
}
continent.VehicleEvents ! VehicleServiceMessage(
continent.id,
VehicleAction.ChangeFireState_Stop(player.GUID, itemGuid)
@ -1366,6 +1420,7 @@ private[support] class WeaponAndProjectileOperations(
addShotsToMap(shotsFired, weaponId, shots)
}
//noinspection SameParameterValue
private def addShotsLanded(weaponId: Int, shots: Int): Unit = {
addShotsToMap(shotsLanded, weaponId, shots)
}
@ -1405,6 +1460,44 @@ private[support] class WeaponAndProjectileOperations(
ToDatabase.reportToolDischarge(avatarId, EquipmentStat(weaponId, fired, landed, 0, 0))
}
private def CompileAutomatedTurretDamageData(
turret: AutomatedTurret,
owner: SourceEntry,
projectileTypeId: Long
): Option[(AutomatedTurret, Tool, SourceEntry, ProjectileDefinition)] = {
turret.Weapons
.values
.flatMap { _.Equipment }
.collect { case weapon: Tool => (turret, weapon, owner, weapon.Projectile) }
.find { case (_, _, _, p) => p.ObjectId == projectileTypeId }
}
private def HandleAIDamage(
target: PlanetSideServerObject with FactionAffinity with Vitality,
results: Option[(AutomatedTurret, Tool, SourceEntry, ProjectileDefinition)]
): Unit = {
results.collect {
case (obj, tool, owner, projectileInfo) =>
val angle = Vector3.Unit(target.Position - obj.Position)
val proj = new Projectile(
projectileInfo,
tool.Definition,
tool.FireMode,
None,
owner,
obj.Definition.ObjectId,
obj.Position + Vector3.z(value = 1f),
angle,
Some(angle * projectileInfo.FinalVelocity)
)
val hitPos = target.Position + Vector3.z(value = 1f)
ResolveProjectileInteraction(proj, DamageResolution.Hit, target, hitPos).collect { resprojectile =>
addShotsLanded(resprojectile.cause.attribution, shots = 1)
sessionData.handleDealingDamage(target, resprojectile)
}
}
}
override protected[session] def stop(): Unit = {
if (player != null && player.HasGUID) {
(prefire ++ shooting).foreach { guid =>

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.serverobject.mount.Seat
import net.psforever.objects.serverobject.tube.SpawnTube
import net.psforever.objects.serverobject.turret.auto.AutomatedTurret
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource}
import net.psforever.objects.vital.{InGameHistory, IncarnationActivity, ReconstructionActivity, SpawningActivity}
import net.psforever.packet.game.{CampaignStatistic, MailMessage, SessionStatistic}
import net.psforever.packet.game.{CampaignStatistic, ChangeFireStateMessage_Start, MailMessage, ObjectDetectedMessage, SessionStatistic}
import scala.collection.mutable
import scala.concurrent.duration._
@ -259,6 +260,19 @@ class ZoningOperations(
)
}
}
//auto turret behavior
(obj match {
case turret: AutomatedTurret with JammableUnit => turret.Target
case _ => None
}).collect {
target =>
val guid = obj.GUID
val turret = obj.asInstanceOf[AutomatedTurret]
sendResponse(ObjectDetectedMessage(guid, guid, 0, List(target.GUID)))
if (!obj.asInstanceOf[JammableUnit].Jammed) {
sendResponse(ChangeFireStateMessage_Start(turret.Weapons.values.head.Equipment.get.GUID))
}
}
})
//sensor animation
normal
@ -555,6 +569,14 @@ class ZoningOperations(
)
case _ => ;
}
turret.Target.collect {
target =>
val guid = turret.GUID
sendResponse(ObjectDetectedMessage(guid, guid, 0, List(target.GUID)))
if (!turret.Jammed) {
sendResponse(ChangeFireStateMessage_Start(turret.Weapons.values.head.Equipment.get.GUID))
}
}
}
//remote projectiles and radiation clouds
continent.Projectiles.foreach { projectile =>
@ -656,7 +678,6 @@ class ZoningOperations(
spawn.handleNewPlayerLoaded(player)
} else {
//alive but doesn't have a GUID; probably logging in?
session = session.copy(zone = Zone.Nowhere)
context.self ! ICS.ZoneResponse(Some(player.Zone))
}
} else {

View file

@ -13,6 +13,8 @@ import akka.actor.typed.scaladsl.adapter._
import net.psforever.actors.zone.building.MajorFacilityLogic
import net.psforever.objects.avatar.scoring.Kill
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.serverobject.terminals.capture.CaptureTerminalAwareBehavior
import net.psforever.objects.serverobject.turret.FacilityTurret
import net.psforever.objects.sourcing.SourceEntry
import net.psforever.objects.vital.{InGameActivity, InGameHistory}
import net.psforever.objects.zones.exp.{ExperienceCalculator, SupportExperienceCalculator}
@ -96,14 +98,18 @@ class ZoneActor(context: ActorContext[ZoneActor.Command], zone: Zone)
case Success(buildings) =>
buildings.foreach { building =>
zone.BuildingByMapId(building.localId) match {
case Some(_: WarpGate) => ;
case Some(_: WarpGate) => ()
//warp gates are controlled by game logic and are better off not restored via the database
case Some(b) =>
if ((b.Faction = PlanetSideEmpire(building.factionId)) != PlanetSideEmpire.NEUTRAL) {
b.ForceDomeActive = MajorFacilityLogic.checkForceDomeStatus(b).getOrElse(false)
b.Neighbours.getOrElse(Nil).foreach { _.Actor ! BuildingActor.AlertToFactionChange(b) }
b.Neighbours.getOrElse(Nil).foreach(_.Actor ! BuildingActor.AlertToFactionChange(b))
b.CaptureTerminal.collect { terminal =>
val msg = CaptureTerminalAwareBehavior.TerminalStatusChanged(terminal, isResecured = true)
b.Amenities.collect { case turret: FacilityTurret => turret.Actor ! msg }
}
}
case None => ;
case None => ()
// TODO this happens during testing, need a way to not always persist during tests
}
}

View file

@ -54,7 +54,7 @@ class ExplosiveDeployableDefinition(private val objectId: Int)
DetonateOnJamming
}
override def Initialize(obj: Deployable, context: ActorContext) = {
override def Initialize(obj: Deployable, context: ActorContext): Unit = {
obj.Actor =
context.actorOf(Props(classOf[MineDeployableControl], obj), PlanetSideServerObject.UniqueActorName(obj))
}
@ -70,8 +70,8 @@ abstract class ExplosiveDeployableControl(mine: ExplosiveDeployable)
extends Actor
with DeployableBehavior
with Damageable {
def DeployableObject = mine
def DamageableObject = mine
def DeployableObject: ExplosiveDeployable = mine
def DamageableObject: ExplosiveDeployable = mine
override def postStop(): Unit = {
super.postStop()

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.terminals.capture.CaptureTerminalDefinition
import net.psforever.objects.serverobject.terminals.implant.{ImplantTerminalDefinition, ImplantTerminalMechDefinition}
import net.psforever.objects.serverobject.turret.{FacilityTurretDefinition, TurretUpgrade}
import net.psforever.objects.serverobject.turret.{AutoChecks, AutoCooldowns, AutoRanges, Automation, FacilityTurretDefinition, TurretUpgrade}
import net.psforever.objects.vehicles.{DestroyedVehicle, InternalTelepadDefinition, UtilityType, VehicleSubsystemEntry}
import net.psforever.objects.vital.base.DamageType
import net.psforever.objects.vital.damage._
@ -1913,6 +1913,20 @@ object GlobalDefinitions {
}
}
/**
* Using the definition for a `Vehicle` determine whether it is an all-terrain vehicle type.
* @param vdef the `VehicleDefinition` of the vehicle
* @return `true`, if it is; `false`, otherwise
*/
def isAtvVehicle(vdef: VehicleDefinition): Boolean = {
vdef match {
case `quadassault` | `fury` | `quadstealth` =>
true
case _ =>
false
}
}
/**
* Using the definition for a `Vehicle` determine whether it can fly.
* Does not count the flying battleframe robotics vehicles.
@ -9059,6 +9073,24 @@ object GlobalDefinitions {
spitfire_turret.DeployTime = Duration.create(5000, "ms")
spitfire_turret.Model = ComplexDeployableResolutions.calculate
spitfire_turret.deployAnimation = DeployAnimation.Standard
spitfire_turret.AutoFire = Automation(
AutoRanges(
detection = 75f,
trigger = 50f,
escape = 50f
),
AutoChecks(
validation = List(
EffectTarget.Validation.SmallRoboticsTurretValidatePlayerTarget,
EffectTarget.Validation.SmallRoboticsTurretValidateMaxTarget,
EffectTarget.Validation.SmallRoboticsTurretValidateGroundVehicleTarget,
EffectTarget.Validation.SmallRoboticsTurretValidateAircraftTarget,
EffectTarget.Validation.AutoTurretValidateMountableEntityTarget
)
),
retaliatoryDelay = 2000L, //8000L
refireTime = 200.milliseconds //150.milliseconds
)
spitfire_turret.innateDamage = new DamageWithPosition {
CausesDamageType = DamageType.One
Damage0 = 200
@ -9085,6 +9117,30 @@ object GlobalDefinitions {
spitfire_cloaked.DeployTime = Duration.create(5000, "ms")
spitfire_cloaked.deployAnimation = DeployAnimation.Standard
spitfire_cloaked.Model = ComplexDeployableResolutions.calculate
spitfire_cloaked.AutoFire = Automation(
AutoRanges(
detection = 75f,
trigger = 50f,
escape = 75f
),
AutoChecks(
validation = List(
EffectTarget.Validation.SmallRoboticsTurretValidatePlayerTarget,
EffectTarget.Validation.SmallRoboticsTurretValidateMaxTarget,
EffectTarget.Validation.SmallRoboticsTurretValidateGroundVehicleTarget,
EffectTarget.Validation.SmallRoboticsTurretValidateAircraftTarget,
EffectTarget.Validation.AutoTurretValidateMountableEntityTarget
)
),
cooldowns = AutoCooldowns(
targetSelect = 0L,
missedShot = 0L
),
detectionSweepTime = 500.milliseconds,
retaliatoryDelay = 1L, //8000L
retaliationOverridesTarget = false,
refireTime = 200.milliseconds //150.milliseconds
)
spitfire_cloaked.innateDamage = new DamageWithPosition {
CausesDamageType = DamageType.One
Damage0 = 50
@ -9111,6 +9167,21 @@ object GlobalDefinitions {
spitfire_aa.DeployTime = Duration.create(5000, "ms")
spitfire_aa.deployAnimation = DeployAnimation.Standard
spitfire_aa.Model = ComplexDeployableResolutions.calculate
spitfire_aa.AutoFire = Automation(
AutoRanges(
detection = 125f,
trigger = 100f,
escape = 200f
),
AutoChecks(
validation = List(EffectTarget.Validation.SmallRoboticsTurretValidateAircraftTarget)
),
retaliatoryDelay = 2000L, //8000L
retaliationOverridesTarget = false,
refireTime = 0.seconds, //300.milliseconds
cylindrical = true,
cylindricalExtraHeight = 50f
)
spitfire_aa.innateDamage = new DamageWithPosition {
CausesDamageType = DamageType.One
Damage0 = 200
@ -9175,9 +9246,10 @@ object GlobalDefinitions {
portable_manned_turret.Damageable = true
portable_manned_turret.Repairable = true
portable_manned_turret.RepairIfDestroyed = false
portable_manned_turret.controlledWeapons(seat = 0, weapon = 1)
portable_manned_turret.WeaponPaths += 1 -> new mutable.HashMap()
portable_manned_turret.WeaponPaths(1) += TurretUpgrade.None -> energy_gun
portable_manned_turret.Seats += 0 -> new SeatDefinition()
portable_manned_turret.controlledWeapons(seat = 0, weapon = 1)
portable_manned_turret.MountPoints += 1 -> MountInfo(0)
portable_manned_turret.MountPoints += 2 -> MountInfo(0)
portable_manned_turret.ReserveAmmunition = true
@ -9209,6 +9281,7 @@ object GlobalDefinitions {
portable_manned_turret_nc.RepairIfDestroyed = false
portable_manned_turret_nc.WeaponPaths += 1 -> new mutable.HashMap()
portable_manned_turret_nc.WeaponPaths(1) += TurretUpgrade.None -> energy_gun_nc
portable_manned_turret_nc.Seats += 0 -> new SeatDefinition()
portable_manned_turret_nc.controlledWeapons(seat = 0, weapon = 1)
portable_manned_turret_nc.MountPoints += 1 -> MountInfo(0)
portable_manned_turret_nc.MountPoints += 2 -> MountInfo(0)
@ -9240,6 +9313,7 @@ object GlobalDefinitions {
portable_manned_turret_tr.RepairIfDestroyed = false
portable_manned_turret_tr.WeaponPaths += 1 -> new mutable.HashMap()
portable_manned_turret_tr.WeaponPaths(1) += TurretUpgrade.None -> energy_gun_tr
portable_manned_turret_tr.Seats += 0 -> new SeatDefinition()
portable_manned_turret_tr.controlledWeapons(seat = 0, weapon = 1)
portable_manned_turret_tr.MountPoints += 1 -> MountInfo(0)
portable_manned_turret_tr.MountPoints += 2 -> MountInfo(0)
@ -9271,6 +9345,7 @@ object GlobalDefinitions {
portable_manned_turret_vs.RepairIfDestroyed = false
portable_manned_turret_vs.WeaponPaths += 1 -> new mutable.HashMap()
portable_manned_turret_vs.WeaponPaths(1) += TurretUpgrade.None -> energy_gun_vs
portable_manned_turret_vs.Seats += 0 -> new SeatDefinition()
portable_manned_turret_vs.controlledWeapons(seat = 0, weapon = 1)
portable_manned_turret_vs.MountPoints += 1 -> MountInfo(0)
portable_manned_turret_vs.MountPoints += 2 -> MountInfo(0)
@ -9999,7 +10074,7 @@ object GlobalDefinitions {
manned_turret.Name = "manned_turret"
manned_turret.MaxHealth = 3600
manned_turret.Damageable = true
manned_turret.DamageDisablesAt = 0
manned_turret.DamageDisablesAt = 1800
manned_turret.Repairable = true
manned_turret.autoRepair = AutoRepairStats(1.0909f, 10000, 1600, 0.05f)
manned_turret.RepairIfDestroyed = true
@ -10007,11 +10082,32 @@ object GlobalDefinitions {
manned_turret.WeaponPaths(1) += TurretUpgrade.None -> phalanx_sgl_hevgatcan
manned_turret.WeaponPaths(1) += TurretUpgrade.AVCombo -> phalanx_avcombo
manned_turret.WeaponPaths(1) += TurretUpgrade.FlakCombo -> phalanx_flakcombo
manned_turret.Seats += 0 -> new SeatDefinition()
manned_turret.controlledWeapons(seat = 0, weapon = 1)
manned_turret.MountPoints += 1 -> MountInfo(0)
manned_turret.FactionLocked = true
manned_turret.ReserveAmmunition = false
manned_turret.RadiationShielding = 0.5f
manned_turret.AutoFire = Automation(
AutoRanges(
detection = 125f,
trigger = 100f,
escape = 200f
),
AutoChecks(
validation = List(
EffectTarget.Validation.FacilityTurretValidateMaxTarget,
EffectTarget.Validation.FacilityTurretValidateGroundVehicleTarget,
EffectTarget.Validation.FacilityTurretValidateAircraftTarget,
EffectTarget.Validation.AutoTurretValidateMountableEntityTarget
)
),
retaliatoryDelay = 4000L, //8000L
cylindrical = true,
cylindricalExtraHeight = 50f,
detectionSweepTime = 2.seconds,
refireTime = 362.milliseconds //312.milliseconds
)
manned_turret.innateDamage = new DamageWithPosition {
CausesDamageType = DamageType.One
Damage0 = 150
@ -10031,6 +10127,7 @@ object GlobalDefinitions {
vanu_sentry_turret.RepairIfDestroyed = true
vanu_sentry_turret.WeaponPaths += 1 -> new mutable.HashMap()
vanu_sentry_turret.WeaponPaths(1) += TurretUpgrade.None -> vanu_sentry_turret_weapon
vanu_sentry_turret.Seats += 0 -> new SeatDefinition()
vanu_sentry_turret.controlledWeapons(seat = 0, weapon = 1)
vanu_sentry_turret.MountPoints += 1 -> MountInfo(0)
vanu_sentry_turret.MountPoints += 2 -> MountInfo(0)

View file

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

View file

@ -4,7 +4,7 @@ package net.psforever.objects
import net.psforever.objects.avatar.interaction.{WithGantry, WithLava, WithWater}
import net.psforever.objects.avatar.{Avatar, LoadoutManager, SpecialCarry}
import net.psforever.objects.ballistics.InteractWithRadiationClouds
import net.psforever.objects.ce.{Deployable, InteractWithMines}
import net.psforever.objects.ce.{Deployable, InteractWithMines, InteractWithTurrets}
import net.psforever.objects.definition.{AvatarDefinition, ExoSuitDefinition, SpecialExoSuitDefinition}
import net.psforever.objects.equipment.{Equipment, EquipmentSize, EquipmentSlot, JammableUnit}
import net.psforever.objects.inventory.{Container, GridInventory, InventoryItem}
@ -45,6 +45,7 @@ class Player(var avatar: Avatar)
new WithMovementTrigger()
)))
interaction(new InteractWithMinesUnlessSpectating(obj = this, range = 10))
interaction(new InteractWithTurrets())
interaction(new InteractWithRadiationClouds(range = 10f, Some(this)))
private var backpack: Boolean = false

View file

@ -110,25 +110,34 @@ object Players {
* For any slots that are not yet occupied by an item, search through the `List` and find an item that fits in that slot.
* Add that item to the slot and remove it from the list.
* @param iter the `Iterator` of `EquipmentSlot`s
* @param list a `List` of all `Equipment` that is not yet assigned to a holster slot or an inventory slot
* @return the `List` of all `Equipment` not yet assigned to a holster slot or an inventory slot
* @param list a list of all `Equipment` not assigned to a new slot
* @param slotNum current slot index associated with the value extracted from `iter` param
* @param placedList a list of all `Equipment` reassigned to a slot
* @return two lists:
* all `Equipment` reassigned to a slot, and
* all `Equipment` not assigned to a new slot
*/
@tailrec def fillEmptyHolsters(iter: Iterator[EquipmentSlot], list: List[InventoryItem]): List[InventoryItem] = {
@tailrec def fillEmptyHolsters(
iter: Iterator[EquipmentSlot],
list: List[InventoryItem],
slotNum: Int = 0,
placedList: List[InventoryItem] = Nil
): (List[InventoryItem], List[InventoryItem]) = {
if (!iter.hasNext) {
list
(placedList, list)
} else {
val slot = iter.next()
if (slot.Equipment.isEmpty) {
list.find(item => item.obj.Size == slot.Size) match {
case Some(obj) =>
val index = list.indexOf(obj)
slot.Equipment = obj.obj
fillEmptyHolsters(iter, list.take(index) ++ list.drop(index + 1))
case None =>
fillEmptyHolsters(iter, list)
list.indexWhere(item => item.obj.Size == slot.Size) match {
case -1 =>
fillEmptyHolsters(iter, list, slotNum + 1, placedList)
case index =>
val entry = list(index)
entry.start = slotNum
fillEmptyHolsters(iter, list.take(index) ++ list.drop(index + 1), slotNum + 1, placedList :+ entry)
}
} else {
fillEmptyHolsters(iter, list)
fillEmptyHolsters(iter, list, slotNum + 1, placedList)
}
}
}

View file

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

View file

@ -118,8 +118,8 @@ object SpecialEmp {
case _ => OwnerGuid_=(Some(owner.GUID))
}
Position = position
def Faction = faction
def Definition = proxy_definition
def Faction: PlanetSideEmpire.Value = faction
def Definition: ObjectDefinition with VitalityDefinition = proxy_definition
})
}

View file

@ -1,35 +1,56 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects
import akka.actor.{Actor, ActorContext, Props}
import net.psforever.objects.ce.{Deployable, DeployableBehavior, DeployedItem}
import akka.actor.{Actor, ActorContext, ActorRef, Props}
import net.psforever.objects.ce.{Deployable, DeployableBehavior, DeployedItem, InteractWithTurrets}
import net.psforever.objects.definition.DeployableDefinition
import net.psforever.objects.definition.converter.SmallTurretConverter
import net.psforever.objects.equipment.{JammableMountedWeapons, JammableUnit}
import net.psforever.objects.equipment.JammableUnit
import net.psforever.objects.guid.{GUIDTask, TaskWorkflow}
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.affinity.FactionAffinityBehavior
import net.psforever.objects.serverobject.damage.Damageable.Target
import net.psforever.objects.serverobject.damage.DamageableWeaponTurret
import net.psforever.objects.serverobject.damage.Damageable
import net.psforever.objects.serverobject.hackable.Hackable
import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior}
import net.psforever.objects.serverobject.repair.RepairableWeaponTurret
import net.psforever.objects.serverobject.turret.{TurretDefinition, WeaponTurret}
import net.psforever.objects.serverobject.mount.{InteractWithRadiationCloudsSeatedInEntity, Mountable}
import net.psforever.objects.serverobject.turret.auto.AutomatedTurret.Target
import net.psforever.objects.serverobject.turret.auto.{AffectedByAutomaticTurretFire, AutomatedTurret, AutomatedTurretBehavior}
import net.psforever.objects.serverobject.turret.{MountableTurretControl, TurretDefinition, WeaponTurret}
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry}
import net.psforever.objects.vital.damage.DamageCalculations
import net.psforever.objects.vital.interaction.DamageResult
import net.psforever.objects.vital.resistance.StandardResistanceProfile
import net.psforever.objects.vital.{SimpleResolutions, StandardVehicleResistance}
import net.psforever.objects.zones.InteractsWithZone
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
import net.psforever.types.PlanetSideGUID
import scala.concurrent.duration.FiniteDuration
class TurretDeployable(tdef: TurretDeployableDefinition)
extends Deployable(tdef)
extends Deployable(tdef)
with AutomatedTurret
with WeaponTurret
with JammableUnit
with InteractsWithZone
with StandardResistanceProfile
with Hackable {
WeaponTurret.LoadDefinition(this)
if (tdef.Seats.nonEmpty) {
interaction(new InteractWithTurrets())
interaction(new InteractWithRadiationCloudsSeatedInEntity(obj = this, range = 100f))
}
WeaponTurret.LoadDefinition(turret = this)
override def Definition = tdef
def TurretOwner: SourceEntry = {
Seats
.values
.headOption
.flatMap(_.occupant)
.map(p => PlayerSource.inSeat(PlayerSource(p), SourceEntry(this), seatNumber=0))
.orElse(Owners.map(PlayerSource(_, Position)))
.getOrElse(SourceEntry(this))
}
override def Definition: TurretDeployableDefinition = tdef
}
class TurretDeployableDefinition(private val objectId: Int)
@ -46,7 +67,7 @@ class TurretDeployableDefinition(private val objectId: Int)
//override to clarify inheritance conflict
override def MaxHealth_=(max: Int): Int = super[DeployableDefinition].MaxHealth_=(max)
override def Initialize(obj: Deployable, context: ActorContext) = {
override def Initialize(obj: Deployable, context: ActorContext): Unit = {
obj.Actor = context.actorOf(Props(classOf[TurretControl], obj), PlanetSideServerObject.UniqueActorName(obj))
}
}
@ -63,35 +84,92 @@ class TurretControl(turret: TurretDeployable)
extends Actor
with DeployableBehavior
with FactionAffinityBehavior.Check
with JammableMountedWeapons //note: jammable status is reported as vehicle events, not local events
with MountableBehavior
with DamageableWeaponTurret
with RepairableWeaponTurret {
def DeployableObject = turret
def MountableObject = turret
def JammableObject = turret
def FactionObject = turret
def DamageableObject = turret
def RepairableObject = turret
with MountableTurretControl
with AutomatedTurretBehavior
with AffectedByAutomaticTurretFire {
def TurretObject: TurretDeployable = turret
def DeployableObject: TurretDeployable = turret
def MountableObject: TurretDeployable = turret
def JammableObject: TurretDeployable = turret
def FactionObject: TurretDeployable = turret
def DamageableObject: TurretDeployable = turret
def RepairableObject: TurretDeployable = turret
def AutomatedTurretObject: TurretDeployable = turret
def AffectedObject: TurretDeployable = turret
override def postStop(): Unit = {
super.postStop()
deployableBehaviorPostStop()
damageableWeaponTurretPostStop()
selfReportingDatabaseUpdate()
automaticTurretPostStop()
}
def receive: Receive =
deployableBehavior
commonBehavior
.orElse(deployableBehavior)
.orElse(checkBehavior)
.orElse(jammableBehavior)
.orElse(mountBehavior)
.orElse(dismountBehavior)
.orElse(takesDamage)
.orElse(canBeRepairedByNanoDispenser)
.orElse(automatedTurretBehavior)
.orElse(takeAutomatedDamage)
.orElse {
case _ => ;
case _ => ()
}
protected def engageNewDetectedTarget(
target: AutomatedTurret.Target,
channel: String,
turretGuid: PlanetSideGUID,
weaponGuid: PlanetSideGUID
): Unit = {
val zone = target.Zone
AutomatedTurretBehavior.startTracking(zone, channel, turretGuid, List(target.GUID))
AutomatedTurretBehavior.startShooting(zone, channel, weaponGuid)
}
protected def noLongerEngageTarget(
target: AutomatedTurret.Target,
channel: String,
turretGuid: PlanetSideGUID,
weaponGuid: PlanetSideGUID
): Option[AutomatedTurret.Target] = {
val zone = target.Zone
AutomatedTurretBehavior.stopTracking(zone, channel, turretGuid)
AutomatedTurretBehavior.stopShooting(zone, channel, weaponGuid)
None
}
protected def testNewDetected(
target: AutomatedTurret.Target,
channel: String,
turretGuid: PlanetSideGUID,
weaponGuid: PlanetSideGUID
): Unit = {
val zone = target.Zone
AutomatedTurretBehavior.startTracking(zone, channel, turretGuid, List(target.GUID))
AutomatedTurretBehavior.startShooting(zone, channel, weaponGuid)
AutomatedTurretBehavior.stopShooting(zone, channel, weaponGuid)
}
protected def testKnownDetected(
target: AutomatedTurret.Target,
channel: String,
turretGuid: PlanetSideGUID,
weaponGuid: PlanetSideGUID
): Unit = {
val zone = target.Zone
AutomatedTurretBehavior.startShooting(zone, channel, weaponGuid)
AutomatedTurretBehavior.stopShooting(zone, channel, weaponGuid)
}
override protected def suspendTargetTesting(
target: Target,
channel: String,
turretGuid: PlanetSideGUID,
weaponGuid: PlanetSideGUID
): Unit = {
AutomatedTurretBehavior.stopTracking(target.Zone, channel, turretGuid)
}
override protected def mountTest(
obj: PlanetSideServerObject with Mountable,
seatNumber: Int,
@ -99,7 +177,40 @@ class TurretControl(turret: TurretDeployable)
(!turret.Definition.FactionLocked || player.Faction == obj.Faction) && !obj.Destroyed
}
override protected def DestructionAwareness(target: Target, cause: DamageResult): Unit = {
override def TryJammerEffectActivate(target: Any, cause: DamageResult): Unit = {
val startsUnjammed = !JammableObject.Jammed
super.TryJammerEffectActivate(target, cause)
if (JammableObject.Jammed && AutomatedTurretObject.Definition.AutoFire.exists(_.retaliatoryDelay > 0)) {
if (startsUnjammed) {
AutomaticOperation = false
}
//look in direction of cause of jamming
val zone = JammableObject.Zone
AutomatedTurretBehavior.getAttackVectorFromCause(zone, cause).foreach { attacker =>
AutomatedTurretBehavior.startTracking(zone, zone.id, AutomatedTurretObject.GUID, List(attacker.GUID))
}
}
}
override def CancelJammeredStatus(target: Any): Unit = {
val startsJammed = JammableObject.Jammed
super.CancelJammeredStatus(target)
if (startsJammed && AutomaticOperation_=(state = true)) {
val zone = TurretObject.Zone
AutomatedTurretBehavior.stopTracking(zone, zone.id, TurretObject.GUID)
}
}
override protected def DamageAwareness(target: Damageable.Target, cause: DamageResult, amount: Any): Unit = {
amount match {
case 0 => ()
case _ => attemptRetaliation(target, cause)
}
super.DamageAwareness(target, cause, amount)
}
override protected def DestructionAwareness(target: Damageable.Target, cause: DamageResult): Unit = {
AutomaticOperation = false
super.DestructionAwareness(target, cause)
CancelJammeredSound(target)
CancelJammeredStatus(target)
@ -107,22 +218,22 @@ class TurretControl(turret: TurretDeployable)
}
override def deconstructDeployable(time: Option[FiniteDuration]) : Unit = {
AutomaticOperation = false
val zone = turret.Zone
val seats = turret.Seats.values
//either we have no seats or no one gets to sit
val retime = if (seats.count(_.isOccupied) > 0) {
//unlike with vehicles, it's possible to request deconstruction of one's own field turret while seated in it
//it's possible to request deconstruction of one's own field turret while seated in it
val wasKickedByDriver = false
seats.foreach { seat =>
seat.occupant match {
case Some(tplayer) =>
seat.unmount(tplayer)
tplayer.VehicleSeated = None
seat.occupant.collect {
case player: Player =>
seat.unmount(player)
player.VehicleSeated = None
zone.VehicleEvents ! VehicleServiceMessage(
zone.id,
VehicleAction.KickPassenger(tplayer.GUID, 4, wasKickedByDriver, turret.GUID)
VehicleAction.KickPassenger(player.GUID, 4, wasKickedByDriver, turret.GUID)
)
case None => ;
}
}
Some(time.getOrElse(Deployable.cleanup) + Deployable.cleanup)
@ -132,6 +243,11 @@ class TurretControl(turret: TurretDeployable)
super.deconstructDeployable(retime)
}
override def finalizeDeployable(callback: ActorRef): Unit = {
super.finalizeDeployable(callback)
AutomaticOperation = true
}
override def unregisterDeployable(obj: Deployable): Unit = {
val zone = obj.Zone
TaskWorkflow.execute(GUIDTask.unregisterDeployableTurret(zone.GUID, turret))

View file

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

View file

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

View file

@ -374,26 +374,8 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
log.info(s"${player.Name} wants to change equipment loadout to their option #${msg.unk1 + 1}")
val originalSuit = player.ExoSuit
val originalSubtype = Loadout.DetermineSubtype(player)
//sanitize exo-suit for change
val dropPred = ContainableBehavior.DropPredicate(player)
val oldHolsters = Players.clearHolsters(player.Holsters().iterator)
val dropHolsters = oldHolsters.filter(dropPred)
val oldInventory = player.Inventory.Clear()
val dropInventory = oldInventory.filter(dropPred)
val toDeleteOrDrop : List[InventoryItem] = (player.FreeHand.Equipment match {
case Some(obj) =>
val out = InventoryItem(obj, -1)
player.FreeHand.Equipment = None
if (dropPred(out)) {
List(out)
} else {
Nil
}
case _ =>
Nil
}) ++ dropHolsters ++ dropInventory
//a loadout with a prohibited exo-suit type will result in the fallback exo-suit type
//imposed 5min delay on mechanized exo-suit switches
//determine player's next exo-suit
val (nextSuit, nextSubtype) = {
lazy val fallbackSuit = if (Players.CertificationToUseExoSuit(player, originalSuit, originalSubtype)) {
//TODO will we ever need to check for the cooldown status of an original non-MAX exo-suit?
@ -423,20 +405,30 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
fallbackSuit
}
}
if (nextSuit == ExoSuitType.MAX) {
player.ResistArmMotion(PlayerControl.maxRestriction)
} else {
player.ResistArmMotion(Player.neverRestrict)
//sanitize current exo-suit for change
val (dropHolsters, oldHolsters) = Players.clearHolsters(player.Holsters().iterator).partition(dropPred)
val (dropInventory, oldInventory) = player.Inventory.Clear().partition(dropPred)
val (dropHand, deleteHand) = player.FreeHand.Equipment match {
case Some(obj) =>
val out = InventoryItem(obj, -1)
player.FreeHand.Equipment = None
if (dropPred(out)) {
(List(out), Nil)
} else {
(Nil, List(out))
}
case _ =>
(Nil, Nil)
}
//sanitize (incoming) inventory
//TODO equipment permissions; these loops may be expanded upon in future
val curatedHolsters = for {
//these dropped items exist and must be accounted for
val itemsToDrop = dropHand ++ dropHolsters ++ dropInventory
val newHolsters = for {
item <- holsters
//id = item.obj.Definition.ObjectId
//lastTime = player.GetLastUsedTime(id)
if true
} yield item
val curatedInventory = for {
val newInventory = for {
item <- inventory
//id = item.obj.Definition.ObjectId
//lastTime = player.GetLastUsedTime(id)
@ -454,60 +446,55 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
player.Armor = originalArmor
}
}
//ensure arm is down, even if it needs to go back up
if (player.DrawnSlot != Player.HandsDownSlot) {
player.DrawnSlot = Player.HandsDownSlot
}
//a change due to exo-suit permissions mismatch will result in (more) items being re-arranged and/or dropped
//dropped items are not registered and can just be forgotten
val (afterHolsters, afterInventory) = if (nextSuit == exosuit) {
(
//melee slot preservation for MAX
if (nextSuit == ExoSuitType.MAX) {
holsters.filter(_.start == 4)
} else {
curatedHolsters.filterNot(dropPred)
},
curatedInventory.filterNot(dropPred)
)
val (afterHolsters, afterInventory) = if (exosuit == nextSuit) {
//proposed loadout inventory matched the projected exo-suit selection
if (nextSuit == ExoSuitType.MAX) {
//loadout for a MAX
player.ResistArmMotion(PlayerControl.maxRestriction)
player.DrawnSlot = Player.HandsDownSlot
(newHolsters.filter(_.start == 4), newInventory.filterNot(dropPred))
} else {
//loadout for a vanilla exo-suit
player.ResistArmMotion(Player.neverRestrict)
(newHolsters.filterNot(dropPred), newInventory.filterNot(dropPred))
}
} else {
//our exo-suit type was hijacked by changing permissions; we shouldn't even be able to use that loadout(!)
//holsters
val leftoversForInventory = Players.fillEmptyHolsters(
//proposed loadout conforms to a different inventory layout than the projected exo-suit
player.ResistArmMotion(Player.neverRestrict)
//holsters (matching holsters will be inserted, the rest will deposited into the inventory)
val (finalHolsters, leftoversForInventory) = Players.fillEmptyHolsters(
player.Holsters().iterator,
(curatedHolsters ++ curatedInventory).filterNot(dropPred)
(newHolsters.filterNot(_.obj.Size == EquipmentSize.Max) ++ newInventory).filterNot(dropPred)
)
val finalHolsters = player.HolsterItems()
//inventory
//inventory (items will be placed to accommodate the change, or dropped)
val (finalInventory, _) = GridInventory.recoverInventory(leftoversForInventory, player.Inventory)
(finalHolsters, finalInventory)
}
(afterHolsters ++ afterInventory).foreach { entry => entry.obj.Faction = player.Faction }
afterHolsters.foreach {
afterHolsters.collect {
case InventoryItem(citem: ConstructionItem, _) =>
Deployables.initializeConstructionItem(player.avatar.certifications, citem)
case _ => ;
}
toDeleteOrDrop.foreach { entry => entry.obj.Faction = PlanetSideEmpire.NEUTRAL }
//deactivate non-passive implants
avatarActor ! AvatarActor.DeactivateActiveImplants()
player.Zone.AvatarEvents ! AvatarServiceMessage(
player.Zone.id,
val zone = player.Zone
zone.AvatarEvents ! AvatarServiceMessage(
zone.id,
AvatarAction.ChangeLoadout(
player.GUID,
toArmor,
nextSuit,
nextSubtype,
player.LastDrawnSlot,
exosuit == ExoSuitType.MAX,
nextSuit == ExoSuitType.MAX,
oldHolsters.map { case InventoryItem(obj, _) => (obj, obj.GUID) },
afterHolsters,
oldInventory.map { case InventoryItem(obj, _) => (obj, obj.GUID) },
(oldInventory ++ deleteHand).map { case InventoryItem(obj, _) => (obj, obj.GUID) },
afterInventory,
toDeleteOrDrop
itemsToDrop
)
)
player.Zone.AvatarEvents ! AvatarServiceMessage(
zone.AvatarEvents ! AvatarServiceMessage(
player.Name,
AvatarAction.TerminalOrderResult(msg.terminal_guid, msg.transaction_type, result=true)
)
@ -610,30 +597,31 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
}
def setExoSuit(exosuit: ExoSuitType.Value, subtype: Int): Boolean = {
var toDelete : List[InventoryItem] = Nil
val willBecomeMax = exosuit == ExoSuitType.MAX
val originalSuit = player.ExoSuit
val originalSubtype = Loadout.DetermineSubtype(player)
val requestToChangeArmor = originalSuit != exosuit || originalSubtype != subtype
val allowedToChangeArmor = Players.CertificationToUseExoSuit(player, exosuit, subtype) &&
(if (exosuit == ExoSuitType.MAX) {
val changeSuit = originalSuit != exosuit
val changeSubtype = originalSubtype != subtype
val doChangeArmor = (changeSuit || changeSubtype) &&
Players.CertificationToUseExoSuit(player, exosuit, subtype) &&
(if (willBecomeMax) {
val weapon = GlobalDefinitions.MAXArms(subtype, player.Faction)
player.avatar.purchaseCooldown(weapon) match {
case Some(_) =>
false
case None =>
player.avatar.purchaseCooldown(weapon)
.collect(_ => false)
.getOrElse {
avatarActor ! AvatarActor.UpdatePurchaseTime(weapon)
true
}
}
} else {
true
})
if (requestToChangeArmor && allowedToChangeArmor) {
if (doChangeArmor) {
log.info(s"${player.Name} wants to change to a different exo-suit - $exosuit")
val beforeHolsters = Players.clearHolsters(player.Holsters().iterator)
val beforeInventory = player.Inventory.Clear()
//change suit
//update suit internally
val originalArmor = player.Armor
player.ExoSuit = exosuit //changes the value of MaxArmor to reflect the new exo-suit
player.ExoSuit = exosuit
val toMaxArmor = player.MaxArmor
val toArmor = toMaxArmor
if (originalSuit != exosuit || originalArmor != toMaxArmor) {
@ -644,50 +632,32 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
if (player.DrawnSlot != Player.HandsDownSlot) {
player.DrawnSlot = Player.HandsDownSlot
}
val normalHolsters = if (originalSuit == ExoSuitType.MAX) {
val (maxWeapons, normalWeapons) = beforeHolsters.partition(elem => elem.obj.Size == EquipmentSize.Max)
toDelete ++= maxWeapons
normalWeapons
}
else {
beforeHolsters
}
//populate holsters
val (afterHolsters, finalInventory) = if (exosuit == ExoSuitType.MAX) {
(
normalHolsters,
Players.fillEmptyHolsters(List(player.Slot(4)).iterator, normalHolsters) ++ beforeInventory
)
}
else if (originalSuit == exosuit) { //note - this will rarely be the situation
(normalHolsters, Players.fillEmptyHolsters(player.Holsters().iterator, normalHolsters))
}
else {
val (afterHolsters, toInventory) =
normalHolsters.partition(elem => elem.obj.Size == player.Slot(elem.start).Size)
afterHolsters.foreach({ elem => player.Slot(elem.start).Equipment = elem.obj })
val remainder = Players.fillEmptyHolsters(player.Holsters().iterator, toInventory ++ beforeInventory)
(
player.HolsterItems(),
remainder
)
}
//put items back into inventory
val (stow, drop) = if (originalSuit == exosuit) {
(finalInventory, Nil)
}
else {
val (a, b) = GridInventory.recoverInventory(finalInventory, player.Inventory)
(
a,
b.map {
InventoryItem(_, -1)
}
)
}
stow.foreach { elem =>
player.Inventory.InsertQuickly(elem.start, elem.obj)
val (toDelete, toDrop, afterHolsters, afterInventory) = if (originalSuit == ExoSuitType.MAX) {
//was max
val (delete, insert) = beforeHolsters.partition(elem => elem.obj.Size == EquipmentSize.Max)
if (willBecomeMax) {
//changing to a different kind(?) of max
(delete, Nil, insert, beforeInventory)
} else {
//changing to a vanilla exo-suit
val (newHolsters, unplacedHolsters) = Players.fillEmptyHolsters(player.Holsters().iterator, insert ++ beforeInventory)
val (inventory, unplacedInventory) = GridInventory.recoverInventory(unplacedHolsters, player.Inventory)
(delete, unplacedInventory.map(InventoryItem(_, -1)), newHolsters, inventory)
}
} else if (willBecomeMax) {
//will be max, drop everything but melee slot
val (melee, other) = beforeHolsters.partition(elem => elem.obj.Size == EquipmentSize.Melee)
val (inventory, unplacedInventory) = GridInventory.recoverInventory(beforeInventory ++ other, player.Inventory)
(Nil, unplacedInventory.map(InventoryItem(_, -1)), melee, inventory)
} else {
//was not a max nor will become a max; vanilla exo-suit to a vanilla-exo-suit
val (insert, unplacedHolsters) = Players.fillEmptyHolsters(player.Holsters().iterator, beforeHolsters ++ beforeInventory)
val (inventory, unplacedInventory) = GridInventory.recoverInventory(unplacedHolsters, player.Inventory)
(Nil, unplacedInventory.map(InventoryItem(_, -1)), insert, inventory)
}
//insert
afterHolsters.foreach(elem => player.Slot(elem.start).Equipment = elem.obj)
afterInventory.foreach(elem => player.Inventory.InsertQuickly(elem.start, elem.obj))
//deactivate non-passive implants
avatarActor ! AvatarActor.DeactivateActiveImplants()
player.Zone.AvatarEvents ! AvatarServiceMessage(
@ -698,18 +668,17 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
exosuit,
subtype,
player.LastDrawnSlot,
exosuit == ExoSuitType.MAX && requestToChangeArmor,
willBecomeMax,
beforeHolsters.map { case InventoryItem(obj, _) => (obj, obj.GUID) },
afterHolsters,
beforeInventory.map { case InventoryItem(obj, _) => (obj, obj.GUID) },
stow,
drop,
afterInventory,
toDrop,
toDelete.map { case InventoryItem(obj, _) => (obj, obj.GUID) }
)
)
true
}
else {
} else {
false
}
}

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

View file

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

View file

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

View file

@ -32,10 +32,16 @@ trait WorldEntity {
def isMoving(test: Vector3): Boolean = WorldEntity.isMoving(Velocity, test)
/**
* This object is not considered moving unless it is moving at least as fast as a certain velocity.
* @param test the (squared) velocity to test against
* @return `true`, if we are moving; `false`, otherwise
*/
* This object is not considered moving unless it is moving at least as fast as a certain velocity.
* @param test the velocity to test against
* @return `true`, if we are moving; `false`, otherwise
*/
def isMoving(test: Double): Boolean = WorldEntity.isMoving(Velocity, (test * test).toFloat)
/**
* This object is not considered moving unless it is moving at least as fast as a certain velocity.
* @param test the (squared) velocity to test against
* @return `true`, if we are moving; `false`, otherwise
*/
def isMoving(test: Float): Boolean = WorldEntity.isMoving(Velocity, test)
}

View file

@ -2,9 +2,11 @@
package net.psforever.objects.equipment
import net.psforever.objects._
import net.psforever.objects.ce.DeployableCategory
import net.psforever.objects.serverobject.turret.FacilityTurret
import net.psforever.objects.vital.DamagingActivity
import net.psforever.objects.ce.{DeployableCategory, DeployedItem}
import net.psforever.objects.serverobject.turret.{FacilityTurret, WeaponTurret}
import net.psforever.objects.vital.{DamagingActivity, InGameHistory, Vitality}
import net.psforever.objects.zones.blockmap.SectorPopulation
import net.psforever.types.{DriveState, ExoSuitType, ImplantType, LatticeBenefit, PlanetSideEmpire, Vector3}
final case class TargetValidation(category: EffectTarget.Category.Value, test: EffectTarget.Validation.Value)
@ -21,6 +23,7 @@ object EffectTarget {
object Validation {
type Value = PlanetSideGameObject => Boolean
//noinspection ScalaUnusedSymbol
def Invalid(target: PlanetSideGameObject): Boolean = false
def Medical(target: PlanetSideGameObject): Boolean =
@ -72,10 +75,10 @@ object EffectTarget {
}
/**
* To repair at this landing pad, the vehicle:
* To repair at this landing pad, the vehicle must:
* be a flight vehicle,
* must have some health already, but does not have all its health,
* and can not have taken damage in the last five seconds.
* have some health already, but does not have all its health, and
* have not taken damage in the last five seconds.
*/
def PadLanding(target: PlanetSideGameObject): Boolean =
target match {
@ -185,5 +188,269 @@ object EffectTarget {
case _ =>
false
}
def SmallRoboticsTurretValidatePlayerTarget(target: PlanetSideGameObject): Boolean =
target match {
case p: Player
if p.ExoSuit != ExoSuitType.MAX && p.VehicleSeated.isEmpty =>
val now = System.currentTimeMillis()
val pos = p.Position
val faction = p.Faction
val sector = p.Zone.blockMap.sector(pos, range = 51f)
//todo equipment-use usually a violation for any equipment type
lazy val usedEquipment = (p.Holsters().flatMap(_.Equipment) ++ p.Inventory.Items.map(_.obj))
.collect {
case t: Tool
if !(t.Projectile == GlobalDefinitions.no_projectile || t.Projectile.GrenadeProjectile || t.Size == EquipmentSize.Melee) =>
now - t.LastDischarge
}
.exists(_ < 2000L)
lazy val cloakedByInfiltrationSuit = p.ExoSuit == ExoSuitType.Infiltration && p.Cloaked
lazy val silentRunActive = p.avatar.implants.flatten.find(a => a.definition.implantType == ImplantType.SilentRun).exists(_.active)
lazy val movingFast = p.isMoving(test = 15.5d)
lazy val isCrouched = p.Crouching
lazy val isMoving = p.isMoving(test = 1d)
lazy val isJumping = p.Jumping
if (radarCloakedAms(sector, pos) || radarCloakedAegis(sector, pos)) false
else if (entityTookDamage(p, now) || usedEquipment) true
else if (radarCloakedSensor(sector, pos, faction) || silentRunActive) false
else if (radarEnhancedInterlink(sector, pos, faction)) true
else if (radarEnhancedSensor(sector, pos, faction)) !isCrouched && isMoving
else if (cloakedByInfiltrationSuit) isJumping || movingFast
else isJumping || movingFast
case _ =>
false
}
def SmallRoboticsTurretValidateMaxTarget(target: PlanetSideGameObject): Boolean =
target match {
case p: Player
if p.ExoSuit == ExoSuitType.MAX && p.VehicleSeated.isEmpty =>
val now = System.currentTimeMillis()
val pos = p.Position
val faction = p.Faction
val sector = p.Zone.blockMap.sector(pos, range = 51f)
lazy val usedEquipment = p.Holsters().flatMap(_.Equipment)
.collect { case t: Tool => now - t.LastDischarge }
.exists(_ < 2000L)
lazy val isMoving = p.isMoving(test = 1d)
if (radarCloakedAms(sector, pos) || radarCloakedAegis(sector, pos)) false
else if (entityTookDamage(p, now) || usedEquipment) true
else if (radarCloakedSensor(sector, pos, faction)) false
else if (radarEnhancedInterlink(sector, pos, faction)) true
else isMoving
case _ =>
false
}
def SmallRoboticsTurretValidateGroundVehicleTarget(target: PlanetSideGameObject): Boolean =
target match {
case v: Vehicle
if !GlobalDefinitions.isFlightVehicle(v.Definition) && v.MountedIn.isEmpty && v.Seats.values.exists(_.isOccupied) =>
val now = System.currentTimeMillis()
val vdef = v.Definition
val pos = v.Position
lazy val sector = v.Zone.blockMap.sector(pos, range = 51f)
lazy val usedEquipment = v.Weapons.values.flatMap(_.Equipment)
.collect { case t: Tool => now - t.LastDischarge }
.exists(_ < 2000L)
if (
(vdef == GlobalDefinitions.ams && v.DeploymentState == DriveState.Deployed) ||
radarCloakedAms(sector, pos) || radarCloakedAegis(sector, pos)
) false
else !v.Cloaked && v.isMoving(test = 1d) || entityTookDamage(v, now) || usedEquipment
case _ =>
false
}
def SmallRoboticsTurretValidateAircraftTarget(target: PlanetSideGameObject): Boolean =
target match {
case v: Vehicle
if GlobalDefinitions.isFlightVehicle(v.Definition) && v.Seats.values.exists(_.isOccupied) =>
val now = System.currentTimeMillis()
val pos = v.Position
val sector = v.Zone.blockMap.sector(pos, range = 51f)
lazy val usedEquipment = v.Weapons.values.flatMap(_.Equipment)
.collect { case t: Tool => now - t.LastDischarge }
.exists(_ < 2000L)
if (radarCloakedAms(sector, pos) || radarCloakedAegis(sector, pos)) false
else !v.Cloaked && (v.isFlying || v.isMoving(test = 1d)) || entityTookDamage(v, now) || usedEquipment
case _ =>
false
}
def FacilityTurretValidateMaxTarget(target: PlanetSideGameObject): Boolean =
target match {
case p: Player
if p.ExoSuit == ExoSuitType.MAX && p.VehicleSeated.isEmpty =>
val now = System.currentTimeMillis()
val pos = p.Position
val faction = p.Faction
val sector = p.Zone.blockMap.sector(p.Position, range = 51f)
lazy val usedEquipment = p.Holsters().flatMap(_.Equipment)
.collect { case t: Tool => now - t.LastDischarge }
.exists(_ < 2000L)
if (radarCloakedAms(sector, pos) || radarCloakedAegis(sector, pos)) false
else if (radarCloakedSensor(sector, pos, faction)) entityTookDamage(p, now) || usedEquipment
else if (radarEnhancedInterlink(sector, pos, faction)) true
else p.isMoving(test = 15.5d)
case _ =>
false
}
def FacilityTurretValidateGroundVehicleTarget(target: PlanetSideGameObject): Boolean =
target match {
case v: Vehicle
if !GlobalDefinitions.isFlightVehicle(v.Definition) && v.MountedIn.isEmpty && v.Seats.values.exists(_.isOccupied) =>
val now = System.currentTimeMillis()
val vdef = v.Definition
val pos = v.Position
lazy val sector = v.Zone.blockMap.sector(pos, range = 51f)
lazy val usedEquipment = v.Weapons.values.flatMap(_.Equipment)
.collect { case t: Tool => now - t.LastDischarge }
.exists(_ < 2000L)
if (
(vdef == GlobalDefinitions.ams && v.DeploymentState == DriveState.Deployed) ||
vdef == GlobalDefinitions.two_man_assault_buggy ||
GlobalDefinitions.isAtvVehicle(vdef) || //todo should all ATV types get carte blanche treatment?
radarCloakedAms(sector, pos) ||
radarCloakedAegis(sector, pos)
) false
else v.isMoving(test = 1d) || entityTookDamage(v, now) || usedEquipment
case _ =>
false
}
def FacilityTurretValidateAircraftTarget(target: PlanetSideGameObject): Boolean =
target match {
case v: Vehicle
if GlobalDefinitions.isFlightVehicle(v.Definition) && v.Seats.values.exists(_.isOccupied) =>
val now = System.currentTimeMillis()
val pos = v.Position
lazy val sector = v.Zone.blockMap.sector(pos, range = 51f)
lazy val usedEquipment = v.Weapons.values.flatMap(_.Equipment)
.collect { case t: Tool => now - t.LastDischarge }
.exists(_ < 2000L)
// from the perspective of a mosquito, at 5th gauge, forward velocity is 59~60
lazy val movingFast = Vector3.MagnitudeSquared(v.Velocity.getOrElse(Vector3.Zero).xy) > 3721f //61
lazy val isMoving = v.isMoving(test = 1d)
if (v.Cloaked || radarCloakedAms(sector, pos) || radarCloakedAegis(sector, pos)) false
else if (v.Definition == GlobalDefinitions.mosquito) movingFast
else v.isFlying && (isMoving || entityTookDamage(v, now) || usedEquipment)
case _ =>
false
}
def AutoTurretValidateMountableEntityTarget(target: PlanetSideGameObject): Boolean =
target match {
case _: Vehicle =>
false //strict vehicles are handled by other validations
case t: WeaponTurret with Vitality =>
t.Seats.values.exists(_.isOccupied)
case _ =>
false
}
def AutoTurretBlankPlayerTarget(target: PlanetSideGameObject): Boolean =
target match {
case p: Player =>
val pos = p.Position
lazy val sector = p.Zone.blockMap.sector(p.Position, range = 51f)
p.VehicleSeated.nonEmpty || radarCloakedAms(sector, pos) || radarCloakedAegis(sector, pos)
case _ =>
false
}
def AutoTurretBlankVehicleTarget(target: PlanetSideGameObject): Boolean =
target match {
case v: Vehicle =>
val pos = v.Position
lazy val sector = v.Zone.blockMap.sector(pos, range = 51f)
(v.Definition == GlobalDefinitions.ams && v.DeploymentState == DriveState.Deployed) ||
v.MountedIn.nonEmpty ||
v.Cloaked ||
radarCloakedAms(sector, pos) ||
radarCloakedAegis(sector, pos)
case _ =>
false
}
}
private def radarEnhancedInterlink(
sector: SectorPopulation,
position: Vector3,
faction: PlanetSideEmpire.Value
): Boolean = {
sector.buildingList.collect {
case b =>
b.Faction != faction &&
b.hasLatticeBenefit(LatticeBenefit.InterlinkFacility) &&
Vector3.DistanceSquared(b.Position, position).toDouble < math.pow(b.Definition.SOIRadius.toDouble, 2d)
}.contains(true)
}
private def radarEnhancedSensor(
sector: SectorPopulation,
position: Vector3,
faction: PlanetSideEmpire.Value
): Boolean = {
sector.deployableList.collect {
case d: SensorDeployable =>
!d.Destroyed &&
d.Definition.Item == DeployedItem.motionalarmsensor &&
d.Faction != faction &&
!d.Jammed && Vector3.DistanceSquared(d.Position, position) < 2500f
}.contains(true)
}
private def radarCloakedAms(
sector: SectorPopulation,
position: Vector3
): Boolean = {
sector.vehicleList.collect {
case v =>
!v.Destroyed &&
v.Definition == GlobalDefinitions.ams &&
v.DeploymentState == DriveState.Deployed &&
!v.Jammed &&
Vector3.DistanceSquared(v.Position, position) < 169f //12+1m
}.contains(true)
}
private def radarCloakedAegis(
sector: SectorPopulation,
position: Vector3
): Boolean = {
sector.deployableList.collect {
case d: ShieldGeneratorDeployable =>
!d.Destroyed &&
!d.Jammed &&
Vector3.DistanceSquared(d.Position, position) < 121f //10+1m
}.contains(true)
}
private def radarCloakedSensor(
sector: SectorPopulation,
position: Vector3,
faction: PlanetSideEmpire.Value
): Boolean = {
sector.deployableList.collect {
case d: SensorDeployable =>
!d.Destroyed &&
d.Definition.Item == DeployedItem.sensor_shield &&
d.Faction == faction &&
!d.Jammed &&
Vector3.DistanceSquared(d.Position, position) < 961f //30+1m
}.contains(true)
}
private def entityTookDamage(
obj: InGameHistory,
now: Long = System.currentTimeMillis(),
interval: Long = 2000L
): Boolean = {
obj.VitalsHistory()
.findLast(_.isInstanceOf[DamagingActivity])
.exists(dam => now - dam.time < interval)
}
}

View file

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

View file

@ -14,6 +14,8 @@ import net.psforever.types.PlanetSideGUID
class InventoryItem(val obj: Equipment, var start: Int = 0) {
//TODO eventually move this object from storing the item directly to just storing its GUID?
def GUID: PlanetSideGUID = obj.GUID
override def toString: String = s"InventoryItem(${obj.Definition.Name}-$start)"
}
object InventoryItem {

View file

@ -84,7 +84,7 @@ object GenericHackables {
HackState.Start
} else if (progress >= 100L) {
HackState.Finished
} else if (target.isMoving(1f)) {
} else if (target.isMoving(test = 1f)) {
// If the object is moving (more than slightly to account for things like magriders rotating, or the last velocity reported being the magrider dipping down on dismount) then cancel the hack
HackState.Cancelled
} else {

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

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.avatar.scoring.Kill
import net.psforever.objects.sourcing.{PlayerSource, UniquePlayer}
import net.psforever.objects.sourcing.UniquePlayer
import net.psforever.types.{PlanetSideEmpire, Vector3}
import scala.collection.mutable
@ -143,7 +143,7 @@ object FacilityHackParticipation {
killTime <= end &&
Vector3.DistanceSquared(centerXY, k.info.interaction.hitPos.xy) < distanceSq
}
(PlayerSource(p).unique, math.min(d, duration).toFloat / duration.toFloat, killList)
(UniquePlayer(p), math.min(d, duration).toFloat / duration.toFloat, killList)
}
}

View file

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

View file

@ -2,16 +2,35 @@
package net.psforever.objects.serverobject.turret
import net.psforever.objects.equipment.JammableUnit
import net.psforever.objects.serverobject.structures.Amenity
import net.psforever.objects.serverobject.structures.{Amenity, AmenityOwner, Building}
import net.psforever.objects.serverobject.terminals.capture.CaptureTerminalAware
import net.psforever.objects.serverobject.turret.auto.AutomatedTurret
import net.psforever.objects.sourcing.SourceEntry
import net.psforever.types.Vector3
class FacilityTurret(tDef: FacilityTurretDefinition)
extends Amenity
with WeaponTurret
with AutomatedTurret
with JammableUnit
with CaptureTerminalAware {
WeaponTurret.LoadDefinition(this)
WeaponTurret.LoadDefinition(turret = this)
def TurretOwner: SourceEntry = {
Seats
.headOption
.collect { case (_, a) => a }
.flatMap(_.occupant)
.map(SourceEntry(_))
.getOrElse(SourceEntry(Owner))
}
override def Owner: AmenityOwner = {
if (Zone.map.cavern) {
Building.NoBuilding
} else {
super.Owner
}
}
def Definition: FacilityTurretDefinition = tDef
}
@ -27,9 +46,6 @@ object FacilityTurret {
new FacilityTurret(tDef)
}
final case class RechargeAmmo()
final case class WeaponDischarged()
import akka.actor.ActorContext
/**

View file

@ -1,190 +1,175 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.serverobject.turret
import akka.actor.Cancellable
import net.psforever.objects.{Default, GlobalDefinitions, Player, Tool}
import net.psforever.objects.equipment.{Ammo, JammableMountedWeapons}
import net.psforever.objects.{GlobalDefinitions, Player, Tool}
import net.psforever.objects.equipment.Ammo
import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject}
import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior}
import net.psforever.objects.serverobject.affinity.FactionAffinityBehavior
import net.psforever.objects.serverobject.damage.{Damageable, DamageableWeaponTurret}
import net.psforever.objects.serverobject.damage.Damageable
import net.psforever.objects.serverobject.hackable.GenericHackables
import net.psforever.objects.serverobject.hackable.GenericHackables.getTurretUpgradeTime
import net.psforever.objects.serverobject.repair.{AmenityAutoRepair, RepairableWeaponTurret}
import net.psforever.objects.serverobject.structures.PoweredAmenityControl
import net.psforever.objects.serverobject.terminals.capture.CaptureTerminalAwareBehavior
import net.psforever.objects.serverobject.mount.Mountable
import net.psforever.objects.serverobject.repair.AmenityAutoRepair
import net.psforever.objects.serverobject.structures.{Building, PoweredAmenityControl}
import net.psforever.objects.serverobject.terminals.capture.{CaptureTerminal, CaptureTerminalAwareBehavior}
import net.psforever.objects.serverobject.turret.auto.AutomatedTurret.Target
import net.psforever.objects.serverobject.turret.auto.{AffectedByAutomaticTurretFire, AutomatedTurret, AutomatedTurretBehavior}
import net.psforever.objects.vital.interaction.DamageResult
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import net.psforever.packet.game.ChangeFireModeMessage
import net.psforever.services.Service
import net.psforever.services.vehicle.support.TurretUpgrader
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
import net.psforever.types.{BailType, PlanetSideEmpire, PlanetSideGUID}
/**
* An `Actor` that handles messages being dispatched to a specific `MannedTurret`.<br>
* <br>
* Mounted turrets have only slightly different entry requirements than a normal vehicle
* because they encompass both faction-specific facility turrets
* and faction-blind cavern sentry turrets.
*
* @param turret the `MannedTurret` object being governed
*/
* A control agency that handles messages being dispatched to a specific `FacilityTurret`.
* These turrets are attached specifically to surface-level facilities and field towers.
* @param turret the `FacilityTurret` object being governed
*/
class FacilityTurretControl(turret: FacilityTurret)
extends PoweredAmenityControl
with FactionAffinityBehavior.Check
with MountableBehavior
with DamageableWeaponTurret
with RepairableWeaponTurret
extends PoweredAmenityControl
with AmenityAutoRepair
with JammableMountedWeapons
with MountableTurretControl
with AutomatedTurretBehavior
with AffectedByAutomaticTurretFire
with CaptureTerminalAwareBehavior {
def TurretObject: FacilityTurret = turret
def FactionObject: FacilityTurret = turret
def MountableObject: FacilityTurret = turret
def JammableObject: FacilityTurret = turret
def DamageableObject: FacilityTurret = turret
def RepairableObject: FacilityTurret = turret
def AutoRepairObject: FacilityTurret = turret
def AutomatedTurretObject: FacilityTurret = turret
def CaptureTerminalAwareObject: FacilityTurret = turret
def AffectedObject: FacilityTurret = turret
// Used for timing ammo recharge for vanu turrets in caves
var weaponAmmoRechargeTimer: Cancellable = Default.Cancellable
private var testToResetToDefaultFireMode: Boolean = false
AutomaticOperation = true
override def postStop(): Unit = {
super.postStop()
damageableWeaponTurretPostStop()
automaticTurretPostStop()
stopAutoRepair()
}
def commonBehavior: Receive =
checkBehavior
.orElse(jammableBehavior)
.orElse(dismountBehavior)
.orElse(takesDamage)
.orElse(canBeRepairedByNanoDispenser)
.orElse(autoRepairBehavior)
.orElse(captureTerminalAwareBehaviour)
private val upgradeableTurret: Receive = {
case CommonMessages.Use(player, Some((item: Tool, upgradeValue: Int)))
if player.Faction == TurretObject.Faction &&
item.Definition == GlobalDefinitions.nano_dispenser && item.AmmoType == Ammo.upgrade_canister &&
item.Magazine > 0 && TurretObject.Seats.values.forall(!_.isOccupied) =>
TurretUpgrade.values.find(_.id == upgradeValue).foreach {
case upgrade
if TurretObject.Upgrade != upgrade && TurretObject.Definition.WeaponPaths.values
.flatMap(_.keySet)
.exists(_ == upgrade) =>
AutomaticOperation = false
sender() ! CommonMessages.Progress(
1.25f,
WeaponTurrets.FinishUpgradingMannedTurret(TurretObject, player, item, upgrade),
GenericHackables.TurretUpgradingTickAction(progressType = 2, player, TurretObject, item.GUID)
)
}
case TurretUpgrader.UpgradeCompleted(_) =>
CurrentTargetLastShotReported = System.currentTimeMillis() + 2000L
AutomaticOperation = true
}
def poweredStateLogic: Receive =
override def commonBehavior: Receive = super.commonBehavior
.orElse(automatedTurretBehavior)
.orElse(takeAutomatedDamage)
.orElse(autoRepairBehavior)
.orElse(captureTerminalAwareBehaviour)
override def poweredStateLogic: Receive =
commonBehavior
.orElse(mountBehavior)
.orElse(upgradeableTurret)
.orElse {
case CommonMessages.Use(player, Some((item: Tool, upgradeValue: Int)))
if player.Faction == turret.Faction &&
item.Definition == GlobalDefinitions.nano_dispenser && item.AmmoType == Ammo.upgrade_canister &&
item.Magazine > 0 && turret.Seats.values.forall(!_.isOccupied) =>
TurretUpgrade.values.find(_.id == upgradeValue) match {
case Some(upgrade)
if turret.Upgrade != upgrade && turret.Definition.WeaponPaths.values
.flatMap(_.keySet)
.exists(_ == upgrade) =>
turret.setMiddleOfUpgrade(true)
sender() ! CommonMessages.Progress(
1.25f,
WeaponTurrets.FinishUpgradingMannedTurret(turret, player, item, upgrade),
GenericHackables.TurretUpgradingTickAction(progressType = 2, player, turret, item.GUID)
)
case _ => ;
}
case FacilityTurret.WeaponDischarged() =>
if (weaponAmmoRechargeTimer != Default.Cancellable) {
weaponAmmoRechargeTimer.cancel()
}
weaponAmmoRechargeTimer = context.system.scheduler.scheduleWithFixedDelay(
3 seconds,
200 milliseconds,
self,
FacilityTurret.RechargeAmmo()
)
case FacilityTurret.RechargeAmmo() =>
turret.ControlledWeapon(wepNumber = 1).foreach {
case weapon: Tool =>
// recharge when last shot fired 3s delay, +1, 200ms interval
if (weapon.Magazine < weapon.MaxMagazine && System.currentTimeMillis() - weapon.LastDischarge > 3000L) {
weapon.Magazine += 1
val seat = turret.Seat(0).get
seat.occupant match {
case Some(player : Player) =>
turret.Zone.LocalEvents ! LocalServiceMessage(
turret.Zone.id,
LocalAction.RechargeVehicleWeapon(player.GUID, turret.GUID, weapon.GUID)
)
case _ => ;
}
}
else if (weapon.Magazine == weapon.MaxMagazine && weaponAmmoRechargeTimer != Default.Cancellable) {
weaponAmmoRechargeTimer.cancel()
weaponAmmoRechargeTimer = Default.Cancellable
}
case _ => ;
}
case _ => ;
case _ => ()
}
def unpoweredStateLogic: Receive =
override def unpoweredStateLogic: Receive =
commonBehavior
.orElse {
case _ => ;
case _ => ()
}
override protected def mountTest(
obj: PlanetSideServerObject with Mountable,
seatNumber: Int,
player: Player): Boolean = {
(!turret.Definition.FactionLocked || player.Faction == obj.Faction) && !obj.Destroyed && !turret.isUpgrading ||
System.currentTimeMillis() - getTurretUpgradeTime >= 1500L
super.mountTest(obj, seatNumber, player) &&
(!TurretObject.isUpgrading || System.currentTimeMillis() - GenericHackables.getTurretUpgradeTime >= 1500L)
}
override protected def tryMount(obj: PlanetSideServerObject with Mountable, seatNumber: Int, player: Player): Boolean = {
AutomaticOperation = false //turn off
if (!super.tryMount(obj, seatNumber, player)) {
AutomaticOperation = true //revert?
false
} else {
true
}
}
override protected def tryDismount(obj: Mountable, seatNumber: Int, player: Player, bailType: BailType.Value): Boolean = {
AutomaticOperation = AutomaticOperationFunctionalityChecksExceptMounting //turn on, if can turn on
if (!super.tryDismount(obj, seatNumber, player, bailType)) {
AutomaticOperation = false //revert
false
} else {
CurrentTargetLastShotReported = System.currentTimeMillis() + 4000L
true
}
}
override protected def DamageAwareness(target: Damageable.Target, cause: DamageResult, amount: Any) : Unit = {
tryAutoRepair()
if (AutomaticOperation) {
if (TurretObject.Health < TurretObject.Definition.DamageDisablesAt) {
AutomaticOperation = false
} else {
amount match {
case 0 => ()
case _ => attemptRetaliation(target, cause)
}
}
}
super.DamageAwareness(target, cause, amount)
}
override protected def DestructionAwareness(target: Damageable.Target, cause: DamageResult): Unit = {
tryAutoRepair()
super.DestructionAwareness(target, cause)
val zone = target.Zone
val zoneId = zone.id
val events = zone.AvatarEvents
val tguid = target.GUID
events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(tguid, 50, 1))
events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(tguid, 51, 1))
tryAutoRepair()
AutomaticOperation = false
selfReportingCleanUp()
}
override def PerformRepairs(target : Damageable.Target, amount : Int) : Int = {
val newHealth = super.PerformRepairs(target, amount)
if(newHealth == target.Definition.MaxHealth) {
if (!AutomaticOperation && newHealth > target.Definition.DamageDisablesAt) {
AutomaticOperation = true
}
if (newHealth == target.Definition.MaxHealth) {
stopAutoRepair()
}
newHealth
}
override def Restoration(obj: Damageable.Target): Unit = {
super.Restoration(obj)
val zone = turret.Zone
val zoneId = zone.id
val events = zone.AvatarEvents
val tguid = turret.GUID
events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(tguid, 50, 0))
events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(tguid, 51, 0))
}
override def tryAutoRepair() : Boolean = {
isPowered && super.tryAutoRepair()
}
def powerTurnOffCallback(): Unit = {
stopAutoRepair()
AutomaticOperation = false
//kick all occupants
val guid = turret.GUID
val zone = turret.Zone
val guid = TurretObject.GUID
val zone = TurretObject.Zone
val zoneId = zone.id
val events = zone.VehicleEvents
turret.Seats.values.foreach(seat =>
TurretObject.Seats.values.foreach(seat =>
seat.occupant match {
case Some(player) =>
seat.unmount(player)
@ -192,12 +177,159 @@ class FacilityTurretControl(turret: FacilityTurret)
if (player.HasGUID) {
events ! VehicleServiceMessage(zoneId, VehicleAction.KickPassenger(player.GUID, 4, unk2=false, guid))
}
case None => ;
case None => ()
}
)
}
def powerTurnOnCallback(): Unit = {
tryAutoRepair()
AutomaticOperation = true
}
override def AutomaticOperation_=(state: Boolean): Boolean = {
val result = super.AutomaticOperation_=(state)
testToResetToDefaultFireMode = result && TurretObject.Definition.AutoFire.exists(_.revertToDefaultFireMode)
result
}
override protected def AutomaticOperationFunctionalityChecks: Boolean = {
AutomaticOperationFunctionalityChecksExceptMounting &&
!TurretObject.Seats.values.exists(_.isOccupied)
}
private def AutomaticOperationFunctionalityChecksExceptMounting: Boolean = {
AutomaticOperationFunctionalityChecksExceptMountingAndHacking &&
(TurretObject.Owner match {
case b: Building => !b.CaptureTerminalIsHacked
case _ => false
})
}
private def AutomaticOperationFunctionalityChecksExceptMountingAndHacking: Boolean = {
super.AutomaticOperationFunctionalityChecks &&
isPowered &&
TurretObject.Owner.Faction != PlanetSideEmpire.NEUTRAL &&
!JammableObject.Jammed &&
TurretObject.Health >= TurretObject.Definition.DamageDisablesAt &&
!TurretObject.isUpgrading
}
private def primaryWeaponFireModeOnly(): Unit = {
if (testToResetToDefaultFireMode) {
val zone = TurretObject.Zone
val zoneid = zone.id
val events = zone.VehicleEvents
TurretObject.Weapons.values
.flatMap(_.Equipment)
.collect { case weapon: Tool if weapon.FireModeIndex > 0 =>
weapon.FireModeIndex = 0
events ! VehicleServiceMessage(
zoneid,
VehicleAction.SendResponse(Service.defaultPlayerGUID, ChangeFireModeMessage(weapon.GUID, 0))
)
}
}
testToResetToDefaultFireMode = false
}
override protected def trySelectNewTarget(): Option[AutomatedTurret.Target] = {
primaryWeaponFireModeOnly()
super.trySelectNewTarget()
}
protected def engageNewDetectedTarget(
target: Target,
channel: String,
turretGuid: PlanetSideGUID,
weaponGuid: PlanetSideGUID
): Unit = {
val zone = target.Zone
primaryWeaponFireModeOnly()
AutomatedTurretBehavior.startTracking(zone, channel, turretGuid, List(target.GUID))
AutomatedTurretBehavior.startShooting(zone, channel, weaponGuid)
}
protected def noLongerEngageTarget(
target: Target,
channel: String,
turretGuid: PlanetSideGUID,
weaponGuid: PlanetSideGUID
): Option[Target] = {
val zone = target.Zone
AutomatedTurretBehavior.stopTracking(zone, channel, turretGuid)
AutomatedTurretBehavior.stopShooting(zone, channel, weaponGuid)
None
}
protected def testNewDetected(
target: Target,
channel: String,
turretGuid: PlanetSideGUID,
weaponGuid: PlanetSideGUID
): Unit = {
val zone = target.Zone
AutomatedTurretBehavior.startTracking(zone, channel, turretGuid, List(target.GUID))
AutomatedTurretBehavior.startShooting(zone, channel, weaponGuid)
AutomatedTurretBehavior.stopShooting(zone, channel, weaponGuid)
AutomatedTurretBehavior.stopTracking(zone, channel, turretGuid)
}
protected def testKnownDetected(
target: Target,
channel: String,
turretGuid: PlanetSideGUID,
weaponGuid: PlanetSideGUID
): Unit = {
val zone = target.Zone
AutomatedTurretBehavior.startTracking(zone, channel, turretGuid, List(target.GUID))
AutomatedTurretBehavior.startShooting(zone, channel, weaponGuid)
AutomatedTurretBehavior.stopShooting(zone, channel, weaponGuid)
AutomatedTurretBehavior.stopTracking(zone, channel, turretGuid)
}
override def TryJammerEffectActivate(target: Any, cause: DamageResult): Unit = {
val startsUnjammed = !JammableObject.Jammed
super.TryJammerEffectActivate(target, cause)
if (JammableObject.Jammed && AutomatedTurretObject.Definition.AutoFire.exists(_.retaliatoryDelay > 0)) {
if (startsUnjammed) {
AutomaticOperation = false
}
//look in direction of cause of jamming
val zone = JammableObject.Zone
AutomatedTurretBehavior.getAttackVectorFromCause(zone, cause).foreach { attacker =>
AutomatedTurretBehavior.startTracking(zone, zone.id, JammableObject.GUID, List(attacker.GUID))
}
}
}
override def CancelJammeredStatus(target: Any): Unit = {
val startsJammed = JammableObject.Jammed
super.CancelJammeredStatus(target)
if (startsJammed && AutomaticOperation_=(state = true)) {
val zone = TurretObject.Zone
AutomatedTurretBehavior.stopTracking(zone, zone.id, TurretObject.GUID)
}
}
override protected def captureTerminalIsResecured(terminal: CaptureTerminal): Unit = {
captureTerminalChanges(terminal, super.captureTerminalIsResecured, actionDelays = 2000L)
}
override protected def captureTerminalIsHacked(terminal: CaptureTerminal): Unit = {
captureTerminalChanges(terminal, super.captureTerminalIsHacked, actionDelays = 3000L)
}
private def captureTerminalChanges(
terminal: CaptureTerminal,
changeFunc: CaptureTerminal=>Unit,
actionDelays: Long
): Unit = {
AutomaticOperation = false
changeFunc(terminal)
if (AutomaticOperationFunctionalityChecks) {
CurrentTargetLastShotReported = System.currentTimeMillis() + actionDelays
AutomaticOperation = true
}
}
}

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
package net.psforever.objects.serverobject.turret
import net.psforever.objects.PlanetSideGameObject
import net.psforever.objects.definition.{ObjectDefinition, ToolDefinition, WithShields}
import net.psforever.objects.vehicles.{MountableWeaponsDefinition, Turrets}
import net.psforever.objects.vital.resistance.ResistanceProfileMutators
import net.psforever.objects.vital.resolution.DamageResistanceModel
import scala.collection.mutable
import scala.concurrent.duration._
final case class Automation(
ranges: AutoRanges,
checks: AutoChecks,
/** the boundary for target searching is typically a sphere of `ranges.detection` radius;
* instead, takes the shape of a cylinder of `ranges.detection` radius and height */
cylindrical: Boolean = false,
/** if target searching is performed in the shape of a cylinder,
* add height on top of the cylinder's normal height */
cylindricalExtraHeight: Float = 0, //m
/** how long after the last target engagement
* or how long into the current target engagement
* before the turret may counterattack damage;
* set to `0L` to never retaliate */
retaliatoryDelay: Long = 0, //ms
/** if the turret has a current target,
* allow for retaliation against a different target */
retaliationOverridesTarget: Boolean = true,
/** frequency at which the turret will test target for reachability */
detectionSweepTime: FiniteDuration = 1.seconds,
cooldowns: AutoCooldowns = AutoCooldowns(),
/** if the turret weapon has multiple fire modes,
* revert to the base fire mode before engaging in target testing or other automatic operations */
revertToDefaultFireMode: Boolean = true,
/** the simulated weapon fire rate for self-reporting (internal damage loop) */
refireTime: FiniteDuration = 1.seconds //60rpm
)
final case class AutoRanges(
/** distance at which a target is first noticed */
detection: Float, //m
/** distance at which the target is tested */
trigger: Float, //m
/** distance away from the source of damage before the turret stops engaging */
escape: Float //m
) {
assert(detection >= trigger, "detection range must be greater than or equal to trigger range")
assert(escape >= trigger, "escape range must be greater than or equal to trigger range")
}
final case class AutoChecks(
/** reasons why this target should be engaged */
validation: List[PlanetSideGameObject => Boolean],
/** reasons why an ongoing target engagement should be stopped */
blanking: List[PlanetSideGameObject => Boolean] = Nil
)
final case class AutoCooldowns(
/** when the target gets switched (generic) */
targetSelect: Long = 1500L, //ms
/** when the target escapes being damaged */
missedShot: Long = 3000L, //ms
/** when the target gets destroyed during an ongoing engagement */
targetElimination: Long = 0L //ms
)
/**
* The definition for any `MannedTurret`.
* The definition for any `WeaponTurret`.
*/
trait TurretDefinition
extends MountableWeaponsDefinition
@ -25,11 +82,12 @@ trait TurretDefinition
/** can only be mounted by owning faction when `true` */
private var factionLocked: Boolean = true
/** creates internal ammunition reserves that can not become depleted
* see `MannedTurret.TurretAmmoBox` for details
*/
/** creates internal ammunition reserves that can not become depleted */
private var hasReserveAmmunition: Boolean = false
/** */
private var turretAutomation: Option[Automation] = None
def WeaponPaths: mutable.HashMap[Int, mutable.HashMap[TurretUpgrade.Value, ToolDefinition]] = weaponPaths
def FactionLocked: Boolean = factionLocked
@ -45,4 +103,15 @@ trait TurretDefinition
hasReserveAmmunition = reserved
ReserveAmmunition
}
def AutoFire: Option[Automation] = turretAutomation
def AutoFire_=(auto: Automation): Option[Automation] = {
AutoFire_=(Some(auto))
}
def AutoFire_=(auto: Option[Automation]): Option[Automation] = {
turretAutomation = auto
turretAutomation
}
}

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.inventory.{Container, GridInventory}
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.serverobject.mount.{SeatDefinition, Seat => Chair}
import net.psforever.objects.serverobject.mount.{Seat => Chair}
import net.psforever.objects.vehicles.MountableWeapons
trait WeaponTurret
@ -14,10 +14,6 @@ trait WeaponTurret
with MountableWeapons
with Container {
_: PlanetSideGameObject =>
/** manned turrets have just one mount; this is just standard interface */
seats = Map(0 -> new Chair(new SeatDefinition()))
/** may or may not have inaccessible inventory space
* see `ReserveAmmunition` in the definition
*/
@ -84,25 +80,27 @@ trait WeaponTurret
}
object WeaponTurret {
/**
* Use the `*Definition` that was provided to this object to initialize its fields and settings.
* @see `{object}.LoadDefinition`
* @param turret the `MannedTurret` being initialized
* Use the definition that was provided to this object to initialize its fields and settings.
* @see `WeaponTurret.LoadDefinition(WeaponTurret, TurretDefinition)`
* @param turret turret being initialized
*/
def LoadDefinition(turret: WeaponTurret): WeaponTurret = {
LoadDefinition(turret, turret.Definition)
}
/**
* Use the `*Definition` that was provided to this object to initialize its fields and settings.
* A default definition is provided to be used.
* @see `{object}.LoadDefinition`
* @param turret the `MannedTurret` being initialized
* @param tdef the object definition
* Use the definition that was provided to this object to initialize its fields and settings.
* @see `WeaponTurret.LoadDefinition(WeaponTurret)`
* @param turret turret being initialized
* @param tdef object's specific definition
*/
def LoadDefinition(turret: WeaponTurret, tdef: TurretDefinition): WeaponTurret = {
import net.psforever.objects.equipment.EquipmentSize.BaseTurretWeapon
//create seats, if any
turret.seats = tdef.Seats.map {
case (num, definition) => num -> new Chair(definition)
}.toMap
//create weapons; note the class
turret.weapons = tdef.WeaponPaths
.map({
@ -160,17 +158,18 @@ class TurretWeapon(
Upgrade
}
override def Definition = udefs(Upgrade)
override def Definition: ToolDefinition = udefs(Upgrade)
}
/**
* A special type of ammunition box contained within a `MannedTurret` for the purposes of infinite reloads.
* A special type of ammunition box contained for the purposes of infinite reloads.
* The original quantity of ammunition does not change.
* @param adef ammunition definition
*/
class TurretAmmoBox(private val adef: AmmoBoxDefinition) extends AmmoBox(adef, Some(65535)) {
class TurretAmmoBox(private val adef: AmmoBoxDefinition)
extends AmmoBox(adef, Some(65535)) {
import net.psforever.objects.inventory.InventoryTile
override def Tile = InventoryTile.Tile11
override def Tile: InventoryTile = InventoryTile.Tile11
override def Capacity_=(toCapacity: Int) = Capacity
override def Capacity_=(toCapacity: Int): Int = Capacity
}

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

View file

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

View file

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

View file

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

View file

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

View file

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

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 identifer = obj match {
case o: TurretDeployable =>
UniqueDeployable(
o.History.headOption match {
case Some(entry) => entry.time
case None => 0L
},
o.OriginalOwnerName.getOrElse("none")
)
UniqueDeployable(o)
case o: FacilityTurret =>
UniqueAmenity(o.Zone.Number, o.GUID, position)
UniqueAmenity(o)
case o =>
throw new IllegalArgumentException(s"was given ${o.Actor.toString()} when only wanted to model turrets")
}

View file

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

View file

@ -1,7 +1,21 @@
// Copyright (c) 2023 PSForever
package net.psforever.objects.sourcing
import net.psforever.objects.ce.Deployable
final case class UniqueDeployable(
spawnTime: Long,
originalOwnerName: String
) extends SourceUniqueness
object UniqueDeployable {
def apply(obj: Deployable): UniqueDeployable = {
UniqueDeployable(
obj.History.headOption match {
case Some(entry) => entry.time
case None => 0L
},
obj.OriginalOwnerName.getOrElse("none")
)
}
}

View file

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

View file

@ -2,17 +2,9 @@
package net.psforever.objects.vehicles
import net.psforever.objects.Vehicle
import net.psforever.objects.ballistics.{Projectile, ProjectileQuality}
import net.psforever.objects.sourcing.SourceEntry
import net.psforever.objects.vital.Vitality
import net.psforever.objects.vital.base.{DamageResolution, DamageType}
import net.psforever.objects.vital.etc.RadiationReason
import net.psforever.objects.vital.interaction.DamageInteraction
import net.psforever.objects.serverobject.mount.{InteractWithRadiationCloudsSeatedInEntity, RadiationInMountableInteraction}
import net.psforever.objects.zones.blockmap.SectorPopulation
import net.psforever.objects.zones.{InteractsWithZone, Zone, ZoneInteraction, ZoneInteractionType}
import net.psforever.types.PlanetSideGUID
case object RadiationInVehicleInteraction extends ZoneInteractionType
import net.psforever.objects.zones.InteractsWithZone
/**
* This game entity may infrequently test whether it may interact with radiation cloud projectiles
@ -21,90 +13,24 @@ case object RadiationInVehicleInteraction extends ZoneInteractionType
*/
class InteractWithRadiationCloudsSeatedInVehicle(
private val obj: Vehicle,
val range: Float
) extends ZoneInteraction {
/**
* radiation clouds that, though detected, are skipped from affecting the target;
* in between interaction tests, a memory of the clouds that were tested last are retained and
* are excluded from being tested this next time;
* clouds that are detected a second time are cleared from the list and are available to be tested next time
*/
private var skipTargets: List[PlanetSideGUID] = List()
def Type = RadiationInVehicleInteraction
override val range: Float
) extends InteractWithRadiationCloudsSeatedInEntity(obj, range) {
/**
* Drive into a radiation cloud and all the vehicle's occupants suffer the consequences.
* @param sector the portion of the block map being tested
* @param target the fixed element in this test
*/
override def interaction(sector: SectorPopulation, target: InteractsWithZone): Unit = {
val position = target.Position
//collect all projectiles in sector/range
val projectiles = sector
.projectileList
.filter { cloud =>
val definition = cloud.Definition
definition.radiation_cloud &&
definition.AllDamageTypes.contains(DamageType.Radiation) &&
{
val radius = definition.DamageRadius
Zone.distanceCheck(target, cloud, radius * radius)
}
}
.distinct
val notSkipped = projectiles.filterNot { t => skipTargets.contains(t.GUID) }
skipTargets = notSkipped.map { _.GUID }
if (notSkipped.nonEmpty) {
(
//isolate one of each type of projectile
notSkipped
.foldLeft(Nil: List[Projectile]) {
(acc, next) => if (acc.exists { _.profile == next.profile }) acc else next :: acc
},
obj.Seats
.values
.collect { case seat => seat.occupant }
.flatten
) match {
case (uniqueProjectiles, targets) if uniqueProjectiles.nonEmpty && targets.nonEmpty =>
val shielding = obj.Definition.RadiationShielding
targets.foreach { t =>
uniqueProjectiles.foreach { p =>
t.Actor ! Vitality.Damage(
DamageInteraction(
SourceEntry(t),
RadiationReason(
ProjectileQuality.modifiers(p, DamageResolution.Radiation, t, t.Position, None),
t.DamageModel,
shielding
),
position
).calculate()
)
}
}
case _ => ;
}
}
super.interaction(sector, target)
obj.CargoHolds
.values
.collect {
case hold if hold.isOccupied =>
val target = hold.occupant.get
target.interaction().find { _.Type == RadiationInVehicleInteraction } match {
case Some(func) => func.interaction(sector, target)
case _ => ;
target
.interaction()
.find(_.Type == RadiationInMountableInteraction)
.foreach(func => func.interaction(sector, target))
}
}
}
/**
* Any radiation clouds blocked from being tested should be cleared.
* All that can be done is blanking our retained previous effect targets.
* @param target the fixed element in this test
*/
def resetInteraction(target: InteractsWithZone): Unit = {
skipTargets = List()
}
}

View file

@ -1,7 +1,7 @@
// Copyright (c) 2021 PSForever
package net.psforever.objects.vehicles.control
import akka.actor.Actor
import akka.actor.{Actor, Cancellable}
import net.psforever.objects._
import net.psforever.services.Service
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
@ -16,7 +16,7 @@ trait VehicleCapacitance {
_: Actor =>
def CapacitanceObject: Vehicle
protected var capacitor = Default.Cancellable
protected var capacitor: Cancellable = Default.Cancellable
startCapacitorTimer()

View file

@ -19,9 +19,10 @@ import net.psforever.objects.serverobject.environment._
import net.psforever.objects.serverobject.environment.interaction.common.Watery
import net.psforever.objects.serverobject.environment.interaction.{InteractWithEnvironment, RespondsToZoneEnvironment}
import net.psforever.objects.serverobject.hackable.GenericHackables
import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior}
import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior, RadiationInMountableInteraction}
import net.psforever.objects.serverobject.repair.RepairableVehicle
import net.psforever.objects.serverobject.terminals.Terminal
import net.psforever.objects.serverobject.turret.auto.AffectedByAutomaticTurretFire
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource}
import net.psforever.objects.vehicles._
import net.psforever.objects.vehicles.interaction.WithWater
@ -58,7 +59,8 @@ class VehicleControl(vehicle: Vehicle)
with ContainableBehavior
with AggravatedBehavior
with RespondsToZoneEnvironment
with CargoBehavior {
with CargoBehavior
with AffectedByAutomaticTurretFire {
//make control actors belonging to utilities when making control actor belonging to vehicle
vehicle.Utilities.foreach { case (_, util) => util.Setup }
@ -71,6 +73,7 @@ class VehicleControl(vehicle: Vehicle)
def ContainerObject: Vehicle = vehicle
def InteractiveObject: Vehicle = vehicle
def CargoObject: Vehicle = vehicle
def AffectedObject: Vehicle = vehicle
// SetInteraction(EnvironmentAttribute.Water, doInteractingWithWater)
// SetInteraction(EnvironmentAttribute.Lava, doInteractingWithLava)
@ -115,6 +118,7 @@ class VehicleControl(vehicle: Vehicle)
.orElse(containerBehavior)
.orElse(environmentBehavior)
.orElse(cargoBehavior)
.orElse(takeAutomatedDamage)
.orElse {
case Vehicle.Ownership(None) =>
LoseOwnership()
@ -244,11 +248,11 @@ class VehicleControl(vehicle: Vehicle)
commonEnabledBehavior
.orElse {
case VehicleControl.RadiationTick =>
vehicle.interaction().find { _.Type == RadiationInVehicleInteraction } match {
case Some(func) => func.interaction(vehicle.getInteractionSector, vehicle)
case _ => ;
vehicle.interaction().find { _.Type == RadiationInMountableInteraction } match {
case Some(func) => func.interaction(vehicle.getInteractionSector(), vehicle)
case _ => ()
}
case _ => ;
case _ => ()
}
def commonDisabledBehavior: Receive = checkBehavior

View file

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

View file

@ -1,9 +1,9 @@
// Copyright (c) 2021 PSForever
package net.psforever.objects.vital.etc
import net.psforever.objects.PlanetSideGameObject
import net.psforever.objects.{PlanetSideGameObject, Vehicle}
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.sourcing.SourceEntry
import net.psforever.objects.sourcing.{SourceEntry, VehicleSource}
import net.psforever.objects.vital.Vitality
import net.psforever.objects.vital.base.{DamageReason, DamageResolution}
import net.psforever.objects.vital.prop.DamageWithPosition
@ -41,6 +41,10 @@ object EmpReason {
source: DamageWithPosition,
target: PlanetSideGameObject with Vitality
): EmpReason = {
EmpReason(SourceEntry(owner), source, target.DamageModel, owner.Definition.ObjectId)
val ownerSource = owner match {
case v: Vehicle => VehicleSource(v).occupants.head
case _ => SourceEntry(owner)
}
EmpReason(ownerSource, source, target.DamageModel, owner.Definition.ObjectId)
}
}

View file

@ -17,19 +17,19 @@ import net.psforever.objects.vital.resolution.DamageAndResistance
final case class TrippedMineReason(mine: DeployableSource, owner: SourceEntry)
extends DamageReason {
def source: DamageProperties = mine.Definition.innateDamage.getOrElse(TrippedMineReason.triggered)
def source: DamageProperties = TrippedMineReason.triggered
def resolution: DamageResolution.Value = DamageResolution.Resolved
def same(test: DamageReason): Boolean = test match {
case trip: TrippedMineReason => mine == trip.mine && mine.OwnerName == trip.mine.OwnerName
case _ => false
case trip: TrippedMineReason => mine.unique == trip.mine.unique && owner.unique == owner.unique
case _ => false
}
/** lay the blame on the player who laid this mine, if possible */
def adversary: Option[SourceEntry] = Some(owner)
override def damageModel : DamageAndResistance = mine.Definition
override def damageModel: DamageAndResistance = mine.Definition
override def attribution: Int = mine.Definition.ObjectId
}

View file

@ -2,15 +2,21 @@ package net.psforever.objects.zones
import enumeratum.values.{IntEnum, IntEnumEntry}
final case class AliasLookup(
zone: Seq[String] = Seq(),
facilities: Map[String, String] = Map()
)
sealed abstract class ZoneInfo(
val value: Int,
val name: String,
val id: String,
val map: MapInfo,
val aliases: Seq[String] = Seq()
val aliases: AliasLookup = ZoneInfo.defaultAliases,
) extends IntEnumEntry {}
case object ZoneInfo extends IntEnum[ZoneInfo] {
private val defaultAliases = AliasLookup(Nil, Map.empty[String, String])
case object Solsar
extends ZoneInfo(
@ -98,7 +104,7 @@ case object ZoneInfo extends IntEnum[ZoneInfo] {
name = "NC Sanctuary",
id = "home1",
map = MapInfo.Map11,
aliases = Seq("nc-sanctuary")
aliases = AliasLookup(zone = Seq("nc-sanctuary"))
)
case object TrSanctuary
@ -107,7 +113,7 @@ case object ZoneInfo extends IntEnum[ZoneInfo] {
name = "TR Sanctuary",
id = "home2",
map = MapInfo.Map12,
aliases = Seq("tr-sanctuary")
aliases = AliasLookup(zone = Seq("tr-sanctuary"))
)
case object VsSanctuary
@ -116,7 +122,7 @@ case object ZoneInfo extends IntEnum[ZoneInfo] {
name = "VS Sanctuary",
id = "home3",
map = MapInfo.Map13,
aliases = Seq("vs-sanctuary")
aliases = AliasLookup(zone = Seq("vs-sanctuary"))
)
case object tzshtr
@ -124,7 +130,8 @@ case object ZoneInfo extends IntEnum[ZoneInfo] {
value = 14,
name = "tzshtr",
id = "tzshtr",
map = MapInfo.Map14
map = MapInfo.Map14,
aliases = AliasLookup(zone = Seq("tr-shooting"))
)
case object tzdrtr
@ -132,7 +139,8 @@ case object ZoneInfo extends IntEnum[ZoneInfo] {
value = 15,
name = "tzdrtr",
id = "tzdrtr",
map = MapInfo.Map15
map = MapInfo.Map15,
aliases = AliasLookup(zone = Seq("tr-driving"))
)
case object tzcotr
@ -148,7 +156,8 @@ case object ZoneInfo extends IntEnum[ZoneInfo] {
value = 17,
name = "tzshnc",
id = "tzshnc",
map = MapInfo.Map14
map = MapInfo.Map14,
aliases = AliasLookup(zone = Seq("nc-shooting"))
)
case object tzdrnc
@ -156,7 +165,8 @@ case object ZoneInfo extends IntEnum[ZoneInfo] {
value = 18,
name = "tzdrnc",
id = "tzdrnc",
map = MapInfo.Map15
map = MapInfo.Map15,
aliases = AliasLookup(zone = Seq("nc-driving"))
)
case object tzconc
@ -172,7 +182,8 @@ case object ZoneInfo extends IntEnum[ZoneInfo] {
value = 20,
name = "tzshvs",
id = "tzshvs",
map = MapInfo.Map14
map = MapInfo.Map14,
aliases = AliasLookup(zone = Seq("vs-shooting"))
)
case object tzdrvs
@ -180,7 +191,8 @@ case object ZoneInfo extends IntEnum[ZoneInfo] {
value = 21,
name = "tzdrvs",
id = "tzdrvs",
map = MapInfo.Map15
map = MapInfo.Map15,
aliases = AliasLookup(zone = Seq("vs-driving"))
)
case object tzcovs
@ -244,7 +256,12 @@ case object ZoneInfo extends IntEnum[ZoneInfo] {
value = 29,
name = "Extinction",
id = "i1",
map = MapInfo.Map99
map = MapInfo.Map99,
aliases = AliasLookup(facilities = Map(
("Mithra", "Blue_Base"),
("Yazata", "Red_Base"),
("Hvar", "Indigo_Base")
))
)
case object Ascension
@ -252,7 +269,12 @@ case object ZoneInfo extends IntEnum[ZoneInfo] {
value = 30,
name = "Ascension",
id = "i2",
map = MapInfo.Map98
map = MapInfo.Map98,
aliases = AliasLookup(facilities = Map(
("Zal", "Base_Alpha"),
("Rashnu", "Base_Bravo"),
("Sraosha", "Base_Charlie")
))
)
case object Desolation
@ -260,7 +282,12 @@ case object ZoneInfo extends IntEnum[ZoneInfo] {
value = 31,
name = "Desolation",
id = "i3",
map = MapInfo.Map97
map = MapInfo.Map97,
aliases = AliasLookup(facilities = Map(
("Dahaka", "Red_Base_97"),
("Jamshid", "Blue_Base_97"),
("Izha", "Indigo_Base_97")
))
)
case object Nexus
@ -268,16 +295,20 @@ case object ZoneInfo extends IntEnum[ZoneInfo] {
value = 32,
name = "Nexus",
id = "i4",
map = MapInfo.Map96
map = MapInfo.Map96,
aliases = AliasLookup(facilities = Map(
("Atar", "Nexus_base")
))
)
val values: IndexedSeq[ZoneInfo] = findValues
def findName(name: String): ZoneInfo = findNameOpt(name).get
def findNameOpt(name: String): Option[ZoneInfo] =
def findNameOpt(name: String): Option[ZoneInfo] = {
val lowerName = name.toLowerCase()
values.find(v =>
v.name.toLowerCase() == name.toLowerCase() || v.aliases.map(_.toLowerCase()).contains(name.toLowerCase())
v.name.toLowerCase().equals(lowerName) || v.aliases.zone.map(_.toLowerCase()).contains(lowerName)
)
}
}

View file

@ -411,7 +411,7 @@ object GamePacketOpcode extends Enumeration {
case 0x59 => noDecoder(UnknownMessage89)
case 0x5a => game.DelayedPathMountMsg.decode
case 0x5b => game.OrbitalShuttleTimeMsg.decode
case 0x5c => noDecoder(AIDamage)
case 0x5c => game.AIDamage.decode
case 0x5d => game.DeployObjectMessage.decode
case 0x5e => game.FavoritesRequest.decode
case 0x5f => noDecoder(FavoritesResponse)
@ -487,7 +487,7 @@ object GamePacketOpcode extends Enumeration {
case 0x99 => noDecoder(EmpireIncentivesMessage)
case 0x9a => game.InvalidTerrainMessage.decode
case 0x9b => noDecoder(SyncMessage)
case 0x9c => noDecoder(DebugDrawMessage)
case 0x9c => game.DebugDrawMessage.decode
case 0x9d => noDecoder(SoulMarkMessage)
case 0x9e => noDecoder(UplinkPositionEvent)
case 0x9f => game.HotSpotUpdateMessage.decode

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

@ -0,0 +1,32 @@
// Copyright (c) 2024 PSForever
package net.psforever.packet.game
import net.psforever.packet.GamePacketOpcode.Type
import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
import net.psforever.types.Vector3
import scodec.bits.BitVector
import scodec.{Attempt, Codec}
import scodec.codecs._
final case class DebugDrawMessage(
unk1: Int,
unk2: Long,
unk3: Long,
unk4: Long,
unk5: List[Vector3]
)
extends PlanetSideGamePacket {
type Packet = DebugDrawMessage
def opcode: Type = GamePacketOpcode.DebugDrawMessage
def encode: Attempt[BitVector] = DebugDrawMessage.encode(this)
}
object DebugDrawMessage extends Marshallable[DebugDrawMessage] {
implicit val codec: Codec[DebugDrawMessage] = (
("unk1" | uint(bits = 3)) ::
("unk2" | ulongL(bits = 32)) ::
("unk3" | ulongL(bits = 32)) ::
("unk4" | ulongL(bits = 32)) ::
("unk5" | listOfN(uint2, Vector3.codec_pos))
).as[DebugDrawMessage]
}

View file

@ -81,13 +81,13 @@ object CommonFieldData extends Marshallable[CommonFieldData] {
CommonFieldData(faction, false, false, false, None, false, None, None, PlanetSideGUID(0))
def apply(faction: PlanetSideEmpire.Value, unk: Int): CommonFieldData =
CommonFieldData(faction, false, false, unk > 1, None, unk % 1 == 1, None, None, PlanetSideGUID(0))
CommonFieldData(faction, false, false, unk > 1, None, unk > 0, None, None, PlanetSideGUID(0))
def apply(faction: PlanetSideEmpire.Value, unk: Int, player_guid: PlanetSideGUID): CommonFieldData =
CommonFieldData(faction, false, false, unk > 1, None, unk % 1 == 1, None, None, player_guid)
CommonFieldData(faction, false, false, unk > 1, None, unk > 0, None, None, player_guid)
def apply(faction: PlanetSideEmpire.Value, destroyed: Boolean, unk: Int): CommonFieldData =
CommonFieldData(faction, false, destroyed, unk > 1, None, unk % 1 == 1, None, None, PlanetSideGUID(0))
CommonFieldData(faction, false, destroyed, unk > 1, None, unk > 0, None, None, PlanetSideGUID(0))
def apply(
faction: PlanetSideEmpire.Value,

View file

@ -15,13 +15,14 @@ import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
import scala.concurrent.Future
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global
class TurretUpgrader extends SupportActor[TurretUpgrader.Entry] {
var task: Cancellable = Default.Cancellable
var list: List[TurretUpgrader.Entry] = List()
val sameEntryComparator = new SimilarityComparator[TurretUpgrader.Entry]() {
val sameEntryComparator: SimilarityComparator[TurretUpgrader.Entry] = new SimilarityComparator[TurretUpgrader.Entry]() {
def Test(entry1: TurretUpgrader.Entry, entry2: TurretUpgrader.Entry): Boolean = {
entry1.obj == entry2.obj && entry1.zone == entry2.zone && entry1.obj.GUID == entry2.obj.GUID
}
@ -41,7 +42,7 @@ class TurretUpgrader extends SupportActor[TurretUpgrader.Entry] {
list = Nil
}
def CreateEntry(obj: PlanetSideGameObject, zone: Zone, upgrade: TurretUpgrade.Value, duration: Long) =
def CreateEntry(obj: PlanetSideGameObject, zone: Zone, upgrade: TurretUpgrade.Value, duration: Long): TurretUpgrader.Entry =
TurretUpgrader.Entry(obj, zone, upgrade, duration)
def InclusionTest(entry: TurretUpgrader.Entry): Boolean = entry.obj.isInstanceOf[FacilityTurret]
@ -89,7 +90,6 @@ class TurretUpgrader extends SupportActor[TurretUpgrader.Entry] {
task.cancel()
if (list.nonEmpty) {
val short_timeout: FiniteDuration = math.max(1, list.head.duration - (now - list.head.time)).milliseconds
import scala.concurrent.ExecutionContext.Implicits.global
task = context.system.scheduler.scheduleOnce(short_timeout, self, TurretUpgrader.Downgrade())
}
}
@ -150,6 +150,7 @@ class TurretUpgrader extends SupportActor[TurretUpgrader.Entry] {
val upgrade = entry.upgrade
val guid = zone.GUID
val turretGUID = target.GUID
target.setMiddleOfUpgrade(true)
//kick all occupying players for duration of conversion
target.Seats.values
.filter { _.isOccupied }
@ -160,7 +161,7 @@ class TurretUpgrader extends SupportActor[TurretUpgrader.Entry] {
if (tplayer.HasGUID) {
context.parent ! VehicleServiceMessage(
zoneId,
VehicleAction.KickPassenger(tplayer.GUID, 4, false, turretGUID)
VehicleAction.KickPassenger(tplayer.GUID, 4, unk2=false, turretGUID)
)
}
})
@ -174,7 +175,6 @@ class TurretUpgrader extends SupportActor[TurretUpgrader.Entry] {
.filterNot { box => newBoxes.exists(_ eq box) }
.map(box => GUIDTask.unregisterEquipment(guid, box))
.toList
import scala.concurrent.ExecutionContext.Implicits.global
val newBoxesTask = TaskBundle(
new StraightforwardTask() {
private val localFunc: () => Unit = FinishUpgradingTurret(entry)
@ -189,22 +189,25 @@ class TurretUpgrader extends SupportActor[TurretUpgrader.Entry] {
.map(box => GUIDTask.registerEquipment(guid, box))
.toList
)
TaskWorkflow.execute(TaskBundle(
val mainTask = TaskWorkflow.execute(TaskBundle(
new StraightforwardTask() {
private val tasks = oldBoxesTask
def action(): Future[Any] = {
tasks.foreach { TaskWorkflow.execute }
tasks.foreach(TaskWorkflow.execute)
Future(this)
}
},
newBoxesTask
))
mainTask.recoverWith {
case _: Exception => Finalize(target, upgrade); Future(true)
}
}
/**
* From an object that has mounted weapons, parse all of the internal ammunition loaded into all of the weapons.
* @param target the object with mounted weaponry
* @param target entity with mounted weaponry
* @return all of the internal ammunition objects
*/
def AllMountedWeaponMagazines(target: MountedWeapons): Iterable[AmmoBox] = {
@ -224,11 +227,10 @@ class TurretUpgrader extends SupportActor[TurretUpgrader.Entry] {
val target = entry.obj.asInstanceOf[FacilityTurret]
val zone = entry.zone
trace(s"Wall turret finished ${target.Upgrade} upgrade")
target.ConfirmUpgrade(entry.upgrade)
val targetGUID = target.GUID
if (target.Health > 0) {
target.Weapons
.map({ case (index: Int, slot: EquipmentSlot) => (index, slot.Equipment) })
.map { case (index: Int, slot: EquipmentSlot) => (index, slot.Equipment) }
.collect {
case (index, Some(tool: Tool)) =>
context.parent ! VehicleServiceMessage(
@ -237,6 +239,17 @@ class TurretUpgrader extends SupportActor[TurretUpgrader.Entry] {
)
}
}
Finalize(target, entry.upgrade)
}
/**
* Dispatch messages to report on the completion of this effort.
* @param target the object with mounted weaponry
* @param upgrade the path of the turret's progression
*/
def Finalize(target: FacilityTurret, upgrade: TurretUpgrade.Value): Unit = {
target.ConfirmUpgrade(upgrade)
target.Actor ! TurretUpgrader.UpgradeCompleted(target.GUID)
}
}
@ -263,6 +276,8 @@ object TurretUpgrader extends SupportActorCaseConversions {
final case class Downgrade()
final case class UpgradeCompleted(targetGuid: PlanetSideGUID)
private def Similarity(entry1: TurretUpgrader.Entry, entry2: TurretUpgrader.Entry): Boolean = {
entry1.obj == entry2.obj && entry1.zone == entry2.zone && entry1.obj.GUID == entry2.obj.GUID
}

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

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

View file

@ -0,0 +1,36 @@
// Copyright (c) 2024 PSForever
package game
import net.psforever.packet.PacketCoding
import net.psforever.packet.game.DebugDrawMessage
import net.psforever.types.Vector3
import org.specs2.mutable.Specification
import scodec.bits._
class DebugDrawMessageTest extends Specification {
val string = hex"9c2040000000600000008000001c0010000186000800c8000f04008807d00016080578"
"decode" in {
PacketCoding.decodePacket(string).require match {
case DebugDrawMessage(u1, u2, u3, u4, u5) =>
u1 mustEqual 1
u2 mustEqual 2L
u3 mustEqual 3L
u4 mustEqual 4L
u5 mustEqual List(
Vector3(5,6,7),
Vector3(50,60,70),
Vector3(500,600,700)
)
case _ =>
ko
}
}
"encode" in {
val msg = DebugDrawMessage(1, 2L, 3L, 4L, List(Vector3(5,6,7), Vector3(50,60,70), Vector3(500,600,700)))
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
pkt mustEqual string
}
}