This commit is contained in:
Fate-JH 2026-01-26 00:20:39 +00:00 committed by GitHub
commit 8422ddf68a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
63 changed files with 2094 additions and 718 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<br>
* Checks for server acknowledgement of the following messages in the following conditions:<br>
* `PlayerStateMessageUpstream` (infantry)<br>
* `VehicleStateMessage` (driver mount only)<br>
* `VehicleStateMessage` and `FrameVehicleStateMessage` (driver mount)<br>
* `ChildObjectStateMessage` (any gunner mount that is not the driver)<br>
* `KeepAliveMessage` (any passenger mount that is not the driver)<br>
* As they should arrive roughly every 250 milliseconds this allows for a very crude method of scheduling tasks up to four times per second

View file

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

View file

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

View file

@ -187,7 +187,8 @@ object ExplosiveDeployableControl {
zone,
target,
Zone.explosionDamage(Some(cause)),
ExplosiveDeployableControl.detectionForExplosiveSource(target)
ExplosiveDeployableControl.detectionForExplosiveSource(target),
Zone.findAllTargets
)
}

View file

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

View file

@ -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
*/

View file

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

View file

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

View file

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

View file

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

View file

@ -43,7 +43,7 @@ object ArmorSiphonBehavior {
val after = item.Discharge()
if (before > after) {
v.Actor ! ArmorSiphonBehavior.Recharge(iguid)
PerformDamage(
PerformDamageIfVulnerable(
obj,
DamageInteraction(
VehicleSource(obj),

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
package net.psforever.objects.serverobject.mount
package net.psforever.objects.serverobject.mount.interaction
import net.psforever.objects.zones.interaction.ZoneInteractionType

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 = {

View file

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

View file

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

View file

@ -60,7 +60,7 @@ trait AffectedByAutomaticTurretFire extends Damageable {
ProjectileReason(DamageResolution.Hit, modProjectile, target.DamageModel),
correctedTargetPosition
)
PerformDamage(target, resolvedProjectile.calculate())
PerformDamageIfVulnerable(target, resolvedProjectile.calculate())
}
}
}

View file

@ -0,0 +1,6 @@
// Copyright (c) 2026 PSForever
package net.psforever.objects.sourcing
trait MountableEntry {
def occupants: List[SourceEntry]
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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
*/

View file

@ -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.<br>
* 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)

View file

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

View file

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

View file

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