diff --git a/src/main/scala/net/psforever/actors/session/csr/ChatLogic.scala b/src/main/scala/net/psforever/actors/session/csr/ChatLogic.scala
index 55e947a8a..eee8cb565 100644
--- a/src/main/scala/net/psforever/actors/session/csr/ChatLogic.scala
+++ b/src/main/scala/net/psforever/actors/session/csr/ChatLogic.scala
@@ -8,6 +8,7 @@ import net.psforever.actors.session.support.{ChatFunctions, ChatOperations, Sess
import net.psforever.objects.{GlobalDefinitions, PlanetSideGameObject, Session, TurretDeployable}
import net.psforever.objects.ce.{Deployable, DeployableCategory}
import net.psforever.objects.serverobject.affinity.FactionAffinity
+import net.psforever.objects.serverobject.dome.ForceDomeControl
import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject}
import net.psforever.objects.serverobject.hackable.Hackable
import net.psforever.objects.serverobject.structures.Building
@@ -227,6 +228,7 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext
case "sayspectator" => customCommandSpeakAsSpectator(params, message)
case "setempire" => customCommandSetEmpire(params)
case "weaponlock" => customCommandZoneWeaponUnlock(session, params)
+ case "forcedome" => customForceDomeCommand(session, params)
case _ =>
// command was not handled
sendResponse(
@@ -509,6 +511,41 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext
true
}
+ private def customForceDomeCommand(session: Session, contents: Seq[String]): Boolean = {
+ //locate force dome
+ var postUsageMessage: Boolean = false
+ val locatedForceDomesInZone = session.zone.Buildings.values.flatMap(_.ForceDome)
+ if (locatedForceDomesInZone.nonEmpty) {
+ contents
+ .headOption
+ .map(_.toLowerCase())
+ .collect {
+ case "on" | "o" | "" =>
+ locatedForceDomesInZone.foreach(_.Actor ! ForceDomeControl.CustomExpand)
+ case "off" | "of" =>
+ locatedForceDomesInZone.foreach(_.Actor ! ForceDomeControl.CustomCollapse)
+ case "protect" =>
+ locatedForceDomesInZone.foreach(_.Actor ! ForceDomeControl.ApplyProtection)
+ case "normal" =>
+ locatedForceDomesInZone.foreach(_.Actor ! ForceDomeControl.NormalBehavior)
+ case "purge" =>
+ locatedForceDomesInZone.foreach(_.Actor ! ForceDomeControl.Purge)
+ case "help" | "usage" =>
+ postUsageMessage = true
+ case token =>
+ sendResponse(ChatMsg(ChatMessageType.UNK_227, s"unknown command - $token"))
+ postUsageMessage = true
+ }
+ if (postUsageMessage) {
+ sendResponse(ChatMsg(ChatMessageType.UNK_227, "!forcedome [o[n]|of[f]|protect|normal|purge]"))
+ }
+ } else {
+ //no force domes in zone
+ sendResponse(ChatMsg(ChatMessageType.UNK_227, "no capitol force dome(s) detected in zone"))
+ }
+ true
+ }
+
private def customCommandOnOffStateOrNone(stateOpt: Option[String]): Option[Boolean] = {
stateOpt match {
case None =>
diff --git a/src/main/scala/net/psforever/actors/session/csr/CustomerServiceRepresentativeMode.scala b/src/main/scala/net/psforever/actors/session/csr/CustomerServiceRepresentativeMode.scala
index a6b17186b..0e454f71b 100644
--- a/src/main/scala/net/psforever/actors/session/csr/CustomerServiceRepresentativeMode.scala
+++ b/src/main/scala/net/psforever/actors/session/csr/CustomerServiceRepresentativeMode.scala
@@ -8,7 +8,6 @@ import net.psforever.objects.serverobject.ServerObject
import net.psforever.objects.serverobject.mount.Mountable
import net.psforever.objects.vital.Vitality
import net.psforever.objects.zones.Zone
-import net.psforever.objects.zones.blockmap.BlockMapEntity
import net.psforever.packet.game.{ChatMsg, ObjectCreateDetailedMessage, PlanetsideAttributeMessage}
import net.psforever.packet.game.objectcreate.RibbonBars
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
@@ -110,69 +109,21 @@ class CustomerServiceRepresentativeMode(data: SessionData) extends ModeLogic {
private def keepAlivePersistanceCSR(): Unit = {
val player = data.player
- data.keepAlivePersistence()
- topOffHealthOfPlayer(player)
player.allowInteraction = false
- topOffHealthOfPlayer(player)
- data.continent.GUID(data.player.VehicleSeated)
- .collect {
- case obj: PlanetSideGameObject with Vitality with BlockMapEntity =>
- topOffHealth(obj)
- data.updateBlockMap(obj, obj.Position)
- obj
- }
- .getOrElse {
- data.updateBlockMap(player, player.Position)
- }
- }
-
- private def topOffHealth(obj: PlanetSideGameObject with Vitality): Unit = {
- obj match {
- case p: Player => topOffHealthOfPlayer(p)
- case v: Vehicle => topOffHealthOfVehicle(v)
- case o: PlanetSideGameObject with Vitality => topOffHealthOfGeneric(o)
- case _ => ()
- }
- }
-
- private def topOffHealthOfPlayer(player: Player): Unit = {
- //driver below half health, full heal
- val maxHealthOfPlayer = player.MaxHealth.toLong
- if (player.Health < maxHealthOfPlayer * 0.5f) {
- player.Health = maxHealthOfPlayer.toInt
- player.LogActivity(player.ClearHistory().head)
- data.sendResponse(PlanetsideAttributeMessage(player.GUID, 0, maxHealthOfPlayer))
- data.continent.AvatarEvents ! AvatarServiceMessage(data.zoning.zoneChannel, AvatarAction.PlanetsideAttribute(player.GUID, 0, maxHealthOfPlayer))
- }
- }
-
- private def topOffHealthOfVehicle(vehicle: Vehicle): Unit = {
- topOffHealthOfGeneric(vehicle)
- //vehicle shields below half, full shields
- val maxShieldsOfVehicle = vehicle.MaxShields.toLong
- val shieldsUi = vehicle.Definition.shieldUiAttribute
- if (vehicle.Shields < maxShieldsOfVehicle) {
- val guid = vehicle.GUID
- vehicle.Shields = maxShieldsOfVehicle.toInt
- data.sendResponse(PlanetsideAttributeMessage(guid, shieldsUi, maxShieldsOfVehicle))
- data.continent.VehicleEvents ! VehicleServiceMessage(
- data.continent.id,
- VehicleAction.PlanetsideAttribute(PlanetSideGUID(0), guid, shieldsUi, maxShieldsOfVehicle)
- )
- }
- }
-
- private def topOffHealthOfGeneric(obj: PlanetSideGameObject with Vitality): Unit = {
- //below half health, full heal
- val guid = obj.GUID
- val maxHealthOf = obj.MaxHealth.toLong
- if (obj.Health < maxHealthOf) {
- obj.Health = maxHealthOf.toInt
- data.sendResponse(PlanetsideAttributeMessage(guid, 0, maxHealthOf))
- data.continent.VehicleEvents ! VehicleServiceMessage(
- data.continent.id,
- VehicleAction.PlanetsideAttribute(PlanetSideGUID(0), guid, 0, maxHealthOf)
- )
+ CustomerServiceRepresentativeMode.topOffHealthOfPlayer(data, player)
+ data.zoning.spawn.interimUngunnedVehicle = None
+ data.keepAlivePersistence()
+ if (player.HasGUID) {
+ data.zoning.spawn.tryQueuedActivity()
+ data.turnCounterFunc(player.GUID)
+ data.continent
+ .GUID(player.VehicleSeated)
+ .collect { case obj: PlanetSideGameObject with Vitality =>
+ CustomerServiceRepresentativeMode.topOffHealth(data, obj)
+ }
+ data.squad.updateSquad()
+ } else {
+ data.turnCounterFunc(PlanetSideGUID(0))
}
}
}
@@ -201,4 +152,66 @@ case object CustomerServiceRepresentativeMode extends PlayerMode {
None
))
}
+
+ def topOffHealth(data: SessionData, obj: PlanetSideGameObject with Vitality): Unit = {
+ obj match {
+ case p: Player => topOffHealthOfPlayer(data, p)
+ case v: Vehicle => topOffHealthOfVehicle(data, v)
+ case o: PlanetSideGameObject with Vitality => topOffHealthOfGeneric(data, o)
+ case _ => ()
+ }
+ }
+
+ def topOffHealthOfPlayer(data: SessionData, player: Player): Unit = {
+ //below half health, full heal
+ val maxHealthOfPlayer = player.MaxHealth.toLong
+ val guid = player.GUID
+ val zoneid = data.zoning.zoneChannel
+ if (player.Health < maxHealthOfPlayer * 0.5f) {
+ if (player.Health == 0) {
+ player.Revive
+ }
+ player.Health = maxHealthOfPlayer.toInt
+ player.LogActivity(player.ClearHistory().head)
+ data.sendResponse(PlanetsideAttributeMessage(guid, 0, maxHealthOfPlayer))
+ data.continent.AvatarEvents ! AvatarServiceMessage(zoneid, AvatarAction.PlanetsideAttribute(guid, 0, maxHealthOfPlayer))
+ }
+ //below half armor, full armor
+ val maxArmor = player.MaxArmor.toLong
+ if (player.Armor < maxArmor) {
+ player.Armor = maxArmor.toInt
+ data.sendResponse(PlanetsideAttributeMessage(guid, 4, maxArmor))
+ data.continent.AvatarEvents ! AvatarServiceMessage(zoneid, AvatarAction.PlanetsideAttribute(guid, 4, maxArmor))
+ }
+ }
+
+ def topOffHealthOfVehicle(data: SessionData, vehicle: Vehicle): Unit = {
+ topOffHealthOfGeneric(data, vehicle)
+ //vehicle shields below half, full shields
+ val maxShieldsOfVehicle = vehicle.MaxShields.toLong
+ val shieldsUi = vehicle.Definition.shieldUiAttribute
+ if (vehicle.Shields < maxShieldsOfVehicle) {
+ val guid = vehicle.GUID
+ vehicle.Shields = maxShieldsOfVehicle.toInt
+ data.sendResponse(PlanetsideAttributeMessage(guid, shieldsUi, maxShieldsOfVehicle))
+ data.continent.VehicleEvents ! VehicleServiceMessage(
+ data.continent.id,
+ VehicleAction.PlanetsideAttribute(PlanetSideGUID(0), guid, shieldsUi, maxShieldsOfVehicle)
+ )
+ }
+ }
+
+ def topOffHealthOfGeneric(data: SessionData, obj: PlanetSideGameObject with Vitality): Unit = {
+ //below half health, full heal
+ val guid = obj.GUID
+ val maxHealthOf = obj.MaxHealth.toLong
+ if (obj.Health < maxHealthOf) {
+ obj.Health = maxHealthOf.toInt
+ data.sendResponse(PlanetsideAttributeMessage(guid, 0, maxHealthOf))
+ data.continent.VehicleEvents ! VehicleServiceMessage(
+ data.continent.id,
+ VehicleAction.PlanetsideAttribute(PlanetSideGUID(0), guid, 0, maxHealthOf)
+ )
+ }
+ }
}
diff --git a/src/main/scala/net/psforever/actors/session/csr/GeneralLogic.scala b/src/main/scala/net/psforever/actors/session/csr/GeneralLogic.scala
index f2b59f251..31ca715e4 100644
--- a/src/main/scala/net/psforever/actors/session/csr/GeneralLogic.scala
+++ b/src/main/scala/net/psforever/actors/session/csr/GeneralLogic.scala
@@ -14,6 +14,7 @@ import net.psforever.objects.equipment.Equipment
import net.psforever.objects.inventory.Container
import net.psforever.objects.serverobject.{CommonMessages, ServerObject}
import net.psforever.objects.serverobject.containable.Containable
+import net.psforever.objects.serverobject.dome.ForceDomePhysics
import net.psforever.objects.serverobject.doors.Door
import net.psforever.objects.serverobject.generator.Generator
import net.psforever.objects.serverobject.llu.CaptureFlag
@@ -30,14 +31,14 @@ import net.psforever.objects.vehicles.Utility
import net.psforever.objects.vital.Vitality
import net.psforever.objects.zones.{ZoneProjectile, Zoning}
import net.psforever.packet.PlanetSideGamePacket
-import net.psforever.packet.game.OutfitEventAction.{OutfitInfo, OutfitRankNames, Initial, Unk1}
+import net.psforever.packet.game.OutfitEventAction.{Initial, OutfitInfo, OutfitRankNames, Unk1}
import net.psforever.packet.game.{ActionCancelMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, BugReportMessage, ChangeFireModeMessage, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestMessage, ChatMsg, CollisionIs, ConnectToWorldRequestMessage, CreateShortcutMessage, DeadState, DeployObjectMessage, DisplayedAwardMessage, DropItemMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FriendsRequest, GenericAction, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, InvalidTerrainMessage, LootItemMessage, MoveItemMessage, ObjectDetectedMessage, ObjectHeldMessage, OutfitEvent, OutfitMemberEvent, OutfitMembershipRequest, OutfitMembershipResponse, OutfitRequest, OutfitRequestAction, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, RequestDestroyMessage, TargetingImplantRequest, TerrainCondition, TradeMessage, UnuseItemMessage, UseItemMessage, VoiceHostInfo, VoiceHostRequest, ZipLineMessage}
import net.psforever.services.RemoverActor
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import net.psforever.types.{CapacitorStateType, ChatMessageType, Cosmetic, ExoSuitType, PlanetSideEmpire, PlanetSideGUID, Vector3}
-import scodec.bits.ByteVector
+import scala.concurrent.duration._
import scala.util.Success
object GeneralLogic {
@@ -77,28 +78,7 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
sessionLogic.persist()
sessionLogic.turnCounterFunc(avatarGuid)
sessionLogic.updateBlockMap(player, pos)
- //below half health, full heal
- val maxHealth = player.MaxHealth.toLong
- if (player.Health < maxHealth) {
- player.Health = maxHealth.toInt
- player.LogActivity(player.ClearHistory().head)
- sendResponse(PlanetsideAttributeMessage(avatarGuid, 0, maxHealth))
- continent.AvatarEvents ! AvatarServiceMessage(continent.id, AvatarAction.PlanetsideAttribute(avatarGuid, 0, maxHealth))
- }
- //below half stamina, full stamina
- val avatar = player.avatar
- val maxStamina = avatar.maxStamina
- if (avatar.stamina < maxStamina) {
- avatarActor ! AvatarActor.RestoreStamina(maxStamina)
- sendResponse(PlanetsideAttributeMessage(player.GUID, 2, maxStamina.toLong))
- }
- //below half armor, full armor
- val maxArmor = player.MaxArmor.toLong
- if (player.Armor < maxArmor) {
- player.Armor = maxArmor.toInt
- sendResponse(PlanetsideAttributeMessage(avatarGuid, 4, maxArmor))
- continent.AvatarEvents ! AvatarServiceMessage(continent.id, AvatarAction.PlanetsideAttribute(avatarGuid, 4, maxArmor))
- }
+ topOffHealthOfPlayer()
//expected
val isMoving = WorldEntity.isMoving(vel)
val isMovingPlus = isMoving || isJumping || jumpThrust
@@ -538,7 +518,7 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
def handleGenericCollision(pkt: GenericCollisionMsg): Unit = {
player.BailProtection = false
- val GenericCollisionMsg(ctype, p, _, _, pv, _, _, _, _, _, _, _) = pkt
+ val GenericCollisionMsg(ctype, p, _, _, pv, t, _, _, _, _, _, _) = pkt
if (pv.z * pv.z >= (pv.x * pv.x + pv.y * pv.y) * 0.5f) {
if (ops.heightTrend) {
ops.heightHistory = ops.heightLast
@@ -555,8 +535,21 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
v.BailProtection = false
case (CollisionIs.OfAircraft, Some(v: Vehicle))
if v.Definition.CanFly && v.Seats(0).occupant.contains(player) => ()
+ case (CollisionIs.BetweenThings, Some(v: Vehicle)) =>
+ v.Actor ! Vehicle.Deconstruct(Some(1 millisecond))
+ continent.GUID(t) match {
+ case Some(_: ForceDomePhysics) =>
+ player.Actor ! Player.Die()
+ case _ => ()
+ }
+ case (CollisionIs.BetweenThings, Some(_: Player)) =>
+ continent.GUID(t) match {
+ case Some(_: ForceDomePhysics) =>
+ player.Actor ! Player.Die()
+ case _ => ()
+ }
case (CollisionIs.BetweenThings, _) =>
- log.warn("GenericCollision: CollisionIs.BetweenThings detected - no handling case")
+ log.warn(s"GenericCollision: CollisionIs.BetweenThings detected - no handling case for obj id:${t.guid}")
case _ => ()
}
}
@@ -805,4 +798,16 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
player.CapacitorState = CapacitorStateType.Idle
}
}
+
+ def topOffHealthOfPlayer(): Unit = {
+ //below half health, full heal
+ CustomerServiceRepresentativeMode.topOffHealthOfPlayer(sessionLogic, player)
+ //below half stamina, full stamina
+ val avatar = player.avatar
+ val maxStamina = avatar.maxStamina
+ if (avatar.stamina < maxStamina) {
+ avatarActor ! AvatarActor.RestoreStamina(maxStamina)
+ sendResponse(PlanetsideAttributeMessage(player.GUID, 2, maxStamina.toLong))
+ }
+ }
}
diff --git a/src/main/scala/net/psforever/actors/session/csr/MountHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/csr/MountHandlerLogic.scala
index 4e9480d54..1965825ab 100644
--- a/src/main/scala/net/psforever/actors/session/csr/MountHandlerLogic.scala
+++ b/src/main/scala/net/psforever/actors/session/csr/MountHandlerLogic.scala
@@ -166,7 +166,7 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act
ops.MountingAction(tplayer, obj, seatNumber)
case Mountable.CanMount(obj: FacilityTurret, seatNumber, _)
- if !obj.isUpgrading || System.currentTimeMillis() - GenericHackables.getTurretUpgradeTime >= 1500L =>
+ if !obj.isUpgrading || System.currentTimeMillis() - obj.CheckTurretUpgradeTime >= 1500L =>
obj.setMiddleOfUpgrade(false)
sessionLogic.zoning.CancelZoningProcess()
sendResponse(PlanetsideAttributeMessage(obj.GUID, attribute_type=0, obj.Health))
diff --git a/src/main/scala/net/psforever/actors/session/csr/VehicleLogic.scala b/src/main/scala/net/psforever/actors/session/csr/VehicleLogic.scala
index 4307dce9f..6870f3c12 100644
--- a/src/main/scala/net/psforever/actors/session/csr/VehicleLogic.scala
+++ b/src/main/scala/net/psforever/actors/session/csr/VehicleLogic.scala
@@ -5,16 +5,15 @@ import akka.actor.{ActorContext, typed}
import net.psforever.actors.session.AvatarActor
import net.psforever.actors.session.support.{SessionData, VehicleFunctions, VehicleOperations}
import net.psforever.objects.serverobject.PlanetSideServerObject
-import net.psforever.objects.{PlanetSideGameObject, Player, Vehicle, Vehicles}
+import net.psforever.objects.{PlanetSideGameObject, Vehicle, Vehicles}
import net.psforever.objects.serverobject.deploy.Deployment
import net.psforever.objects.serverobject.mount.Mountable
import net.psforever.objects.vehicles.control.BfrFlight
-import net.psforever.objects.vital.Vitality
import net.psforever.objects.zones.Zone
-import net.psforever.packet.game.{ChildObjectStateMessage, DeployRequestMessage, FrameVehicleStateMessage, PlanetsideAttributeMessage, VehicleStateMessage, VehicleSubStateMessage}
-import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
+import net.psforever.objects.zones.interaction.InteractsWithZone
+import net.psforever.packet.game.{ChildObjectStateMessage, DeployRequestMessage, FrameVehicleStateMessage, VehicleStateMessage, VehicleSubStateMessage}
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
-import net.psforever.types.{DriveState, PlanetSideGUID, Vector3}
+import net.psforever.types.{DriveState, Vector3}
object VehicleLogic {
def apply(ops: VehicleOperations): VehicleLogic = {
@@ -30,6 +29,7 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex
/* packets */
def handleVehicleState(pkt: VehicleStateMessage): Unit = {
+ player.allowInteraction = false
val VehicleStateMessage(
vehicle_guid,
unk1,
@@ -46,23 +46,21 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex
ops.GetVehicleAndSeat() match {
case (Some(obj), Some(0)) =>
//we're driving the vehicle
+ sessionLogic.zoning.spawn.tryQueuedActivity(vel)
sessionLogic.persist()
sessionLogic.turnCounterFunc(player.GUID)
- sessionLogic.general.fallHeightTracker(pos.z)
- if (obj.MountedIn.isEmpty) {
- sessionLogic.updateBlockMap(obj, pos)
+ CustomerServiceRepresentativeMode.topOffHealthOfPlayer(sessionLogic, player)
+ CustomerServiceRepresentativeMode.topOffHealth(sessionLogic, obj)
+ val (position, angle, velocity, notMountedState) = continent.GUID(obj.MountedIn) match {
+ case Some(v: Vehicle) =>
+ (pos, v.Orientation - Vector3.z(value = 90f) * Vehicles.CargoOrientation(obj).toFloat, v.Velocity, false)
+ case _ =>
+ (pos, ang, vel, true)
}
- topOffHealthOfPlayer()
- topOffHealth(obj)
- player.Position = pos //convenient
- if (obj.WeaponControlledFromSeat(0).isEmpty) {
- player.Orientation = Vector3.z(ang.z) //convenient
- }
- obj.Position = pos
- obj.Orientation = ang
- if (obj.MountedIn.isEmpty) {
+ if (notMountedState) {
+ sessionLogic.updateBlockMap(obj, position)
if (obj.DeploymentState != DriveState.Deployed) {
- obj.Velocity = vel
+ obj.Velocity = velocity
} else {
obj.Velocity = Some(Vector3.Zero)
}
@@ -74,20 +72,20 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex
obj.Velocity = None
obj.Flying = None
}
+ player.Position = position //convenient
+ obj.Position = position
+ obj.Orientation = angle
+ //
continent.VehicleEvents ! VehicleServiceMessage(
continent.id,
VehicleAction.VehicleState(
player.GUID,
vehicle_guid,
unk1,
- obj.Position,
- ang,
- obj.Velocity,
- if (obj.isFlying) {
- is_flying
- } else {
- None
- },
+ position,
+ angle,
+ velocity,
+ obj.Flying,
unk6,
unk7,
wheels,
@@ -96,8 +94,6 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex
)
)
sessionLogic.squad.updateSquad()
- player.allowInteraction = false
- obj.zoneInteractions()
case (None, _) =>
//log.error(s"VehicleState: no vehicle $vehicle_guid found in zone")
//TODO placing a "not driving" warning here may trigger as we are disembarking the vehicle
@@ -113,6 +109,7 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex
}
def handleFrameVehicleState(pkt: FrameVehicleStateMessage): Unit = {
+ player.allowInteraction = false
val FrameVehicleStateMessage(
vehicle_guid,
unk1,
@@ -132,34 +129,21 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex
ops.GetVehicleAndSeat() match {
case (Some(obj), Some(0)) =>
//we're driving the vehicle
+ sessionLogic.zoning.spawn.tryQueuedActivity(vel)
sessionLogic.persist()
sessionLogic.turnCounterFunc(player.GUID)
- topOffHealthOfPlayer()
- topOffHealth(obj)
+ CustomerServiceRepresentativeMode.topOffHealthOfPlayer(sessionLogic, player)
+ CustomerServiceRepresentativeMode.topOffHealth(sessionLogic, obj)
val (position, angle, velocity, notMountedState) = continent.GUID(obj.MountedIn) match {
case Some(v: Vehicle) =>
- sessionLogic.updateBlockMap(obj, pos)
(pos, v.Orientation - Vector3.z(value = 90f) * Vehicles.CargoOrientation(obj).toFloat, v.Velocity, false)
case _ =>
(pos, ang, vel, true)
}
- player.Position = position //convenient
- if (obj.WeaponControlledFromSeat(seatNumber = 0).isEmpty) {
- player.Orientation = Vector3.z(ang.z) //convenient
- }
- obj.Position = position
- obj.Orientation = angle
- obj.Velocity = velocity
- // if (is_crouched && obj.DeploymentState != DriveState.Kneeling) {
- // //dev stuff goes here
- // }
- // else
- // if (!is_crouched && obj.DeploymentState == DriveState.Kneeling) {
- // //dev stuff goes here
- // }
- obj.DeploymentState = if (is_crouched || !notMountedState) DriveState.Kneeling else DriveState.Mobile
if (notMountedState) {
+ sessionLogic.updateBlockMap(obj, position)
if (obj.DeploymentState != DriveState.Kneeling) {
+ obj.Velocity = velocity
if (is_airborne) {
val flight = if (ascending_flight) flight_time else -flight_time
obj.Flying = Some(flight)
@@ -172,12 +156,14 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex
obj.Velocity = None
obj.Flying = None
}
- player.allowInteraction = false
- obj.zoneInteractions()
} else {
obj.Velocity = None
obj.Flying = None
}
+ player.Position = position //convenient
+ obj.Position = position
+ obj.Orientation = angle
+ obj.DeploymentState = if (is_crouched || !notMountedState) DriveState.Kneeling else DriveState.Mobile
continent.VehicleEvents ! VehicleServiceMessage(
continent.id,
VehicleAction.FrameVehicleState(
@@ -214,34 +200,40 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex
}
def handleChildObjectState(pkt: ChildObjectStateMessage): Unit = {
+ player.allowInteraction = false
val ChildObjectStateMessage(object_guid, pitch, yaw) = pkt
val (o, tools) = sessionLogic.shooting.FindContainedWeapon
- //is COSM our primary upstream packet?
(o match {
- case Some(mount: Mountable) => (o, mount.PassengerInSeat(player))
+ case Some(mount: Mountable) => (mount, mount.PassengerInSeat(player))
case _ => (None, None)
}) match {
- case (None, None) | (_, None) | (Some(_: Vehicle), Some(0)) =>
+ case (None, _) | (_, None) => //error - we do not recognize being mounted or controlling anything, but what can we do about it?
()
- case (Some(obj: PlanetSideGameObject with Vitality), _) =>
+ case (Some(_: Vehicle), Some(0)) => //see VSM or FVSM for valid cases
+ ()
+ case (Some(entity: PlanetSideGameObject with Mountable with InteractsWithZone), Some(_)) => //COSM is our primary upstream packet
+ sessionLogic.zoning.spawn.tryQueuedActivity(player.Velocity)
sessionLogic.persist()
sessionLogic.turnCounterFunc(player.GUID)
- topOffHealthOfPlayer()
- topOffHealth(obj)
- case _ =>
+ CustomerServiceRepresentativeMode.topOffHealthOfPlayer(sessionLogic, player)
+ CustomerServiceRepresentativeMode.topOffHealth(sessionLogic, entity)
+ sessionLogic.squad.updateSquad()
+ case _ => //we can't disprove that COSM is our primary upstream packet, it's just that we may be missing some details
+ sessionLogic.zoning.spawn.tryQueuedActivity(player.Velocity)
sessionLogic.persist()
sessionLogic.turnCounterFunc(player.GUID)
}
- //the majority of the following check retrieves information to determine if we are in control of the child
- tools.find { _.GUID == object_guid } match {
+ //in the following condition we are in control of the child
+ tools.find(_.GUID == object_guid) match {
case None =>
- //todo: old warning; this state is problematic, but can trigger in otherwise valid instances
+ //old warning; this state is problematic, but can trigger in otherwise valid instances
//log.warn(
// s"ChildObjectState: ${player.Name} is using a different controllable agent than entity ${object_guid.guid}"
//)
- case Some(_) =>
- //TODO set tool orientation?
- player.Orientation = Vector3(0f, pitch, yaw)
+ case Some(tool) =>
+ val angle = Vector3(0f, pitch, yaw)
+ tool.Orientation = angle
+ player.Orientation = angle
continent.VehicleEvents ! VehicleServiceMessage(
continent.id,
VehicleAction.ChildObjectState(player.GUID, object_guid, pitch, yaw)
@@ -342,56 +334,4 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex
)
}
}
-
- private def topOffHealth(obj: PlanetSideGameObject with Vitality): Unit = {
- obj match {
- case _: Player => topOffHealthOfPlayer()
- case v: Vehicle => topOffHealthOfVehicle(v)
- case o: PlanetSideGameObject with Vitality => topOffHealthOfGeneric(o)
- case _ => ()
- }
- }
-
- private def topOffHealthOfPlayer(): Unit = {
- //driver below half health, full heal
- val maxHealthOfPlayer = player.MaxHealth.toLong
- if (player.Health < maxHealthOfPlayer * 0.5f) {
- player.Health = maxHealthOfPlayer.toInt
- player.LogActivity(player.ClearHistory().head)
- sendResponse(PlanetsideAttributeMessage(player.GUID, 0, maxHealthOfPlayer))
- continent.AvatarEvents ! AvatarServiceMessage(sessionLogic.zoning.zoneChannel, AvatarAction.PlanetsideAttribute(player.GUID, 0, maxHealthOfPlayer))
- }
- }
-
- private def topOffHealthOfVehicle(vehicle: Vehicle): Unit = {
- topOffHealthOfPlayer()
- topOffHealthOfGeneric(vehicle)
- //vehicle shields below half, full shields
- val maxShieldsOfVehicle = vehicle.MaxShields.toLong
- val shieldsUi = vehicle.Definition.shieldUiAttribute
- if (vehicle.Shields < maxShieldsOfVehicle) {
- val guid = vehicle.GUID
- vehicle.Shields = maxShieldsOfVehicle.toInt
- sendResponse(PlanetsideAttributeMessage(guid, shieldsUi, maxShieldsOfVehicle))
- continent.VehicleEvents ! VehicleServiceMessage(
- continent.id,
- VehicleAction.PlanetsideAttribute(PlanetSideGUID(0), guid, shieldsUi, maxShieldsOfVehicle)
- )
- }
- }
-
- private def topOffHealthOfGeneric(obj: PlanetSideGameObject with Vitality): Unit = {
- topOffHealthOfPlayer()
- //vehicle below half health, full heal
- val guid = obj.GUID
- val maxHealthOf = obj.MaxHealth.toLong
- if (obj.Health < maxHealthOf) {
- obj.Health = maxHealthOf.toInt
- sendResponse(PlanetsideAttributeMessage(guid, 0, maxHealthOf))
- continent.VehicleEvents ! VehicleServiceMessage(
- continent.id,
- VehicleAction.PlanetsideAttribute(PlanetSideGUID(0), guid, 0, maxHealthOf)
- )
- }
- }
}
diff --git a/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala b/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala
index 449980c49..823d09923 100644
--- a/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala
+++ b/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala
@@ -16,12 +16,15 @@ import net.psforever.objects.inventory.Container
import net.psforever.objects.serverobject.{PlanetSideServerObject, ServerObject}
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.serverobject.containable.Containable
+import net.psforever.objects.serverobject.damage.Damageable
+import net.psforever.objects.serverobject.dome.ForceDomePhysics
import net.psforever.objects.serverobject.doors.Door
import net.psforever.objects.serverobject.generator.Generator
import net.psforever.objects.serverobject.interior.Sidedness.OutsideOf
import net.psforever.objects.serverobject.llu.CaptureFlag
import net.psforever.objects.serverobject.locks.IFFLock
import net.psforever.objects.serverobject.mblocker.Locker
+import net.psforever.objects.serverobject.mount.MountableEntity
import net.psforever.objects.serverobject.resourcesilo.ResourceSilo
import net.psforever.objects.serverobject.structures.WarpGate
import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal
@@ -29,11 +32,11 @@ import net.psforever.objects.serverobject.terminals.{ProximityUnit, Terminal}
import net.psforever.objects.serverobject.terminals.implant.ImplantTerminalMech
import net.psforever.objects.serverobject.tube.SpawnTube
import net.psforever.objects.serverobject.turret.FacilityTurret
-import net.psforever.objects.sourcing.SourceEntry
+import net.psforever.objects.sourcing.{PlayerSource, SourceEntry}
import net.psforever.objects.vehicles.Utility
import net.psforever.objects.vital.Vitality
import net.psforever.objects.vital.collision.{CollisionReason, CollisionWithReason}
-import net.psforever.objects.vital.etc.SuicideReason
+import net.psforever.objects.vital.etc.{ForceDomeExposure, SuicideReason}
import net.psforever.objects.vital.interaction.DamageInteraction
import net.psforever.objects.zones.{ZoneProjectile, Zoning}
import net.psforever.packet.PlanetSideGamePacket
@@ -636,10 +639,15 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
case (CollisionIs.OfAircraft, out @ Some(v: Vehicle))
if v.Definition.CanFly && v.Seats(0).occupant.contains(player) =>
(out, sessionLogic.validObject(t, decorator = "GenericCollision/Aircraft"), false, pv)
- case (CollisionIs.BetweenThings, _) =>
- log.warn("GenericCollision: CollisionIs.BetweenThings detected - no handling case")
+ case (CollisionIs.BetweenThings, out @ Some(target: PlanetSideServerObject with MountableEntity)) =>
+ target.BailProtection = false
+ player.BailProtection = false
+ (out, sessionLogic.validObject(t, decorator = "GenericCollision/Surface"), false, pv)
+ case (_, Some(obj)) =>
+ log.error(s"GenericCollision: $ctype detected: no handling case for ${obj.Definition.Name}")
(None, None, false, Vector3.Zero)
- case _ =>
+ case (_, None) =>
+ log.error(s"GenericCollision: $ctype detected: no entity detected as 'Primary'")
(None, None, false, Vector3.Zero)
}
val curr = System.currentTimeMillis()
@@ -661,6 +669,16 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
}
}
+ case (Some(us: PlanetSideServerObject with Vitality with FactionAffinity), _, Some(field: ForceDomePhysics)) =>
+ us.Actor ! Damageable.MakeVulnerable
+ us.Actor ! Vitality.Damage(
+ DamageInteraction(
+ PlayerSource(player),
+ ForceDomeExposure(SourceEntry(field)),
+ player.Position
+ ).calculate()
+ )
+
case (Some(us: Vehicle), _, Some(victim: SensorDeployable)) =>
collisionBetweenVehicleAndFragileDeployable(us, ppos, victim, tpos, velocity - tv, fallHeight, curr)
diff --git a/src/main/scala/net/psforever/actors/session/normal/MountHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/normal/MountHandlerLogic.scala
index f7230008f..ae8399757 100644
--- a/src/main/scala/net/psforever/actors/session/normal/MountHandlerLogic.scala
+++ b/src/main/scala/net/psforever/actors/session/normal/MountHandlerLogic.scala
@@ -9,7 +9,6 @@ import net.psforever.objects.{GlobalDefinitions, PlanetSideGameObject, Player, V
import net.psforever.objects.definition.{BasicDefinition, ObjectDefinition}
import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.serverobject.environment.interaction.ResetAllEnvironmentInteractions
-import net.psforever.objects.serverobject.hackable.GenericHackables
import net.psforever.objects.serverobject.mount.Mountable
import net.psforever.objects.serverobject.structures.WarpGate
import net.psforever.objects.serverobject.terminals.implant.ImplantTerminalMech
@@ -105,7 +104,8 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act
ops.MountingAction(tplayer, obj, seatNumber)
case Mountable.CanMount(obj: Vehicle, seatNumber, _)
- if seatNumber == 0 && obj.Definition.MaxCapacitor > 0 =>
+ if seatNumber == 0 &&
+ obj.Definition.MaxCapacitor > 0 =>
log.info(s"${player.Name} mounts the driver seat of the ${obj.Definition.Name}")
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
val obj_guid: PlanetSideGUID = obj.GUID
@@ -134,13 +134,9 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act
ops.MountingAction(tplayer, obj, seatNumber)
case Mountable.CanMount(obj: Vehicle, seatNumber, _)
- if obj.Definition.MaxCapacitor > 0 =>
- log.info(s"${player.Name} mounts ${
- obj.SeatPermissionGroup(seatNumber) match {
- case Some(seatType) => s"a $seatType seat (#$seatNumber)"
- case None => "a seat"
- }
- } of the ${obj.Definition.Name}")
+ if obj.Definition.MaxCapacitor > 0 &&
+ obj.SeatPermissionGroup(seatNumber).contains(AccessPermissionGroup.Gunner) =>
+ log.info(s"${player.Name} mounts the #$seatNumber gunner seat of the ${obj.Definition.Name}")
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
val obj_guid: PlanetSideGUID = obj.GUID
sessionLogic.terminals.CancelAllProximityUnits()
@@ -149,17 +145,26 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act
sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=113, obj.Capacitor))
sessionLogic.general.accessContainer(obj)
ops.updateWeaponAtSeatPosition(obj, seatNumber)
- sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistenceFunc
tplayer.Actor ! ResetAllEnvironmentInteractions
ops.MountingAction(tplayer, obj, seatNumber)
- case Mountable.CanMount(obj: Vehicle, seatNumber, _) =>
- log.info(s"${player.Name} mounts the ${
- obj.SeatPermissionGroup(seatNumber) match {
- case Some(seatType) => s"a $seatType seat (#$seatNumber)"
- case None => "a seat"
- }
- } of the ${obj.Definition.Name}")
+ case Mountable.CanMount(obj: Vehicle, seatNumber, _)
+ if obj.Definition.MaxCapacitor > 0 =>
+ log.info(s"${player.Name} mounts the #$seatNumber seat of the ${obj.Definition.Name}")
+ sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
+ val obj_guid: PlanetSideGUID = obj.GUID
+ sessionLogic.terminals.CancelAllProximityUnits()
+ sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health))
+ sendResponse(PlanetsideAttributeMessage(obj_guid, obj.Definition.shieldUiAttribute, obj.Shields))
+ sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=113, obj.Capacitor))
+ sessionLogic.general.accessContainer(obj)
+ tplayer.Actor ! ResetAllEnvironmentInteractions
+ ops.MountingAction(tplayer, obj, seatNumber)
+ sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistenceFunc
+
+ case Mountable.CanMount(obj: Vehicle, seatNumber, _)
+ if obj.SeatPermissionGroup(seatNumber).contains(AccessPermissionGroup.Gunner) =>
+ log.info(s"${player.Name} mounts the #$seatNumber gunner seat of the ${obj.Definition.Name}")
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
val obj_guid: PlanetSideGUID = obj.GUID
sessionLogic.terminals.CancelAllProximityUnits()
@@ -167,10 +172,21 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act
sendResponse(PlanetsideAttributeMessage(obj_guid, obj.Definition.shieldUiAttribute, obj.Shields))
sessionLogic.general.accessContainer(obj)
ops.updateWeaponAtSeatPosition(obj, seatNumber)
- sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistenceFunc
tplayer.Actor ! ResetAllEnvironmentInteractions
ops.MountingAction(tplayer, obj, seatNumber)
+ case Mountable.CanMount(obj: Vehicle, seatNumber, _) =>
+ log.info(s"${player.Name} mounts the #$seatNumber seat of the ${obj.Definition.Name}")
+ sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
+ val obj_guid: PlanetSideGUID = obj.GUID
+ sessionLogic.terminals.CancelAllProximityUnits()
+ sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=0, obj.Health))
+ sendResponse(PlanetsideAttributeMessage(obj_guid, obj.Definition.shieldUiAttribute, obj.Shields))
+ sessionLogic.general.accessContainer(obj)
+ tplayer.Actor ! ResetAllEnvironmentInteractions
+ ops.MountingAction(tplayer, obj, seatNumber)
+ sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistenceFunc
+
case Mountable.CanMount(obj: FacilityTurret, seatNumber, _)
if obj.Definition == GlobalDefinitions.vanu_sentry_turret =>
log.info(s"${player.Name} mounts the ${obj.Definition.Name}")
@@ -181,7 +197,7 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act
ops.MountingAction(tplayer, obj, seatNumber)
case Mountable.CanMount(obj: FacilityTurret, seatNumber, _)
- if !obj.isUpgrading || System.currentTimeMillis() - GenericHackables.getTurretUpgradeTime >= 1500L =>
+ if !obj.isUpgrading || System.currentTimeMillis() - obj.CheckTurretUpgradeTime >= 1500L =>
log.info(s"${player.Name} mounts the ${obj.Definition.Name}")
obj.setMiddleOfUpgrade(false)
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
diff --git a/src/main/scala/net/psforever/actors/session/normal/VehicleLogic.scala b/src/main/scala/net/psforever/actors/session/normal/VehicleLogic.scala
index 77d3a99fa..883fbfe94 100644
--- a/src/main/scala/net/psforever/actors/session/normal/VehicleLogic.scala
+++ b/src/main/scala/net/psforever/actors/session/normal/VehicleLogic.scala
@@ -5,11 +5,12 @@ import akka.actor.{ActorContext, typed}
import net.psforever.actors.session.AvatarActor
import net.psforever.actors.session.support.{SessionData, VehicleFunctions, VehicleOperations}
import net.psforever.objects.serverobject.PlanetSideServerObject
-import net.psforever.objects.{Vehicle, Vehicles}
+import net.psforever.objects.{PlanetSideGameObject, Vehicle, Vehicles}
import net.psforever.objects.serverobject.deploy.Deployment
import net.psforever.objects.serverobject.mount.Mountable
import net.psforever.objects.vehicles.control.BfrFlight
import net.psforever.objects.zones.Zone
+import net.psforever.objects.zones.interaction.InteractsWithZone
import net.psforever.packet.game.{ChatMsg, ChildObjectStateMessage, DeployRequestMessage, FrameVehicleStateMessage, VehicleStateMessage, VehicleSubStateMessage}
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
import net.psforever.types.{ChatMessageType, DriveState, Vector3}
@@ -48,18 +49,16 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex
sessionLogic.persist()
sessionLogic.turnCounterFunc(player.GUID)
sessionLogic.general.fallHeightTracker(pos.z)
- if (obj.MountedIn.isEmpty) {
+ val (position, angle, velocity, notMountedState) = continent.GUID(obj.MountedIn) match {
+ case Some(v: Vehicle) =>
+ (pos, v.Orientation - Vector3.z(value = 90f) * Vehicles.CargoOrientation(obj).toFloat, v.Velocity, false)
+ case _ =>
+ (pos, ang, vel, true)
+ }
+ if (notMountedState) {
sessionLogic.updateBlockMap(obj, pos)
- }
- player.Position = pos //convenient
- if (obj.WeaponControlledFromSeat(0).isEmpty) {
- player.Orientation = Vector3.z(ang.z) //convenient
- }
- obj.Position = pos
- obj.Orientation = ang
- if (obj.MountedIn.isEmpty) {
if (obj.DeploymentState != DriveState.Deployed) {
- obj.Velocity = vel
+ obj.Velocity = velocity
} else {
obj.Velocity = Some(Vector3.Zero)
}
@@ -67,10 +66,14 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex
obj.Flying = is_flying //usually Some(7)
}
obj.Cloaked = obj.Definition.CanCloak && is_cloaked
+ obj.zoneInteractions()
} else {
obj.Velocity = None
obj.Flying = None
}
+ player.Position = position //convenient
+ obj.Position = position
+ obj.Orientation = angle
//
continent.VehicleEvents ! VehicleServiceMessage(
continent.id,
@@ -78,14 +81,10 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex
player.GUID,
vehicle_guid,
unk1,
- obj.Position,
- ang,
- obj.Velocity,
- if (obj.isFlying) {
- is_flying
- } else {
- None
- },
+ position,
+ angle,
+ velocity,
+ obj.Flying,
unk6,
unk7,
wheels,
@@ -94,10 +93,9 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex
)
)
sessionLogic.squad.updateSquad()
- obj.zoneInteractions()
case (None, _) =>
- //log.error(s"VehicleState: no vehicle $vehicle_guid found in zone")
- //TODO placing a "not driving" warning here may trigger as we are disembarking the vehicle
+ //log.error(s"VehicleState: no vehicle $vehicle_guid found in zone")
+ //placing a "not driving" warning here may trigger as we are disembarking the vehicle
case (_, Some(index)) =>
log.error(
s"VehicleState: ${player.Name} should not be dispatching this kind of packet from vehicle ${vehicle_guid.guid} when not the driver (actually, seat $index)"
@@ -132,30 +130,17 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex
sessionLogic.zoning.spawn.tryQueuedActivity(vel)
sessionLogic.persist()
sessionLogic.turnCounterFunc(player.GUID)
+ sessionLogic.general.fallHeightTracker(pos.z)
val (position, angle, velocity, notMountedState) = continent.GUID(obj.MountedIn) match {
case Some(v: Vehicle) =>
- sessionLogic.updateBlockMap(obj, pos)
(pos, v.Orientation - Vector3.z(value = 90f) * Vehicles.CargoOrientation(obj).toFloat, v.Velocity, false)
case _ =>
(pos, ang, vel, true)
}
- player.Position = position //convenient
- if (obj.WeaponControlledFromSeat(seatNumber = 0).isEmpty) {
- player.Orientation = Vector3.z(ang.z) //convenient
- }
- obj.Position = position
- obj.Orientation = angle
- obj.Velocity = velocity
- // if (is_crouched && obj.DeploymentState != DriveState.Kneeling) {
- // //dev stuff goes here
- // }
- // else
- // if (!is_crouched && obj.DeploymentState == DriveState.Kneeling) {
- // //dev stuff goes here
- // }
- obj.DeploymentState = if (is_crouched || !notMountedState) DriveState.Kneeling else DriveState.Mobile
if (notMountedState) {
+ sessionLogic.updateBlockMap(obj, position)
if (obj.DeploymentState != DriveState.Kneeling) {
+ obj.Velocity = velocity
if (is_airborne) {
val flight = if (ascending_flight) flight_time else -flight_time
obj.Flying = Some(flight)
@@ -173,6 +158,10 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex
obj.Velocity = None
obj.Flying = None
}
+ player.Position = position //convenient
+ obj.Position = position
+ obj.Orientation = angle
+ obj.DeploymentState = if (is_crouched || !notMountedState) DriveState.Kneeling else DriveState.Mobile
continent.VehicleEvents ! VehicleServiceMessage(
continent.id,
VehicleAction.FrameVehicleState(
@@ -195,8 +184,8 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex
)
sessionLogic.squad.updateSquad()
case (None, _) =>
- //log.error(s"VehicleState: no vehicle $vehicle_guid found in zone")
- //TODO placing a "not driving" warning here may trigger as we are disembarking the vehicle
+ //log.error(s"VehicleState: no vehicle $vehicle_guid found in zone")
+ //placing a "not driving" warning here may trigger as we are disembarking the vehicle
case (_, Some(index)) =>
log.error(
s"VehicleState: ${player.Name} should not be dispatching this kind of packet from vehicle ${vehicle_guid.guid} when not the driver (actually, seat $index)"
@@ -211,28 +200,36 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex
def handleChildObjectState(pkt: ChildObjectStateMessage): Unit = {
val ChildObjectStateMessage(object_guid, pitch, yaw) = pkt
val (o, tools) = sessionLogic.shooting.FindContainedWeapon
- //is COSM our primary upstream packet?
(o match {
- case Some(mount: Mountable) => (o, mount.PassengerInSeat(player))
+ case Some(mount: Mountable) => (mount, mount.PassengerInSeat(player))
case _ => (None, None)
}) match {
- case (None, None) | (_, None) | (Some(_: Vehicle), Some(0)) =>
+ case (None, _) | (_, None) => //error - we do not recognize being mounted or controlling anything, but what can we do about it?
()
- case _ =>
- sessionLogic.zoning.spawn.tryQueuedActivity() //todo conditionals?
+ case (Some(_: Vehicle), Some(0)) => //see VSM or FVSM for valid cases
+ ()
+ case (Some(entity: PlanetSideGameObject with Mountable with InteractsWithZone), Some(seatNumber)) => //COSM is our primary upstream packet
+ sessionLogic.zoning.spawn.tryQueuedActivity(player.Velocity)
+ sessionLogic.persist()
+ sessionLogic.turnCounterFunc(player.GUID)
+ VehicleOperations.updateMountableZoneInteractionFromEarliestSeat(entity, seatNumber)
+ sessionLogic.squad.updateSquad()
+ case _ => //we can't disprove that COSM is our primary upstream packet, it's just that we may be missing some details
+ sessionLogic.zoning.spawn.tryQueuedActivity(player.Velocity)
sessionLogic.persist()
sessionLogic.turnCounterFunc(player.GUID)
}
- //the majority of the following check retrieves information to determine if we are in control of the child
- tools.find { _.GUID == object_guid } match {
+ //in the following condition we are in control of the child
+ tools.find(_.GUID == object_guid) match {
case None =>
- //todo: old warning; this state is problematic, but can trigger in otherwise valid instances
+ //old warning; this state is problematic, but can trigger in otherwise valid instances
//log.warn(
// s"ChildObjectState: ${player.Name} is using a different controllable agent than entity ${object_guid.guid}"
//)
- case Some(_) =>
- //TODO set tool orientation?
- player.Orientation = Vector3(0f, pitch, yaw)
+ case Some(tool) =>
+ val angle = Vector3(0f, pitch, yaw)
+ tool.Orientation = angle
+ player.Orientation = angle
continent.VehicleEvents ! VehicleServiceMessage(
continent.id,
VehicleAction.ChildObjectState(player.GUID, object_guid, pitch, yaw)
diff --git a/src/main/scala/net/psforever/actors/session/spectator/GeneralLogic.scala b/src/main/scala/net/psforever/actors/session/spectator/GeneralLogic.scala
index 8c332585a..0987bfd1c 100644
--- a/src/main/scala/net/psforever/actors/session/spectator/GeneralLogic.scala
+++ b/src/main/scala/net/psforever/actors/session/spectator/GeneralLogic.scala
@@ -9,15 +9,18 @@ import net.psforever.objects.avatar.{Avatar, Implant}
import net.psforever.objects.ballistics.Projectile
import net.psforever.objects.definition.{BasicDefinition, KitDefinition, SpecialExoSuitDefinition}
import net.psforever.objects.serverobject.containable.Containable
+import net.psforever.objects.serverobject.dome.ForceDomePhysics
import net.psforever.objects.serverobject.doors.Door
import net.psforever.objects.vehicles.Utility
import net.psforever.objects.zones.ZoneProjectile
import net.psforever.packet.PlanetSideGamePacket
-import net.psforever.packet.game.{ActionCancelMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, BugReportMessage, ChangeFireModeMessage, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestMessage, ConnectToWorldRequestMessage, CreateShortcutMessage, DeployObjectMessage, DisplayedAwardMessage, DropItemMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FriendsRequest, GenericAction, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, ImplantAction, InvalidTerrainMessage, LootItemMessage, MoveItemMessage, ObjectDetectedMessage, ObjectHeldMessage, OutfitMembershipRequest, OutfitMembershipResponse, OutfitRequest, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, RequestDestroyMessage, TargetingImplantRequest, TradeMessage, UnuseItemMessage, UseItemMessage, VoiceHostInfo, VoiceHostRequest, ZipLineMessage}
+import net.psforever.packet.game.{ActionCancelMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, BugReportMessage, ChangeFireModeMessage, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestMessage, CollisionIs, ConnectToWorldRequestMessage, CreateShortcutMessage, DeployObjectMessage, DisplayedAwardMessage, DropItemMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FriendsRequest, GenericAction, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, ImplantAction, InvalidTerrainMessage, LootItemMessage, MoveItemMessage, ObjectDetectedMessage, ObjectHeldMessage, OutfitMembershipRequest, OutfitMembershipResponse, OutfitRequest, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, RequestDestroyMessage, TargetingImplantRequest, TradeMessage, UnuseItemMessage, UseItemMessage, VoiceHostInfo, VoiceHostRequest, ZipLineMessage}
import net.psforever.services.account.AccountPersistenceService
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.types.{ExoSuitType, Vector3}
+import scala.concurrent.duration.DurationInt
+
object GeneralLogic {
def apply(ops: GeneralOperations): GeneralLogic = {
new GeneralLogic(ops, ops.context)
@@ -283,7 +286,34 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
}
}
- def handleGenericCollision(pkt: GenericCollisionMsg): Unit = { /* intentionally blank */ }
+ def handleGenericCollision(pkt: GenericCollisionMsg): Unit = {
+ player.BailProtection = false
+ val GenericCollisionMsg(ctype, p, _, _, pv, t, _, _, _, _, _, _) = pkt
+ if (pv.z * pv.z >= (pv.x * pv.x + pv.y * pv.y) * 0.5f) {
+ if (ops.heightTrend) {
+ ops.heightHistory = ops.heightLast
+ }
+ else {
+ ops.heightLast = ops.heightHistory
+ }
+ }
+ (ctype, sessionLogic.validObject(p, decorator = "GenericCollision/Primary")) match {
+ case (CollisionIs.BetweenThings, Some(v: Vehicle)) =>
+ v.Actor ! Vehicle.Deconstruct(Some(1 millisecond))
+ continent.GUID(t) match {
+ case Some(_: ForceDomePhysics) =>
+ player.Actor ! Player.Die()
+ case _ => ()
+ }
+ case (CollisionIs.BetweenThings, Some(_: Player)) =>
+ continent.GUID(t) match {
+ case Some(_: ForceDomePhysics) =>
+ player.Actor ! Player.Die()
+ case _ => ()
+ }
+ case _ => ()
+ }
+ }
def handleAvatarFirstTimeEvent(pkt: AvatarFirstTimeEventMessage): Unit = { /* intentionally blank */ }
diff --git a/src/main/scala/net/psforever/actors/session/support/SessionData.scala b/src/main/scala/net/psforever/actors/session/support/SessionData.scala
index 44769b870..7e672cba6 100644
--- a/src/main/scala/net/psforever/actors/session/support/SessionData.scala
+++ b/src/main/scala/net/psforever/actors/session/support/SessionData.scala
@@ -466,7 +466,14 @@ class SessionData(
zoning.spawn.interimUngunnedVehicle = None
persist()
if (player.HasGUID) {
+ zoning.spawn.tryQueuedActivity(player.Velocity)
turnCounterFunc(player.GUID)
+ continent
+ .GUID(player.VehicleSeated)
+ .collect { case v: PlanetSideGameObject with Mountable =>
+ VehicleOperations.updateMountableZoneInteractionFromEarliestSeat(v, player)
+ }
+ squad.updateSquad()
} else {
turnCounterFunc(PlanetSideGUID(0))
}
diff --git a/src/main/scala/net/psforever/actors/session/support/VehicleOperations.scala b/src/main/scala/net/psforever/actors/session/support/VehicleOperations.scala
index a9f4dc2e8..22a743c5e 100644
--- a/src/main/scala/net/psforever/actors/session/support/VehicleOperations.scala
+++ b/src/main/scala/net/psforever/actors/session/support/VehicleOperations.scala
@@ -7,6 +7,7 @@ import net.psforever.objects.serverobject.deploy.Deployment
import net.psforever.objects.serverobject.mount.Mountable
import net.psforever.objects.zones.Zone
import net.psforever.objects._
+import net.psforever.objects.zones.interaction.InteractsWithZone
import net.psforever.packet.game.{ChildObjectStateMessage, DeployRequestMessage, VehicleSubStateMessage, _}
import net.psforever.types.DriveState
@@ -195,3 +196,55 @@ class VehicleOperations(
sendResponse(pkt)
}
}
+
+object VehicleOperations {
+ def updateMountableZoneInteractionFromEarliestSeat(obj: PlanetSideGameObject with Mountable, passenger: Player): Unit = {
+ obj.PassengerInSeat(passenger).foreach { seatNumber =>
+ updateMountableZoneInteractionFromEarliestSeat(obj, seatNumber)
+ }
+ }
+
+ def updateMountableZoneInteractionFromEarliestSeat(obj: PlanetSideGameObject with Mountable, seatNumber: Int): Unit = {
+ obj match {
+ case obj: Vehicle =>
+ updateVehicleZoneInteractionFromEarliestSeat(obj, seatNumber)
+ case obj: Mountable with InteractsWithZone =>
+ updateEntityZoneInteractionFromEarliestSeat(obj, seatNumber, obj)
+ case _ => ()
+ }
+ }
+
+ private def updateVehicleZoneInteractionFromEarliestSeat(obj: Vehicle, seatNumber: Int): Unit = {
+ //vehicle being ferried; check if the ferry has occupants that might have speaking rights before us
+ var targetVehicle = obj
+ val carrierSeatVacancy: Boolean = obj match {
+ case v if v.MountedIn.nonEmpty =>
+ obj.Zone.GUID(v.MountedIn) match {
+ case Some(carrier: Vehicle) =>
+ targetVehicle = carrier
+ !carrier.Seats.values.exists(_.isOccupied)
+ case _ =>
+ true
+ }
+ case _ => true
+ }
+ if (carrierSeatVacancy) {
+ updateEntityZoneInteractionFromEarliestSeat(obj, seatNumber, targetVehicle)
+ }
+ }
+
+ private def updateEntityZoneInteractionFromEarliestSeat(
+ obj: Mountable with InteractsWithZone,
+ seatNumber: Int,
+ updateTarget: InteractsWithZone
+ ): Unit = {
+ if (seatNumber == 0) {
+ //we're responsible as the primary operator
+ updateTarget.zoneInteractions()
+ } else if(!obj.Seat(seatNumber = 0).exists(_.isOccupied) && obj.OccupiedSeats().headOption.contains(seatNumber)) {
+ //there is no primary operator
+ //we are responsible as the player in the seat closest to the "front"
+ updateTarget.zoneInteractions()
+ }
+ }
+}
diff --git a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala
index bbbb3a982..60d27e123 100644
--- a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala
+++ b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala
@@ -1060,7 +1060,7 @@ class ZoningOperations(
case _ => ()
}
// capitol force dome state
- if (building.IsCapitol && building.ForceDomeActive) {
+ if (building.IsCapitol && building.ForceDome.exists(_.Energized)) {
sendResponse(GenericObjectActionMessage(guid, 13))
}
// amenities
@@ -1930,7 +1930,7 @@ class ZoningOperations(
/** Upstream message counter
* Checks for server acknowledgement of the following messages in the following conditions:
* `PlayerStateMessageUpstream` (infantry)
- * `VehicleStateMessage` (driver mount only)
+ * `VehicleStateMessage` and `FrameVehicleStateMessage` (driver mount)
* `ChildObjectStateMessage` (any gunner mount that is not the driver)
* `KeepAliveMessage` (any passenger mount that is not the driver)
* As they should arrive roughly every 250 milliseconds this allows for a very crude method of scheduling tasks up to four times per second
diff --git a/src/main/scala/net/psforever/actors/zone/ZoneActor.scala b/src/main/scala/net/psforever/actors/zone/ZoneActor.scala
index 28a3c2015..391b0a7c8 100644
--- a/src/main/scala/net/psforever/actors/zone/ZoneActor.scala
+++ b/src/main/scala/net/psforever/actors/zone/ZoneActor.scala
@@ -10,7 +10,6 @@ import net.psforever.objects.zones.blockmap.{BlockMapEntity, SectorGroup}
import net.psforever.objects.{ConstructionItem, PlanetSideGameObject, Player, Vehicle}
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, PlanetSideGeneratorState, Vector3}
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
@@ -110,7 +109,6 @@ class ZoneActor(
//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.CaptureTerminal.collect { terminal =>
val msg = CaptureTerminalAwareBehavior.TerminalStatusChanged(terminal, isResecured = true)
diff --git a/src/main/scala/net/psforever/actors/zone/building/MajorFacilityLogic.scala b/src/main/scala/net/psforever/actors/zone/building/MajorFacilityLogic.scala
index 4dd21dcb7..c90579c55 100644
--- a/src/main/scala/net/psforever/actors/zone/building/MajorFacilityLogic.scala
+++ b/src/main/scala/net/psforever/actors/zone/building/MajorFacilityLogic.scala
@@ -6,7 +6,10 @@ import akka.actor.typed.{ActorRef, Behavior}
import akka.actor.typed.scaladsl.{ActorContext, Behaviors}
import net.psforever.actors.commands.NtuCommand
import net.psforever.actors.zone.{BuildingActor, BuildingControlDetails, ZoneActor}
+import net.psforever.objects.serverobject.damage.Damageable
+import net.psforever.objects.serverobject.dome.ForceDomePhysics
import net.psforever.objects.serverobject.generator.{Generator, GeneratorControl}
+import net.psforever.objects.serverobject.resourcesilo.ResourceSiloControl
import net.psforever.objects.serverobject.structures.{Amenity, Building}
import net.psforever.objects.serverobject.terminals.capture.{CaptureTerminal, CaptureTerminalAware, CaptureTerminalAwareBehavior}
import net.psforever.objects.sourcing.PlayerSource
@@ -15,7 +18,7 @@ import net.psforever.services.{InterstellarClusterService, Service}
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.galaxy.{GalaxyAction, GalaxyServiceMessage}
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
-import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, PlanetSideGeneratorState}
+import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID}
/**
* A package class that conveys the important information for handling facility updates.
@@ -56,99 +59,6 @@ case object MajorFacilityLogic
MajorFacilityWrapper(building, context, details.galaxyService, details.interstellarCluster)
}
- /**
- * Evaluate the conditions of the building
- * and determine if its capitol force dome state should be updated
- * to reflect the actual conditions of the base or its surrounding bases.
- * If this building is considered a subcapitol facility to the zone's actual capitol facility,
- * and has the capitol force dome has a dependency upon it,
- * pass a message onto that facility that it should check its own state alignment.
- * @param mapUpdateOnChange if `true`, dispatch a `MapUpdate` message for this building
- */
- private def alignForceDomeStatus(details: BuildingWrapper, mapUpdateOnChange: Boolean = true): Behavior[Command] = {
- val building = details.building
- checkForceDomeStatus(building) match {
- case Some(updatedStatus) if updatedStatus != building.ForceDomeActive =>
- updateForceDomeStatus(details, updatedStatus, mapUpdateOnChange)
- case _ => ;
- }
- Behaviors.same
- }
-
- /**
- * Dispatch a message to update the state of the clients with the server state of the capitol force dome.
- * @param updatedStatus the new capitol force dome status
- * @param mapUpdateOnChange if `true`, dispatch a `MapUpdate` message for this building
- */
- private def updateForceDomeStatus(
- details: BuildingWrapper,
- updatedStatus: Boolean,
- mapUpdateOnChange: Boolean
- ): Unit = {
- val building = details.building
- val zone = building.Zone
- building.ForceDomeActive = updatedStatus
- zone.LocalEvents ! LocalServiceMessage(
- zone.id,
- LocalAction.UpdateForceDomeStatus(Service.defaultPlayerGUID, building.GUID, updatedStatus)
- )
- if (mapUpdateOnChange) {
- details.context.self ! BuildingActor.MapUpdate()
- }
- }
-
- /**
- * The natural conditions of a facility that is not eligible for its capitol force dome to be expanded.
- * The only test not employed is whether or not the target building is a capitol.
- * Ommission of this condition makes this test capable of evaluating subcapitol eligibility
- * for capitol force dome expansion.
- * @param building the target building
- * @return `true`, if the conditions for capitol force dome are not met;
- * `false`, otherwise
- */
- private def invalidBuildingCapitolForceDomeConditions(building: Building): Boolean = {
- building.Faction == PlanetSideEmpire.NEUTRAL ||
- building.NtuLevel == 0 ||
- (building.Generator match {
- case Some(o) => o.Condition == PlanetSideGeneratorState.Destroyed
- case _ => false
- })
- }
-
- /**
- * If this building is a capitol major facility,
- * use the faction affinity, the generator status, and the resource silo's capacitance level
- * to determine if the capitol force dome should be active.
- * @param building the building being evaluated
- * @return the condition of the capitol force dome;
- * `None`, if the facility is not a capitol building;
- * `Some(true|false)` to indicate the state of the force dome
- */
- def checkForceDomeStatus(building: Building): Option[Boolean] = {
- if (building.IsCapitol) {
- val originalStatus = building.ForceDomeActive
- val faction = building.Faction
- val updatedStatus = if (invalidBuildingCapitolForceDomeConditions(building)) {
- false
- } else {
- val ownedSubCapitols = building.Neighbours(faction) match {
- case Some(buildings: Set[Building]) => buildings.count { b => !invalidBuildingCapitolForceDomeConditions(b) }
- case None => 0
- }
- if (originalStatus && ownedSubCapitols <= 1) {
- false
- } else if (!originalStatus && ownedSubCapitols > 1) {
- true
- } else {
- originalStatus
- }
- }
- Some(updatedStatus)
- } else {
- None
- }
- }
-
/**
* The power structure of major facilities has to be statused on the continental map
* via the state of its nanite-to-energy generator, and
@@ -197,6 +107,18 @@ case object MajorFacilityLogic
)
}
// No map update needed - will be sent by `HackCaptureActor` when required
+ case dome: ForceDomePhysics =>
+ val building = details.building
+ // The protection of the force dome modifies the NTU drain rate
+ val multiplier: Float = calculateNtuDrainMultiplierFrom(details.building, domeOpt = Some(dome))
+ building.NtuSource.foreach(_.Actor ! ResourceSiloControl.DrainMultiplier(multiplier))
+ // The protection of the force dome marks the generator (and some other amenities) as being invulnerable
+ val msg = Damageable.Vulnerability(dome.Perimeter.nonEmpty)
+ val applicable = dome.Definition.ApplyProtectionTo
+ building
+ .Amenities
+ .filter(amenity => applicable.contains(amenity.Definition))
+ .foreach { _.Actor ! msg }
case _ =>
details.galaxyService ! GalaxyServiceMessage(GalaxyAction.MapUpdate(details.building.infoUpdateMessage()))
}
@@ -267,7 +189,7 @@ case object MajorFacilityLogic
}
/**
- * The generator is an extrememly important amenity of a major facility
+ * The generator is an extremely important amenity of a major facility
* that is given its own status indicators that are apparent from the continental map
* and warning messages that are displayed to everyone who might have an interest in the that particular generator.
* @param details package class that conveys the important information
@@ -314,7 +236,6 @@ case object MajorFacilityLogic
true
case Some(GeneratorControl.Event.Offline) =>
powerLost(details)
- alignForceDomeStatus(details, mapUpdateOnChange = false)
val zone = building.Zone
val msg = AvatarAction.PlanetsideAttributeToAll(building.GUID, 46, 2)
building.PlayersInSOI.foreach { player =>
@@ -326,7 +247,6 @@ case object MajorFacilityLogic
case Some(GeneratorControl.Event.Online) =>
// Power restored. Reactor Online. Sensors Online. Weapons Online. All systems nominal.
powerRestored(details)
- alignForceDomeStatus(details, mapUpdateOnChange = false)
val events = zone.AvatarEvents
val guid = building.GUID
val msg1 = AvatarAction.PlanetsideAttributeToAll(guid, 46, 0)
@@ -348,16 +268,17 @@ case object MajorFacilityLogic
): Behavior[Command] = {
if (details.asInstanceOf[MajorFacilityWrapper].hasNtuSupply) {
BuildingActor.setFactionTo(details, faction, log)
- alignForceDomeStatus(details, mapUpdateOnChange = false)
val building = details.building
- building.Neighbours.getOrElse(Nil).foreach { _.Actor ! BuildingActor.AlertToFactionChange(building) }
+ val alertMsg = BuildingActor.AlertToFactionChange(building)
+ building.Neighbours.getOrElse(Nil).foreach { _.Actor ! alertMsg }
+ building.Amenities.foreach { _.Actor ! alertMsg }
}
Behaviors.same
}
def alertToFactionChange(details: BuildingWrapper, building: Building): Behavior[Command] = {
- alignForceDomeStatus(details)
val bldg = details.building
+ bldg.Amenities.foreach { _.Actor ! BuildingActor.AlertToFactionChange(building) } //todo map update?
//the presence of the flag means that we are involved in an ongoing llu hack
(bldg.GetFlag, bldg.CaptureTerminal) match {
case (Some(flag), Some(terminal)) if (flag.Target eq building) && flag.Faction != building.Faction =>
@@ -439,4 +360,35 @@ case object MajorFacilityLogic
}
Behaviors.same
}
+
+ private def calculateNtuDrainMultiplierFrom(
+ building: Building,
+ domeOpt: Option[ForceDomePhysics] = None,
+ mainTerminalOpt: Option[Any] = None
+ ): Float = {
+ val domeParam = domeOpt.orElse {
+ building.Amenities.find(_.isInstanceOf[ForceDomePhysics]) match {
+ case Some(d: ForceDomePhysics) => Some(d)
+ case _ => None
+ }
+ }
+ val mainTerminalParam = mainTerminalOpt.orElse(None) //todo main terminal and viruses
+ getNtuDrainMultiplierFromAmenities(domeParam, mainTerminalParam)
+ }
+
+ private def getNtuDrainMultiplierFromAmenities(
+ dome: Option[ForceDomePhysics],
+ mainTerminal: Option[Any]
+ ): Float = {
+ // The force dome being expanded means all repairs are essentially for free
+ dome
+ .flatMap {
+ case d if d.Energized => Some(0f)
+ case _ => None
+ }
+ .orElse {
+ mainTerminal.flatMap { _ => Some(2f) } //todo main terminal and viruses
+ }
+ .getOrElse(1f)
+ }
}
diff --git a/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala b/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala
index b32bf2e4d..ceea45622 100644
--- a/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala
+++ b/src/main/scala/net/psforever/objects/ExplosiveDeployable.scala
@@ -187,7 +187,8 @@ object ExplosiveDeployableControl {
zone,
target,
Zone.explosionDamage(Some(cause)),
- ExplosiveDeployableControl.detectionForExplosiveSource(target)
+ ExplosiveDeployableControl.detectionForExplosiveSource(target),
+ Zone.findAllTargets
)
}
diff --git a/src/main/scala/net/psforever/objects/FieldTurretDeployable.scala b/src/main/scala/net/psforever/objects/FieldTurretDeployable.scala
index 1745f6476..be51a5cba 100644
--- a/src/main/scala/net/psforever/objects/FieldTurretDeployable.scala
+++ b/src/main/scala/net/psforever/objects/FieldTurretDeployable.scala
@@ -9,12 +9,14 @@ import net.psforever.objects.serverobject.hackable.GenericHackables
import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior}
import net.psforever.objects.serverobject.turret.{MountableTurret, WeaponTurrets}
import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject}
-import net.psforever.objects.sourcing.SourceEntry
-import net.psforever.objects.vital.{InGameActivity, ShieldCharge}
+import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, TurretSource}
+import net.psforever.objects.vital.{DismountingActivity, InGameActivity, MountingActivity, ShieldCharge}
import net.psforever.packet.game.HackState1
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
import net.psforever.types.PlanetSideGUID
+import scala.annotation.unused
+
/** definition */
class FieldTurretDeployableDefinition(private val objectId: Int)
@@ -70,6 +72,21 @@ class FieldTurretControl(turret: TurretDeployable)
player: Player
): Boolean = MountableTurret.MountTest(TurretObject, player)
+ override def mountActionResponse(user: Player, @unused mountPoint: Int, seatNumber: Int): Unit = {
+ super.mountActionResponse(user, mountPoint, seatNumber)
+ if (turret.PassengerInSeat(user).contains(0)) {
+ val vsrc = TurretSource(turret)
+ user.LogActivity(MountingActivity(vsrc, PlayerSource.inSeat(user, vsrc, seatNumber = 0), turret.Zone.Number))
+ }
+ }
+
+ override def dismountActionResponse(user: Player, seatBeingDismounted: Int): Unit = {
+ super.dismountActionResponse(user, seatBeingDismounted)
+ if (!turret.Seats(seatBeingDismounted).isOccupied) { //this seat really was vacated
+ user.LogActivity(DismountingActivity(TurretSource(turret), PlayerSource(user), turret.Zone.Number))
+ }
+ }
+
//make certain vehicles don't charge shields too quickly
private def canChargeShields: Boolean = {
val func: InGameActivity => Boolean = WithShields.LastShieldChargeOrDamage(System.currentTimeMillis(), turret.Definition)
diff --git a/src/main/scala/net/psforever/objects/GlobalDefinitions.scala b/src/main/scala/net/psforever/objects/GlobalDefinitions.scala
index c5b198fb1..573220471 100644
--- a/src/main/scala/net/psforever/objects/GlobalDefinitions.scala
+++ b/src/main/scala/net/psforever/objects/GlobalDefinitions.scala
@@ -8,6 +8,7 @@ import net.psforever.objects.definition.converter._
import net.psforever.objects.equipment._
import net.psforever.objects.global.{GlobalDefinitionsAmmo, GlobalDefinitionsBuilding, GlobalDefinitionsDeployable, GlobalDefinitionsExoSuit, GlobalDefinitionsImplant, GlobalDefinitionsKit, GlobalDefinitionsMiscellaneous, GlobalDefinitionsProjectile, GlobalDefinitionsTool, GlobalDefinitionsVehicle}
import net.psforever.objects.locker.LockerContainerDefinition
+import net.psforever.objects.serverobject.dome.ForceDomeDefinition
import net.psforever.objects.serverobject.doors.DoorDefinition
import net.psforever.objects.serverobject.generator.GeneratorDefinition
import net.psforever.objects.serverobject.locks.IFFLockDefinition
@@ -1286,6 +1287,18 @@ object GlobalDefinitions {
val zipline = new GenericTeleportationDefinition(1047)
+ val force_dome_generator = new ForceDomeDefinition(322)
+
+ val force_dome_amp_physics = new ForceDomeDefinition(313)
+
+ val force_dome_comm_physics = new ForceDomeDefinition(316)
+
+ val force_dome_cryo_physics = new ForceDomeDefinition(319)
+
+ val force_dome_dsp_physics = new ForceDomeDefinition(321)
+
+ val force_dome_tech_physics = new ForceDomeDefinition(323)
+
/*
Buildings
*/
diff --git a/src/main/scala/net/psforever/objects/Player.scala b/src/main/scala/net/psforever/objects/Player.scala
index c0023ebb6..3681936fa 100644
--- a/src/main/scala/net/psforever/objects/Player.scala
+++ b/src/main/scala/net/psforever/objects/Player.scala
@@ -1,7 +1,7 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects
-import net.psforever.objects.avatar.interaction.{TriggerOnPlayerRule, WithEntrance, WithGantry, WithLava, WithWater}
+import net.psforever.objects.avatar.interaction.{InteractWithForceDomeProtection, TriggerOnPlayerRule, WithEntrance, WithGantry, WithLava, WithWater}
import net.psforever.objects.avatar.{Avatar, LoadoutManager, SpecialCarry}
import net.psforever.objects.ballistics.InteractWithRadiationClouds
import net.psforever.objects.ce.{Deployable, InteractWithMines, InteractWithTurrets}
@@ -40,6 +40,7 @@ class Player(var avatar: Avatar)
with InteriorAwareFromInteraction
with AuraContainer
with MountableEntity {
+ interaction(new InteractWithForceDomeProtection())
interaction(environment.interaction.InteractWithEnvironment(Seq(
new WithEntrance(),
new WithWater(avatar.name),
diff --git a/src/main/scala/net/psforever/objects/TurretDeployable.scala b/src/main/scala/net/psforever/objects/TurretDeployable.scala
index 17dd8dc3b..f21f74707 100644
--- a/src/main/scala/net/psforever/objects/TurretDeployable.scala
+++ b/src/main/scala/net/psforever/objects/TurretDeployable.scala
@@ -10,7 +10,7 @@ import net.psforever.objects.guid.{GUIDTask, TaskWorkflow}
import net.psforever.objects.serverobject.affinity.FactionAffinityBehavior
import net.psforever.objects.serverobject.damage.Damageable
import net.psforever.objects.serverobject.hackable.Hackable
-import net.psforever.objects.serverobject.mount.InteractWithRadiationCloudsSeatedInEntity
+import net.psforever.objects.serverobject.mount.interaction.{InteractWithForceDomeProtectionSeatedInEntity, InteractWithRadiationCloudsSeatedInEntity}
import net.psforever.objects.serverobject.turret.auto.{AffectedByAutomaticTurretFire, AutomatedTurret}
import net.psforever.objects.serverobject.turret.{TurretControl, TurretDefinition, WeaponTurret}
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry}
@@ -36,6 +36,7 @@ class TurretDeployable(tdef: TurretDeployableDefinition)
HackDuration = Array(0, 20, 10, 5)
if (tdef.Seats.nonEmpty) {
+ interaction(new InteractWithForceDomeProtectionSeatedInEntity)
interaction(new InteractWithTurrets())
interaction(new InteractWithRadiationCloudsSeatedInEntity(obj = this, range = 100f))
}
diff --git a/src/main/scala/net/psforever/objects/Vehicle.scala b/src/main/scala/net/psforever/objects/Vehicle.scala
index 3658294d5..60ef15996 100644
--- a/src/main/scala/net/psforever/objects/Vehicle.scala
+++ b/src/main/scala/net/psforever/objects/Vehicle.scala
@@ -16,7 +16,7 @@ import net.psforever.objects.serverobject.hackable.Hackable
import net.psforever.objects.serverobject.interior.{InteriorAwareFromInteraction, Sidedness}
import net.psforever.objects.serverobject.structures.AmenityOwner
import net.psforever.objects.vehicles._
-import net.psforever.objects.vehicles.interaction.{TriggerOnVehicleRule, WithLava, WithWater}
+import net.psforever.objects.vehicles.interaction.{InteractWithForceDomeProtectionSeatedInVehicle, InteractWithRadiationCloudsSeatedInVehicle, TriggerOnVehicleRule, WithLava, WithWater}
import net.psforever.objects.vital.resistance.StandardResistanceProfile
import net.psforever.objects.vital.Vitality
import net.psforever.objects.vital.resolution.DamageResistanceModel
@@ -94,6 +94,7 @@ class Vehicle(private val vehicleDef: VehicleDefinition)
with AuraContainer
with MountableEntity
with InteriorAwareFromInteraction {
+ interaction(new InteractWithForceDomeProtectionSeatedInVehicle)
interaction(environment.interaction.InteractWithEnvironment(Seq(
new WithEntrance(),
new WithWater(),
diff --git a/src/main/scala/net/psforever/objects/avatar/interaction/InteractWithForceDomeProtection.scala b/src/main/scala/net/psforever/objects/avatar/interaction/InteractWithForceDomeProtection.scala
new file mode 100644
index 000000000..3b6e30605
--- /dev/null
+++ b/src/main/scala/net/psforever/objects/avatar/interaction/InteractWithForceDomeProtection.scala
@@ -0,0 +1,96 @@
+// Copyright (c) 2025 PSForever
+package net.psforever.objects.avatar.interaction
+
+import net.psforever.objects.serverobject.damage.Damageable
+import net.psforever.objects.serverobject.dome.{ForceDomeControl, ForceDomePhysics}
+import net.psforever.objects.zones.blockmap.SectorPopulation
+import net.psforever.objects.zones.interaction.{InteractsWithZone, ZoneInteraction, ZoneInteractionType}
+
+case object ForceZoneProtection extends ZoneInteractionType
+
+/**
+ * Entities under the capitol force dome that have not died in its initial activation
+ * do not take further damage until removed from under the dome or until the dome is deactivated.
+ */
+class InteractWithForceDomeProtection
+ extends ZoneInteraction {
+ def Type: ZoneInteractionType = ForceZoneProtection
+
+ def range: Float = 10f
+
+ /** increment to n, reevaluate the dome protecting the target, reset counter to 0 */
+ private var protectSkipCounter: Int = 0
+ /** dome currently protecting the target */
+ private var protectedBy: Option[ForceDomePhysics] = None
+
+ /**
+ * If the target is protected, do conditions allow it to remain protected?
+ * If the target was vulnerable, can it be protected?
+ * Five second pause between evaluations (0-3, wait; 4, test).
+ * @see `ForceDomeControl.TargetUnderForceDome`
+ * @param sector the portion of the block map being tested
+ * @param target the fixed element in this test
+ */
+ def interaction(sector: SectorPopulation, target: InteractsWithZone): Unit = {
+ if (protectSkipCounter < 4) {
+ protectSkipCounter += 1
+ } else {
+ protectSkipCounter = 0
+ protectedBy match {
+ case Some(dome)
+ if dome.Perimeter.isEmpty ||
+ target.Zone != dome.Zone ||
+ !ForceDomeControl.TargetUnderForceDome(dome.Perimeter)(dome, target, maxDistance = 0f) =>
+ resetInteraction(target)
+ case Some(_) =>
+ () //no action
+ case None =>
+ searchForInteractionCause(sector, target)
+ }
+ }
+ }
+
+ /**
+ * Look the through the list of amenities in this sector for capitol force domes,
+ * determine which force domes are energized (activated, expanded, enveloping, etc.),
+ * and find the first active dome under which the target `entity` is positioned.
+ * The target `entity` is considered protected and can not be damaged until further notice.
+ * @see `Damageable.MakeInvulnerable`
+ * @see `ForceDomeControl.TargetUnderForceDome`
+ * @param sector – the portion of the block map being tested
+ * @param target – the fixed element in this test
+ * @return whichever force dome entity is detected to encircle this target `entity`, if any
+ */
+ private def searchForInteractionCause(sector: SectorPopulation, target: InteractsWithZone): Option[ForceDomePhysics] = {
+ sector
+ .amenityList
+ .flatMap {
+ case dome: ForceDomePhysics if dome.Perimeter.nonEmpty => Some(dome)
+ case _ => None
+ }
+ .find { dome =>
+ ForceDomeControl.TargetUnderForceDome(dome.Perimeter)(dome, target, maxDistance = 0f)
+ }
+ .map { dome =>
+ applyProtection(target, dome)
+ dome
+ }
+ }
+
+ def applyProtection(target: InteractsWithZone, dome: ForceDomePhysics): Unit = {
+ protectedBy = Some(dome)
+ target.Actor ! Damageable.MakeInvulnerable
+ }
+
+ /**
+ * No longer invulnerable (if ever).
+ * Set the counter to force a reevaluation of the vulnerability state next turn.
+ * @see `Damageable.MakeVulnerable`
+ * @param target the fixed element in this test
+ */
+ def resetInteraction(target: InteractsWithZone): Unit = {
+ protectSkipCounter = 5
+ protectedBy = None
+ target.Actor ! Damageable.MakeVulnerable
+ }
+}
diff --git a/src/main/scala/net/psforever/objects/equipment/ArmorSiphonBehavior.scala b/src/main/scala/net/psforever/objects/equipment/ArmorSiphonBehavior.scala
index 9c153e42b..5547f36fd 100644
--- a/src/main/scala/net/psforever/objects/equipment/ArmorSiphonBehavior.scala
+++ b/src/main/scala/net/psforever/objects/equipment/ArmorSiphonBehavior.scala
@@ -43,7 +43,7 @@ object ArmorSiphonBehavior {
val after = item.Discharge()
if (before > after) {
v.Actor ! ArmorSiphonBehavior.Recharge(iguid)
- PerformDamage(
+ PerformDamageIfVulnerable(
obj,
DamageInteraction(
VehicleSource(obj),
diff --git a/src/main/scala/net/psforever/objects/global/GlobalDefinitionsMiscellaneous.scala b/src/main/scala/net/psforever/objects/global/GlobalDefinitionsMiscellaneous.scala
index ff136ba94..b85b9e872 100644
--- a/src/main/scala/net/psforever/objects/global/GlobalDefinitionsMiscellaneous.scala
+++ b/src/main/scala/net/psforever/objects/global/GlobalDefinitionsMiscellaneous.scala
@@ -956,5 +956,65 @@ object GlobalDefinitionsMiscellaneous {
zipline.Name = "zipline"
zipline.interference = InterferenceRange(deployables = 5.5f)
+
+ force_dome_amp_physics.Name = "force_dome_amp_physics"
+ force_dome_amp_physics.UseRadius = 142.26f
+ force_dome_amp_physics.PerimeterOffsets = List(
+ Vector3(83.05469f, 114.875f, 0f),
+ Vector3(-90.328125f, 114.875f, 0f),
+ Vector3(-90.328125f, -106.90625f, 0f),
+ Vector3(83.05469f, -106.90625f, 0f)
+ )
+ force_dome_amp_physics.ApplyProtectionTo = List(generator, manned_turret)
+
+ force_dome_comm_physics.Name = "force_dome_comm_physics"
+ force_dome_comm_physics.UseRadius = 121.8149f
+ force_dome_comm_physics.PerimeterOffsets = List(
+ Vector3(35.1875f, -89.859375f, 0f),
+ Vector3(80.96875f, -43.773438f, 0f),
+ Vector3(80.96875f, 91.08594f, 0f),
+ Vector3(-37.296875f, 91.08594f, 0f),
+ Vector3(-83.640625f, 45.601562f, 0f),
+ Vector3(-83.640625f, -89.859375f, 0f)
+ )
+ force_dome_comm_physics.ApplyProtectionTo = List(generator, manned_turret)
+
+ force_dome_cryo_physics.Name = "force_dome_cryo_physics"
+ force_dome_cryo_physics.UseRadius = 127.9241f //127.7963f
+ force_dome_cryo_physics.PerimeterOffsets = List(
+ Vector3(72.75476f, 39.902725f, 0),
+ Vector3(24.505968f, 88.03482f, 0),
+ Vector3(-74.73426f, 88.03482f, 0),
+ Vector3(-74.73426f, -103.47f, 0),
+ Vector3(72.75476f, -103.47f, 0)
+ )
+ force_dome_cryo_physics.ApplyProtectionTo = List(generator, implant_terminal_mech, manned_turret)
+
+ force_dome_dsp_physics.Name = "force_dome_dsp_physics"
+ force_dome_dsp_physics.UseRadius = 175.8838f //175.7081f
+ force_dome_dsp_physics.PerimeterOffsets = List(
+ Vector3(35.03125f, -93.25f, 0f),
+ Vector3(-83.1875f, -93.25f, 0f),
+ Vector3(-83.1875f, 114.515625f, 0f),
+ Vector3(-12.109375f, 188.26562f, 0f),
+ Vector3(130.44531f, 188.26562f, 0f),
+ Vector3(130.44531f, -93.28125f, 0f)
+ )
+ force_dome_dsp_physics.ApplyProtectionTo = List(generator, manned_turret)
+
+ force_dome_tech_physics.Name = "force_dome_tech_physics"
+ force_dome_tech_physics.UseRadius = 150.1284f
+ force_dome_tech_physics.PerimeterOffsets = List( //todo double-check, e.g., eisa, esamir
+ Vector3(130.14636f, -95.20665f, 0f),
+ Vector3(130.14636f, 34.441734f, 0f),
+ Vector3(103.98575f, 52.58408f, 0f),
+ Vector3(16.405174f, 54.746464f, 0f),
+ Vector3(14.256668f, 107.01521f, 0f),
+ Vector3(-92.08687f, 107.01521f, 0f),
+ Vector3(-92.08687f, -96.176155f, 0f),
+ Vector3(-73.64424f, -114.65837f, 0f),
+ Vector3(102.12191f, -114.65837f, 0f)
+ )
+ force_dome_tech_physics.ApplyProtectionTo = List(generator, manned_turret)
}
}
diff --git a/src/main/scala/net/psforever/objects/serverobject/damage/Damageable.scala b/src/main/scala/net/psforever/objects/serverobject/damage/Damageable.scala
index 7f1863ffb..65d94d0d4 100644
--- a/src/main/scala/net/psforever/objects/serverobject/damage/Damageable.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/damage/Damageable.scala
@@ -17,7 +17,6 @@ import net.psforever.objects.vital.resolution.ResolutionCalculations
* All of these should be affected by the damage where applicable.
*/
trait Damageable {
-
/**
* Contextual access to the object being the target of this damage.
* Needs declaration in lowest implementing code.
@@ -25,24 +24,46 @@ trait Damageable {
*/
def DamageableObject: Damageable.Target
+ /** a local `canDamage` flag */
+ private var isVulnerable: Boolean = true
+
/** the official mixin hook;
* `orElse` onto the "control" `Actor` `receive`; or,
* cite the `originalTakesDamage` protocol during inheritance overrides */
val takesDamage: Receive = {
+ case Damageable.MakeVulnerable =>
+ isVulnerable = true
+
+ case Damageable.MakeInvulnerable =>
+ isVulnerable = false
+
case Vitality.Damage(damage_func) =>
- val obj = DamageableObject
- if (obj.CanDamage) {
- PerformDamage(obj, damage_func)
- }
+ PerformDamageIfVulnerable(DamageableObject, damage_func)
}
/** a duplicate of the core implementation for the default mixin hook, for use in overriding */
final val originalTakesDamage: Receive = {
+ case Damageable.MakeVulnerable =>
+ isVulnerable = true
+
+ case Damageable.MakeInvulnerable =>
+ isVulnerable = false
+
case Vitality.Damage(damage_func) =>
- val obj = DamageableObject
- if (obj.CanDamage) {
- PerformDamage(obj, damage_func)
- }
+ PerformDamageIfVulnerable(DamageableObject, damage_func)
+ }
+
+ /**
+ * Assess if the target is vulnerable to damage.
+ * If so, attempt damage calculations.
+ * @see `ResolutionCalculations.Output`
+ * @param obj the entity to be damaged
+ * @param applyDamageTo the function that applies the damage to the target in a target-tailored fashion
+ */
+ def PerformDamageIfVulnerable(obj: Damageable.Target, applyDamageTo: ResolutionCalculations.Output): Unit = {
+ if (isVulnerable && obj.CanDamage) {
+ PerformDamage(obj, applyDamageTo)
+ }
}
/**
@@ -67,6 +88,20 @@ object Damageable {
*/
final val LogChannel: String = "DamageResolution"
+ trait PersonalVulnerability
+
+ final case object MakeVulnerable extends PersonalVulnerability
+
+ final case object MakeInvulnerable extends PersonalVulnerability
+
+ def Vulnerability(state: Boolean): PersonalVulnerability = {
+ if (state) {
+ MakeInvulnerable
+ } else {
+ MakeVulnerable
+ }
+ }
+
/**
* Does the possibility exist that the designated target can be affected by this projectile's damage?
* @see `Hackable`
diff --git a/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeControl.scala b/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeControl.scala
new file mode 100644
index 000000000..e959dd836
--- /dev/null
+++ b/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeControl.scala
@@ -0,0 +1,494 @@
+// Copyright (c) 2025 PSForever
+package net.psforever.objects.serverobject.dome
+
+import net.psforever.actors.zone.BuildingActor
+import net.psforever.objects.serverobject.PlanetSideServerObject
+import net.psforever.objects.PlanetSideGameObject
+import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior}
+import net.psforever.objects.serverobject.structures.{Amenity, Building, PoweredAmenityControl}
+import net.psforever.objects.serverobject.terminals.capture.{CaptureTerminal, CaptureTerminalAware, CaptureTerminalAwareBehavior}
+import net.psforever.objects.sourcing.SourceEntry
+import net.psforever.objects.vital.Vitality
+import net.psforever.objects.vital.etc.ForceDomeExposure
+import net.psforever.objects.vital.interaction.DamageInteraction
+import net.psforever.objects.vital.prop.DamageWithPosition
+import net.psforever.objects.zones.Zone
+import net.psforever.packet.game.ChatMsg
+import net.psforever.services.Service
+import net.psforever.services.local.{LocalAction, LocalServiceMessage}
+import net.psforever.types.{ChatMessageType, PlanetSideEmpire, PlanetSideGeneratorState, Vector3}
+
+import scala.annotation.unused
+
+object ForceDomeControl {
+ trait Command
+
+ final case object CustomExpand extends Command
+
+ final case object CustomCollapse extends Command
+
+ final case object NormalBehavior extends Command
+
+ final case object ApplyProtection extends Command
+
+ final case object RemoveProtection extends Command
+
+ final case object Purge extends Command
+
+ /**
+ * Dispatch a message to update the state of the clients with the server state of the capitol force dome.
+ * @param dome force dome
+ * @param activationState new force dome status
+ */
+ def ChangeDomeEnergizedState(dome: ForceDomePhysics, activationState: Boolean): Unit = {
+ dome.Energized = activationState
+ val owner = dome.Owner
+ val zone = owner.Zone
+ owner.Actor ! BuildingActor.AmenityStateChange(dome)
+ zone.LocalEvents ! LocalServiceMessage(
+ zone.id,
+ LocalAction.UpdateForceDomeStatus(Service.defaultPlayerGUID, owner.GUID, activationState)
+ )
+ }
+
+ /**
+ * If this building is a capitol major facility,
+ * use the faction affinity, the generator status, and the resource silo's capacitance level
+ * to determine if the capitol force dome should be active.
+ * @param building building being evaluated
+ * @param dome force dome
+ * @return the condition of the capitol force dome;
+ * `None`, if the facility is not a capitol building;
+ * `Some(true|false)` to indicate the state of the force dome
+ */
+ def CheckForceDomeStatus(building: Building, dome: ForceDomePhysics): Option[Boolean] = {
+ if (building.IsCapitol) {
+ Some(
+ if (InvalidBuildingCapitolForceDomeConditions(building)) {
+ false
+ } else {
+ building
+ .Neighbours(building.Faction)
+ .map(_.count(b => !InvalidBuildingCapitolForceDomeConditions(b)))
+ .exists(_ > 1)
+ }
+ )
+ } else {
+ None
+ }
+ }
+
+ /**
+ * The natural conditions of a facility that is not eligible for its capitol force dome to be expanded.
+ * The only test not employed is whether or not the target building is a capitol.
+ * Omission of this condition makes this test capable of evaluating subcapitol eligibility
+ * for capitol force dome expansion.
+ * @param building target building
+ * @return `true`, if the conditions for capitol force dome are not met;
+ * `false`, otherwise
+ */
+ def InvalidBuildingCapitolForceDomeConditions(building: Building): Boolean = {
+ building.Faction == PlanetSideEmpire.NEUTRAL ||
+ building.NtuLevel == 0 ||
+ building.Generator.exists(_.Condition == PlanetSideGeneratorState.Destroyed)
+ }
+
+ /**
+ * Apply a fixed point and a rotation value to a series of vertex offsets,
+ * then daisy-chain the resulting vertices in such a way that
+ * it creates a perimeter around the (building) owner of the capitol force dome.
+ * The resulting capitol force dome barrier is a blocky pyramoid shape.
+ * @param dome force dome
+ * @return perimeter of the force dome barrier
+ */
+ def SetupForceDomePerimeter(dome: ForceDomePhysics): List[(Vector3, Vector3)] = {
+ val center = dome.Position.xy
+ val rotation = math.toRadians(dome.Owner.Orientation.z).toFloat
+ val perimeterOffsets = dome.Definition.PerimeterOffsets
+ val perimeterPoints = perimeterOffsets.map {
+ center + Vector3.PlanarRotateAroundPoint(_, Vector3(0, 0, 1), rotation)
+ }
+ ((0 until perimeterPoints.size - 1).map { index =>
+ (perimeterPoints(index), perimeterPoints(index + 1))
+ } :+ (perimeterPoints.last, perimeterPoints.head)).toList
+ }
+
+ /**
+ * The capitol force dome should have changed states but it will not!
+ * Make certain everyone knows!
+ * @param building target building
+ * @param state whether the force dome is energized or not
+ */
+ def CustomDomeStateEnforcedMessage(
+ building: Building,
+ state: Boolean
+ ): Unit = {
+ val zone = building.Zone
+ val message = LocalAction.SendResponse(ChatMsg(
+ ChatMessageType.UNK_229,
+ s"The Capitol force dome at ${building.Name} will remain ${if (state) "activated" else "deactivated"}."
+ ))
+ zone.LocalEvents ! LocalServiceMessage(zone.id, message)
+ }
+
+ /**
+ * The capitol force dome will start changing states normally.
+ * Make certain everyone knows.
+ * @param building facility
+ */
+ def NormalDomeStateMessage(building: Building): Unit = {
+ val events = building.Zone.LocalEvents
+ val message = LocalAction.SendResponse(ChatMsg(
+ ChatMessageType.UNK_227,
+ "Expected capitol force dome state change will resume."
+ ))
+ building.PlayersInSOI.foreach { player =>
+ events ! LocalServiceMessage(player.Name, message)
+ }
+ }
+
+ /**
+ * Evaluate the conditions of the building
+ * and determine if its capitol force dome state should be updated
+ * to reflect the actual conditions of the base or its surrounding bases.
+ * If this building is considered a subcapitol facility to the zone's actual capitol facility,
+ * and has the capitol force dome has a dependency upon it,
+ * pass a message onto that facility that it should check its own state alignment.
+ * @param building facility with `dome`
+ * @param dome force dome
+ * @return current state of the capitol force dome
+ */
+ def AlignForceDomeStatusAndUpdate(building: Building, dome: ForceDomePhysics): Boolean = {
+ val energizedState = dome.Energized
+ CheckForceDomeStatus(building, dome).exists {
+ case true if !energizedState =>
+ ChangeDomeEnergizedState(dome, activationState = true)
+ dome.Owner.Actor ! BuildingActor.MapUpdate()
+ true
+ case false if energizedState =>
+ ChangeDomeEnergizedState(dome, activationState = false)
+ dome.Owner.Actor ! BuildingActor.MapUpdate()
+ false
+ case _ =>
+ energizedState
+ }
+ }
+
+ /**
+ * Evaluate the conditions of the building
+ * and determine if its capitol force dome state should be updated
+ * to reflect the actual conditions of the base or its surrounding bases.
+ * If this building is considered a subcapitol facility to the zone's actual capitol facility,
+ * and has the capitol force dome has a dependency upon it,
+ * pass a message onto that facility that it should check its own state alignment.
+ * @param building facility with `dome`
+ * @param dome force dome
+ * @return current state of the capitol force dome
+ */
+ private def AlignForceDomeStatus(building: Building, dome: ForceDomePhysics): Boolean = {
+ val energizedState = dome.Energized
+ CheckForceDomeStatus(building, dome).exists {
+ case true if !energizedState =>
+ ChangeDomeEnergizedState(dome, activationState = true)
+ true
+ case false if energizedState =>
+ ChangeDomeEnergizedState(dome, activationState = false)
+ false
+ case _ =>
+ energizedState
+ }
+ }
+
+ /**
+ * Being too close to the force dome can destroy targets if they do not match the faction alignment of the dome.
+ * This is the usual fate of opponents upon it being expanded (energized).
+ * @see `Zone.serverSideDamage`
+ * @param dome force dome
+ * @param perimeter ground-level perimeter of the force dome is defined by these segments (as vertex pairs)
+ * @return list of affected entities
+ */
+ def ForceDomeKills(dome: ForceDomePhysics, perimeter: List[(Vector3, Vector3)]): List[PlanetSideServerObject] = {
+ Zone.serverSideDamage(
+ dome.Zone,
+ dome,
+ ForceDomeExposure.damageProperties,
+ makesContactWithForceDome,
+ TargetUnderForceDome(perimeter),
+ forceDomeTargets(dome.Definition.UseRadius, dome.Faction)
+ )
+ }
+
+ /**
+ * Prepare damage information related to being caugt underneath the capitol force dome when it expands.
+ * @param source a game object that represents the source of the explosion
+ * @param target a game object that is affected by the explosion
+ * @return a `DamageInteraction` object
+ */
+ private def makesContactWithForceDome(
+ source: PlanetSideGameObject with FactionAffinity with Vitality,
+ target: PlanetSideGameObject with FactionAffinity with Vitality
+ ): DamageInteraction = {
+ DamageInteraction(
+ SourceEntry(target),
+ ForceDomeExposure(SourceEntry(source)),
+ target.Position
+ )
+ }
+
+ /**
+ * To be considered within a force dome, a target entity must satisfy two orientations
+ * where the second condition is one of two qualifications:
+ * 1. within an angular perimeter boundary, and
+ * 2a. below the base coordinate of the force dome or
+ * 2b. within a region above the base of the force dome represented by a literal "dome" (half of a sphere).
+ * @see `Zone.distanceCheck`
+ * @param segments ground-level perimeter of the force dome is defined by these segments (as vertex pairs)
+ * @param obj1 a game entity, should be the force dome
+ * @param obj2 a game entity, should be a damageable target of the force dome's wrath
+ * @param maxDistance not applicable
+ * @return `true`, if target is detected within the force dome kill region
+ * `false`, otherwise
+ */
+ def TargetUnderForceDome(
+ segments: List[(Vector3, Vector3)]
+ )
+ (
+ obj1: PlanetSideGameObject,
+ obj2: PlanetSideGameObject,
+ @unused maxDistance: Float
+ ): Boolean = {
+ val centerPos @ Vector3(centerX, centerY, centerZ) = obj1.Position
+ val Vector3(targetX, targetY, _) = obj2.Position.xy - centerPos.xy //deltas of segment of target to dome
+ lazy val checkForIntersection = segments.exists { case (point1, point2) =>
+ //want targets within the perimeter; if there's an intersection, target is outside of the perimeter
+ segmentIntersectionTestPerSegment(centerX, centerY, targetX, targetY, point1.x, point1.y, point2.x, point2.y)
+ }
+ segments.nonEmpty && !checkForIntersection && (obj2.Position.z <= centerZ || Zone.distanceCheck(obj1, obj2, math.pow(obj1.Definition.UseRadius, 2).toFloat))
+ }
+
+ /**
+ * A function to assist line segment intersection tests.
+ * The important frame of reference is checking whether a hypothetical segment between a point and a target
+ * intersects with an established line segment between two other points.
+ * For our purposes, the resulting line segments will never be collinear, so there is no reason to test that.
+ * @param pointX x-coordinate used to create a test segment
+ * @param pointY y-coordinate used to create a test segment
+ * @param targetX x-coordinate of an important point for a test segment
+ * @param targetY y-coordinate of an important point for a test segment
+ * @param segmentPoint1x x-coordinate of one point from a segment
+ * @param segmentPoint1y y-coordinate of one point from a segment
+ * @param segmentPoint2x x-coordinate of a different point from a segment
+ * @param segmentPoint2y y-coordinate of a different point from a segment
+ * @return `true`, if the points form into two segments that intersect;
+ * `false`, otherwise
+ */
+ private def segmentIntersectionTestPerSegment(
+ pointX: Float,
+ pointY: Float,
+ targetX: Float,
+ targetY: Float,
+ segmentPoint1x: Float,
+ segmentPoint1y: Float,
+ segmentPoint2x: Float,
+ segmentPoint2y: Float
+ ): Boolean = {
+ //based on Franklin Antonio's "Faster Line Segment Intersection" topic "in Graphics Gems III" book (http://www.graphicsgems.org/)
+ //compare, java.awt.geom.Line2D.linesIntersect
+ val bx = segmentPoint1x - segmentPoint2x //delta-x of segment
+ val by = segmentPoint1y - segmentPoint2y //delta-y of segment
+ val cx = pointX - segmentPoint1x //delta-x of hypotenuse of triangle formed by center, segment endpoint, and intersection point
+ val cy = pointY - segmentPoint1y //delta-y of hypotenuse of triangle formed by center, segment endpoint, and intersection point
+ val alphaNumerator = by * cx - bx * cy
+ val commonDenominator = targetY * bx - targetX * by
+ val betaNumerator = targetX * cy - targetY * cx
+ if (
+ commonDenominator > 0 &&
+ (alphaNumerator < 0 || alphaNumerator > commonDenominator || betaNumerator < 0 || betaNumerator > commonDenominator)
+ ) {
+ false
+ } else if (
+ commonDenominator < 0 &&
+ (alphaNumerator > 0 || alphaNumerator < commonDenominator || betaNumerator > 0 || betaNumerator < commonDenominator)
+ ) {
+ false
+ } else {
+ //a collinear line test could go here, but we don't need it
+ true
+ }
+ }
+
+ /**
+ * Collect all enemy players, vehicles, and combat engineering deployables in a sector.
+ * @see `DamageWithPosition`
+ * @see `Zone.blockMap.sector`
+ * @param zone the zone in which the explosion should occur
+ * @param source a game entity that is treated as the origin and is excluded from results
+ * @param damagePropertiesBySource information about the effect/damage
+ * @return a list of affected entities
+ */
+ private def forceDomeTargets(
+ radius: Float,
+ targetFaction: PlanetSideEmpire.Value
+ )
+ (
+ zone: Zone,
+ source: PlanetSideGameObject with Vitality,
+ damagePropertiesBySource: DamageWithPosition
+ ): List[PlanetSideServerObject with Vitality] = {
+ val sector = zone.blockMap.sector(source.Position.xy, radius)
+ val playerTargets = sector.livePlayerList.filterNot { _.VehicleSeated.nonEmpty }
+ //vehicles
+ val vehicleTargets = sector.vehicleList.filterNot { v => v.Destroyed || v.MountedIn.nonEmpty }
+ //deployables
+ val deployableTargets = sector.deployableList.filterNot { _.Destroyed }
+ //altogether ...
+ (playerTargets ++ vehicleTargets ++ deployableTargets).filterNot(_.Faction == targetFaction)
+ }
+}
+
+/**
+ * An `Actor` that handles messages being dispatched to a specific capitol facility's force dome.
+ * @param dome the `ForceDomePhysics` object being governed
+ */
+class ForceDomeControl(dome: ForceDomePhysics)
+ extends PoweredAmenityControl
+ with CaptureTerminalAwareBehavior
+ with FactionAffinityBehavior.Check {
+ def CaptureTerminalAwareObject: Amenity with CaptureTerminalAware = dome
+ def FactionObject: FactionAffinity = dome
+
+ /** a capitol force dome's owner should always be a facility;
+ * to save time, cast this entity and cache it for repeated use once;
+ * force dome is not immediately owned by its correct facility so delay determination */
+ private lazy val domeOwnerAsABuilding = dome.Owner.asInstanceOf[Building]
+ /** ground-level perimeter of the force dome is defined by these segments (as vertex pairs) */
+ private lazy val perimeterSegments: List[(Vector3, Vector3)] = ForceDomeControl.SetupForceDomePerimeter(dome)
+ /** force the dome into a certain state regardless of what conditions would normally transition it into that state */
+ private var customState: Option[Boolean] = None
+
+ def commonBehavior: Receive = checkBehavior
+ .orElse {
+ case ForceDomeControl.CustomExpand
+ if !dome.Energized && (customState.isEmpty || customState.contains(false)) =>
+ customState = Some(true)
+ ForceDomeControl.CustomDomeStateEnforcedMessage(domeOwnerAsABuilding, state = true)
+ ForceDomeControl.ChangeDomeEnergizedState(dome, activationState = true)
+
+ case ForceDomeControl.CustomExpand
+ if customState.isEmpty =>
+ customState = Some(true)
+ ForceDomeControl.CustomDomeStateEnforcedMessage(domeOwnerAsABuilding, state = true)
+
+ case ForceDomeControl.CustomCollapse
+ if dome.Energized && (customState.isEmpty || customState.contains(true)) =>
+ customState = Some(false)
+ ForceDomeControl.CustomDomeStateEnforcedMessage(domeOwnerAsABuilding, state = false)
+ ForceDomeControl.ChangeDomeEnergizedState(dome, activationState = false)
+
+ case ForceDomeControl.CustomCollapse
+ if customState.isEmpty =>
+ customState = Some(false)
+ ForceDomeControl.CustomDomeStateEnforcedMessage(domeOwnerAsABuilding, state = false)
+
+ case ForceDomeControl.NormalBehavior
+ if customState.nonEmpty =>
+ customState = None
+ ForceDomeControl.NormalDomeStateMessage(domeOwnerAsABuilding)
+ if (!blockedByCustomStateOr(ForceDomeControl.AlignForceDomeStatusAndUpdate)) {
+ ForceDomeControl.ForceDomeKills(dome, perimeterSegments)
+ }
+
+ case ForceDomeControl.ApplyProtection
+ if dome.Energized =>
+ dome.Perimeter = perimeterSegments
+ dome.Owner.Actor ! BuildingActor.AmenityStateChange(dome)
+
+ case ForceDomeControl.RemoveProtection =>
+ dome.Perimeter = List.empty
+ dome.Owner.Actor ! BuildingActor.AmenityStateChange(dome)
+
+ case ForceDomeControl.Purge =>
+ ForceDomeControl.ForceDomeKills(dome, perimeterSegments)
+ }
+
+ def poweredStateLogic: Receive = {
+ commonBehavior
+ .orElse(captureTerminalAwareBehaviour)
+ .orElse {
+ case BuildingActor.AlertToFactionChange(_) =>
+ blockedByCustomStateOr(ForceDomeControl.AlignForceDomeStatusAndUpdate)
+
+ case _ => ()
+ }
+ }
+
+ def unpoweredStateLogic: Receive = {
+ commonBehavior
+ .orElse {
+ case _ => ()
+ }
+ }
+
+ def powerTurnOffCallback() : Unit = {
+ deenergizeUnlessSuppressedDueToCustomState()
+ }
+
+ def powerTurnOnCallback() : Unit = {
+ blockedByCustomStateOr(ForceDomeControl.AlignForceDomeStatus)
+ }
+
+ override protected def captureTerminalIsResecured(terminal: CaptureTerminal): Unit = {
+ super.captureTerminalIsResecured(terminal)
+ blockedByCustomStateOr(ForceDomeControl.AlignForceDomeStatus)
+ }
+
+ override protected def captureTerminalIsHacked(terminal: CaptureTerminal): Unit = {
+ super.captureTerminalIsHacked(terminal)
+ deenergizeUnlessSuppressedDueToCustomState()
+ }
+
+ /**
+ * Power down the force dome if it was previously being powered and
+ * as long as a custom state of being energized is not being enforced.
+ */
+ private def deenergizeUnlessSuppressedDueToCustomState(): Unit = {
+ if (dome.Energized) {
+ if (customState.isEmpty) {
+ ForceDomeControl.ChangeDomeEnergizedState(dome, activationState = false)
+ } else {
+ ForceDomeControl.CustomDomeStateEnforcedMessage(domeOwnerAsABuilding, state = true)
+ }
+ }
+ }
+
+ /**
+ * Yield to a custom value enforcing a certain force dome state - energized or powered down.
+ * If the custom state is not declared, run the function and analyze any change in the force dome's natural state.
+ * Apply changes to region represented as "bound" by the perimeter as indicated by a state change.
+ * @param func function to run if not blocked
+ * @return current energized state of the dome
+ */
+ private def blockedByCustomStateOr(func: (Building, ForceDomePhysics) => Boolean): Boolean = {
+ import scala.concurrent.duration._
+ import scala.concurrent.ExecutionContext.Implicits.global
+ customState match {
+ case None =>
+ val oldState = dome.Energized
+ val newState = func(domeOwnerAsABuilding, dome)
+ if (!oldState && newState) {
+ //dome activating
+ context.system.scheduler.scheduleOnce(delay = 1500 milliseconds, self, ForceDomeControl.Purge)
+ context.system.scheduler.scheduleOnce(delay = 4000 milliseconds, self, ForceDomeControl.ApplyProtection)
+ } else if (oldState && !newState) {
+ context.system.scheduler.scheduleOnce(delay = 1500 milliseconds, self, ForceDomeControl.RemoveProtection)
+ }
+ newState
+ case Some(state)
+ if !ForceDomeControl.CheckForceDomeStatus(domeOwnerAsABuilding, dome).contains(state) =>
+ ForceDomeControl.CustomDomeStateEnforcedMessage(domeOwnerAsABuilding, state)
+ state
+ case Some(state) =>
+ state
+ }
+ }
+}
diff --git a/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeDefinition.scala b/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeDefinition.scala
new file mode 100644
index 000000000..efca36455
--- /dev/null
+++ b/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomeDefinition.scala
@@ -0,0 +1,53 @@
+// Copyright (c) 2025 PSForever
+package net.psforever.objects.serverobject.dome
+
+import net.psforever.objects.geometry.d3.{Sphere, VolumetricGeometry}
+import net.psforever.objects.serverobject.structures.AmenityDefinition
+import net.psforever.types.Vector3
+
+class ForceDomeDefinition(objectId: Int)
+ extends AmenityDefinition(objectId) {
+ Name = "force_dome"
+ Geometry = ForceDomeDefinition.representBy
+
+ /** offsets that define the perimeter of the pyramidal force "dome" barrier;
+ * these points are the closest to where the dome interacts with the ground at a corner;
+ * should be sequential, either clockwise or counterclockwise */
+ private var perimeter: List[Vector3] = List()
+
+ def PerimeterOffsets: List[Vector3] = perimeter
+
+ def PerimeterOffsets_=(points: List[Vector3]): List[Vector3] = {
+ perimeter = points
+ PerimeterOffsets
+ }
+
+ private var protects: List[AmenityDefinition] = List()
+
+ def ApplyProtectionTo: List[AmenityDefinition] = protects
+
+ def ApplyProtectionTo_=(protect: AmenityDefinition): List[AmenityDefinition] = {
+ ApplyProtectionTo_=(List(protect))
+ }
+
+ def ApplyProtectionTo_=(protect: List[AmenityDefinition]): List[AmenityDefinition] = {
+ protects = protect
+ ApplyProtectionTo
+ }
+}
+
+object ForceDomeDefinition {
+ /**
+ * Transform a capitol force dome into a bounded geometric representation.
+ * @param o any entity from which to produce a geometric representation
+ * @return geometric representation
+ */
+ def representBy(o: Any): VolumetricGeometry = {
+ o match {
+ case fdp: ForceDomePhysics =>
+ Sphere(fdp.Position, fdp.Definition.UseRadius)
+ case _ =>
+ net.psforever.objects.geometry.GeometryForm.invalidPoint
+ }
+ }
+}
diff --git a/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomePhysics.scala b/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomePhysics.scala
new file mode 100644
index 000000000..1cd60c2a0
--- /dev/null
+++ b/src/main/scala/net/psforever/objects/serverobject/dome/ForceDomePhysics.scala
@@ -0,0 +1,59 @@
+// Copyright (c) 2025 PSForever
+package net.psforever.objects.serverobject.dome
+
+import net.psforever.objects.serverobject.structures.Amenity
+import net.psforever.objects.serverobject.terminals.capture.CaptureTerminalAware
+import net.psforever.types.Vector3
+
+class ForceDomePhysics(private val cfddef: ForceDomeDefinition)
+ extends Amenity
+ with CaptureTerminalAware {
+ /** whether the dome is active or not */
+ private var energized: Boolean = false
+ /** defined perimeter of this force dome on the floor;
+ * the walls created by this perimeter are angled inwards towards the facility, but that's not a consideration */
+ private var perimeter: List[(Vector3, Vector3)] = List()
+
+ override def Position: Vector3 = Owner.Position
+
+ override def Position_=(vec: Vector3): Vector3 = Owner.Position
+
+ override def Orientation: Vector3 = Owner.Orientation
+
+ override def Orientation_=(vec: Vector3): Vector3 = Owner.Orientation
+
+ def Energized: Boolean = energized
+
+ def Energized_=(state: Boolean): Boolean = {
+ energized = state
+ Energized
+ }
+
+ def Perimeter: List[(Vector3, Vector3)] = perimeter
+
+ def Perimeter_=(list: List[(Vector3, Vector3)]): List[(Vector3, Vector3)] = {
+ perimeter = list
+ Perimeter
+ }
+
+ def Definition: ForceDomeDefinition = cfddef
+}
+
+object ForceDomePhysics {
+ import akka.actor.ActorContext
+
+ /**
+ * Instantiate and configure a `CapitolForceDome` object.
+ * @param fddef specific type of force dome
+ * @param id the unique id that will be assigned to this entity
+ * @param context a context to allow the object to properly set up `ActorSystem` functionality
+ * @return the `CapitolForceDome` object
+ */
+ def Constructor(fddef: ForceDomeDefinition)(id: Int, context: ActorContext): ForceDomePhysics = {
+ import akka.actor.Props
+
+ val obj = new ForceDomePhysics(fddef)
+ obj.Actor = context.actorOf(Props(classOf[ForceDomeControl], obj), name = s"${fddef.Name}_$id")
+ obj
+ }
+}
diff --git a/src/main/scala/net/psforever/objects/serverobject/generator/GeneratorControl.scala b/src/main/scala/net/psforever/objects/serverobject/generator/GeneratorControl.scala
index 07e2b9d9f..a7b0c78d5 100644
--- a/src/main/scala/net/psforever/objects/serverobject/generator/GeneratorControl.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/generator/GeneratorControl.scala
@@ -122,7 +122,7 @@ class GeneratorControl(gen: Generator)
queuedExplosion = Default.Cancellable
imminentExplosion = false
//hate on everything nearby
- Zone.serverSideDamage(gen.Zone, gen, Zone.explosionDamage(gen.LastDamage), explosionFunc)
+ Zone.serverSideDamage(gen.Zone, gen, Zone.explosionDamage(gen.LastDamage), explosionFunc, Zone.findAllTargets)
case GeneratorControl.Restored() =>
gen.ClearHistory()
diff --git a/src/main/scala/net/psforever/objects/serverobject/hackable/GenericHackables.scala b/src/main/scala/net/psforever/objects/serverobject/hackable/GenericHackables.scala
index 5933beb0d..b52f4afd6 100644
--- a/src/main/scala/net/psforever/objects/serverobject/hackable/GenericHackables.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/hackable/GenericHackables.scala
@@ -2,38 +2,25 @@
package net.psforever.objects.serverobject.hackable
import net.psforever.actors.zone.BuildingActor
-import net.psforever.objects.serverobject.structures.{Building, StructureType, WarpGate}
+import net.psforever.objects.serverobject.dome.ForceDomeControl
+import net.psforever.objects.serverobject.structures.{Amenity, Building, StructureType, WarpGate}
import net.psforever.objects.serverobject.terminals.Terminal
import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal
import net.psforever.objects.{GlobalDefinitions, Player, Vehicle}
import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject}
+import net.psforever.objects.zones.blockmap.BlockMapEntity
import net.psforever.packet.game.{GenericObjectActionMessage, HackMessage, HackState, HackState1, HackState7, TriggeredSound}
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID}
import net.psforever.services.Service
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
+import scala.annotation.unused
import scala.util.{Failure, Success}
object GenericHackables {
private val log = org.log4s.getLogger("HackableBehavior")
- private var turretUpgradeTime: Long = System.currentTimeMillis()
- private var turretUpgradeTimeSet: Boolean = false
- def updateTurretUpgradeTime(): Long = {
- turretUpgradeTime = System.currentTimeMillis()
- turretUpgradeTimeSet = true
- turretUpgradeTime
- }
-
- // Used for checking the time without updating it
- def getTurretUpgradeTime: Long = {
- if (!turretUpgradeTimeSet) {
- turretUpgradeTime = System.currentTimeMillis()
- turretUpgradeTimeSet = true
- }
- turretUpgradeTime
- }
/**
* na
*
@@ -63,6 +50,8 @@ object GenericHackables {
}
}
+ private def DontStopHackAttempt(@unused target: PlanetSideServerObject, @unused hacker: Player): Boolean = false
+
/**
* Evaluate the progress of the user applying a tool to modify some server object.
* This action is using the remote electronics kit to convert an enemy unit into an allied unit, primarily.
@@ -76,10 +65,17 @@ object GenericHackables {
* @param target the object being affected
* @param tool_guid the tool being used to affest the object
* @param progress the current progress value
+ * @param additionalCancellationTests context-specific tests for hack continuation
* @return `true`, if the next cycle of progress should occur;
* `false`, otherwise
*/
- def HackingTickAction(progressType: HackState1, hacker: Player, target: PlanetSideServerObject, tool_guid: PlanetSideGUID)(
+ def HackingTickAction(
+ progressType: HackState1,
+ hacker: Player,
+ target: PlanetSideServerObject,
+ tool_guid: PlanetSideGUID,
+ additionalCancellationTests: (PlanetSideServerObject, Player) => Boolean
+ )(
progress: Float
): Boolean = {
//hack state for progress bar visibility
@@ -87,9 +83,7 @@ object GenericHackables {
(HackState.Start, 0)
} else if (progress >= 100L) {
(HackState.Finished, 100)
- } else if (target.isMoving(test = 1f) || target.Destroyed || !target.HasGUID) {
- (HackState.Cancelled, 0)
- } else if (target.isInstanceOf[CaptureTerminal] && EndHackProgress(target, hacker)) {
+ } else if (target.isMoving(test = 1f) || target.Destroyed || !target.HasGUID || additionalCancellationTests(target, hacker)) {
(HackState.Cancelled, 0)
} else {
(HackState.Ongoing, progress.toInt)
@@ -103,6 +97,55 @@ object GenericHackables {
)
progressState != HackState.Cancelled
}
+ /**
+ * Evaluate the progress of the user applying a tool to modify some server object.
+ * This action is using the remote electronics kit to convert an enemy unit into an allied unit, primarily.
+ * The act of transforming allied units of one kind into allied units of another kind (facility turret upgrades)
+ * is also governed by this action per tick of progress.
+ * @param progressType 1 - remote electronics kit hack (various ...);
+ * 2 - nano dispenser (upgrade canister) turret upgrade
+ * @param hacker the player performing the action
+ * @param target the object being affected
+ * @param tool_guid the tool being used to affest the object
+ * @param progress the current progress value
+ * @return `true`, if the next cycle of progress should occur;
+ * `false`, otherwise
+ */
+ def HackingTickAction(
+ progressType: HackState1,
+ hacker: Player,
+ target: PlanetSideServerObject,
+ tool_guid: PlanetSideGUID
+ )(
+ progress: Float
+ ): Boolean = {
+ HackingTickAction(progressType, hacker, target, tool_guid, DontStopHackAttempt)(progress)
+ }
+
+ /**
+ * The force dome prevents hacking if its protection has been declared over a capitol.
+ * Under normal circumstances, the dome will be visible in the sky at his point,
+ * blocking enemy encounter within its boundaries,
+ * so anything that can be hacked is on that boundary perimeter,
+ * or an alternate method of entry (Router) has been compromised.
+ * @see `ForceDomeControl.TargetUnderForceDome`
+ * @see `Sector`
+ * @param target the `Hackable` object that has been hacked
+ * @param hacker the player performing the action
+ * @return `true`, if the target is within boundary of a working force dome and thus protected;
+ * `false`, otherwise
+ */
+ def ForceDomeProtectsFromHacking(target: PlanetSideServerObject, hacker: Player): Boolean = {
+ //explicitly allow friendly hacking which is typically clearing a hack
+ target.Faction != hacker.Faction &&
+ (target match {
+ case obj: Amenity => obj.Owner.asInstanceOf[Building].ForceDome.toList
+ case obj: BlockMapEntity => target.Zone.blockMap.sector(obj).buildingList.flatMap(_.ForceDome)
+ case _ => List()
+ })
+ .filter(_.Perimeter.nonEmpty)
+ .exists(dome => ForceDomeControl.TargetUnderForceDome(dome.Perimeter)(dome, target, maxDistance = 0f))
+ }
/**
* The process of hacking an object is completed.
diff --git a/src/main/scala/net/psforever/objects/serverobject/mount/Mountable.scala b/src/main/scala/net/psforever/objects/serverobject/mount/Mountable.scala
index c797371ac..16167151d 100644
--- a/src/main/scala/net/psforever/objects/serverobject/mount/Mountable.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/mount/Mountable.scala
@@ -34,6 +34,17 @@ trait Mountable {
}
}
+ /**
+ * All the seats that have occupants by their seat number.
+ * @return list of the numbers of all occupied seats
+ */
+ def OccupiedSeats(): List[Int] = {
+ seats
+ .collect { case (index, seat) if seat.isOccupied => index }
+ .toList
+ .sorted
+ }
+
/**
* Retrieve a mapping of each mount from its mount point index.
* @return the mapping of mount point to mount
diff --git a/src/main/scala/net/psforever/objects/serverobject/mount/MountableBehavior.scala b/src/main/scala/net/psforever/objects/serverobject/mount/MountableBehavior.scala
index aca66da06..2bef37bf1 100644
--- a/src/main/scala/net/psforever/objects/serverobject/mount/MountableBehavior.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/mount/MountableBehavior.scala
@@ -9,6 +9,7 @@ import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.hackable.Hackable
import net.psforever.types.BailType
+import scala.annotation.unused
import scala.collection.mutable
trait MountableBehavior {
@@ -45,13 +46,17 @@ trait MountableBehavior {
case Some(seatNum) if mountTest(obj, seatNum, user) && tryMount(obj, seatNum, user) =>
user.VehicleSeated = obj.GUID
usedMountPoint.put(user.Name, mount_point)
- obj.Zone.actor ! ZoneActor.RemoveFromBlockMap(user)
+ mountActionResponse(user, mount_point, seatNum)
sender() ! Mountable.MountMessages(user, Mountable.CanMount(obj, seatNum, mount_point))
case _ =>
sender() ! Mountable.MountMessages(user, Mountable.CanNotMount(obj, mount_point))
}
}
+ def mountActionResponse(user: Player, @unused mountPoint: Int, @unused seatIndex: Int): Unit = {
+ MountableObject.Zone.actor ! ZoneActor.RemoveFromBlockMap(user)
+ }
+
protected def mountTest(
obj: PlanetSideServerObject with Mountable,
seatNumber: Int,
@@ -87,7 +92,7 @@ trait MountableBehavior {
val obj = MountableObject
if (dismountTest(obj, seat_number, user) && tryDismount(obj, seat_number, user, bail_type)) {
user.VehicleSeated = None
- obj.Zone.actor ! ZoneActor.AddToBlockMap(user, obj.Position)
+ dismountActionResponse(user, seat_number)
sender() ! Mountable.MountMessages(
user,
Mountable.CanDismount(obj, seat_number, getUsedMountPoint(user.Name, seat_number))
@@ -98,6 +103,10 @@ trait MountableBehavior {
}
}
+ def dismountActionResponse(user: Player, @unused seatIndex: Int): Unit = {
+ MountableObject.Zone.actor ! ZoneActor.AddToBlockMap(user, MountableObject.Position)
+ }
+
protected def dismountTest(
obj: Mountable with WorldEntity,
seatNumber: Int,
diff --git a/src/main/scala/net/psforever/objects/serverobject/mount/interaction/InteractWithForceDomeProtectionSeatedInEntity.scala b/src/main/scala/net/psforever/objects/serverobject/mount/interaction/InteractWithForceDomeProtectionSeatedInEntity.scala
new file mode 100644
index 000000000..272c31c43
--- /dev/null
+++ b/src/main/scala/net/psforever/objects/serverobject/mount/interaction/InteractWithForceDomeProtectionSeatedInEntity.scala
@@ -0,0 +1,37 @@
+// Copyright (c) 2025 PSForever
+package net.psforever.objects.serverobject.mount.interaction
+
+import net.psforever.objects.avatar.interaction.InteractWithForceDomeProtection
+import net.psforever.objects.serverobject.damage.Damageable
+import net.psforever.objects.serverobject.dome.ForceDomePhysics
+import net.psforever.objects.serverobject.mount.Mountable
+import net.psforever.objects.zones.interaction.InteractsWithZone
+
+class InteractWithForceDomeProtectionSeatedInEntity
+extends InteractWithForceDomeProtection {
+ override def range: Float = 30f
+
+ override def applyProtection(target: InteractsWithZone, dome: ForceDomePhysics): Unit = {
+ super.applyProtection(target, dome)
+ target
+ .asInstanceOf[Mountable]
+ .Seats
+ .values
+ .flatMap(_.occupants)
+ .foreach { occupant =>
+ occupant.Actor ! Damageable.MakeInvulnerable
+ }
+ }
+
+ override def resetInteraction(target: InteractsWithZone): Unit = {
+ super.resetInteraction(target)
+ target
+ .asInstanceOf[Mountable]
+ .Seats
+ .values
+ .flatMap(_.occupants)
+ .foreach { occupant =>
+ occupant.Actor ! Damageable.MakeVulnerable
+ }
+ }
+}
diff --git a/src/main/scala/net/psforever/objects/serverobject/mount/InteractWithRadiationCloudsSeatedInEntity.scala b/src/main/scala/net/psforever/objects/serverobject/mount/interaction/InteractWithRadiationCloudsSeatedInEntity.scala
similarity index 94%
rename from src/main/scala/net/psforever/objects/serverobject/mount/InteractWithRadiationCloudsSeatedInEntity.scala
rename to src/main/scala/net/psforever/objects/serverobject/mount/interaction/InteractWithRadiationCloudsSeatedInEntity.scala
index 6e404519c..29a0b2f97 100644
--- a/src/main/scala/net/psforever/objects/serverobject/mount/InteractWithRadiationCloudsSeatedInEntity.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/mount/interaction/InteractWithRadiationCloudsSeatedInEntity.scala
@@ -1,7 +1,8 @@
// Copyright (c) 2024 PSForever
-package net.psforever.objects.serverobject.mount
+package net.psforever.objects.serverobject.mount.interaction
import net.psforever.objects.ballistics.{Projectile, ProjectileQuality}
+import net.psforever.objects.serverobject.mount.Mountable
import net.psforever.objects.sourcing.SourceEntry
import net.psforever.objects.vital.Vitality
import net.psforever.objects.vital.base.DamageResolution
diff --git a/src/main/scala/net/psforever/objects/serverobject/mount/RadiationInMountableInteraction.scala b/src/main/scala/net/psforever/objects/serverobject/mount/interaction/RadiationInMountableInteraction.scala
similarity index 69%
rename from src/main/scala/net/psforever/objects/serverobject/mount/RadiationInMountableInteraction.scala
rename to src/main/scala/net/psforever/objects/serverobject/mount/interaction/RadiationInMountableInteraction.scala
index 5d6f32ca1..98c6b0336 100644
--- a/src/main/scala/net/psforever/objects/serverobject/mount/RadiationInMountableInteraction.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/mount/interaction/RadiationInMountableInteraction.scala
@@ -1,4 +1,4 @@
-package net.psforever.objects.serverobject.mount
+package net.psforever.objects.serverobject.mount.interaction
import net.psforever.objects.zones.interaction.ZoneInteractionType
diff --git a/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlRailJack.scala b/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlRailJack.scala
index 0f87181bc..c7740c7da 100644
--- a/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlRailJack.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/pad/process/VehicleSpawnControlRailJack.scala
@@ -37,7 +37,8 @@ class VehicleSpawnControlRailJack(pad: VehicleSpawnPad) extends VehicleSpawnCont
pad.Zone,
pad,
VehicleSpawnControlRailJack.prepareSpawnExplosion(pad, SourceEntry(driver), SourceEntry(vehicle)),
- pad.Definition.killBox(pad, vehicle.Definition.CanFly)
+ pad.Definition.killBox(pad, vehicle.Definition.CanFly),
+ Zone.findAllTargets
)
pad.Zone.VehicleEvents ! VehicleSpawnPad.AttachToRails(vehicle, pad)
context.system.scheduler.scheduleOnce(10 milliseconds, seatDriver, order)
diff --git a/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloControl.scala b/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloControl.scala
index ab4d3499c..996b4d876 100644
--- a/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloControl.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/resourcesilo/ResourceSiloControl.scala
@@ -18,6 +18,10 @@ import net.psforever.util.Config
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
+object ResourceSiloControl {
+ final case class DrainMultiplier(multiplier: Float)
+}
+
/**
* An `Actor` that handles messages being dispatched to a specific `ResourceSilo` entity.
*
@@ -30,7 +34,9 @@ class ResourceSiloControl(resourceSilo: ResourceSilo)
def FactionObject: FactionAffinity = resourceSilo
private[this] val log = org.log4s.getLogger
- var panelAnimationFunc: (ActorRef, Float) => Unit = PanelAnimation
+ private var panelAnimationFunc: (ActorRef, Float) => Unit = PanelAnimation
+ /** the higher the multiplier, the greater the drain */
+ private var drainMultiplier: Float = 1.0f
def receive: Receive = {
case Service.Startup() =>
@@ -53,6 +59,9 @@ class ResourceSiloControl(resourceSilo: ResourceSilo)
checkBehavior
.orElse(storageBehavior)
.orElse {
+ case ResourceSiloControl.DrainMultiplier(multiplier) =>
+ drainMultiplier = multiplier
+
case CommonMessages.Use(_, Some(vehicle: Vehicle))
if GlobalDefinitions.isBattleFrameVehicle(vehicle.Definition) =>
val siloFaction = resourceSilo.Faction
@@ -171,7 +180,7 @@ class ResourceSiloControl(resourceSilo: ResourceSilo)
*/
def HandleNtuRequest(sender: ActorRef, min: Float, max: Float): Unit = {
val originalAmount = resourceSilo.NtuCapacitor
- UpdateChargeLevel(-min)
+ UpdateChargeLevel(-min * drainMultiplier)
sender ! Ntu.Grant(resourceSilo, originalAmount - resourceSilo.NtuCapacitor)
}
diff --git a/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala b/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala
index a7863ab56..e1ef6f2c7 100644
--- a/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/structures/Building.scala
@@ -15,6 +15,7 @@ import net.psforever.packet.game.{Additional3, BuildingInfoUpdateMessage, Densit
import net.psforever.types._
import scalax.collection.{Graph, GraphEdge}
import akka.actor.typed.scaladsl.adapter._
+import net.psforever.objects.serverobject.dome.ForceDomePhysics
import net.psforever.objects.serverobject.llu.{CaptureFlag, CaptureFlagSocket}
import net.psforever.objects.serverobject.structures.participation.{MajorFacilityHackParticipation, NoParticipation, ParticipationLogic, TowerHackParticipation}
import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal
@@ -32,7 +33,6 @@ class Building(
private var faction: PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL
private var playersInSOI: List[Player] = List.empty
- private var forceDomeActive: Boolean = false
private var participationFunc: ParticipationLogic = NoParticipation
var virusId: Long = 8 // 8 default = no virus
var virusInstalledBy: Option[Int] = None // faction id
@@ -59,11 +59,6 @@ class Building(
case None => false
}
}
- def ForceDomeActive: Boolean = forceDomeActive
- def ForceDomeActive_=(activated: Boolean): Boolean = {
- forceDomeActive = activated
- forceDomeActive
- }
def Faction: PlanetSideEmpire.Value = faction
@@ -108,6 +103,13 @@ class Building(
}
}
+ def ForceDome: Option[ForceDomePhysics] = {
+ Amenities.find(_.isInstanceOf[ForceDomePhysics]) match {
+ case Some(out: ForceDomePhysics) => Some(out)
+ case _ => None
+ }
+ }
+
def NtuSource: Option[NtuContainer] = {
Amenities.find(_.isInstanceOf[NtuContainer]) match {
case Some(o: NtuContainer) => Some(o)
@@ -223,6 +225,7 @@ class Building(
else {
(virusId.toInt, Some(Additional3(inform_defenders=true, virusInstalledBy.getOrElse(3))))
}
+ val forceDomeActive = ForceDome.exists(_.Energized)
BuildingInfoUpdateMessage(
Zone.Number,
diff --git a/src/main/scala/net/psforever/objects/serverobject/terminals/capture/CaptureTerminalControl.scala b/src/main/scala/net/psforever/objects/serverobject/terminals/capture/CaptureTerminalControl.scala
index f5dc91a72..fef28d51f 100644
--- a/src/main/scala/net/psforever/objects/serverobject/terminals/capture/CaptureTerminalControl.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/terminals/capture/CaptureTerminalControl.scala
@@ -29,7 +29,7 @@ class CaptureTerminalControl(terminal: CaptureTerminal)
sender() ! CommonMessages.Progress(
GenericHackables.GetHackSpeed(player, terminal),
CaptureTerminals.FinishHackingCaptureConsole(terminal, player, unk = -1),
- GenericHackables.HackingTickAction(HackState1.Unk1, player, terminal, item.GUID)
+ GenericHackables.HackingTickAction(HackState1.Unk1, player, terminal, item.GUID, CaptureTerminals.EndHackProgress)
)
}
diff --git a/src/main/scala/net/psforever/objects/serverobject/terminals/capture/CaptureTerminals.scala b/src/main/scala/net/psforever/objects/serverobject/terminals/capture/CaptureTerminals.scala
index 399d45d84..9f07bce14 100644
--- a/src/main/scala/net/psforever/objects/serverobject/terminals/capture/CaptureTerminals.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/terminals/capture/CaptureTerminals.scala
@@ -1,13 +1,18 @@
+// Copyright (c) 2021 PSForever
package net.psforever.objects.serverobject.terminals.capture
import net.psforever.objects.Player
-import net.psforever.objects.serverobject.CommonMessages
+import net.psforever.objects.serverobject.hackable.GenericHackables
+import net.psforever.objects.serverobject.structures.{Building, StructureType, WarpGate}
+import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject}
import net.psforever.objects.sourcing.PlayerSource
import net.psforever.services.local.{LocalAction, LocalServiceMessage}
+import net.psforever.types.PlanetSideEmpire
+import scala.concurrent.duration._
import scala.util.{Failure, Success}
-object CaptureTerminals {import scala.concurrent.duration._
+object CaptureTerminals {
private val log = org.log4s.getLogger("CaptureTerminals")
/**
@@ -55,4 +60,47 @@ object CaptureTerminals {import scala.concurrent.duration._
log.warn(s"Hack message failed on target guid: ${target.GUID}")
}
}
+
+ /**
+ * Check if the state of connected facilities has changed since the hack progress began. It accounts for a friendly facility
+ * on the other side of a warpgate as well in case there are no friendly facilities in the same zone
+ * @param target the `Hackable` object that has been hacked
+ * @param hacker the player performing the action
+ * @return `true`, if the hack should be ended; `false`, otherwise
+ */
+ def EndHackProgress(target: PlanetSideServerObject, hacker: Player): Boolean = {
+ val building = target.asInstanceOf[CaptureTerminal].Owner.asInstanceOf[Building]
+ val hackerFaction = hacker.Faction
+ if (GenericHackables.ForceDomeProtectsFromHacking(target, hacker)) {
+ true
+ } else if (building.Faction == PlanetSideEmpire.NEUTRAL ||
+ building.BuildingType == StructureType.Tower ||
+ building.Faction == hackerFaction) {
+ false
+ } else {
+ val stopHackingCount = building.Neighbours match {
+ case Some(neighbors) =>
+ neighbors.count {
+ case wg: WarpGate if wg.Faction == hackerFaction =>
+ true
+ case wg: WarpGate =>
+ val friendlyBaseOpt = for {
+ otherWg <- wg.Neighbours.flatMap(_.find(_.isInstanceOf[WarpGate]))
+ friendly <- otherWg.Neighbours.flatMap(_.collectFirst { case b: Building if !b.isInstanceOf[WarpGate] => b })
+ } yield friendly
+ friendlyBaseOpt.exists { fb =>
+ fb.Faction == hackerFaction &&
+ !fb.CaptureTerminalIsHacked &&
+ fb.NtuLevel > 0
+ }
+ case b =>
+ b.Faction == hackerFaction &&
+ !b.CaptureTerminalIsHacked &&
+ b.NtuLevel > 0
+ }
+ case None => 0
+ }
+ stopHackingCount == 0
+ }
+ }
}
diff --git a/src/main/scala/net/psforever/objects/serverobject/terminals/implant/ImplantTerminalMech.scala b/src/main/scala/net/psforever/objects/serverobject/terminals/implant/ImplantTerminalMech.scala
index d63b8070a..ed0f38e8e 100644
--- a/src/main/scala/net/psforever/objects/serverobject/terminals/implant/ImplantTerminalMech.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/terminals/implant/ImplantTerminalMech.scala
@@ -2,7 +2,8 @@
package net.psforever.objects.serverobject.terminals.implant
import net.psforever.objects.serverobject.hackable.Hackable
-import net.psforever.objects.serverobject.mount.{InteractWithRadiationCloudsSeatedInEntity, Mountable, Seat}
+import net.psforever.objects.serverobject.mount.interaction.InteractWithRadiationCloudsSeatedInEntity
+import net.psforever.objects.serverobject.mount.{Mountable, Seat}
import net.psforever.objects.serverobject.structures.Amenity
import net.psforever.objects.serverobject.terminals.capture.CaptureTerminalAware
import net.psforever.objects.vital.resistance.StandardResistanceProfile
diff --git a/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurret.scala b/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurret.scala
index 28d975950..118fca04e 100644
--- a/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurret.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurret.scala
@@ -3,7 +3,7 @@ package net.psforever.objects.serverobject.turret
import net.psforever.objects.equipment.JammableUnit
import net.psforever.objects.serverobject.interior.Sidedness
-import net.psforever.objects.serverobject.mount.InteractWithRadiationCloudsSeatedInEntity
+import net.psforever.objects.serverobject.mount.interaction.InteractWithRadiationCloudsSeatedInEntity
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
@@ -23,6 +23,28 @@ class FacilityTurret(tDef: FacilityTurretDefinition)
WeaponTurret.LoadDefinition(turret = this)
WhichSide = Sidedness.OutsideOf
+ private var turretUpgradeTime: Long = System.currentTimeMillis()
+ private var turretUpgradeTimeSet: Boolean = false
+
+ def UpdateTurretUpgradeTime(): Long = {
+ turretUpgradeTime = System.currentTimeMillis()
+ turretUpgradeTimeSet = true
+ turretUpgradeTime
+ }
+
+ // Used for checking the time without updating it
+ def CheckTurretUpgradeTime: Long = {
+ if (!turretUpgradeTimeSet) {
+ turretUpgradeTime = System.currentTimeMillis()
+ turretUpgradeTimeSet = true
+ }
+ turretUpgradeTime
+ }
+
+ def FinishedTurretUpgradeReset(): Unit = {
+ turretUpgradeTimeSet = false
+ }
+
def TurretOwner: SourceEntry = {
Seats
.headOption
diff --git a/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala b/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala
index 12be65269..24f1bdea4 100644
--- a/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/turret/FacilityTurretControl.scala
@@ -5,7 +5,6 @@ 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.damage.Damageable
-import net.psforever.objects.serverobject.hackable.GenericHackables
import net.psforever.objects.serverobject.mount.Mountable
import net.psforever.objects.serverobject.repair.AmenityAutoRepair
import net.psforever.objects.serverobject.structures.{Building, PoweredAmenityControl}
@@ -100,7 +99,7 @@ class FacilityTurretControl(turret: FacilityTurret)
seatNumber: Int,
player: Player): Boolean = {
super.mountTest(obj, seatNumber, player) &&
- (!TurretObject.isUpgrading || System.currentTimeMillis() - GenericHackables.getTurretUpgradeTime >= 1500L)
+ (!TurretObject.isUpgrading || System.currentTimeMillis() - TurretObject.CheckTurretUpgradeTime >= 1500L)
}
override protected def tryMount(obj: PlanetSideServerObject with Mountable, seatNumber: Int, player: Player): Boolean = {
diff --git a/src/main/scala/net/psforever/objects/serverobject/turret/MountableTurretControl.scala b/src/main/scala/net/psforever/objects/serverobject/turret/MountableTurretControl.scala
index f347f6bc2..bc4f7bd16 100644
--- a/src/main/scala/net/psforever/objects/serverobject/turret/MountableTurretControl.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/turret/MountableTurretControl.scala
@@ -4,6 +4,10 @@ package net.psforever.objects.serverobject.turret
import net.psforever.objects.Player
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior}
+import net.psforever.objects.sourcing.{PlayerSource, TurretSource}
+import net.psforever.objects.vital.{DismountingActivity, MountingActivity}
+
+import scala.annotation.unused
trait MountableTurretControl
extends TurretControl
@@ -11,9 +15,22 @@ trait MountableTurretControl
override def TurretObject: PlanetSideServerObject with WeaponTurret with Mountable
/** commonBehavior does not implement mountingBehavior; please do so when implementing */
- override def commonBehavior: Receive =
- super.commonBehavior
- .orElse(dismountBehavior)
+ override def commonBehavior: Receive = super.commonBehavior.orElse(dismountBehavior)
+
+ override def mountActionResponse(user: Player, @unused mountPoint: Int, seatNumber: Int): Unit = {
+ super.mountActionResponse(user, mountPoint, seatNumber)
+ if (TurretObject.PassengerInSeat(user).contains(0)) {
+ val vsrc = TurretSource(TurretObject)
+ user.LogActivity(MountingActivity(vsrc, PlayerSource.inSeat(user, vsrc, seatNumber = 0), TurretObject.Zone.Number))
+ }
+ }
+
+ override def dismountActionResponse(user: Player, seatBeingDismounted: Int): Unit = {
+ super.dismountActionResponse(user, seatBeingDismounted)
+ if (!TurretObject.Seats(seatBeingDismounted).isOccupied) { //this seat really was vacated
+ user.LogActivity(DismountingActivity(TurretSource(TurretObject), PlayerSource(user), TurretObject.Zone.Number))
+ }
+ }
override protected def mountTest(
obj: PlanetSideServerObject with Mountable,
diff --git a/src/main/scala/net/psforever/objects/serverobject/turret/WeaponTurrets.scala b/src/main/scala/net/psforever/objects/serverobject/turret/WeaponTurrets.scala
index 9dd64c311..03b8925ae 100644
--- a/src/main/scala/net/psforever/objects/serverobject/turret/WeaponTurrets.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/turret/WeaponTurrets.scala
@@ -3,7 +3,6 @@ package net.psforever.objects.serverobject.turret
import net.psforever.objects.avatar.Certification
import net.psforever.objects.ce.Deployable
-import net.psforever.objects.serverobject.hackable.GenericHackables.updateTurretUpgradeTime
import net.psforever.objects.{Player, Tool, TurretDeployable}
import net.psforever.packet.game.{HackMessage, HackState, HackState1, HackState7, InventoryStateMessage}
import net.psforever.services.Service
@@ -83,7 +82,7 @@ object WeaponTurrets {
} else if (turret.Destroyed) {
(HackState.Cancelled, 0)
} else {
- updateTurretUpgradeTime()
+ turret.UpdateTurretUpgradeTime()
(HackState.Ongoing, progress.toInt)
}
turret.Zone.AvatarEvents ! AvatarServiceMessage(
diff --git a/src/main/scala/net/psforever/objects/serverobject/turret/auto/AffectedByAutomaticTurretFire.scala b/src/main/scala/net/psforever/objects/serverobject/turret/auto/AffectedByAutomaticTurretFire.scala
index 7485c3a47..d2b4ecab2 100644
--- a/src/main/scala/net/psforever/objects/serverobject/turret/auto/AffectedByAutomaticTurretFire.scala
+++ b/src/main/scala/net/psforever/objects/serverobject/turret/auto/AffectedByAutomaticTurretFire.scala
@@ -60,7 +60,7 @@ trait AffectedByAutomaticTurretFire extends Damageable {
ProjectileReason(DamageResolution.Hit, modProjectile, target.DamageModel),
correctedTargetPosition
)
- PerformDamage(target, resolvedProjectile.calculate())
+ PerformDamageIfVulnerable(target, resolvedProjectile.calculate())
}
}
}
diff --git a/src/main/scala/net/psforever/objects/sourcing/MountableEntry.scala b/src/main/scala/net/psforever/objects/sourcing/MountableEntry.scala
new file mode 100644
index 000000000..ae0c2f41e
--- /dev/null
+++ b/src/main/scala/net/psforever/objects/sourcing/MountableEntry.scala
@@ -0,0 +1,6 @@
+// Copyright (c) 2026 PSForever
+package net.psforever.objects.sourcing
+
+trait MountableEntry {
+ def occupants: List[SourceEntry]
+}
diff --git a/src/main/scala/net/psforever/objects/sourcing/TurretSource.scala b/src/main/scala/net/psforever/objects/sourcing/TurretSource.scala
index 1261cb281..0e2880e4f 100644
--- a/src/main/scala/net/psforever/objects/sourcing/TurretSource.scala
+++ b/src/main/scala/net/psforever/objects/sourcing/TurretSource.scala
@@ -18,7 +18,7 @@ final case class TurretSource(
Orientation: Vector3,
occupants: List[SourceEntry],
unique: SourceUniqueness
- ) extends SourceWithHealthEntry with SourceWithShieldsEntry {
+ ) extends SourceWithHealthEntry with SourceWithShieldsEntry with MountableEntry {
def Name: String = SourceEntry.NameFormat(Definition.Descriptor)
def Health: Int = health
def Shields: Int = shields
diff --git a/src/main/scala/net/psforever/objects/sourcing/VehicleSource.scala b/src/main/scala/net/psforever/objects/sourcing/VehicleSource.scala
index 25f60b710..91617bd0f 100644
--- a/src/main/scala/net/psforever/objects/sourcing/VehicleSource.scala
+++ b/src/main/scala/net/psforever/objects/sourcing/VehicleSource.scala
@@ -33,7 +33,7 @@ final case class VehicleSource(
occupants: List[SourceEntry],
Modifiers: ResistanceProfile,
unique: UniqueVehicle
- ) extends SourceWithHealthEntry with SourceWithShieldsEntry {
+ ) extends SourceWithHealthEntry with SourceWithShieldsEntry with MountableEntry {
def Name: String = SourceEntry.NameFormat(Definition.Name)
def Health: Int = health
def Shields: Int = shields
diff --git a/src/main/scala/net/psforever/objects/vehicles/CargoBehavior.scala b/src/main/scala/net/psforever/objects/vehicles/CargoBehavior.scala
index dec138367..d90baa787 100644
--- a/src/main/scala/net/psforever/objects/vehicles/CargoBehavior.scala
+++ b/src/main/scala/net/psforever/objects/vehicles/CargoBehavior.scala
@@ -19,12 +19,12 @@ trait CargoBehavior {
val zone = obj.Zone
zone.GUID(isMounting) match {
case Some(v : Vehicle) => v.Actor ! CargoBehavior.EndCargoMounting(obj.GUID)
- case _ => ;
+ case _ => ()
}
isMounting = None
zone.GUID(isDismounting) match {
case Some(v: Vehicle) => v.Actor ! CargoBehavior.EndCargoDismounting(obj.GUID)
- case _ => ;
+ case _ => ()
}
isDismounting = None
startCargoDismountingNoCleanup(bailed = false)
@@ -38,14 +38,10 @@ trait CargoBehavior {
startCargoDismounting(bailed)
case CargoBehavior.EndCargoMounting(carrier_guid) =>
- if (isMounting.contains(carrier_guid)) {
- isMounting = None
- }
+ endCargoMounting(carrier_guid)
case CargoBehavior.EndCargoDismounting(carrier_guid) =>
- if (isDismounting.contains(carrier_guid)) {
- isDismounting = None
- }
+ endCargoDismounting(carrier_guid)
}
def startCargoMounting(carrier_guid: PlanetSideGUID, mountPoint: Int): Unit = {
@@ -84,6 +80,18 @@ trait CargoBehavior {
}
.nonEmpty
}
+
+ def endCargoMounting(carrierGuid: PlanetSideGUID): Unit = {
+ if (isMounting.contains(carrierGuid)) {
+ isMounting = None
+ }
+ }
+
+ def endCargoDismounting(carrierGuid: PlanetSideGUID): Unit = {
+ if (isDismounting.contains(carrierGuid)) {
+ isDismounting = None
+ }
+ }
}
object CargoBehavior {
diff --git a/src/main/scala/net/psforever/objects/vehicles/control/BfrControl.scala b/src/main/scala/net/psforever/objects/vehicles/control/BfrControl.scala
index 9dbc3436d..bd1aeaa1d 100644
--- a/src/main/scala/net/psforever/objects/vehicles/control/BfrControl.scala
+++ b/src/main/scala/net/psforever/objects/vehicles/control/BfrControl.scala
@@ -20,6 +20,7 @@ import net.psforever.services.Service
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
import net.psforever.types._
+import scala.annotation.unused
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
@@ -170,8 +171,21 @@ class BfrControl(vehicle: Vehicle)
specialArmWeaponEquipManagement(item, slot, handiness)
}
- override def dismountCleanup(seatBeingDismounted: Int, player: Player): Unit = {
- super.dismountCleanup(seatBeingDismounted, player)
+ override def mountActionResponse(user: Player, @unused mountPoint: Int, seatNumber: Int): Unit = {
+ super.mountActionResponse(user, mountPoint, seatNumber)
+ if (vehicle.Seats.values.exists(_.isOccupied)) {
+ vehicle.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator) match {
+ case Some(subsys)
+ if !subsys.Enabled && vehicle.Shields > 0 && subsys.Enabled_=(state = true) =>
+ //if the shield is damaged, it does not turn on until the damaged is cleared
+ vehicleSubsystemMessages(subsys.changedMessages(vehicle))
+ case _ => ()
+ }
+ }
+ }
+
+ override def dismountActionResponse(user: Player, seatBeingDismounted: Int): Unit = {
+ super.dismountActionResponse(user, seatBeingDismounted)
if (!vehicle.Seats.values.exists(_.isOccupied)) {
vehicle
.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator) match {
@@ -196,19 +210,6 @@ class BfrControl(vehicle: Vehicle)
}
}
- override def mountCleanup(mount_point: Int, user: Player): Unit = {
- super.mountCleanup(mount_point, user)
- if (vehicle.Seats.values.exists(_.isOccupied)) {
- vehicle.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator) match {
- case Some(subsys)
- if !subsys.Enabled && vehicle.Shields > 0 && subsys.Enabled_=(state = true) =>
- //if the shield is damaged, it does not turn on until the damaged is cleared
- vehicleSubsystemMessages(subsys.changedMessages(vehicle))
- case _ => ()
- }
- }
- }
-
override def permitTerminalMessage(player: Player, msg: ItemTransactionMessage): Boolean = {
if (msg.transaction_type == TransactionType.Loadout) {
!vehicle.Jammed
@@ -455,7 +456,7 @@ class BfrControl(vehicle: Vehicle)
}
}
- def specialArmWeaponEquipManagement(item: Equipment, slot: Int, handiness: equipment.Hand): Unit = {
+ def specialArmWeaponEquipManagement(item: Equipment, slot: Int, @unused handiness: equipment.Hand): Unit = {
if (item.Size == EquipmentSize.BFRArmWeapon && vehicle.VisibleSlots.contains(slot)) {
val weapons = vehicle.Weapons
//budget logic: the arm weapons are "next to each other" index-wise
diff --git a/src/main/scala/net/psforever/objects/vehicles/control/CargoCarrierControl.scala b/src/main/scala/net/psforever/objects/vehicles/control/CargoCarrierControl.scala
index a8028d017..5892d974f 100644
--- a/src/main/scala/net/psforever/objects/vehicles/control/CargoCarrierControl.scala
+++ b/src/main/scala/net/psforever/objects/vehicles/control/CargoCarrierControl.scala
@@ -14,7 +14,16 @@ import net.psforever.objects.vital.interaction.DamageResult
class CargoCarrierControl(vehicle: Vehicle)
extends VehicleControl(vehicle)
with CarrierBehavior {
- def CarrierObject = vehicle
+ def CarrierObject: Vehicle = vehicle
+
+ override def TestToStartSelfReporting(): Boolean = {
+ super.TestToStartSelfReporting() &&
+ !CarrierObject
+ .CargoHolds
+ .values
+ .flatMap(_.occupants)
+ .exists(_.Seats.values.exists(_.isOccupied))
+ }
override def postStop() : Unit = {
super.postStop()
diff --git a/src/main/scala/net/psforever/objects/vehicles/control/DeployingVehicleControl.scala b/src/main/scala/net/psforever/objects/vehicles/control/DeployingVehicleControl.scala
index 85d567390..7f913cff4 100644
--- a/src/main/scala/net/psforever/objects/vehicles/control/DeployingVehicleControl.scala
+++ b/src/main/scala/net/psforever/objects/vehicles/control/DeployingVehicleControl.scala
@@ -5,7 +5,6 @@ import akka.actor.ActorRef
import net.psforever.objects._
import net.psforever.objects.serverobject.deploy.Deployment.DeploymentObject
import net.psforever.objects.serverobject.deploy.{Deployment, DeploymentBehavior}
-import net.psforever.objects.serverobject.mount.Mountable
import net.psforever.types._
/**
@@ -36,13 +35,10 @@ class DeployingVehicleControl(vehicle: Vehicle)
*/
override def commonDisabledBehavior : Receive =
super.commonDisabledBehavior
+ .orElse(dismountBehavior)
.orElse {
- case msg : Deployment.TryUndeploy =>
+ case msg: Deployment.TryUndeploy =>
deployBehavior.apply(msg)
-
- case msg @ Mountable.TryDismount(player, seat_num, _) =>
- dismountBehavior.apply(msg)
- dismountCleanup(seat_num, player)
}
/**
diff --git a/src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala b/src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala
index 4fd8a9ce1..d110fdc98 100644
--- a/src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala
+++ b/src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala
@@ -20,7 +20,7 @@ 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, RadiationInMountableInteraction}
+import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior}
import net.psforever.objects.serverobject.repair.RepairableVehicle
import net.psforever.objects.serverobject.structures.WarpGate
import net.psforever.objects.serverobject.terminals.Terminal
@@ -29,8 +29,9 @@ import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource}
import net.psforever.objects.vehicles._
import net.psforever.objects.vehicles.interaction.WithWater
import net.psforever.objects.vital.interaction.DamageResult
-import net.psforever.objects.vital.{DamagingActivity, InGameActivity, ShieldCharge, SpawningActivity, VehicleDismountActivity, VehicleMountActivity}
+import net.psforever.objects.vital.{DamagingActivity, DismountingActivity, InGameActivity, MountingActivity, ShieldCharge, SpawningActivity}
import net.psforever.objects.zones._
+import net.psforever.objects.zones.interaction.IndependentZoneInteraction
import net.psforever.packet.PlanetSideGamePacket
import net.psforever.packet.game._
import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent
@@ -39,6 +40,7 @@ import net.psforever.services.Service
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
+import scala.annotation.unused
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
import scala.util.Random
@@ -63,7 +65,8 @@ class VehicleControl(vehicle: Vehicle)
with AggravatedBehavior
with RespondsToZoneEnvironment
with CargoBehavior
- with AffectedByAutomaticTurretFire {
+ with AffectedByAutomaticTurretFire
+ with IndependentZoneInteraction {
//make control actors belonging to utilities when making control actor belonging to vehicle
vehicle.Utilities.foreach { case (_, util) => util.Setup }
@@ -77,6 +80,7 @@ class VehicleControl(vehicle: Vehicle)
def InteractiveObject: Vehicle = vehicle
def CargoObject: Vehicle = vehicle
def AffectedObject: Vehicle = vehicle
+ def ZoneInteractionObject: Vehicle = vehicle
/** cheap flag for whether the vehicle is decaying */
var decaying : Boolean = false
@@ -84,8 +88,6 @@ class VehicleControl(vehicle: Vehicle)
var decayTimer : Cancellable = Default.Cancellable
/** becoming waterlogged, or drying out? */
var submergedCondition : Option[OxygenState] = None
- /** ... */
- var passengerRadiationCloudTimer: Cancellable = Default.Cancellable
def receive : Receive = Enabled
@@ -94,7 +96,7 @@ class VehicleControl(vehicle: Vehicle)
damageableVehiclePostStop()
decaying = false
decayTimer.cancel()
- passengerRadiationCloudTimer.cancel()
+ StopInteractionSelfReporting()
vehicle.Utilities.values.foreach { util =>
context.stop(util().Actor)
util().Actor = Default.Actor
@@ -103,7 +105,80 @@ class VehicleControl(vehicle: Vehicle)
endAllCargoOperations()
}
+ private val mountingFailureReasons: Receive = {
+ case Mountable.TryMount(user, mountPoint)
+ if vehicle.DeploymentState == DriveState.AutoPilot =>
+ sender() ! Mountable.MountMessages(user, Mountable.CanNotMount(vehicle, mountPoint))
+
+ case Mountable.TryMount(user, mountPoint)
+ if vehicle.Zone.blockMap.sector(vehicle).buildingList.exists {
+ case wg: WarpGate =>
+ Vector3.DistanceSquared(vehicle.Position, wg.Position) < math.pow(wg.Definition.SOIRadius, 2)
+ case _ => false
+ } && user.Carrying.contains(SpecialCarry.CaptureFlag) =>
+ sender() ! Mountable.MountMessages(user, Mountable.CanNotMount(vehicle, mountPoint))
+ }
+
+ private val dismountingFailureReasons: Receive = {
+ case Mountable.TryDismount(user, seat_num, bailType)
+ if vehicle.DeploymentState == DriveState.AutoPilot =>
+ sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num, bailType))
+
+ // Issue 1133. Todo: There may be a better way to address the issue?
+ case Mountable.TryDismount(user, seat_num, bailType) if GlobalDefinitions.isFlightVehicle(vehicle.Definition) &&
+ (vehicle.History.find { entry => entry.isInstanceOf[SpawningActivity] } match {
+ case Some(entry) if System.currentTimeMillis() - entry.time < 3000L => true
+ case _ => false
+ }) =>
+ sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num, bailType))
+
+ case Mountable.TryDismount(user, seat_num, bailType) if !GlobalDefinitions.isFlightVehicle(vehicle.Definition) &&
+ (vehicle.History.find { entry => entry.isInstanceOf[SpawningActivity] } match {
+ case Some(entry) if System.currentTimeMillis() - entry.time < 8500L => true
+ case _ => false
+ }) =>
+ sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num, bailType))
+
+ case Mountable.TryDismount(user, seat_num, bailType)
+ if vehicle.Health <= (vehicle.Definition.MaxHealth * .1).round && bailType == BailType.Bailed
+ && GlobalDefinitions.isFlightVehicle(vehicle.Definition)
+ && (seat_num == 0 || vehicle.SeatPermissionGroup(seat_num).contains(AccessPermissionGroup.Gunner))
+ && (vehicle.History.findLast { entry => entry.isInstanceOf[DamagingActivity] } match {
+ case Some(entry) if System.currentTimeMillis() - entry.time < 4000L => true
+ case _ if Random.nextInt(10) == 1 => false
+ case _ => true }) =>
+ sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num, bailType))
+
+ case Mountable.TryDismount(user, seat_num, bailType)
+ if vehicle.Health <= (vehicle.Definition.MaxHealth * .2).round && bailType == BailType.Bailed
+ && GlobalDefinitions.isFlightVehicle(vehicle.Definition)
+ && (seat_num == 0 || vehicle.SeatPermissionGroup(seat_num).contains(AccessPermissionGroup.Gunner))
+ && (vehicle.History.findLast { entry => entry.isInstanceOf[DamagingActivity] } match {
+ case Some(entry) if System.currentTimeMillis() - entry.time < 3500L => true
+ case _ if Random.nextInt(5) == 1 => false
+ case _ => true }) =>
+ sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num, bailType))
+
+ case Mountable.TryDismount(user, seat_num, bailType)
+ if vehicle.Health <= (vehicle.Definition.MaxHealth * .35).round && bailType == BailType.Bailed
+ && GlobalDefinitions.isFlightVehicle(vehicle.Definition)
+ && (seat_num == 0 || vehicle.SeatPermissionGroup(seat_num).contains(AccessPermissionGroup.Gunner))
+ && (vehicle.History.findLast { entry => entry.isInstanceOf[DamagingActivity] } match {
+ case Some(entry) if System.currentTimeMillis() - entry.time < 3000L => true
+ case _ if Random.nextInt(4) == 1 => false
+ case _ => true }) =>
+ sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num, bailType))
+
+ case Mountable.TryDismount(user, seat_num, bailType)
+ if vehicle.isMoving(test = 1f) && bailType == BailType.Normal =>
+ sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num, bailType))
+ }
+
def commonEnabledBehavior: Receive = checkBehavior
+ .orElse(mountingFailureReasons)
+ .orElse(mountBehavior)
+ .orElse(dismountingFailureReasons)
+ .orElse(dismountBehavior)
.orElse(attributeBehavior)
.orElse(jammableBehavior)
.orElse(takesDamage)
@@ -113,6 +188,7 @@ class VehicleControl(vehicle: Vehicle)
.orElse(environmentBehavior)
.orElse(cargoBehavior)
.orElse(takeAutomatedDamage)
+ .orElse(zoneInteractionBehavior)
.orElse {
case Vehicle.Ownership(None) =>
LoseOwnership()
@@ -120,79 +196,6 @@ class VehicleControl(vehicle: Vehicle)
case Vehicle.Ownership(Some(player)) =>
GainOwnership(player)
- case Mountable.TryMount(user, mountPoint)
- if vehicle.DeploymentState == DriveState.AutoPilot =>
- sender() ! Mountable.MountMessages(user, Mountable.CanNotMount(vehicle, mountPoint))
-
- case Mountable.TryMount(user, mountPoint)
- if vehicle.Zone.blockMap.sector(vehicle).buildingList.exists {
- case wg: WarpGate =>
- Vector3.DistanceSquared(vehicle.Position, wg.Position) < math.pow(wg.Definition.SOIRadius, 2)
- case _ => false
- } && user.Carrying.contains(SpecialCarry.CaptureFlag) =>
- sender() ! Mountable.MountMessages(user, Mountable.CanNotMount(vehicle, mountPoint))
-
- case msg @ Mountable.TryMount(player, mount_point) =>
- mountBehavior.apply(msg)
- mountCleanup(mount_point, player)
-
- // Issue 1133. Todo: There may be a better way to address the issue?
- case Mountable.TryDismount(user, seat_num, bailType) if GlobalDefinitions.isFlightVehicle(vehicle.Definition) &&
- (vehicle.History.find { entry => entry.isInstanceOf[SpawningActivity] } match {
- case Some(entry) if System.currentTimeMillis() - entry.time < 3000L => true
- case _ => false
- }) =>
- sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num, bailType))
-
- case Mountable.TryDismount(user, seat_num, bailType) if !GlobalDefinitions.isFlightVehicle(vehicle.Definition) &&
- (vehicle.History.find { entry => entry.isInstanceOf[SpawningActivity] } match {
- case Some(entry) if System.currentTimeMillis() - entry.time < 8500L => true
- case _ => false
- }) =>
- sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num, bailType))
-
- case Mountable.TryDismount(user, seat_num, bailType)
- if vehicle.Health <= (vehicle.Definition.MaxHealth * .1).round && bailType == BailType.Bailed
- && GlobalDefinitions.isFlightVehicle(vehicle.Definition)
- && (seat_num == 0 || vehicle.SeatPermissionGroup(seat_num).getOrElse(0) == AccessPermissionGroup.Gunner)
- && (vehicle.History.findLast { entry => entry.isInstanceOf[DamagingActivity] } match {
- case Some(entry) if System.currentTimeMillis() - entry.time < 4000L => true
- case _ if Random.nextInt(10) == 1 => false
- case _ => true }) =>
- sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num, bailType))
-
- case Mountable.TryDismount(user, seat_num, bailType)
- if vehicle.Health <= (vehicle.Definition.MaxHealth * .2).round && bailType == BailType.Bailed
- && GlobalDefinitions.isFlightVehicle(vehicle.Definition)
- && (seat_num == 0 || vehicle.SeatPermissionGroup(seat_num).getOrElse(0) == AccessPermissionGroup.Gunner)
- && (vehicle.History.findLast { entry => entry.isInstanceOf[DamagingActivity] } match {
- case Some(entry) if System.currentTimeMillis() - entry.time < 3500L => true
- case _ if Random.nextInt(5) == 1 => false
- case _ => true }) =>
- sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num, bailType))
-
- case Mountable.TryDismount(user, seat_num, bailType)
- if vehicle.Health <= (vehicle.Definition.MaxHealth * .35).round && bailType == BailType.Bailed
- && GlobalDefinitions.isFlightVehicle(vehicle.Definition)
- && (seat_num == 0 || vehicle.SeatPermissionGroup(seat_num).getOrElse(0) == AccessPermissionGroup.Gunner)
- && (vehicle.History.findLast { entry => entry.isInstanceOf[DamagingActivity] } match {
- case Some(entry) if System.currentTimeMillis() - entry.time < 3000L => true
- case _ if Random.nextInt(4) == 1 => false
- case _ => true }) =>
- sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num, bailType))
-
- case Mountable.TryDismount(user, seat_num, bailType)
- if vehicle.DeploymentState == DriveState.AutoPilot =>
- sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num, bailType))
-
- case Mountable.TryDismount(user, seat_num, bailType)
- if vehicle.isMoving(test = 1f) && bailType == BailType.Normal =>
- sender() ! Mountable.MountMessages(user, Mountable.CanNotDismount(vehicle, seat_num, bailType))
-
- case msg @ Mountable.TryDismount(player, seat_num, _) =>
- dismountBehavior.apply(msg)
- dismountCleanup(seat_num, player)
-
case CommonMessages.ChargeShields(amount, motivator) =>
chargeShields(amount, motivator.collect { case o: PlanetSideGameObject with FactionAffinity => SourceEntry(o) })
@@ -213,7 +216,6 @@ class VehicleControl(vehicle: Vehicle)
events ! VehicleServiceMessage(toChannel, VehicleAction.SendResponse(guid0, pkt))
}
-
case FactionAffinity.ConvertFactionAffinity(faction) =>
val originalAffinity = vehicle.Faction
if (originalAffinity != (vehicle.Faction = faction)) {
@@ -251,7 +253,7 @@ class VehicleControl(vehicle: Vehicle)
AvatarAction.TerminalOrderResult(msg.terminal_guid, msg.transaction_type, result = true)
)
- case _ => ;
+ case _ => ()
}
} else {
zone.AvatarEvents ! AvatarServiceMessage(
@@ -288,20 +290,12 @@ class VehicleControl(vehicle: Vehicle)
final def Enabled: Receive =
commonEnabledBehavior
.orElse {
- case VehicleControl.RadiationTick if !passengerRadiationCloudTimer.isCancelled =>
- vehicle
- .interaction()
- .find(_.Type == RadiationInMountableInteraction)
- .foreach(_.interaction(vehicle.getInteractionSector, vehicle))
case _ => ()
}
def commonDisabledBehavior: Receive = checkBehavior
+ .orElse(dismountBehavior)
.orElse {
- case msg @ Mountable.TryDismount(user, seat_num, _) =>
- dismountBehavior.apply(msg)
- dismountCleanup(seat_num, user)
-
case Vehicle.Deconstruct(time) =>
time match {
case Some(delay) if vehicle.Definition.undergoesDecay =>
@@ -320,7 +314,7 @@ class VehicleControl(vehicle: Vehicle)
final def Disabled: Receive = commonDisabledBehavior
.orElse {
- case _ => ;
+ case _ => ()
}
def commonDeleteBehavior: Receive = checkBehavior
@@ -336,7 +330,7 @@ class VehicleControl(vehicle: Vehicle)
final def ReadyToDelete: Receive = commonDeleteBehavior
.orElse {
- case _ => ;
+ case _ => ()
}
override protected def mountTest(
@@ -354,39 +348,32 @@ class VehicleControl(vehicle: Vehicle)
super.mountTest(obj, seatNumber, user)
}
- def mountCleanup(mount_point: Int, user: Player): Unit = {
- vehicle.PassengerInSeat(user) match {
- case Some(0) => //driver seat
- val vsrc = VehicleSource(vehicle)
- user.LogActivity(VehicleMountActivity(vsrc, PlayerSource.inSeat(user, vsrc, seatNumber = 0), vehicle.Zone.Number))
- //if the driver mount, change ownership if that is permissible for this vehicle
- if (!vehicle.OwnerName.contains(user.Name) && vehicle.Definition.CanBeOwned.nonEmpty) {
- //whatever vehicle was previously owned
- vehicle.Zone.GUID(user.avatar.vehicle) match {
- case Some(v: Vehicle) =>
- v.Actor ! Vehicle.Ownership(None)
- case _ =>
- user.avatar.vehicle = None
- }
- GainOwnership(user) //gain new ownership
- } else {
- decaying = false
- decayTimer.cancel()
+ override def mountActionResponse(user: Player, @unused mountPoint: Int, seatNumber: Int): Unit = {
+ super.mountActionResponse(user, mountPoint, seatNumber)
+ val vsrc = VehicleSource(vehicle)
+ user.LogActivity(MountingActivity(vsrc, PlayerSource.inSeat(user, vsrc, seatNumber), vehicle.Zone.Number))
+ if (seatNumber == 0) {
+ //if the driver mount, change ownership if that is permissible for this vehicle
+ if (!vehicle.OwnerName.contains(user.Name) && vehicle.Definition.CanBeOwned.nonEmpty) {
+ //whatever vehicle was previously owned
+ vehicle.Zone.GUID(user.avatar.vehicle) match {
+ case Some(v: Vehicle) =>
+ v.Actor ! Vehicle.Ownership(None)
+ case _ =>
+ user.avatar.vehicle = None
}
- passengerRadiationCloudTimer.cancel()
- updateZoneInteractionProgressUI(user)
-
- case Some(seatNumber) => //literally any other seat
- val vsrc = VehicleSource(vehicle)
- user.LogActivity(VehicleMountActivity(vsrc, PlayerSource.inSeat(user, vsrc, seatNumber), vehicle.Zone.Number))
+ GainOwnership(user) //gain new ownership
+ } else {
decaying = false
decayTimer.cancel()
- if (!vehicle.Seats(0).isOccupied && passengerRadiationCloudTimer.isCancelled) {
- StartRadiationSelfReporting()
- }
- updateZoneInteractionProgressUI(user)
-
- case None => ()
+ }
+ TryStopInteractionSelfReporting()
+ updateZoneInteractionProgressUI(user)
+ } else {
+ decaying = false
+ decayTimer.cancel()
+ StopInteractionSelfReporting()
+ updateZoneInteractionProgressUI(user)
}
}
@@ -398,48 +385,37 @@ class VehicleControl(vehicle: Vehicle)
vehicle.DeploymentState == DriveState.Deployed || super.dismountTest(obj, seatNumber, user)
}
- def dismountCleanup(seatBeingDismounted: Int, user: Player): Unit = {
+ override def dismountActionResponse(user: Player, @unused seatBeingDismounted: Int): Unit = {
+ super.dismountActionResponse(user, seatBeingDismounted)
+ user.LogActivity(DismountingActivity(VehicleSource(vehicle), PlayerSource(user), vehicle.Zone.Number))
val obj = MountableObject
- val allSeatsUnoccupied = !obj.Seats.values.exists(_.isOccupied)
- // Reset velocity to zero when driver dismounts, to allow jacking/repair if vehicle was moving slightly before dismount
- if (!obj.Seats(0).isOccupied) {
+ if (seatBeingDismounted == 0) {
obj.Velocity = Some(Vector3.Zero)
}
- if (allSeatsUnoccupied) {
- passengerRadiationCloudTimer.cancel()
- } else if (seatBeingDismounted == 0) {
- StartRadiationSelfReporting()
+ if (TestToStartSelfReporting()) {
+ StartInteractionSelfReporting()
}
- if (!obj.Seats(seatBeingDismounted).isOccupied) { //this seat really was vacated
- user.LogActivity(VehicleDismountActivity(VehicleSource(vehicle), PlayerSource(user), vehicle.Zone.Number))
- //we were only owning the vehicle while we sat in its driver seat
- val canBeOwned = obj.Definition.CanBeOwned
- if (canBeOwned.contains(false) && seatBeingDismounted == 0) {
- LoseOwnership()
- }
- //are we already decaying? are we unowned? is no one seated anywhere?
- if (!decaying &&
- obj.Definition.undergoesDecay &&
- obj.OwnerGuid.isEmpty &&
- allSeatsUnoccupied) {
- decaying = true
- decayTimer = context.system.scheduler.scheduleOnce(
- MountableObject.Definition.DeconstructionTime.getOrElse(5 minutes),
- self,
- VehicleControl.PrepareForDeletion()
- )
- }
+ //we were only owning the vehicle while we sat in its driver seat
+ val canBeOwned = obj.Definition.CanBeOwned
+ if (canBeOwned.contains(false) && seatBeingDismounted == 0) {
+ LoseOwnership()
+ }
+ //are we already decaying? are we unowned? is no one seated anywhere?
+ if (!decaying &&
+ obj.Definition.undergoesDecay &&
+ obj.OwnerGuid.isEmpty &&
+ !vehicle.Seats.values.exists(_.isOccupied)) {
+ decaying = true
+ decayTimer = context.system.scheduler.scheduleOnce(
+ MountableObject.Definition.DeconstructionTime.getOrElse(5 minutes),
+ self,
+ VehicleControl.PrepareForDeletion()
+ )
}
}
- private def StartRadiationSelfReporting(): Unit = {
- passengerRadiationCloudTimer.cancel()
- passengerRadiationCloudTimer = context.system.scheduler.scheduleWithFixedDelay(
- 250.milliseconds,
- 250.milliseconds,
- self,
- VehicleControl.RadiationTick
- )
+ def TestToStartSelfReporting(): Boolean = {
+ vehicle.MountedIn.isEmpty && !vehicle.Seats.values.exists(_.isOccupied)
}
def PrepareForDisabled(kickPassengers: Boolean) : Unit = {
@@ -568,7 +544,7 @@ class VehicleControl(vehicle: Vehicle)
VehicleAction.InventoryState2(Service.defaultPlayerGUID, box.GUID, iguid, box.Capacity)
)
}
- case _ => ;
+ case _ => ()
}
}
@@ -712,7 +688,7 @@ class VehicleControl(vehicle: Vehicle)
VehicleAction.KickPassenger(tplayer.GUID, 4, unk2 = false, vguid)
)
}
- case _ => ; // No player seated
+ case _ => () // No player seated
}
}
vehicle.CargoHolds.foreach {
@@ -724,11 +700,11 @@ class VehicleControl(vehicle: Vehicle)
// Instruct client to start bail dismount procedure
self ! DismountVehicleCargoMsg(dguid, cargo.GUID, bailed = true, requestedByPassenger = false, kicked = false)
}
- case None => ; // No vehicle in cargo
+ case None => () // No vehicle in cargo
}
}
}
- case None => ;
+ case None => ()
}
} else {
log.warn(
@@ -767,9 +743,29 @@ class VehicleControl(vehicle: Vehicle)
}
override protected def DestructionAwareness(target: Target, cause: DamageResult): Unit = {
- passengerRadiationCloudTimer.cancel()
+ StopInteractionSelfReportingNoReset()
super.DestructionAwareness(target, cause)
}
+
+ override def endCargoMounting(carrierGuid: PlanetSideGUID): Unit = {
+ super.endCargoMounting(carrierGuid)
+ StopInteractionSelfReporting()
+ vehicle.Zone.GUID(carrierGuid) match {
+ case Some(v: Vehicle) => v.Actor ! IndependentZoneInteraction.SelfReportRunCheck
+ case _ => ()
+ }
+ }
+
+ override def endCargoDismounting(carrierGuid: PlanetSideGUID): Unit = {
+ super.endCargoDismounting(carrierGuid)
+ if (TestToStartSelfReporting()) {
+ StartInteractionSelfReporting()
+ }
+ vehicle.Zone.GUID(carrierGuid) match {
+ case Some(v: Vehicle) => v.Actor ! IndependentZoneInteraction.SelfReportRunCheck
+ case _ => ()
+ }
+ }
}
object VehicleControl {
@@ -779,7 +775,5 @@ object VehicleControl {
private case class Deletion()
- private case object RadiationTick
-
final case class AssignOwnership(player: Option[Player])
}
diff --git a/src/main/scala/net/psforever/objects/vehicles/interaction/InteractWithForceDomeProtectionSeatedInVehicle.scala b/src/main/scala/net/psforever/objects/vehicles/interaction/InteractWithForceDomeProtectionSeatedInVehicle.scala
new file mode 100644
index 000000000..de9671711
--- /dev/null
+++ b/src/main/scala/net/psforever/objects/vehicles/interaction/InteractWithForceDomeProtectionSeatedInVehicle.scala
@@ -0,0 +1,49 @@
+// Copyright (c) 2025 PSForever
+package net.psforever.objects.vehicles.interaction
+
+import net.psforever.objects.Vehicle
+import net.psforever.objects.avatar.interaction.{ForceZoneProtection, InteractWithForceDomeProtection}
+import net.psforever.objects.serverobject.dome.ForceDomePhysics
+import net.psforever.objects.serverobject.mount.interaction.InteractWithForceDomeProtectionSeatedInEntity
+import net.psforever.objects.zones.interaction.InteractsWithZone
+
+class InteractWithForceDomeProtectionSeatedInVehicle
+ extends InteractWithForceDomeProtectionSeatedInEntity {
+ override def applyProtection(target: InteractsWithZone, dome: ForceDomePhysics): Unit = {
+ super.applyProtection(target, dome)
+ target
+ .asInstanceOf[Vehicle]
+ .CargoHolds
+ .values
+ .flatMap(_.occupants)
+ .foreach { vehicle =>
+ vehicle
+ .interaction()
+ .find(_.Type == ForceZoneProtection)
+ .foreach {
+ case interaction: InteractWithForceDomeProtection =>
+ interaction.applyProtection(vehicle, dome)
+ case _ => ()
+ }
+ }
+ }
+
+ override def resetInteraction(target: InteractsWithZone): Unit = {
+ super.resetInteraction(target)
+ target
+ .asInstanceOf[Vehicle]
+ .CargoHolds
+ .values
+ .flatMap(_.occupants)
+ .foreach { vehicle =>
+ vehicle
+ .interaction()
+ .find(_.Type == ForceZoneProtection)
+ .foreach {
+ case interaction: InteractWithForceDomeProtection =>
+ interaction.resetInteraction(vehicle)
+ case _ => ()
+ }
+ }
+ }
+}
diff --git a/src/main/scala/net/psforever/objects/vehicles/InteractWithRadiationCloudsSeatedInVehicle.scala b/src/main/scala/net/psforever/objects/vehicles/interaction/InteractWithRadiationCloudsSeatedInVehicle.scala
similarity index 89%
rename from src/main/scala/net/psforever/objects/vehicles/InteractWithRadiationCloudsSeatedInVehicle.scala
rename to src/main/scala/net/psforever/objects/vehicles/interaction/InteractWithRadiationCloudsSeatedInVehicle.scala
index 2a95e783d..51efe167f 100644
--- a/src/main/scala/net/psforever/objects/vehicles/InteractWithRadiationCloudsSeatedInVehicle.scala
+++ b/src/main/scala/net/psforever/objects/vehicles/interaction/InteractWithRadiationCloudsSeatedInVehicle.scala
@@ -1,8 +1,8 @@
// Copyright (c) 2021 PSForever
-package net.psforever.objects.vehicles
+package net.psforever.objects.vehicles.interaction
import net.psforever.objects.Vehicle
-import net.psforever.objects.serverobject.mount.{InteractWithRadiationCloudsSeatedInEntity, RadiationInMountableInteraction}
+import net.psforever.objects.serverobject.mount.interaction.{InteractWithRadiationCloudsSeatedInEntity, RadiationInMountableInteraction}
import net.psforever.objects.zones.blockmap.SectorPopulation
import net.psforever.objects.zones.interaction.InteractsWithZone
diff --git a/src/main/scala/net/psforever/objects/vital/InGameHistory.scala b/src/main/scala/net/psforever/objects/vital/InGameHistory.scala
index 70f8b4193..6da32a780 100644
--- a/src/main/scala/net/psforever/objects/vital/InGameHistory.scala
+++ b/src/main/scala/net/psforever/objects/vital/InGameHistory.scala
@@ -4,7 +4,7 @@ package net.psforever.objects.vital
import net.psforever.objects.PlanetSideGameObject
import net.psforever.objects.definition.{EquipmentDefinition, KitDefinition, ToolDefinition}
import net.psforever.objects.serverobject.affinity.FactionAffinity
-import net.psforever.objects.sourcing.{AmenitySource, DeployableSource, PlayerSource, SourceEntry, SourceUniqueness, SourceWithHealthEntry, VehicleSource}
+import net.psforever.objects.sourcing.{AmenitySource, DeployableSource, MountableEntry, PlayerSource, SourceEntry, SourceUniqueness, SourceWithHealthEntry, VehicleSource}
import net.psforever.objects.vital.environment.EnvironmentReason
import net.psforever.objects.vital.etc.{ExplodingEntityReason, PainboxReason, SuicideReason}
import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult}
@@ -79,44 +79,48 @@ final case class RevivingActivity(target: SourceEntry, user: PlayerSource, amoun
final case class ShieldCharge(amount: Int, cause: Option[SourceEntry])
extends GeneralActivity
+trait TerminalUse {
+ def terminal: AmenitySource
+}
+
final case class TerminalUsedActivity(terminal: AmenitySource, transaction: TransactionType.Value)
- extends GeneralActivity
+ extends GeneralActivity with TerminalUse
final case class TelepadUseActivity(router: VehicleSource, telepad: DeployableSource, player: PlayerSource)
extends GeneralActivity
-sealed trait VehicleMountChange extends GeneralActivity {
- def vehicle: VehicleSource
+sealed trait MountChange extends GeneralActivity {
+ def mount: SourceEntry with MountableEntry
def zoneNumber: Int
}
-sealed trait VehiclePassengerMountChange extends VehicleMountChange {
+sealed trait PassengerMountChange extends MountChange {
def player: PlayerSource
}
-sealed trait VehicleCargoMountChange extends VehicleMountChange {
+sealed trait CargoMountChange extends MountChange {
def cargo: VehicleSource
}
-final case class VehicleMountActivity(vehicle: VehicleSource, player: PlayerSource, zoneNumber: Int)
- extends VehiclePassengerMountChange
+final case class MountingActivity(mount: SourceEntry with MountableEntry, player: PlayerSource, zoneNumber: Int)
+ extends PassengerMountChange
-final case class VehicleDismountActivity(
- vehicle: VehicleSource,
- player: PlayerSource,
- zoneNumber: Int,
- pairedEvent: Option[VehicleMountActivity] = None
- ) extends VehiclePassengerMountChange
+final case class DismountingActivity(
+ mount: SourceEntry with MountableEntry,
+ player: PlayerSource,
+ zoneNumber: Int,
+ pairedEvent: Option[MountingActivity] = None
+ ) extends PassengerMountChange
-final case class VehicleCargoMountActivity(vehicle: VehicleSource, cargo: VehicleSource, zoneNumber: Int)
- extends VehicleCargoMountChange
+final case class VehicleCargoMountActivity(mount: VehicleSource, cargo: VehicleSource, zoneNumber: Int)
+ extends CargoMountChange
final case class VehicleCargoDismountActivity(
- vehicle: VehicleSource,
+ mount: VehicleSource,
cargo: VehicleSource,
zoneNumber: Int,
pairedEvent: Option[VehicleCargoMountActivity] = None
- ) extends VehicleCargoMountChange
+ ) extends CargoMountChange
final case class Contribution(src: SourceUniqueness, entries: List[InGameActivity])
extends GeneralActivity {
@@ -165,8 +169,8 @@ final case class HealFromKit(kit_def: KitDefinition, amount: Int)
final case class HealFromEquipment(user: PlayerSource, equipment_def: EquipmentDefinition, amount: Int)
extends HealingActivity with SupportActivityCausedByAnother
-final case class HealFromTerminal(term: AmenitySource, amount: Int)
- extends HealingActivity
+final case class HealFromTerminal(terminal: AmenitySource, amount: Int)
+ extends HealingActivity with TerminalUse
final case class HealFromImplant(implant: ImplantType, amount: Int)
extends HealingActivity
@@ -180,7 +184,8 @@ final case class RepairFromKit(kit_def: KitDefinition, amount: Int)
final case class RepairFromEquipment(user: PlayerSource, equipment_def: EquipmentDefinition, amount: Int)
extends RepairingActivity with SupportActivityCausedByAnother
-final case class RepairFromTerminal(term: AmenitySource, amount: Int) extends RepairingActivity
+final case class RepairFromTerminal(terminal: AmenitySource, amount: Int)
+ extends RepairingActivity with TerminalUse
final case class RepairFromArmorSiphon(siphon_def: ToolDefinition, vehicle: VehicleSource, amount: Int)
extends RepairingActivity
@@ -251,24 +256,24 @@ trait InGameHistory {
*/
def LogActivity(action: Option[InGameActivity]): List[InGameActivity] = {
action match {
- case Some(act: VehicleDismountActivity) if act.pairedEvent.isEmpty =>
+ case Some(act: DismountingActivity) if act.pairedEvent.isEmpty =>
history
- .findLast(_.isInstanceOf[VehicleMountActivity])
+ .findLast(_.isInstanceOf[MountingActivity])
.collect {
- case event: VehicleMountActivity if event.vehicle.unique == act.vehicle.unique =>
+ case event: MountingActivity if event.mount.unique == act.mount.unique =>
history = history :+ InGameActivity.ShareTime(act.copy(pairedEvent = Some(event)), act)
}
.orElse {
history = history :+ act
None
}
- case Some(act: VehicleDismountActivity) =>
+ case Some(act: DismountingActivity) =>
history = history :+ act
case Some(act: VehicleCargoDismountActivity) =>
history
.findLast(_.isInstanceOf[VehicleCargoMountActivity])
.collect {
- case event: VehicleCargoMountActivity if event.vehicle.unique == act.vehicle.unique =>
+ case event: VehicleCargoMountActivity if event.mount.unique == act.mount.unique =>
history = history :+ InGameActivity.ShareTime(act.copy(pairedEvent = Some(event)), act)
}
.orElse {
diff --git a/src/main/scala/net/psforever/objects/vital/etc/ForceDomeExposure.scala b/src/main/scala/net/psforever/objects/vital/etc/ForceDomeExposure.scala
new file mode 100644
index 000000000..d2aa5ce4d
--- /dev/null
+++ b/src/main/scala/net/psforever/objects/vital/etc/ForceDomeExposure.scala
@@ -0,0 +1,56 @@
+// Copyright (c) 2025 PSForever
+package net.psforever.objects.vital.etc
+
+import net.psforever.objects.sourcing.{AmenitySource, SourceEntry}
+import net.psforever.objects.vital.{NoResistanceSelection, SimpleResolutions}
+import net.psforever.objects.vital.base.{DamageReason, DamageResolution}
+import net.psforever.objects.vital.damage.DamageCalculations
+import net.psforever.objects.vital.prop.{DamageProperties, DamageWithPosition}
+import net.psforever.objects.vital.resolution.{DamageAndResistance, DamageResistanceModel}
+
+/**
+ * A wrapper for a "damage source" in damage calculations that indicates a harmful interaction from a capitol force dome.
+ * @param field the target of the field in question
+ */
+final case class ForceDomeExposure(field: SourceEntry)
+ extends DamageReason {
+ def resolution: DamageResolution.Value = DamageResolution.Collision
+
+ def same(test: DamageReason): Boolean = test match {
+ case eer: ForceDomeExposure => eer.field eq field
+ case _ => false
+ }
+
+ /**
+ * Blame the capitol facility that is being protected.
+ */
+ override def attribution: Int = field match {
+ case a: AmenitySource => a.installation.Definition.ObjectId
+ case _ => field.Definition.ObjectId
+ }
+
+ override def source: DamageProperties = ForceDomeExposure.damageProperties
+
+ override def damageModel: DamageAndResistance = ForceDomeExposure.drm
+
+ /**
+ * No one person will be blamed for this.
+ */
+ override def adversary: Option[SourceEntry] = None
+}
+
+object ForceDomeExposure {
+ final val drm = new DamageResistanceModel {
+ DamageUsing = DamageCalculations.AgainstExoSuit
+ ResistUsing = NoResistanceSelection
+ Model = SimpleResolutions.calculate
+ }
+
+ final val damageProperties = new DamageWithPosition {
+ Damage0 = 99999
+ DamageToHealthOnly = true
+ DamageToVehicleOnly = true
+ DamageToBattleframeOnly = true
+ }
+}
+
diff --git a/src/main/scala/net/psforever/objects/vital/etc/SuicideReason.scala b/src/main/scala/net/psforever/objects/vital/etc/SuicideReason.scala
index cf7f1f085..c80ce9c64 100644
--- a/src/main/scala/net/psforever/objects/vital/etc/SuicideReason.scala
+++ b/src/main/scala/net/psforever/objects/vital/etc/SuicideReason.scala
@@ -29,7 +29,7 @@ final case class SuicideReason()
eventually, they stop logging in.
Anyway, this has nothing to do with that.
- Most playes probably just want to jump to the next base over.
+ Most players probably just want to jump to the next base over.
*/
def source: DamageProperties = SuicideReason.damageProperties
diff --git a/src/main/scala/net/psforever/objects/zones/Zone.scala b/src/main/scala/net/psforever/objects/zones/Zone.scala
index 1caf87d72..b47182a6f 100644
--- a/src/main/scala/net/psforever/objects/zones/Zone.scala
+++ b/src/main/scala/net/psforever/objects/zones/Zone.scala
@@ -1809,6 +1809,29 @@ object Zone {
/* explosions */
+ /**
+ * Allocates `Damageable` targets within the vicinity of server-prepared damage dealing
+ * and informs those entities that they have affected by the aforementioned damage.
+ * Usually, this is considered an "explosion;" but, the application can be utilized for a variety of unbound damage.
+ * @param zone the zone in which the damage should occur
+ * @param source the entity that embodies the damage (information)
+ * @param createInteraction how the interaction for this damage is to prepared
+ * @return a list of affected entities;
+ * only mostly complete due to the exclusion of objects whose damage resolution is different than usual
+ */
+ def serverSideDamage(
+ zone: Zone,
+ source: PlanetSideGameObject with FactionAffinity with Vitality,
+ createInteraction: (PlanetSideGameObject with FactionAffinity with Vitality, PlanetSideGameObject with FactionAffinity with Vitality) => DamageInteraction
+ ): List[PlanetSideServerObject] = {
+ source.Definition.innateDamage match {
+ case Some(damage) =>
+ serverSideDamage(zone, source, damage, createInteraction, distanceCheck, findAllTargets)
+ case None =>
+ Nil
+ }
+ }
+
/**
* Allocates `Damageable` targets within the vicinity of server-prepared damage dealing
* and informs those entities that they have affected by the aforementioned damage.
@@ -1816,8 +1839,10 @@ object Zone {
* @param zone the zone in which the damage should occur
* @param source the entity that embodies the damage (information)
* @param createInteraction how the interaction for this damage is to prepared
- * @param testTargetsFromZone a custom test for determining whether the allocated targets are affected by the damage
- * @param acquireTargetsFromZone the main target-collecting algorithm
+ * @param testTargetsFromZone a custom test for determining whether the allocated targets are affected by the damage;
+ * filters targets from the existing selection
+ * @param acquireTargetsFromZone the main target-collecting algorithm;
+ * collects targets from sector information
* @return a list of affected entities;
* only mostly complete due to the exclusion of objects whose damage resolution is different than usual
*/
@@ -1825,8 +1850,8 @@ object Zone {
zone: Zone,
source: PlanetSideGameObject with FactionAffinity with Vitality,
createInteraction: (PlanetSideGameObject with FactionAffinity with Vitality, PlanetSideGameObject with FactionAffinity with Vitality) => DamageInteraction,
- testTargetsFromZone: (PlanetSideGameObject, PlanetSideGameObject, Float) => Boolean = distanceCheck,
- acquireTargetsFromZone: (Zone, PlanetSideGameObject with FactionAffinity with Vitality, DamageWithPosition) => List[PlanetSideServerObject with Vitality] = findAllTargets
+ testTargetsFromZone: (PlanetSideGameObject, PlanetSideGameObject, Float) => Boolean,
+ acquireTargetsFromZone: (Zone, PlanetSideGameObject with FactionAffinity with Vitality, DamageWithPosition) => List[PlanetSideServerObject with Vitality]
): List[PlanetSideServerObject] = {
source.Definition.innateDamage match {
case Some(damage) =>
@@ -1851,8 +1876,10 @@ object Zone {
* @param zone the zone in which the damage should occur
* @param source the entity that embodies the damage (information)
* @param createInteraction how the interaction for this damage is to prepared
- * @param testTargetsFromZone a custom test for determining whether the allocated targets are affected by the damage
- * @param acquireTargetsFromZone the main target-collecting algorithm
+ * @param testTargetsFromZone a custom test for determining whether the allocated targets are affected by the damage;
+ * filters targets from the existing selection
+ * @param acquireTargetsFromZone the main target-collecting algorithm;
+ * collects targets from sector information
* @return a list of affected entities;
* only mostly complete due to the exclusion of objects whose damage resolution is different than usual
*/
diff --git a/src/main/scala/net/psforever/objects/zones/exp/KillContributions.scala b/src/main/scala/net/psforever/objects/zones/exp/KillContributions.scala
index 5a58f8b82..441d28f8c 100644
--- a/src/main/scala/net/psforever/objects/zones/exp/KillContributions.scala
+++ b/src/main/scala/net/psforever/objects/zones/exp/KillContributions.scala
@@ -4,8 +4,8 @@ package net.psforever.objects.zones.exp
import akka.actor.ActorRef
import net.psforever.objects.GlobalDefinitions
import net.psforever.objects.avatar.scoring.{Kill, SupportActivity}
-import net.psforever.objects.sourcing.{BuildingSource, PlayerSource, SourceEntry, SourceUniqueness, TurretSource, VehicleSource}
-import net.psforever.objects.vital.{Contribution, HealFromTerminal, InGameActivity, RepairFromTerminal, RevivingActivity, TelepadUseActivity, TerminalUsedActivity, VehicleCargoDismountActivity, VehicleCargoMountActivity, VehicleDismountActivity, VehicleMountActivity}
+import net.psforever.objects.sourcing.{BuildingSource, MountableEntry, PlayerSource, SourceEntry, SourceUniqueness, TurretSource, UniquePlayer, VehicleSource}
+import net.psforever.objects.vital.{Contribution, InGameActivity, RevivingActivity, TelepadUseActivity, TerminalUsedActivity, VehicleCargoDismountActivity, VehicleCargoMountActivity, DismountingActivity, MountingActivity}
import net.psforever.objects.vital.projectile.ProjectileReason
import net.psforever.objects.zones.exp.rec.{CombinedHealthAndArmorContributionProcess, MachineRecoveryExperienceContributionProcess}
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
@@ -388,13 +388,15 @@ object KillContributions {
the player should not get credit from being the vehicle owner in matters of transportation
there are considerations of time and distance traveled before the kill as well
*/
- case out: VehicleDismountActivity
- if !out.vehicle.owner.contains(out.player.unique) && out.pairedEvent.nonEmpty => (out.pairedEvent.get, out)
+ case out: DismountingActivity
+ if out.mount.isInstanceOf[VehicleSource] &&
+ !ownershipFromMount(out.mount).contains(out.player.unique) &&
+ out.pairedEvent.nonEmpty => (out.pairedEvent.get, out)
}
.collect {
- case (in: VehicleMountActivity, out: VehicleDismountActivity)
- if in.vehicle.unique == out.vehicle.unique &&
- out.vehicle.Faction == out.player.Faction &&
+ case (in: MountingActivity, out: DismountingActivity)
+ if in.mount.unique == out.mount.unique &&
+ out.mount.Faction == out.player.Faction &&
/*
considerations of time and distance transported before the kill
*/
@@ -407,7 +409,7 @@ object KillContributions {
}
} || {
val sameZone = in.zoneNumber == out.zoneNumber
- val distanceTransported = Vector3.DistanceSquared(in.vehicle.Position.xy, out.vehicle.Position.xy)
+ val distanceTransported = Vector3.DistanceSquared(in.mount.Position.xy, out.mount.Position.xy)
val distanceMoved = {
val killLocation = killerOpt.map(_.Position.xy).getOrElse(Vector3.Zero)
Vector3.DistanceSquared(killLocation, out.player.Position.xy)
@@ -423,9 +425,9 @@ object KillContributions {
}
//apply
dismountActivity
- .groupBy { _.vehicle }
- .collect { case (mount, dismountsFromVehicle) if mount.owner.nonEmpty =>
- val promotedOwner = PlayerSource(mount.owner.get, mount.Position)
+ .groupBy { _.mount }
+ .collect { case (mount, dismountsFromVehicle) if ownershipFromMount(mount).nonEmpty =>
+ val promotedOwner = PlayerSource(ownershipFromMount(mount).get, mount.Position)
val size = dismountsFromVehicle.size
val time = dismountsFromVehicle.maxBy(_.time).time
List((HotDropKillAssist(mount.Definition.ObjectId, 0), "hotdrop", promotedOwner))
@@ -457,6 +459,24 @@ object KillContributions {
}
}
+ /**
+ * Determine the owner of the entity based on information about the entity.
+ * @param mount mountable entity which can be owned
+ * @return the optional unique referential signature for the owner
+ */
+ private def ownershipFromMount(mount: SourceEntry with MountableEntry): Option[UniquePlayer] = {
+ mount match {
+ case v: VehicleSource =>
+ v.owner
+ case t: TurretSource => t.occupants.headOption.flatMap {
+ case p: PlayerSource => Some(p.unique)
+ case _ => None
+ }
+ case _ =>
+ None
+ }
+ }
+
/**
* Gather and reward specific in-game equipment use activity.
* na
@@ -486,14 +506,14 @@ object KillContributions {
val dismountActivity = history
.collect {
case out: VehicleCargoDismountActivity
- if out.vehicle.owner.nonEmpty && out.pairedEvent.nonEmpty => (out.pairedEvent.get, out)
+ if ownershipFromMount(out.mount).nonEmpty && out.pairedEvent.nonEmpty => (out.pairedEvent.get, out)
}
.collect {
case (in: VehicleCargoMountActivity, out: VehicleCargoDismountActivity)
- if in.vehicle.unique == out.vehicle.unique &&
- out.vehicle.Faction == out.cargo.Faction &&
- (in.vehicle.Definition == GlobalDefinitions.router || {
- val distanceTransported = Vector3.DistanceSquared(in.vehicle.Position.xy, out.vehicle.Position.xy)
+ if in.mount.unique == out.mount.unique &&
+ out.mount.Faction == out.cargo.Faction &&
+ (in.mount.Definition == GlobalDefinitions.router || {
+ val distanceTransported = Vector3.DistanceSquared(in.mount.Position.xy, out.mount.Position.xy)
val distanceMoved = {
val killLocation = kill.info.adversarial
.collect { adversarial => adversarial.attacker.Position.xy }
@@ -513,7 +533,7 @@ object KillContributions {
val promotedOwner = PlayerSource(mount.owner.get, mount.Position)
val mountId = mount.Definition.ObjectId
dismountsFromVehicle
- .groupBy(_.vehicle)
+ .groupBy(_.mount)
.map { case (vehicle, events) =>
val size = events.size
val time = events.maxBy(_.time).time
@@ -673,8 +693,6 @@ object KillContributions {
): Unit = {
history
.collect {
- case h: HealFromTerminal => (h.term, h)
- case r: RepairFromTerminal => (r.term, r)
case t: TerminalUsedActivity => (t.terminal, t)
}
.groupBy(_._1.unique)
diff --git a/src/main/scala/net/psforever/objects/zones/exp/Support.scala b/src/main/scala/net/psforever/objects/zones/exp/Support.scala
index 1ad7ead6e..d9017e600 100644
--- a/src/main/scala/net/psforever/objects/zones/exp/Support.scala
+++ b/src/main/scala/net/psforever/objects/zones/exp/Support.scala
@@ -2,7 +2,7 @@
package net.psforever.objects.zones.exp
import net.psforever.objects.sourcing.PlayerSource
-import net.psforever.objects.vital.{ExoSuitChange, InGameActivity, RevivingActivity, TerminalUsedActivity, VehicleDismountActivity, VehicleMountActivity, VehicleMountChange, VitalityDefinition}
+import net.psforever.objects.vital.{ExoSuitChange, InGameActivity, RevivingActivity, TerminalUsedActivity, DismountingActivity, MountingActivity, MountChange, VitalityDefinition}
import net.psforever.types.{ExoSuitType, PlanetSideEmpire}
import net.psforever.util.{Config, DefinitionUtil, ThreatAssessment, ThreatLevel}
@@ -82,7 +82,7 @@ object Support {
val wornTime: mutable.HashMap[Int, Long] = mutable.HashMap[Int, Long]()
var currentSuit: Int = initialExosuit.id
var lastActTime: Long = history.head.time
- var lastMountAct: Option[VehicleMountChange] = None
+ var lastMountAct: Option[MountChange] = None
//collect history events that encompass changes to exo-suits and to mounting conditions
history.collect {
case suitChange: ExoSuitChange =>
@@ -93,7 +93,7 @@ object Support {
)
currentSuit = suitChange.exosuit.id
lastActTime = suitChange.time
- case mount: VehicleMountActivity =>
+ case mount: MountingActivity =>
updateEquippedEntry(
currentSuit,
mount.time - lastActTime,
@@ -101,18 +101,18 @@ object Support {
)
lastActTime = mount.time
lastMountAct = Some(mount)
- case dismount: VehicleDismountActivity
+ case dismount: DismountingActivity
if dismount.pairedEvent.isEmpty =>
updateEquippedEntry(
- dismount.vehicle.Definition.ObjectId,
+ dismount.mount.Definition.ObjectId,
dismount.time - lastActTime,
wornTime
)
lastActTime = dismount.time
lastMountAct = None
- case dismount: VehicleDismountActivity =>
+ case dismount: DismountingActivity =>
updateEquippedEntry(
- dismount.vehicle.Definition.ObjectId,
+ dismount.mount.Definition.ObjectId,
dismount.time - dismount.pairedEvent.get.time,
wornTime
)
@@ -125,7 +125,7 @@ object Support {
.collect { mount =>
//dying in a vehicle is a reason to care about the last mount activity
updateEquippedEntry(
- mount.vehicle.Definition.ObjectId,
+ mount.mount.Definition.ObjectId,
lastTime - mount.time,
wornTime
)
diff --git a/src/main/scala/net/psforever/objects/zones/interaction/IndependentZoneInteraction.scala b/src/main/scala/net/psforever/objects/zones/interaction/IndependentZoneInteraction.scala
new file mode 100644
index 000000000..9450ece1e
--- /dev/null
+++ b/src/main/scala/net/psforever/objects/zones/interaction/IndependentZoneInteraction.scala
@@ -0,0 +1,95 @@
+// Copyright (c) 2026 PSForever
+package net.psforever.objects.zones.interaction
+
+import akka.actor.{Actor, Cancellable}
+import net.psforever.objects.Default
+
+import scala.concurrent.ExecutionContext.Implicits.global
+import scala.concurrent.duration._
+
+trait IndependentZoneInteraction {
+ _: Actor =>
+ /** ... */
+ private var zoneInteractionIntervalDefault: FiniteDuration = 250.milliseconds
+ /** ... */
+ private var zoneInteractionTimer: Cancellable = Default.Cancellable
+
+ def ZoneInteractionObject: InteractsWithZone
+
+ val zoneInteractionBehavior: Receive = {
+ case IndependentZoneInteraction.InteractionTick =>
+ PerformZoneInteractionSelfReporting()
+
+ case IndependentZoneInteraction.SelfReportRunCheck =>
+ PerformSelfReportRunCheck()
+ }
+
+ def ZoneInteractionInterval: FiniteDuration = zoneInteractionIntervalDefault
+
+ def ZoneInteractionInterval_=(interval: FiniteDuration): FiniteDuration = {
+ zoneInteractionIntervalDefault = interval
+ ZoneInteractionInterval
+ }
+
+ def TestToStartSelfReporting(): Boolean
+
+ def PerformZoneInteractionSelfReporting(): Unit = {
+ if (!zoneInteractionTimer.isCancelled) {
+ ZoneInteractionObject.zoneInteractions()
+ }
+ }
+
+ def PerformSelfReportRunCheck(): Unit = {
+ if (TestToStartSelfReporting()) {
+ StartInteractionSelfReporting()
+ } else {
+ StopInteractionSelfReporting()
+ }
+ }
+
+ final def StartInteractionSelfReporting(): Unit = {
+ zoneInteractionTimer.cancel()
+ zoneInteractionTimer = context.system.scheduler.scheduleWithFixedDelay(
+ 0.seconds,
+ zoneInteractionIntervalDefault,
+ self,
+ IndependentZoneInteraction.InteractionTick
+ )
+ }
+
+ final def StartInteractionSelfReporting(initialDelay: FiniteDuration): Unit = {
+ zoneInteractionTimer.cancel()
+ zoneInteractionTimer = context.system.scheduler.scheduleWithFixedDelay(
+ initialDelay,
+ zoneInteractionIntervalDefault,
+ self,
+ IndependentZoneInteraction.InteractionTick
+ )
+ }
+
+ final def TryStopInteractionSelfReporting(): Boolean = {
+ if (!zoneInteractionTimer.isCancelled) {
+ ZoneInteractionObject.resetInteractions()
+ zoneInteractionTimer.cancel()
+ } else {
+ false
+ }
+ }
+
+ final def StopInteractionSelfReporting(): Boolean = {
+ ZoneInteractionObject.resetInteractions()
+ zoneInteractionTimer.cancel()
+ }
+
+ final def StopInteractionSelfReportingNoReset(): Boolean = {
+ zoneInteractionTimer.cancel()
+ }
+
+ final def ZoneInteractionSelfReportingIsRunning: Boolean = !zoneInteractionTimer.isCancelled
+}
+
+object IndependentZoneInteraction {
+ private case object InteractionTick
+
+ final case object SelfReportRunCheck
+}
diff --git a/src/main/scala/net/psforever/zones/Zones.scala b/src/main/scala/net/psforever/zones/Zones.scala
index 4ac2da6a6..c94862623 100644
--- a/src/main/scala/net/psforever/zones/Zones.scala
+++ b/src/main/scala/net/psforever/zones/Zones.scala
@@ -11,6 +11,7 @@ import io.circe.parser._
import net.psforever.objects.{GlobalDefinitions, LocalLockerItem, LocalProjectile}
import net.psforever.objects.definition.BasicDefinition
import net.psforever.objects.guid.selector.{NumberSelector, RandomSelector, SpecificSelector}
+import net.psforever.objects.serverobject.dome.{ForceDomeDefinition, ForceDomePhysics}
import net.psforever.objects.serverobject.doors.{Door, DoorDefinition, SpawnTubeDoor}
import net.psforever.objects.serverobject.generator.Generator
import net.psforever.objects.serverobject.llu.{CaptureFlagSocket, CaptureFlagSocketDefinition}
@@ -100,17 +101,9 @@ object Zones {
"PathPoints"
)(ZipLinePath.apply)
- // monolith, hst, warpgate are ignored for now as the scala code isn't ready to handle them.
// BFR terminals/doors are ignored as top level elements as sanctuaries have them with no associated building. (repair_silo also has this problem, but currently is ignored in the AmenityExtrator project)
// Force domes have GUIDs but are currently classed as separate entities. The dome is controlled by sending GOAM 44 / 48 / 52 to the building GUID
- private val ignoredEntities = Seq(
- "monolith",
- "force_dome_dsp_physics",
- "force_dome_comm_physics",
- "force_dome_cryo_physics",
- "force_dome_tech_physics",
- "force_dome_amp_physics"
- )
+ private val ignoredEntities = Seq("monolith")
private val towerTypes = Seq("tower_a", "tower_b", "tower_c")
private val facilityTypes = Seq("amp_station", "cryo_facility", "comm_station", "comm_station_dsp", "tech_plant")
@@ -127,6 +120,13 @@ object Zones {
"vt_spawn",
"vt_vehicle"
)
+ private val forceDomeTypes = Seq(
+ "force_dome_dsp_physics",
+ "force_dome_comm_physics",
+ "force_dome_cryo_physics",
+ "force_dome_tech_physics",
+ "force_dome_amp_physics"
+ )
private val cavernBuildingTypes = Seq(
"ceiling_bldg_a",
"ceiling_bldg_b",
@@ -380,11 +380,27 @@ object Zones {
createObjects(
zoneMap,
- zoneObjects.filterNot { _.objectType.startsWith("bfr_") },
+ zoneObjects.filterNot { obj => obj.objectType.startsWith("bfr_") || forceDomeTypes.contains(obj.objectType) },
ownerGuid = 0,
None,
turretWeaponGuid
)
+ //force dome physics objects have no owner
+ //for our benefit, we can attach them as amenities to the zone's capitol facility
+ zoneObjects
+ .find { obj => forceDomeTypes.contains(obj.objectType) }
+ .foreach { forceDome =>
+ structures
+ .find { structure => Building.Capitols.contains(structure.objectName) }
+ .foreach { structure =>
+ val definition = DefinitionUtil.fromString(forceDome.objectType).asInstanceOf[ForceDomeDefinition]
+ zoneMap.addLocalObject(
+ forceDome.guid,
+ ForceDomePhysics.Constructor(definition),
+ owningBuildingGuid = structure.guid
+ )
+ }
+ }
lattice.asObject.get(mapid).foreach { obj =>
obj.asArray.get.foreach { entry =>
@@ -710,7 +726,6 @@ object Zones {
case _ => ()
}
-
}
}