Capitol Force Dome (#1350)

* adding entity to represent force dome, and wiring force dome to capitol facility; touching the force dome while it is active causes death on both server and client

* force dome is now an amenity owned by its encompassing capitol facility and has its own control agency that is aware of the faction affinity, hack state, etc. conditions that would cause it to close over or open up

* force dome messages owner about change in state, triggering the NTU silo to give away repairs for free; activating the force dome kills or destroys all enemies within its radius

* the force dome exhibits a perimeter in which enemies will be destroyed when it energizes; the facility generator will become undestroyable when the force dome is energized

* force domes should be on the zone blockmap; correct issue with force dome death; interaction that sets players found under the force dome to be invulnerable works

* wrote variable and method documentation; prepared interaction handlers for force domes for mountable (vehicle) targets

* force dome provides damage protection to certain amenities, e.g., the generator, the turrets, and any implant machines (cryo); force dome will also suspend hacking attempts under it's envelope, but counter-hacking (resecure) should still be possible; operated turret deployables gain protection while manned; turrets no longer share knowledge of each other's upgrade cycles

* csr-level commands for force dome manipulation

* extending the force dome protection over a variety of entities in a different manner, with a focus on how to perform state reset (dismounting and dome protect end); completely refactored and reworked the self-reported zone interaction timer for vehicles; separated passenger seat mounting from gunner seat mounting

* adjusted self-reporting zone interaction logic; allowed for status of mounting into turret entities; made routine mounting behavior callbacks

* streamline pass on the vehicle operation and aaccountability

* expanded scope of 'csr override message for Capitol force dome state; spelling and guard condition changes to dismount blocking cases

* corrected death by force dome contact, for all players; corrected reset of force dome protection field condition

* corrected input data to force door wall inclusion test; adjusted force dome ceiling inclusion test(s); force dome generator locations now retained; invulnerability can not be granted to players or vehicles if they've taken damage recently
This commit is contained in:
Fate-JH 2026-04-21 11:22:40 -04:00 committed by GitHub
parent 1b31b3bad7
commit ceb6e58119
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
64 changed files with 2157 additions and 719 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.{GlobalDefinitions, PlanetSideGameObject, Session, TurretDeployable}
import net.psforever.objects.ce.{Deployable, DeployableCategory} import net.psforever.objects.ce.{Deployable, DeployableCategory}
import net.psforever.objects.serverobject.affinity.FactionAffinity 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.{CommonMessages, PlanetSideServerObject}
import net.psforever.objects.serverobject.hackable.Hackable import net.psforever.objects.serverobject.hackable.Hackable
import net.psforever.objects.serverobject.structures.Building 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 "sayspectator" => customCommandSpeakAsSpectator(params, message)
case "setempire" => customCommandSetEmpire(params) case "setempire" => customCommandSetEmpire(params)
case "weaponlock" => customCommandZoneWeaponUnlock(session, params) case "weaponlock" => customCommandZoneWeaponUnlock(session, params)
case "forcedome" => customForceDomeCommand(session, params)
case _ => case _ =>
// command was not handled // command was not handled
sendResponse( sendResponse(
@ -509,6 +511,41 @@ class ChatLogic(val ops: ChatOperations, implicit val context: ActorContext) ext
true 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] = { private def customCommandOnOffStateOrNone(stateOpt: Option[String]): Option[Boolean] = {
stateOpt match { stateOpt match {
case None => 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.serverobject.mount.Mountable
import net.psforever.objects.vital.Vitality import net.psforever.objects.vital.Vitality
import net.psforever.objects.zones.Zone 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.{ChatMsg, ObjectCreateDetailedMessage, PlanetsideAttributeMessage}
import net.psforever.packet.game.objectcreate.RibbonBars import net.psforever.packet.game.objectcreate.RibbonBars
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
@ -110,69 +109,21 @@ class CustomerServiceRepresentativeMode(data: SessionData) extends ModeLogic {
private def keepAlivePersistanceCSR(): Unit = { private def keepAlivePersistanceCSR(): Unit = {
val player = data.player val player = data.player
data.keepAlivePersistence()
topOffHealthOfPlayer(player)
player.allowInteraction = false player.allowInteraction = false
topOffHealthOfPlayer(player) CustomerServiceRepresentativeMode.topOffHealthOfPlayer(data, player)
data.continent.GUID(data.player.VehicleSeated) data.zoning.spawn.interimUngunnedVehicle = None
.collect { data.keepAlivePersistence()
case obj: PlanetSideGameObject with Vitality with BlockMapEntity => if (player.HasGUID) {
topOffHealth(obj) data.zoning.spawn.tryQueuedActivity()
data.updateBlockMap(obj, obj.Position) data.turnCounterFunc(player.GUID)
obj data.continent
} .GUID(player.VehicleSeated)
.getOrElse { .collect { case obj: PlanetSideGameObject with Vitality =>
data.updateBlockMap(player, player.Position) CustomerServiceRepresentativeMode.topOffHealth(data, obj)
} }
} data.squad.updateSquad()
} else {
private def topOffHealth(obj: PlanetSideGameObject with Vitality): Unit = { data.turnCounterFunc(PlanetSideGUID(0))
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)
)
} }
} }
} }
@ -201,4 +152,66 @@ case object CustomerServiceRepresentativeMode extends PlayerMode {
None 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.inventory.Container
import net.psforever.objects.serverobject.{CommonMessages, ServerObject} import net.psforever.objects.serverobject.{CommonMessages, ServerObject}
import net.psforever.objects.serverobject.containable.Containable 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.doors.Door
import net.psforever.objects.serverobject.generator.Generator import net.psforever.objects.serverobject.generator.Generator
import net.psforever.objects.serverobject.llu.CaptureFlag 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.vital.Vitality
import net.psforever.objects.zones.{ZoneProjectile, Zoning} import net.psforever.objects.zones.{ZoneProjectile, Zoning}
import net.psforever.packet.PlanetSideGamePacket 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.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.RemoverActor
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.local.{LocalAction, LocalServiceMessage} import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import net.psforever.types.{CapacitorStateType, ChatMessageType, Cosmetic, ExoSuitType, PlanetSideEmpire, PlanetSideGUID, Vector3} import net.psforever.types.{CapacitorStateType, ChatMessageType, Cosmetic, ExoSuitType, PlanetSideEmpire, PlanetSideGUID, Vector3}
import scodec.bits.ByteVector
import scala.concurrent.duration._
import scala.util.Success import scala.util.Success
object GeneralLogic { object GeneralLogic {
@ -77,28 +78,7 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
sessionLogic.persist() sessionLogic.persist()
sessionLogic.turnCounterFunc(avatarGuid) sessionLogic.turnCounterFunc(avatarGuid)
sessionLogic.updateBlockMap(player, pos) sessionLogic.updateBlockMap(player, pos)
//below half health, full heal topOffHealthOfPlayer()
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))
}
//expected //expected
val isMoving = WorldEntity.isMoving(vel) val isMoving = WorldEntity.isMoving(vel)
val isMovingPlus = isMoving || isJumping || jumpThrust val isMovingPlus = isMoving || isJumping || jumpThrust
@ -538,7 +518,7 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
def handleGenericCollision(pkt: GenericCollisionMsg): Unit = { def handleGenericCollision(pkt: GenericCollisionMsg): Unit = {
player.BailProtection = false 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 (pv.z * pv.z >= (pv.x * pv.x + pv.y * pv.y) * 0.5f) {
if (ops.heightTrend) { if (ops.heightTrend) {
ops.heightHistory = ops.heightLast ops.heightHistory = ops.heightLast
@ -555,8 +535,21 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
v.BailProtection = false v.BailProtection = false
case (CollisionIs.OfAircraft, Some(v: Vehicle)) case (CollisionIs.OfAircraft, Some(v: Vehicle))
if v.Definition.CanFly && v.Seats(0).occupant.contains(player) => () 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, _) => 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 _ => () case _ => ()
} }
} }
@ -805,4 +798,16 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
player.CapacitorState = CapacitorStateType.Idle 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) ops.MountingAction(tplayer, obj, seatNumber)
case Mountable.CanMount(obj: FacilityTurret, 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) obj.setMiddleOfUpgrade(false)
sessionLogic.zoning.CancelZoningProcess() sessionLogic.zoning.CancelZoningProcess()
sendResponse(PlanetsideAttributeMessage(obj.GUID, attribute_type=0, obj.Health)) 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.AvatarActor
import net.psforever.actors.session.support.{SessionData, VehicleFunctions, VehicleOperations} import net.psforever.actors.session.support.{SessionData, VehicleFunctions, VehicleOperations}
import net.psforever.objects.serverobject.PlanetSideServerObject 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.deploy.Deployment
import net.psforever.objects.serverobject.mount.Mountable import net.psforever.objects.serverobject.mount.Mountable
import net.psforever.objects.vehicles.control.BfrFlight import net.psforever.objects.vehicles.control.BfrFlight
import net.psforever.objects.vital.Vitality
import net.psforever.objects.zones.Zone import net.psforever.objects.zones.Zone
import net.psforever.packet.game.{ChildObjectStateMessage, DeployRequestMessage, FrameVehicleStateMessage, PlanetsideAttributeMessage, VehicleStateMessage, VehicleSubStateMessage} import net.psforever.objects.zones.interaction.InteractsWithZone
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.packet.game.{ChildObjectStateMessage, DeployRequestMessage, FrameVehicleStateMessage, VehicleStateMessage, VehicleSubStateMessage}
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
import net.psforever.types.{DriveState, PlanetSideGUID, Vector3} import net.psforever.types.{DriveState, Vector3}
object VehicleLogic { object VehicleLogic {
def apply(ops: VehicleOperations): VehicleLogic = { def apply(ops: VehicleOperations): VehicleLogic = {
@ -30,6 +29,7 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex
/* packets */ /* packets */
def handleVehicleState(pkt: VehicleStateMessage): Unit = { def handleVehicleState(pkt: VehicleStateMessage): Unit = {
player.allowInteraction = false
val VehicleStateMessage( val VehicleStateMessage(
vehicle_guid, vehicle_guid,
unk1, unk1,
@ -46,23 +46,21 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex
ops.GetVehicleAndSeat() match { ops.GetVehicleAndSeat() match {
case (Some(obj), Some(0)) => case (Some(obj), Some(0)) =>
//we're driving the vehicle //we're driving the vehicle
sessionLogic.zoning.spawn.tryQueuedActivity(vel)
sessionLogic.persist() sessionLogic.persist()
sessionLogic.turnCounterFunc(player.GUID) sessionLogic.turnCounterFunc(player.GUID)
sessionLogic.general.fallHeightTracker(pos.z) CustomerServiceRepresentativeMode.topOffHealthOfPlayer(sessionLogic, player)
if (obj.MountedIn.isEmpty) { CustomerServiceRepresentativeMode.topOffHealth(sessionLogic, obj)
sessionLogic.updateBlockMap(obj, pos) 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() if (notMountedState) {
topOffHealth(obj) sessionLogic.updateBlockMap(obj, position)
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) { if (obj.DeploymentState != DriveState.Deployed) {
obj.Velocity = vel obj.Velocity = velocity
} else { } else {
obj.Velocity = Some(Vector3.Zero) obj.Velocity = Some(Vector3.Zero)
} }
@ -74,20 +72,20 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex
obj.Velocity = None obj.Velocity = None
obj.Flying = None obj.Flying = None
} }
player.Position = position //convenient
obj.Position = position
obj.Orientation = angle
//
continent.VehicleEvents ! VehicleServiceMessage( continent.VehicleEvents ! VehicleServiceMessage(
continent.id, continent.id,
VehicleAction.VehicleState( VehicleAction.VehicleState(
player.GUID, player.GUID,
vehicle_guid, vehicle_guid,
unk1, unk1,
obj.Position, position,
ang, angle,
obj.Velocity, velocity,
if (obj.isFlying) { obj.Flying,
is_flying
} else {
None
},
unk6, unk6,
unk7, unk7,
wheels, wheels,
@ -96,8 +94,6 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex
) )
) )
sessionLogic.squad.updateSquad() sessionLogic.squad.updateSquad()
player.allowInteraction = false
obj.zoneInteractions()
case (None, _) => case (None, _) =>
//log.error(s"VehicleState: no vehicle $vehicle_guid found in zone") //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 //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 = { def handleFrameVehicleState(pkt: FrameVehicleStateMessage): Unit = {
player.allowInteraction = false
val FrameVehicleStateMessage( val FrameVehicleStateMessage(
vehicle_guid, vehicle_guid,
unk1, unk1,
@ -132,34 +129,21 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex
ops.GetVehicleAndSeat() match { ops.GetVehicleAndSeat() match {
case (Some(obj), Some(0)) => case (Some(obj), Some(0)) =>
//we're driving the vehicle //we're driving the vehicle
sessionLogic.zoning.spawn.tryQueuedActivity(vel)
sessionLogic.persist() sessionLogic.persist()
sessionLogic.turnCounterFunc(player.GUID) sessionLogic.turnCounterFunc(player.GUID)
topOffHealthOfPlayer() CustomerServiceRepresentativeMode.topOffHealthOfPlayer(sessionLogic, player)
topOffHealth(obj) CustomerServiceRepresentativeMode.topOffHealth(sessionLogic, obj)
val (position, angle, velocity, notMountedState) = continent.GUID(obj.MountedIn) match { val (position, angle, velocity, notMountedState) = continent.GUID(obj.MountedIn) match {
case Some(v: Vehicle) => case Some(v: Vehicle) =>
sessionLogic.updateBlockMap(obj, pos)
(pos, v.Orientation - Vector3.z(value = 90f) * Vehicles.CargoOrientation(obj).toFloat, v.Velocity, false) (pos, v.Orientation - Vector3.z(value = 90f) * Vehicles.CargoOrientation(obj).toFloat, v.Velocity, false)
case _ => case _ =>
(pos, ang, vel, true) (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) { if (notMountedState) {
sessionLogic.updateBlockMap(obj, position)
if (obj.DeploymentState != DriveState.Kneeling) { if (obj.DeploymentState != DriveState.Kneeling) {
obj.Velocity = velocity
if (is_airborne) { if (is_airborne) {
val flight = if (ascending_flight) flight_time else -flight_time val flight = if (ascending_flight) flight_time else -flight_time
obj.Flying = Some(flight) obj.Flying = Some(flight)
@ -172,12 +156,14 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex
obj.Velocity = None obj.Velocity = None
obj.Flying = None obj.Flying = None
} }
player.allowInteraction = false
obj.zoneInteractions()
} else { } else {
obj.Velocity = None obj.Velocity = None
obj.Flying = 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.VehicleEvents ! VehicleServiceMessage(
continent.id, continent.id,
VehicleAction.FrameVehicleState( VehicleAction.FrameVehicleState(
@ -214,34 +200,40 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex
} }
def handleChildObjectState(pkt: ChildObjectStateMessage): Unit = { def handleChildObjectState(pkt: ChildObjectStateMessage): Unit = {
player.allowInteraction = false
val ChildObjectStateMessage(object_guid, pitch, yaw) = pkt val ChildObjectStateMessage(object_guid, pitch, yaw) = pkt
val (o, tools) = sessionLogic.shooting.FindContainedWeapon val (o, tools) = sessionLogic.shooting.FindContainedWeapon
//is COSM our primary upstream packet?
(o match { (o match {
case Some(mount: Mountable) => (o, mount.PassengerInSeat(player)) case Some(mount: Mountable) => (mount, mount.PassengerInSeat(player))
case _ => (None, None) case _ => (None, None)
}) match { }) 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.persist()
sessionLogic.turnCounterFunc(player.GUID) sessionLogic.turnCounterFunc(player.GUID)
topOffHealthOfPlayer() CustomerServiceRepresentativeMode.topOffHealthOfPlayer(sessionLogic, player)
topOffHealth(obj) CustomerServiceRepresentativeMode.topOffHealth(sessionLogic, entity)
case _ => 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.persist()
sessionLogic.turnCounterFunc(player.GUID) sessionLogic.turnCounterFunc(player.GUID)
} }
//the majority of the following check retrieves information to determine if we are in control of the child //in the following condition we are in control of the child
tools.find { _.GUID == object_guid } match { tools.find(_.GUID == object_guid) match {
case None => 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( //log.warn(
// s"ChildObjectState: ${player.Name} is using a different controllable agent than entity ${object_guid.guid}" // s"ChildObjectState: ${player.Name} is using a different controllable agent than entity ${object_guid.guid}"
//) //)
case Some(_) => case Some(tool) =>
//TODO set tool orientation? val angle = Vector3(0f, pitch, yaw)
player.Orientation = Vector3(0f, pitch, yaw) tool.Orientation = angle
player.Orientation = angle
continent.VehicleEvents ! VehicleServiceMessage( continent.VehicleEvents ! VehicleServiceMessage(
continent.id, continent.id,
VehicleAction.ChildObjectState(player.GUID, object_guid, pitch, yaw) 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.{PlanetSideServerObject, ServerObject}
import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.serverobject.containable.Containable 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.doors.Door
import net.psforever.objects.serverobject.generator.Generator import net.psforever.objects.serverobject.generator.Generator
import net.psforever.objects.serverobject.interior.Sidedness.OutsideOf import net.psforever.objects.serverobject.interior.Sidedness.OutsideOf
import net.psforever.objects.serverobject.llu.CaptureFlag import net.psforever.objects.serverobject.llu.CaptureFlag
import net.psforever.objects.serverobject.locks.IFFLock import net.psforever.objects.serverobject.locks.IFFLock
import net.psforever.objects.serverobject.mblocker.Locker 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.resourcesilo.ResourceSilo
import net.psforever.objects.serverobject.structures.WarpGate import net.psforever.objects.serverobject.structures.WarpGate
import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal 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.terminals.implant.ImplantTerminalMech
import net.psforever.objects.serverobject.tube.SpawnTube import net.psforever.objects.serverobject.tube.SpawnTube
import net.psforever.objects.serverobject.turret.FacilityTurret 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.vehicles.Utility
import net.psforever.objects.vital.Vitality import net.psforever.objects.vital.Vitality
import net.psforever.objects.vital.collision.{CollisionReason, CollisionWithReason} 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.vital.interaction.DamageInteraction
import net.psforever.objects.zones.{ZoneProjectile, Zoning} import net.psforever.objects.zones.{ZoneProjectile, Zoning}
import net.psforever.packet.PlanetSideGamePacket 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)) case (CollisionIs.OfAircraft, out @ Some(v: Vehicle))
if v.Definition.CanFly && v.Seats(0).occupant.contains(player) => if v.Definition.CanFly && v.Seats(0).occupant.contains(player) =>
(out, sessionLogic.validObject(t, decorator = "GenericCollision/Aircraft"), false, pv) (out, sessionLogic.validObject(t, decorator = "GenericCollision/Aircraft"), false, pv)
case (CollisionIs.BetweenThings, _) => case (CollisionIs.BetweenThings, out @ Some(target: PlanetSideServerObject with MountableEntity)) =>
log.warn("GenericCollision: CollisionIs.BetweenThings detected - no handling case") 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) (None, None, false, Vector3.Zero)
case _ => case (_, None) =>
log.error(s"GenericCollision: $ctype detected: no entity detected as 'Primary'")
(None, None, false, Vector3.Zero) (None, None, false, Vector3.Zero)
} }
val curr = System.currentTimeMillis() 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)) => case (Some(us: Vehicle), _, Some(victim: SensorDeployable)) =>
collisionBetweenVehicleAndFragileDeployable(us, ppos, victim, tpos, velocity - tv, fallHeight, curr) 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.definition.{BasicDefinition, ObjectDefinition}
import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.serverobject.environment.interaction.ResetAllEnvironmentInteractions 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.mount.Mountable
import net.psforever.objects.serverobject.structures.WarpGate import net.psforever.objects.serverobject.structures.WarpGate
import net.psforever.objects.serverobject.terminals.implant.ImplantTerminalMech 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) ops.MountingAction(tplayer, obj, seatNumber)
case Mountable.CanMount(obj: Vehicle, 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}") log.info(s"${player.Name} mounts the driver seat of the ${obj.Definition.Name}")
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount") sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
val obj_guid: PlanetSideGUID = obj.GUID val obj_guid: PlanetSideGUID = obj.GUID
@ -134,13 +134,9 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act
ops.MountingAction(tplayer, obj, seatNumber) ops.MountingAction(tplayer, obj, seatNumber)
case Mountable.CanMount(obj: Vehicle, seatNumber, _) case Mountable.CanMount(obj: Vehicle, seatNumber, _)
if obj.Definition.MaxCapacitor > 0 => if obj.Definition.MaxCapacitor > 0 &&
log.info(s"${player.Name} mounts ${ obj.SeatPermissionGroup(seatNumber).contains(AccessPermissionGroup.Gunner) =>
obj.SeatPermissionGroup(seatNumber) match { log.info(s"${player.Name} mounts the #$seatNumber gunner seat of the ${obj.Definition.Name}")
case Some(seatType) => s"a $seatType seat (#$seatNumber)"
case None => "a seat"
}
} of the ${obj.Definition.Name}")
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount") sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
val obj_guid: PlanetSideGUID = obj.GUID val obj_guid: PlanetSideGUID = obj.GUID
sessionLogic.terminals.CancelAllProximityUnits() 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)) sendResponse(PlanetsideAttributeMessage(obj_guid, attribute_type=113, obj.Capacitor))
sessionLogic.general.accessContainer(obj) sessionLogic.general.accessContainer(obj)
ops.updateWeaponAtSeatPosition(obj, seatNumber) ops.updateWeaponAtSeatPosition(obj, seatNumber)
sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistenceFunc
tplayer.Actor ! ResetAllEnvironmentInteractions tplayer.Actor ! ResetAllEnvironmentInteractions
ops.MountingAction(tplayer, obj, seatNumber) ops.MountingAction(tplayer, obj, seatNumber)
case Mountable.CanMount(obj: Vehicle, seatNumber, _) => case Mountable.CanMount(obj: Vehicle, seatNumber, _)
log.info(s"${player.Name} mounts the ${ if obj.Definition.MaxCapacitor > 0 =>
obj.SeatPermissionGroup(seatNumber) match { log.info(s"${player.Name} mounts the #$seatNumber seat of the ${obj.Definition.Name}")
case Some(seatType) => s"a $seatType seat (#$seatNumber)" sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
case None => "a seat" val obj_guid: PlanetSideGUID = obj.GUID
} sessionLogic.terminals.CancelAllProximityUnits()
} of the ${obj.Definition.Name}") 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") sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount")
val obj_guid: PlanetSideGUID = obj.GUID val obj_guid: PlanetSideGUID = obj.GUID
sessionLogic.terminals.CancelAllProximityUnits() 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)) sendResponse(PlanetsideAttributeMessage(obj_guid, obj.Definition.shieldUiAttribute, obj.Shields))
sessionLogic.general.accessContainer(obj) sessionLogic.general.accessContainer(obj)
ops.updateWeaponAtSeatPosition(obj, seatNumber) ops.updateWeaponAtSeatPosition(obj, seatNumber)
sessionLogic.keepAliveFunc = sessionLogic.keepAlivePersistenceFunc
tplayer.Actor ! ResetAllEnvironmentInteractions tplayer.Actor ! ResetAllEnvironmentInteractions
ops.MountingAction(tplayer, obj, seatNumber) 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, _) case Mountable.CanMount(obj: FacilityTurret, seatNumber, _)
if obj.Definition == GlobalDefinitions.vanu_sentry_turret => if obj.Definition == GlobalDefinitions.vanu_sentry_turret =>
log.info(s"${player.Name} mounts the ${obj.Definition.Name}") 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) ops.MountingAction(tplayer, obj, seatNumber)
case Mountable.CanMount(obj: FacilityTurret, 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}") log.info(s"${player.Name} mounts the ${obj.Definition.Name}")
obj.setMiddleOfUpgrade(false) obj.setMiddleOfUpgrade(false)
sessionLogic.zoning.CancelZoningProcessWithDescriptiveReason("cancel_mount") 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.AvatarActor
import net.psforever.actors.session.support.{SessionData, VehicleFunctions, VehicleOperations} import net.psforever.actors.session.support.{SessionData, VehicleFunctions, VehicleOperations}
import net.psforever.objects.serverobject.PlanetSideServerObject 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.deploy.Deployment
import net.psforever.objects.serverobject.mount.Mountable import net.psforever.objects.serverobject.mount.Mountable
import net.psforever.objects.vehicles.control.BfrFlight import net.psforever.objects.vehicles.control.BfrFlight
import net.psforever.objects.zones.Zone 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.packet.game.{ChatMsg, ChildObjectStateMessage, DeployRequestMessage, FrameVehicleStateMessage, VehicleStateMessage, VehicleSubStateMessage}
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
import net.psforever.types.{ChatMessageType, DriveState, Vector3} import net.psforever.types.{ChatMessageType, DriveState, Vector3}
@ -48,18 +49,16 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex
sessionLogic.persist() sessionLogic.persist()
sessionLogic.turnCounterFunc(player.GUID) sessionLogic.turnCounterFunc(player.GUID)
sessionLogic.general.fallHeightTracker(pos.z) 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) 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) { if (obj.DeploymentState != DriveState.Deployed) {
obj.Velocity = vel obj.Velocity = velocity
} else { } else {
obj.Velocity = Some(Vector3.Zero) 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.Flying = is_flying //usually Some(7)
} }
obj.Cloaked = obj.Definition.CanCloak && is_cloaked obj.Cloaked = obj.Definition.CanCloak && is_cloaked
obj.zoneInteractions()
} else { } else {
obj.Velocity = None obj.Velocity = None
obj.Flying = None obj.Flying = None
} }
player.Position = position //convenient
obj.Position = position
obj.Orientation = angle
// //
continent.VehicleEvents ! VehicleServiceMessage( continent.VehicleEvents ! VehicleServiceMessage(
continent.id, continent.id,
@ -78,14 +81,10 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex
player.GUID, player.GUID,
vehicle_guid, vehicle_guid,
unk1, unk1,
obj.Position, position,
ang, angle,
obj.Velocity, velocity,
if (obj.isFlying) { obj.Flying,
is_flying
} else {
None
},
unk6, unk6,
unk7, unk7,
wheels, wheels,
@ -94,10 +93,9 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex
) )
) )
sessionLogic.squad.updateSquad() sessionLogic.squad.updateSquad()
obj.zoneInteractions()
case (None, _) => case (None, _) =>
//log.error(s"VehicleState: no vehicle $vehicle_guid found in zone") //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 //placing a "not driving" warning here may trigger as we are disembarking the vehicle
case (_, Some(index)) => case (_, Some(index)) =>
log.error( 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)" 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.zoning.spawn.tryQueuedActivity(vel)
sessionLogic.persist() sessionLogic.persist()
sessionLogic.turnCounterFunc(player.GUID) sessionLogic.turnCounterFunc(player.GUID)
sessionLogic.general.fallHeightTracker(pos.z)
val (position, angle, velocity, notMountedState) = continent.GUID(obj.MountedIn) match { val (position, angle, velocity, notMountedState) = continent.GUID(obj.MountedIn) match {
case Some(v: Vehicle) => case Some(v: Vehicle) =>
sessionLogic.updateBlockMap(obj, pos)
(pos, v.Orientation - Vector3.z(value = 90f) * Vehicles.CargoOrientation(obj).toFloat, v.Velocity, false) (pos, v.Orientation - Vector3.z(value = 90f) * Vehicles.CargoOrientation(obj).toFloat, v.Velocity, false)
case _ => case _ =>
(pos, ang, vel, true) (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) { if (notMountedState) {
sessionLogic.updateBlockMap(obj, position)
if (obj.DeploymentState != DriveState.Kneeling) { if (obj.DeploymentState != DriveState.Kneeling) {
obj.Velocity = velocity
if (is_airborne) { if (is_airborne) {
val flight = if (ascending_flight) flight_time else -flight_time val flight = if (ascending_flight) flight_time else -flight_time
obj.Flying = Some(flight) obj.Flying = Some(flight)
@ -173,6 +158,10 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex
obj.Velocity = None obj.Velocity = None
obj.Flying = 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.VehicleEvents ! VehicleServiceMessage(
continent.id, continent.id,
VehicleAction.FrameVehicleState( VehicleAction.FrameVehicleState(
@ -195,8 +184,8 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex
) )
sessionLogic.squad.updateSquad() sessionLogic.squad.updateSquad()
case (None, _) => case (None, _) =>
//log.error(s"VehicleState: no vehicle $vehicle_guid found in zone") //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 //placing a "not driving" warning here may trigger as we are disembarking the vehicle
case (_, Some(index)) => case (_, Some(index)) =>
log.error( 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)" 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 = { def handleChildObjectState(pkt: ChildObjectStateMessage): Unit = {
val ChildObjectStateMessage(object_guid, pitch, yaw) = pkt val ChildObjectStateMessage(object_guid, pitch, yaw) = pkt
val (o, tools) = sessionLogic.shooting.FindContainedWeapon val (o, tools) = sessionLogic.shooting.FindContainedWeapon
//is COSM our primary upstream packet?
(o match { (o match {
case Some(mount: Mountable) => (o, mount.PassengerInSeat(player)) case Some(mount: Mountable) => (mount, mount.PassengerInSeat(player))
case _ => (None, None) case _ => (None, None)
}) match { }) 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 _ => case (Some(_: Vehicle), Some(0)) => //see VSM or FVSM for valid cases
sessionLogic.zoning.spawn.tryQueuedActivity() //todo conditionals? ()
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.persist()
sessionLogic.turnCounterFunc(player.GUID) sessionLogic.turnCounterFunc(player.GUID)
} }
//the majority of the following check retrieves information to determine if we are in control of the child //in the following condition we are in control of the child
tools.find { _.GUID == object_guid } match { tools.find(_.GUID == object_guid) match {
case None => 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( //log.warn(
// s"ChildObjectState: ${player.Name} is using a different controllable agent than entity ${object_guid.guid}" // s"ChildObjectState: ${player.Name} is using a different controllable agent than entity ${object_guid.guid}"
//) //)
case Some(_) => case Some(tool) =>
//TODO set tool orientation? val angle = Vector3(0f, pitch, yaw)
player.Orientation = Vector3(0f, pitch, yaw) tool.Orientation = angle
player.Orientation = angle
continent.VehicleEvents ! VehicleServiceMessage( continent.VehicleEvents ! VehicleServiceMessage(
continent.id, continent.id,
VehicleAction.ChildObjectState(player.GUID, object_guid, pitch, yaw) 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.ballistics.Projectile
import net.psforever.objects.definition.{BasicDefinition, KitDefinition, SpecialExoSuitDefinition} import net.psforever.objects.definition.{BasicDefinition, KitDefinition, SpecialExoSuitDefinition}
import net.psforever.objects.serverobject.containable.Containable 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.doors.Door
import net.psforever.objects.vehicles.Utility import net.psforever.objects.vehicles.Utility
import net.psforever.objects.zones.ZoneProjectile import net.psforever.objects.zones.ZoneProjectile
import net.psforever.packet.PlanetSideGamePacket 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.account.AccountPersistenceService
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.types.{ExoSuitType, Vector3} import net.psforever.types.{ExoSuitType, Vector3}
import scala.concurrent.duration.DurationInt
object GeneralLogic { object GeneralLogic {
def apply(ops: GeneralOperations): GeneralLogic = { def apply(ops: GeneralOperations): GeneralLogic = {
new GeneralLogic(ops, ops.context) 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 */ } def handleAvatarFirstTimeEvent(pkt: AvatarFirstTimeEventMessage): Unit = { /* intentionally blank */ }

View file

@ -486,7 +486,14 @@ class SessionData(
zoning.spawn.interimUngunnedVehicle = None zoning.spawn.interimUngunnedVehicle = None
persist() persist()
if (player.HasGUID) { if (player.HasGUID) {
zoning.spawn.tryQueuedActivity(player.Velocity)
turnCounterFunc(player.GUID) turnCounterFunc(player.GUID)
continent
.GUID(player.VehicleSeated)
.collect { case v: PlanetSideGameObject with Mountable =>
VehicleOperations.updateMountableZoneInteractionFromEarliestSeat(v, player)
}
squad.updateSquad()
} else { } else {
turnCounterFunc(PlanetSideGUID(0)) 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.serverobject.mount.Mountable
import net.psforever.objects.zones.Zone import net.psforever.objects.zones.Zone
import net.psforever.objects._ import net.psforever.objects._
import net.psforever.objects.zones.interaction.InteractsWithZone
import net.psforever.packet.game.{ChildObjectStateMessage, DeployRequestMessage, VehicleSubStateMessage, _} import net.psforever.packet.game.{ChildObjectStateMessage, DeployRequestMessage, VehicleSubStateMessage, _}
import net.psforever.types.DriveState import net.psforever.types.DriveState
@ -195,3 +196,55 @@ class VehicleOperations(
sendResponse(pkt) 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

@ -1130,7 +1130,7 @@ class ZoningOperations(
case _ => () case _ => ()
} }
// capitol force dome state // capitol force dome state
if (building.IsCapitol && building.ForceDomeActive) { if (building.IsCapitol && building.ForceDome.exists(_.Energized)) {
sendResponse(GenericObjectActionMessage(guid, 13)) sendResponse(GenericObjectActionMessage(guid, 13))
} }
// amenities // amenities
@ -2007,7 +2007,7 @@ class ZoningOperations(
/** Upstream message counter<br> /** Upstream message counter<br>
* Checks for server acknowledgement of the following messages in the following conditions:<br> * Checks for server acknowledgement of the following messages in the following conditions:<br>
* `PlayerStateMessageUpstream` (infantry)<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> * `ChildObjectStateMessage` (any gunner mount that is not the driver)<br>
* `KeepAliveMessage` (any passenger 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 * 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.objects.{ConstructionItem, PlanetSideGameObject, Player, Vehicle}
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, PlanetSideGeneratorState, Vector3} import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, PlanetSideGeneratorState, Vector3}
import akka.actor.typed.scaladsl.adapter._ import akka.actor.typed.scaladsl.adapter._
import net.psforever.actors.zone.building.MajorFacilityLogic
import net.psforever.objects.avatar.scoring.Kill import net.psforever.objects.avatar.scoring.Kill
import net.psforever.objects.serverobject.affinity.FactionAffinity import net.psforever.objects.serverobject.affinity.FactionAffinity
import net.psforever.objects.serverobject.terminals.capture.CaptureTerminalAwareBehavior import net.psforever.objects.serverobject.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 //warp gates are controlled by game logic and are better off not restored via the database
case Some(b) => case Some(b) =>
if ((b.Faction = PlanetSideEmpire(building.factionId)) != PlanetSideEmpire.NEUTRAL) { if ((b.Faction = PlanetSideEmpire(building.factionId)) != PlanetSideEmpire.NEUTRAL) {
b.ForceDomeActive = MajorFacilityLogic.checkForceDomeStatus(b).getOrElse(false)
b.Neighbours.getOrElse(Nil).foreach(_.Actor ! BuildingActor.AlertToFactionChange(b)) b.Neighbours.getOrElse(Nil).foreach(_.Actor ! BuildingActor.AlertToFactionChange(b))
b.CaptureTerminal.collect { terminal => b.CaptureTerminal.collect { terminal =>
val msg = CaptureTerminalAwareBehavior.TerminalStatusChanged(terminal, isResecured = true) 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 akka.actor.typed.scaladsl.{ActorContext, Behaviors}
import net.psforever.actors.commands.NtuCommand import net.psforever.actors.commands.NtuCommand
import net.psforever.actors.zone.{BuildingActor, BuildingControlDetails, ZoneActor} 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.generator.{Generator, GeneratorControl}
import net.psforever.objects.serverobject.resourcesilo.ResourceSiloControl
import net.psforever.objects.serverobject.structures.{Amenity, Building} import net.psforever.objects.serverobject.structures.{Amenity, Building}
import net.psforever.objects.serverobject.terminals.capture.{CaptureTerminal, CaptureTerminalAware, CaptureTerminalAwareBehavior} import net.psforever.objects.serverobject.terminals.capture.{CaptureTerminal, CaptureTerminalAware, CaptureTerminalAwareBehavior}
import net.psforever.objects.sourcing.PlayerSource 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.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.galaxy.{GalaxyAction, GalaxyServiceMessage} import net.psforever.services.galaxy.{GalaxyAction, GalaxyServiceMessage}
import net.psforever.services.local.{LocalAction, LocalServiceMessage} 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. * 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) 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 * The power structure of major facilities has to be statused on the continental map
* via the state of its nanite-to-energy generator, and * 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 // 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 _ => case _ =>
details.galaxyService ! GalaxyServiceMessage(GalaxyAction.MapUpdate(details.building.infoUpdateMessage())) 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 * 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. * 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 * @param details package class that conveys the important information
@ -314,7 +236,6 @@ case object MajorFacilityLogic
true true
case Some(GeneratorControl.Event.Offline) => case Some(GeneratorControl.Event.Offline) =>
powerLost(details) powerLost(details)
alignForceDomeStatus(details, mapUpdateOnChange = false)
val zone = building.Zone val zone = building.Zone
val msg = AvatarAction.PlanetsideAttributeToAll(building.GUID, 46, 2) val msg = AvatarAction.PlanetsideAttributeToAll(building.GUID, 46, 2)
building.PlayersInSOI.foreach { player => building.PlayersInSOI.foreach { player =>
@ -326,7 +247,6 @@ case object MajorFacilityLogic
case Some(GeneratorControl.Event.Online) => case Some(GeneratorControl.Event.Online) =>
// Power restored. Reactor Online. Sensors Online. Weapons Online. All systems nominal. // Power restored. Reactor Online. Sensors Online. Weapons Online. All systems nominal.
powerRestored(details) powerRestored(details)
alignForceDomeStatus(details, mapUpdateOnChange = false)
val events = zone.AvatarEvents val events = zone.AvatarEvents
val guid = building.GUID val guid = building.GUID
val msg1 = AvatarAction.PlanetsideAttributeToAll(guid, 46, 0) val msg1 = AvatarAction.PlanetsideAttributeToAll(guid, 46, 0)
@ -348,16 +268,17 @@ case object MajorFacilityLogic
): Behavior[Command] = { ): Behavior[Command] = {
if (details.asInstanceOf[MajorFacilityWrapper].hasNtuSupply) { if (details.asInstanceOf[MajorFacilityWrapper].hasNtuSupply) {
BuildingActor.setFactionTo(details, faction, log) BuildingActor.setFactionTo(details, faction, log)
alignForceDomeStatus(details, mapUpdateOnChange = false)
val building = details.building 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 Behaviors.same
} }
def alertToFactionChange(details: BuildingWrapper, building: Building): Behavior[Command] = { def alertToFactionChange(details: BuildingWrapper, building: Building): Behavior[Command] = {
alignForceDomeStatus(details)
val bldg = details.building 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 //the presence of the flag means that we are involved in an ongoing llu hack
(bldg.GetFlag, bldg.CaptureTerminal) match { (bldg.GetFlag, bldg.CaptureTerminal) match {
case (Some(flag), Some(terminal)) if (flag.Target eq building) && flag.Faction != building.Faction => case (Some(flag), Some(terminal)) if (flag.Target eq building) && flag.Faction != building.Faction =>
@ -439,4 +360,35 @@ case object MajorFacilityLogic
} }
Behaviors.same 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, zone,
target, target,
Zone.explosionDamage(Some(cause)), 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.mount.{Mountable, MountableBehavior}
import net.psforever.objects.serverobject.turret.{MountableTurret, WeaponTurrets} import net.psforever.objects.serverobject.turret.{MountableTurret, WeaponTurrets}
import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject}
import net.psforever.objects.sourcing.SourceEntry import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, TurretSource}
import net.psforever.objects.vital.{InGameActivity, ShieldCharge} import net.psforever.objects.vital.{DismountingActivity, InGameActivity, MountingActivity, ShieldCharge}
import net.psforever.packet.game.HackState1 import net.psforever.packet.game.HackState1
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
import net.psforever.types.PlanetSideGUID import net.psforever.types.PlanetSideGUID
import scala.annotation.unused
/** definition */ /** definition */
class FieldTurretDeployableDefinition(private val objectId: Int) class FieldTurretDeployableDefinition(private val objectId: Int)
@ -70,6 +72,21 @@ class FieldTurretControl(turret: TurretDeployable)
player: Player player: Player
): Boolean = MountableTurret.MountTest(TurretObject, 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 //make certain vehicles don't charge shields too quickly
private def canChargeShields: Boolean = { private def canChargeShields: Boolean = {
val func: InGameActivity => Boolean = WithShields.LastShieldChargeOrDamage(System.currentTimeMillis(), turret.Definition) val func: InGameActivity => Boolean = WithShields.LastShieldChargeOrDamage(System.currentTimeMillis(), turret.Definition)

View file

@ -9,6 +9,7 @@ import net.psforever.objects.definition.converter._
import net.psforever.objects.equipment._ import net.psforever.objects.equipment._
import net.psforever.objects.global.{GlobalDefinitionsAmmo, GlobalDefinitionsBuilding, GlobalDefinitionsDeployable, GlobalDefinitionsExoSuit, GlobalDefinitionsImplant, GlobalDefinitionsKit, GlobalDefinitionsMiscellaneous, GlobalDefinitionsProjectile, GlobalDefinitionsTool, GlobalDefinitionsVehicle} 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.locker.LockerContainerDefinition
import net.psforever.objects.serverobject.dome.ForceDomeDefinition
import net.psforever.objects.serverobject.doors.DoorDefinition import net.psforever.objects.serverobject.doors.DoorDefinition
import net.psforever.objects.serverobject.generator.GeneratorDefinition import net.psforever.objects.serverobject.generator.GeneratorDefinition
import net.psforever.objects.serverobject.locks.IFFLockDefinition import net.psforever.objects.serverobject.locks.IFFLockDefinition
@ -1291,6 +1292,18 @@ object GlobalDefinitions {
val zipline = new GenericTeleportationDefinition(1047) 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 Buildings
*/ */

View file

@ -1,7 +1,7 @@
// Copyright (c) 2017 PSForever // Copyright (c) 2017 PSForever
package net.psforever.objects package net.psforever.objects
import net.psforever.objects.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.avatar.{Avatar, LoadoutManager, SpecialCarry}
import net.psforever.objects.ballistics.InteractWithRadiationClouds import net.psforever.objects.ballistics.InteractWithRadiationClouds
import net.psforever.objects.ce.{Deployable, InteractWithMines, InteractWithTurrets} import net.psforever.objects.ce.{Deployable, InteractWithMines, InteractWithTurrets}
@ -40,6 +40,7 @@ class Player(var avatar: Avatar)
with InteriorAwareFromInteraction with InteriorAwareFromInteraction
with AuraContainer with AuraContainer
with MountableEntity { with MountableEntity {
interaction(new InteractWithForceDomeProtection())
interaction(environment.interaction.InteractWithEnvironment(Seq( interaction(environment.interaction.InteractWithEnvironment(Seq(
new WithEntrance(), new WithEntrance(),
new WithWater(avatar.name), 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.affinity.FactionAffinityBehavior
import net.psforever.objects.serverobject.damage.Damageable import net.psforever.objects.serverobject.damage.Damageable
import net.psforever.objects.serverobject.hackable.Hackable 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.auto.{AffectedByAutomaticTurretFire, AutomatedTurret}
import net.psforever.objects.serverobject.turret.{TurretControl, TurretDefinition, WeaponTurret} import net.psforever.objects.serverobject.turret.{TurretControl, TurretDefinition, WeaponTurret}
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry} import net.psforever.objects.sourcing.{PlayerSource, SourceEntry}
@ -36,6 +36,7 @@ class TurretDeployable(tdef: TurretDeployableDefinition)
HackDuration = Array(0, 20, 10, 5) HackDuration = Array(0, 20, 10, 5)
if (tdef.Seats.nonEmpty) { if (tdef.Seats.nonEmpty) {
interaction(new InteractWithForceDomeProtectionSeatedInEntity)
interaction(new InteractWithTurrets()) interaction(new InteractWithTurrets())
interaction(new InteractWithRadiationCloudsSeatedInEntity(obj = this, range = 100f)) 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.interior.{InteriorAwareFromInteraction, Sidedness}
import net.psforever.objects.serverobject.structures.AmenityOwner import net.psforever.objects.serverobject.structures.AmenityOwner
import net.psforever.objects.vehicles._ 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.resistance.StandardResistanceProfile
import net.psforever.objects.vital.Vitality import net.psforever.objects.vital.Vitality
import net.psforever.objects.vital.resolution.DamageResistanceModel import net.psforever.objects.vital.resolution.DamageResistanceModel
@ -94,6 +94,7 @@ class Vehicle(private val vehicleDef: VehicleDefinition)
with AuraContainer with AuraContainer
with MountableEntity with MountableEntity
with InteriorAwareFromInteraction { with InteriorAwareFromInteraction {
interaction(new InteractWithForceDomeProtectionSeatedInVehicle)
interaction(environment.interaction.InteractWithEnvironment(Seq( interaction(environment.interaction.InteractWithEnvironment(Seq(
new WithEntrance(), new WithEntrance(),
new WithWater(), new WithWater(),

View file

@ -805,6 +805,15 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
} }
} }
override protected def canChangeVulnerability(state: Damageable.PersonalVulnerability): Boolean = {
state match {
case Damageable.MakeInvulnerable =>
player.LastDamage.exists { d => System.currentTimeMillis() - d.interaction.hitTime > 2500L }
case _ =>
true
}
}
/** /**
Prevent a player from deploying a telepad that belongs to a Router of a different faction Prevent a player from deploying a telepad that belongs to a Router of a different faction
*/ */

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() val after = item.Discharge()
if (before > after) { if (before > after) {
v.Actor ! ArmorSiphonBehavior.Recharge(iguid) v.Actor ! ArmorSiphonBehavior.Recharge(iguid)
PerformDamage( PerformDamageIfVulnerable(
obj, obj,
DamageInteraction( DamageInteraction(
VehicleSource(obj), VehicleSource(obj),

View file

@ -956,5 +956,70 @@ object GlobalDefinitionsMiscellaneous {
zipline.Name = "zipline" zipline.Name = "zipline"
zipline.interference = InterferenceRange(deployables = 5.5f) 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_amp_physics.GeneratorOffset = Vector3(0.148f, -21.867f, 17.623f)
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_comm_physics.GeneratorOffset = Vector3(-19.481f, -4.25f, 12.649f)
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_cryo_physics.GeneratorOffset = Vector3(-20.709f, 5.387f, 19.466f)
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_dsp_physics.GeneratorOffset = Vector3(12.698f, 25.15f, 12.541f)
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)
force_dome_tech_physics.GeneratorOffset = Vector3(13.001f, -32.612f, 22.59f)
} }
} }

View file

@ -10,6 +10,8 @@ import net.psforever.objects.vital.Vitality
import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult} import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult}
import net.psforever.objects.vital.resolution.ResolutionCalculations import net.psforever.objects.vital.resolution.ResolutionCalculations
import scala.annotation.unused
/** /**
* The base "control" `Actor` mixin for damage-handling code. * The base "control" `Actor` mixin for damage-handling code.
* A valid entity requires health points and * A valid entity requires health points and
@ -17,7 +19,6 @@ import net.psforever.objects.vital.resolution.ResolutionCalculations
* All of these should be affected by the damage where applicable. * All of these should be affected by the damage where applicable.
*/ */
trait Damageable { trait Damageable {
/** /**
* Contextual access to the object being the target of this damage. * Contextual access to the object being the target of this damage.
* Needs declaration in lowest implementing code. * Needs declaration in lowest implementing code.
@ -25,24 +26,50 @@ trait Damageable {
*/ */
def DamageableObject: Damageable.Target def DamageableObject: Damageable.Target
/** a local `canDamage` flag */
private var isVulnerable: Boolean = true
/** the official mixin hook; /** the official mixin hook;
* `orElse` onto the "control" `Actor` `receive`; or, * `orElse` onto the "control" `Actor` `receive`; or,
* cite the `originalTakesDamage` protocol during inheritance overrides */ * cite the `originalTakesDamage` protocol during inheritance overrides */
val takesDamage: Receive = { val takesDamage: Receive = {
case Damageable.MakeVulnerable if canChangeVulnerability(Damageable.MakeVulnerable) =>
isVulnerable = true
case Damageable.MakeInvulnerable if canChangeVulnerability(Damageable.MakeInvulnerable) =>
isVulnerable = false
case Vitality.Damage(damage_func) => case Vitality.Damage(damage_func) =>
val obj = DamageableObject PerformDamageIfVulnerable(DamageableObject, damage_func)
if (obj.CanDamage) {
PerformDamage(obj, damage_func)
}
} }
/** a duplicate of the core implementation for the default mixin hook, for use in overriding */ /** a duplicate of the core implementation for the default mixin hook, for use in overriding */
final val originalTakesDamage: Receive = { final val originalTakesDamage: Receive = {
case Damageable.MakeVulnerable if canChangeVulnerability(Damageable.MakeVulnerable) =>
isVulnerable = true
case Damageable.MakeInvulnerable if canChangeVulnerability(Damageable.MakeInvulnerable) =>
isVulnerable = false
case Vitality.Damage(damage_func) => case Vitality.Damage(damage_func) =>
val obj = DamageableObject PerformDamageIfVulnerable(DamageableObject, damage_func)
if (obj.CanDamage) { }
PerformDamage(obj, damage_func)
} protected def canChangeVulnerability(@unused state: Damageable.PersonalVulnerability): Boolean = {
true
}
/**
* 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 +94,20 @@ object Damageable {
*/ */
final val LogChannel: String = "DamageResolution" 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? * Does the possibility exist that the designated target can be affected by this projectile's damage?
* @see `Hackable` * @see `Hackable`

View file

@ -0,0 +1,518 @@
// 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 facility encapsulated by the force dome
* @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 Vector3(centerX, centerY, centerZ) = obj1.Position
val Vector3(targetX, targetY, targetZ) = obj2.Position
lazy val insideOfThePerimeter = !segments.exists { case (point1, point2) =>
//want targets within the perimeter; if there's any intersection, target is outside of the perimeter
segmentIntersectionTestPerSegment(centerX, centerY, targetX, targetY, point1.x, point1.y, point2.x, point2.y)
}
segments.nonEmpty && insideOfThePerimeter && (targetZ <= centerZ || underForceDomeCeiling(obj1, obj2, targetZ))
}
/**
* The force dome is the top portion of an irregular prism.
* It's walls are similarly angled inwards, usually.
* There's no way to properly represent this without too much work.
* Just ensure the target is either below the dome or within a circular region
* defined by the base of the generator to the top of the extended stalk as the measurement of a hemisphere.
* @see `Zone.distanceCheck`
* @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 height target height that must be under the ceiling
* @return `true`, if target is detected within the force dome kill region
* `false`, otherwise
*/
private def underForceDomeCeiling(obj1: PlanetSideGameObject, obj2: PlanetSideGameObject, height: Float): Boolean = {
obj1 match {
case dome: ForceDomePhysics =>
val generatorHeightOffsetZ = dome.Definition.GeneratorOffset.z
height < dome.Owner.Position.z + generatorHeightOffsetZ * 1.5f || Zone.distanceCheck(obj1, obj2, generatorHeightOffsetZ + 42f)
case _ =>
false
}
}
/**
* 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 ax = targetX - pointX //delta-x of segment from center to target
val ay = targetY - pointY //delta-y of segment from center to target
val bx = segmentPoint1x - segmentPoint2x //delta-x of test segment
val by = segmentPoint1y - segmentPoint2y //delta-y of test segment
val cx = pointX - segmentPoint1x //delta-x of hypotenuse of triangle formed by center, test segment endpoint, and intersection point
val cy = pointY - segmentPoint1y //delta-y of hypotenuse of triangle formed by center, test segment endpoint, and intersection point
val alphaNumerator = by * cx - bx * cy //cross product of matrix [(by, bx)(cy, cx)]
val denominator = ay * bx - ax * by //cross product of matrix [(ay, ax)(cy, cx)]
val betaNumerator = ax * cy - ay * cx //cross product of matrix [(ax, ay)(cx, cy)]
if (
denominator > 0 &&
(alphaNumerator < 0 || alphaNumerator > denominator || betaNumerator < 0 || betaNumerator > denominator)
) {
false
} else if (
denominator < 0 &&
(alphaNumerator > 0 || alphaNumerator < denominator || betaNumerator > 0 || betaNumerator < denominator)
) {
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,62 @@
// 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()
/** offset of the physical location of the force dome generator stalk */
private var genOffset: Vector3 = Vector3.Zero
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
}
def GeneratorOffset: Vector3 = genOffset
def GeneratorOffset_=(offset: Vector3): Vector3 = {
genOffset = offset
GeneratorOffset
}
}
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 vertically, but that's not a consideration here */
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 queuedExplosion = Default.Cancellable
imminentExplosion = false imminentExplosion = false
//hate on everything nearby //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() => case GeneratorControl.Restored() =>
gen.ClearHistory() gen.ClearHistory()

View file

@ -2,38 +2,25 @@
package net.psforever.objects.serverobject.hackable package net.psforever.objects.serverobject.hackable
import net.psforever.actors.zone.BuildingActor 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.Terminal
import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal
import net.psforever.objects.{GlobalDefinitions, Player, Vehicle} import net.psforever.objects.{GlobalDefinitions, Player, Vehicle}
import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} 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.packet.game.{GenericObjectActionMessage, HackMessage, HackState, HackState1, HackState7, TriggeredSound}
import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID} import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID}
import net.psforever.services.Service import net.psforever.services.Service
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.local.{LocalAction, LocalServiceMessage} import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import scala.annotation.unused
import scala.util.{Failure, Success} import scala.util.{Failure, Success}
object GenericHackables { object GenericHackables {
private val log = org.log4s.getLogger("HackableBehavior") 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 * 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. * 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. * 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 target the object being affected
* @param tool_guid the tool being used to affest the object * @param tool_guid the tool being used to affest the object
* @param progress the current progress value * @param progress the current progress value
* @param additionalCancellationTests context-specific tests for hack continuation
* @return `true`, if the next cycle of progress should occur; * @return `true`, if the next cycle of progress should occur;
* `false`, otherwise * `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 progress: Float
): Boolean = { ): Boolean = {
//hack state for progress bar visibility //hack state for progress bar visibility
@ -87,9 +83,7 @@ object GenericHackables {
(HackState.Start, 0) (HackState.Start, 0)
} else if (progress >= 100L) { } else if (progress >= 100L) {
(HackState.Finished, 100) (HackState.Finished, 100)
} else if (target.isMoving(test = 1f) || target.Destroyed || !target.HasGUID) { } else if (target.isMoving(test = 1f) || target.Destroyed || !target.HasGUID || additionalCancellationTests(target, hacker)) {
(HackState.Cancelled, 0)
} else if (target.isInstanceOf[CaptureTerminal] && EndHackProgress(target, hacker)) {
(HackState.Cancelled, 0) (HackState.Cancelled, 0)
} else { } else {
(HackState.Ongoing, progress.toInt) (HackState.Ongoing, progress.toInt)
@ -103,6 +97,55 @@ object GenericHackables {
) )
progressState != HackState.Cancelled 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. * 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. * Retrieve a mapping of each mount from its mount point index.
* @return the mapping of mount point to mount * @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.objects.serverobject.hackable.Hackable
import net.psforever.types.BailType import net.psforever.types.BailType
import scala.annotation.unused
import scala.collection.mutable import scala.collection.mutable
trait MountableBehavior { trait MountableBehavior {
@ -45,13 +46,17 @@ trait MountableBehavior {
case Some(seatNum) if mountTest(obj, seatNum, user) && tryMount(obj, seatNum, user) => case Some(seatNum) if mountTest(obj, seatNum, user) && tryMount(obj, seatNum, user) =>
user.VehicleSeated = obj.GUID user.VehicleSeated = obj.GUID
usedMountPoint.put(user.Name, mount_point) 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)) sender() ! Mountable.MountMessages(user, Mountable.CanMount(obj, seatNum, mount_point))
case _ => case _ =>
sender() ! Mountable.MountMessages(user, Mountable.CanNotMount(obj, mount_point)) 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( protected def mountTest(
obj: PlanetSideServerObject with Mountable, obj: PlanetSideServerObject with Mountable,
seatNumber: Int, seatNumber: Int,
@ -87,7 +92,7 @@ trait MountableBehavior {
val obj = MountableObject val obj = MountableObject
if (dismountTest(obj, seat_number, user) && tryDismount(obj, seat_number, user, bail_type)) { if (dismountTest(obj, seat_number, user) && tryDismount(obj, seat_number, user, bail_type)) {
user.VehicleSeated = None user.VehicleSeated = None
obj.Zone.actor ! ZoneActor.AddToBlockMap(user, obj.Position) dismountActionResponse(user, seat_number)
sender() ! Mountable.MountMessages( sender() ! Mountable.MountMessages(
user, user,
Mountable.CanDismount(obj, seat_number, getUsedMountPoint(user.Name, seat_number)) 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( protected def dismountTest(
obj: Mountable with WorldEntity, obj: Mountable with WorldEntity,
seatNumber: Int, 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 // 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.ballistics.{Projectile, ProjectileQuality}
import net.psforever.objects.serverobject.mount.Mountable
import net.psforever.objects.sourcing.SourceEntry import net.psforever.objects.sourcing.SourceEntry
import net.psforever.objects.vital.Vitality import net.psforever.objects.vital.Vitality
import net.psforever.objects.vital.base.DamageResolution 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 import net.psforever.objects.zones.interaction.ZoneInteractionType

View file

@ -37,7 +37,8 @@ class VehicleSpawnControlRailJack(pad: VehicleSpawnPad) extends VehicleSpawnCont
pad.Zone, pad.Zone,
pad, pad,
VehicleSpawnControlRailJack.prepareSpawnExplosion(pad, SourceEntry(driver), SourceEntry(vehicle)), 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) pad.Zone.VehicleEvents ! VehicleSpawnPad.AttachToRails(vehicle, pad)
context.system.scheduler.scheduleOnce(10 milliseconds, seatDriver, order) 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.ExecutionContext.Implicits.global
import scala.concurrent.duration._ import scala.concurrent.duration._
object ResourceSiloControl {
final case class DrainMultiplier(multiplier: Float)
}
/** /**
* An `Actor` that handles messages being dispatched to a specific `ResourceSilo` entity. * An `Actor` that handles messages being dispatched to a specific `ResourceSilo` entity.
* *
@ -30,7 +34,9 @@ class ResourceSiloControl(resourceSilo: ResourceSilo)
def FactionObject: FactionAffinity = resourceSilo def FactionObject: FactionAffinity = resourceSilo
private[this] val log = org.log4s.getLogger 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 = { def receive: Receive = {
case Service.Startup() => case Service.Startup() =>
@ -53,6 +59,9 @@ class ResourceSiloControl(resourceSilo: ResourceSilo)
checkBehavior checkBehavior
.orElse(storageBehavior) .orElse(storageBehavior)
.orElse { .orElse {
case ResourceSiloControl.DrainMultiplier(multiplier) =>
drainMultiplier = multiplier
case CommonMessages.Use(_, Some(vehicle: Vehicle)) case CommonMessages.Use(_, Some(vehicle: Vehicle))
if GlobalDefinitions.isBattleFrameVehicle(vehicle.Definition) => if GlobalDefinitions.isBattleFrameVehicle(vehicle.Definition) =>
val siloFaction = resourceSilo.Faction val siloFaction = resourceSilo.Faction
@ -171,7 +180,7 @@ class ResourceSiloControl(resourceSilo: ResourceSilo)
*/ */
def HandleNtuRequest(sender: ActorRef, min: Float, max: Float): Unit = { def HandleNtuRequest(sender: ActorRef, min: Float, max: Float): Unit = {
val originalAmount = resourceSilo.NtuCapacitor val originalAmount = resourceSilo.NtuCapacitor
UpdateChargeLevel(-min) UpdateChargeLevel(-min * drainMultiplier)
sender ! Ntu.Grant(resourceSilo, originalAmount - resourceSilo.NtuCapacitor) 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 net.psforever.types._
import scalax.collection.{Graph, GraphEdge} import scalax.collection.{Graph, GraphEdge}
import akka.actor.typed.scaladsl.adapter._ 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.llu.{CaptureFlag, CaptureFlagSocket}
import net.psforever.objects.serverobject.structures.participation.{MajorFacilityHackParticipation, NoParticipation, ParticipationLogic, TowerHackParticipation} import net.psforever.objects.serverobject.structures.participation.{MajorFacilityHackParticipation, NoParticipation, ParticipationLogic, TowerHackParticipation}
import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal import net.psforever.objects.serverobject.terminals.capture.CaptureTerminal
@ -32,7 +33,6 @@ class Building(
private var faction: PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL private var faction: PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL
private var playersInSOI: List[Player] = List.empty private var playersInSOI: List[Player] = List.empty
private var forceDomeActive: Boolean = false
private var participationFunc: ParticipationLogic = NoParticipation private var participationFunc: ParticipationLogic = NoParticipation
var virusId: Long = 8 // 8 default = no virus var virusId: Long = 8 // 8 default = no virus
var virusInstalledBy: Option[Int] = None // faction id var virusInstalledBy: Option[Int] = None // faction id
@ -59,11 +59,6 @@ class Building(
case None => false case None => false
} }
} }
def ForceDomeActive: Boolean = forceDomeActive
def ForceDomeActive_=(activated: Boolean): Boolean = {
forceDomeActive = activated
forceDomeActive
}
def Faction: PlanetSideEmpire.Value = faction 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] = { def NtuSource: Option[NtuContainer] = {
Amenities.find(_.isInstanceOf[NtuContainer]) match { Amenities.find(_.isInstanceOf[NtuContainer]) match {
case Some(o: NtuContainer) => Some(o) case Some(o: NtuContainer) => Some(o)
@ -223,6 +225,7 @@ class Building(
else { else {
(virusId.toInt, Some(Additional3(inform_defenders=true, virusInstalledBy.getOrElse(3)))) (virusId.toInt, Some(Additional3(inform_defenders=true, virusInstalledBy.getOrElse(3))))
} }
val forceDomeActive = ForceDome.exists(_.Energized)
BuildingInfoUpdateMessage( BuildingInfoUpdateMessage(
Zone.Number, Zone.Number,

View file

@ -29,7 +29,7 @@ class CaptureTerminalControl(terminal: CaptureTerminal)
sender() ! CommonMessages.Progress( sender() ! CommonMessages.Progress(
GenericHackables.GetHackSpeed(player, terminal), GenericHackables.GetHackSpeed(player, terminal),
CaptureTerminals.FinishHackingCaptureConsole(terminal, player, unk = -1), 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 package net.psforever.objects.serverobject.terminals.capture
import net.psforever.objects.Player 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.objects.sourcing.PlayerSource
import net.psforever.services.local.{LocalAction, LocalServiceMessage} import net.psforever.services.local.{LocalAction, LocalServiceMessage}
import net.psforever.types.PlanetSideEmpire
import scala.concurrent.duration._
import scala.util.{Failure, Success} import scala.util.{Failure, Success}
object CaptureTerminals {import scala.concurrent.duration._ object CaptureTerminals {
private val log = org.log4s.getLogger("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}") 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 package net.psforever.objects.serverobject.terminals.implant
import net.psforever.objects.serverobject.hackable.Hackable 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.structures.Amenity
import net.psforever.objects.serverobject.terminals.capture.CaptureTerminalAware import net.psforever.objects.serverobject.terminals.capture.CaptureTerminalAware
import net.psforever.objects.vital.resistance.StandardResistanceProfile 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.equipment.JammableUnit
import net.psforever.objects.serverobject.interior.Sidedness 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.structures.{Amenity, AmenityOwner, Building}
import net.psforever.objects.serverobject.terminals.capture.CaptureTerminalAware import net.psforever.objects.serverobject.terminals.capture.CaptureTerminalAware
import net.psforever.objects.serverobject.turret.auto.AutomatedTurret import net.psforever.objects.serverobject.turret.auto.AutomatedTurret
@ -23,6 +23,28 @@ class FacilityTurret(tDef: FacilityTurretDefinition)
WeaponTurret.LoadDefinition(turret = this) WeaponTurret.LoadDefinition(turret = this)
WhichSide = Sidedness.OutsideOf 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 = { def TurretOwner: SourceEntry = {
Seats Seats
.headOption .headOption

View file

@ -5,7 +5,6 @@ import net.psforever.objects.{GlobalDefinitions, Player, Tool}
import net.psforever.objects.equipment.Ammo import net.psforever.objects.equipment.Ammo
import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject}
import net.psforever.objects.serverobject.damage.Damageable 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.mount.Mountable
import net.psforever.objects.serverobject.repair.AmenityAutoRepair import net.psforever.objects.serverobject.repair.AmenityAutoRepair
import net.psforever.objects.serverobject.structures.{Building, PoweredAmenityControl} import net.psforever.objects.serverobject.structures.{Building, PoweredAmenityControl}
@ -100,7 +99,7 @@ class FacilityTurretControl(turret: FacilityTurret)
seatNumber: Int, seatNumber: Int,
player: Player): Boolean = { player: Player): Boolean = {
super.mountTest(obj, seatNumber, player) && 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 = { 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.Player
import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.mount.{Mountable, MountableBehavior} 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 trait MountableTurretControl
extends TurretControl extends TurretControl
@ -11,9 +15,22 @@ trait MountableTurretControl
override def TurretObject: PlanetSideServerObject with WeaponTurret with Mountable override def TurretObject: PlanetSideServerObject with WeaponTurret with Mountable
/** commonBehavior does not implement mountingBehavior; please do so when implementing */ /** commonBehavior does not implement mountingBehavior; please do so when implementing */
override def commonBehavior: Receive = override def commonBehavior: Receive = super.commonBehavior.orElse(dismountBehavior)
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( override protected def mountTest(
obj: PlanetSideServerObject with Mountable, 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.avatar.Certification
import net.psforever.objects.ce.Deployable import net.psforever.objects.ce.Deployable
import net.psforever.objects.serverobject.hackable.GenericHackables.updateTurretUpgradeTime
import net.psforever.objects.{Player, Tool, TurretDeployable} import net.psforever.objects.{Player, Tool, TurretDeployable}
import net.psforever.packet.game.{HackMessage, HackState, HackState1, HackState7, InventoryStateMessage} import net.psforever.packet.game.{HackMessage, HackState, HackState1, HackState7, InventoryStateMessage}
import net.psforever.services.Service import net.psforever.services.Service
@ -83,7 +82,7 @@ object WeaponTurrets {
} else if (turret.Destroyed) { } else if (turret.Destroyed) {
(HackState.Cancelled, 0) (HackState.Cancelled, 0)
} else { } else {
updateTurretUpgradeTime() turret.UpdateTurretUpgradeTime()
(HackState.Ongoing, progress.toInt) (HackState.Ongoing, progress.toInt)
} }
turret.Zone.AvatarEvents ! AvatarServiceMessage( turret.Zone.AvatarEvents ! AvatarServiceMessage(

View file

@ -60,7 +60,7 @@ trait AffectedByAutomaticTurretFire extends Damageable {
ProjectileReason(DamageResolution.Hit, modProjectile, target.DamageModel), ProjectileReason(DamageResolution.Hit, modProjectile, target.DamageModel),
correctedTargetPosition 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, Orientation: Vector3,
occupants: List[SourceEntry], occupants: List[SourceEntry],
unique: SourceUniqueness unique: SourceUniqueness
) extends SourceWithHealthEntry with SourceWithShieldsEntry { ) extends SourceWithHealthEntry with SourceWithShieldsEntry with MountableEntry {
def Name: String = SourceEntry.NameFormat(Definition.Descriptor) def Name: String = SourceEntry.NameFormat(Definition.Descriptor)
def Health: Int = health def Health: Int = health
def Shields: Int = shields def Shields: Int = shields

View file

@ -33,7 +33,7 @@ final case class VehicleSource(
occupants: List[SourceEntry], occupants: List[SourceEntry],
Modifiers: ResistanceProfile, Modifiers: ResistanceProfile,
unique: UniqueVehicle unique: UniqueVehicle
) extends SourceWithHealthEntry with SourceWithShieldsEntry { ) extends SourceWithHealthEntry with SourceWithShieldsEntry with MountableEntry {
def Name: String = SourceEntry.NameFormat(Definition.Name) def Name: String = SourceEntry.NameFormat(Definition.Name)
def Health: Int = health def Health: Int = health
def Shields: Int = shields def Shields: Int = shields

View file

@ -19,12 +19,12 @@ trait CargoBehavior {
val zone = obj.Zone val zone = obj.Zone
zone.GUID(isMounting) match { zone.GUID(isMounting) match {
case Some(v : Vehicle) => v.Actor ! CargoBehavior.EndCargoMounting(obj.GUID) case Some(v : Vehicle) => v.Actor ! CargoBehavior.EndCargoMounting(obj.GUID)
case _ => ; case _ => ()
} }
isMounting = None isMounting = None
zone.GUID(isDismounting) match { zone.GUID(isDismounting) match {
case Some(v: Vehicle) => v.Actor ! CargoBehavior.EndCargoDismounting(obj.GUID) case Some(v: Vehicle) => v.Actor ! CargoBehavior.EndCargoDismounting(obj.GUID)
case _ => ; case _ => ()
} }
isDismounting = None isDismounting = None
startCargoDismountingNoCleanup(bailed = false) startCargoDismountingNoCleanup(bailed = false)
@ -38,14 +38,10 @@ trait CargoBehavior {
startCargoDismounting(bailed) startCargoDismounting(bailed)
case CargoBehavior.EndCargoMounting(carrier_guid) => case CargoBehavior.EndCargoMounting(carrier_guid) =>
if (isMounting.contains(carrier_guid)) { endCargoMounting(carrier_guid)
isMounting = None
}
case CargoBehavior.EndCargoDismounting(carrier_guid) => case CargoBehavior.EndCargoDismounting(carrier_guid) =>
if (isDismounting.contains(carrier_guid)) { endCargoDismounting(carrier_guid)
isDismounting = None
}
} }
def startCargoMounting(carrier_guid: PlanetSideGUID, mountPoint: Int): Unit = { def startCargoMounting(carrier_guid: PlanetSideGUID, mountPoint: Int): Unit = {
@ -84,6 +80,18 @@ trait CargoBehavior {
} }
.nonEmpty .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 { object CargoBehavior {

View file

@ -20,6 +20,7 @@ import net.psforever.services.Service
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
import net.psforever.types._ import net.psforever.types._
import scala.annotation.unused
import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._ import scala.concurrent.duration._
@ -170,8 +171,21 @@ class BfrControl(vehicle: Vehicle)
specialArmWeaponEquipManagement(item, slot, handiness) specialArmWeaponEquipManagement(item, slot, handiness)
} }
override def dismountCleanup(seatBeingDismounted: Int, player: Player): Unit = { override def mountActionResponse(user: Player, @unused mountPoint: Int, seatNumber: Int): Unit = {
super.dismountCleanup(seatBeingDismounted, player) 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)) { if (!vehicle.Seats.values.exists(_.isOccupied)) {
vehicle vehicle
.Subsystems(VehicleSubsystemEntry.BattleframeShieldGenerator) match { .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 = { override def permitTerminalMessage(player: Player, msg: ItemTransactionMessage): Boolean = {
if (msg.transaction_type == TransactionType.Loadout) { if (msg.transaction_type == TransactionType.Loadout) {
!vehicle.Jammed !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)) { if (item.Size == EquipmentSize.BFRArmWeapon && vehicle.VisibleSlots.contains(slot)) {
val weapons = vehicle.Weapons val weapons = vehicle.Weapons
//budget logic: the arm weapons are "next to each other" index-wise //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) class CargoCarrierControl(vehicle: Vehicle)
extends VehicleControl(vehicle) extends VehicleControl(vehicle)
with CarrierBehavior { 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 = { override def postStop() : Unit = {
super.postStop() super.postStop()

View file

@ -5,7 +5,6 @@ import akka.actor.ActorRef
import net.psforever.objects._ import net.psforever.objects._
import net.psforever.objects.serverobject.deploy.Deployment.DeploymentObject import net.psforever.objects.serverobject.deploy.Deployment.DeploymentObject
import net.psforever.objects.serverobject.deploy.{Deployment, DeploymentBehavior} import net.psforever.objects.serverobject.deploy.{Deployment, DeploymentBehavior}
import net.psforever.objects.serverobject.mount.Mountable
import net.psforever.types._ import net.psforever.types._
/** /**
@ -36,13 +35,10 @@ class DeployingVehicleControl(vehicle: Vehicle)
*/ */
override def commonDisabledBehavior : Receive = override def commonDisabledBehavior : Receive =
super.commonDisabledBehavior super.commonDisabledBehavior
.orElse(dismountBehavior)
.orElse { .orElse {
case msg : Deployment.TryUndeploy => case msg: Deployment.TryUndeploy =>
deployBehavior.apply(msg) deployBehavior.apply(msg)
case msg @ Mountable.TryDismount(player, seat_num, _) =>
dismountBehavior.apply(msg)
dismountCleanup(seat_num, player)
} }
/** /**

View file

@ -15,12 +15,12 @@ import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObjec
import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior} import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior}
import net.psforever.objects.serverobject.containable.{Containable, ContainableBehavior} import net.psforever.objects.serverobject.containable.{Containable, ContainableBehavior}
import net.psforever.objects.serverobject.damage.Damageable.Target import net.psforever.objects.serverobject.damage.Damageable.Target
import net.psforever.objects.serverobject.damage.{AggravatedBehavior, DamageableVehicle} import net.psforever.objects.serverobject.damage.{AggravatedBehavior, Damageable, DamageableVehicle}
import net.psforever.objects.serverobject.environment._ import net.psforever.objects.serverobject.environment._
import net.psforever.objects.serverobject.environment.interaction.common.Watery import net.psforever.objects.serverobject.environment.interaction.common.Watery
import net.psforever.objects.serverobject.environment.interaction.{InteractWithEnvironment, RespondsToZoneEnvironment} import net.psforever.objects.serverobject.environment.interaction.{InteractWithEnvironment, RespondsToZoneEnvironment}
import net.psforever.objects.serverobject.hackable.GenericHackables 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.repair.RepairableVehicle
import net.psforever.objects.serverobject.structures.WarpGate import net.psforever.objects.serverobject.structures.WarpGate
import net.psforever.objects.serverobject.terminals.Terminal 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._
import net.psforever.objects.vehicles.interaction.WithWater import net.psforever.objects.vehicles.interaction.WithWater
import net.psforever.objects.vital.interaction.DamageResult 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._
import net.psforever.objects.zones.interaction.IndependentZoneInteraction
import net.psforever.packet.PlanetSideGamePacket import net.psforever.packet.PlanetSideGamePacket
import net.psforever.packet.game._ import net.psforever.packet.game._
import net.psforever.packet.game.objectcreate.ObjectCreateMessageParent 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.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage} import net.psforever.services.vehicle.{VehicleAction, VehicleServiceMessage}
import scala.annotation.unused
import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._ import scala.concurrent.duration._
import scala.util.Random import scala.util.Random
@ -63,7 +65,8 @@ class VehicleControl(vehicle: Vehicle)
with AggravatedBehavior with AggravatedBehavior
with RespondsToZoneEnvironment with RespondsToZoneEnvironment
with CargoBehavior with CargoBehavior
with AffectedByAutomaticTurretFire { with AffectedByAutomaticTurretFire
with IndependentZoneInteraction {
//make control actors belonging to utilities when making control actor belonging to vehicle //make control actors belonging to utilities when making control actor belonging to vehicle
vehicle.Utilities.foreach { case (_, util) => util.Setup } vehicle.Utilities.foreach { case (_, util) => util.Setup }
@ -77,6 +80,7 @@ class VehicleControl(vehicle: Vehicle)
def InteractiveObject: Vehicle = vehicle def InteractiveObject: Vehicle = vehicle
def CargoObject: Vehicle = vehicle def CargoObject: Vehicle = vehicle
def AffectedObject: Vehicle = vehicle def AffectedObject: Vehicle = vehicle
def ZoneInteractionObject: Vehicle = vehicle
/** cheap flag for whether the vehicle is decaying */ /** cheap flag for whether the vehicle is decaying */
var decaying : Boolean = false var decaying : Boolean = false
@ -84,8 +88,6 @@ class VehicleControl(vehicle: Vehicle)
var decayTimer : Cancellable = Default.Cancellable var decayTimer : Cancellable = Default.Cancellable
/** becoming waterlogged, or drying out? */ /** becoming waterlogged, or drying out? */
var submergedCondition : Option[OxygenState] = None var submergedCondition : Option[OxygenState] = None
/** ... */
var passengerRadiationCloudTimer: Cancellable = Default.Cancellable
def receive : Receive = Enabled def receive : Receive = Enabled
@ -94,7 +96,7 @@ class VehicleControl(vehicle: Vehicle)
damageableVehiclePostStop() damageableVehiclePostStop()
decaying = false decaying = false
decayTimer.cancel() decayTimer.cancel()
passengerRadiationCloudTimer.cancel() StopInteractionSelfReporting()
vehicle.Utilities.values.foreach { util => vehicle.Utilities.values.foreach { util =>
context.stop(util().Actor) context.stop(util().Actor)
util().Actor = Default.Actor util().Actor = Default.Actor
@ -103,7 +105,80 @@ class VehicleControl(vehicle: Vehicle)
endAllCargoOperations() 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 def commonEnabledBehavior: Receive = checkBehavior
.orElse(mountingFailureReasons)
.orElse(mountBehavior)
.orElse(dismountingFailureReasons)
.orElse(dismountBehavior)
.orElse(attributeBehavior) .orElse(attributeBehavior)
.orElse(jammableBehavior) .orElse(jammableBehavior)
.orElse(takesDamage) .orElse(takesDamage)
@ -113,6 +188,7 @@ class VehicleControl(vehicle: Vehicle)
.orElse(environmentBehavior) .orElse(environmentBehavior)
.orElse(cargoBehavior) .orElse(cargoBehavior)
.orElse(takeAutomatedDamage) .orElse(takeAutomatedDamage)
.orElse(zoneInteractionBehavior)
.orElse { .orElse {
case Vehicle.Ownership(None) => case Vehicle.Ownership(None) =>
LoseOwnership() LoseOwnership()
@ -120,79 +196,6 @@ class VehicleControl(vehicle: Vehicle)
case Vehicle.Ownership(Some(player)) => case Vehicle.Ownership(Some(player)) =>
GainOwnership(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) => case CommonMessages.ChargeShields(amount, motivator) =>
chargeShields(amount, motivator.collect { case o: PlanetSideGameObject with FactionAffinity => SourceEntry(o) }) 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)) events ! VehicleServiceMessage(toChannel, VehicleAction.SendResponse(guid0, pkt))
} }
case FactionAffinity.ConvertFactionAffinity(faction) => case FactionAffinity.ConvertFactionAffinity(faction) =>
val originalAffinity = vehicle.Faction val originalAffinity = vehicle.Faction
if (originalAffinity != (vehicle.Faction = faction)) { if (originalAffinity != (vehicle.Faction = faction)) {
@ -251,7 +253,7 @@ class VehicleControl(vehicle: Vehicle)
AvatarAction.TerminalOrderResult(msg.terminal_guid, msg.transaction_type, result = true) AvatarAction.TerminalOrderResult(msg.terminal_guid, msg.transaction_type, result = true)
) )
case _ => ; case _ => ()
} }
} else { } else {
zone.AvatarEvents ! AvatarServiceMessage( zone.AvatarEvents ! AvatarServiceMessage(
@ -288,20 +290,12 @@ class VehicleControl(vehicle: Vehicle)
final def Enabled: Receive = final def Enabled: Receive =
commonEnabledBehavior commonEnabledBehavior
.orElse { .orElse {
case VehicleControl.RadiationTick if !passengerRadiationCloudTimer.isCancelled =>
vehicle
.interaction()
.find(_.Type == RadiationInMountableInteraction)
.foreach(_.interaction(vehicle.getInteractionSector, vehicle))
case _ => () case _ => ()
} }
def commonDisabledBehavior: Receive = checkBehavior def commonDisabledBehavior: Receive = checkBehavior
.orElse(dismountBehavior)
.orElse { .orElse {
case msg @ Mountable.TryDismount(user, seat_num, _) =>
dismountBehavior.apply(msg)
dismountCleanup(seat_num, user)
case Vehicle.Deconstruct(time) => case Vehicle.Deconstruct(time) =>
time match { time match {
case Some(delay) if vehicle.Definition.undergoesDecay => case Some(delay) if vehicle.Definition.undergoesDecay =>
@ -320,7 +314,7 @@ class VehicleControl(vehicle: Vehicle)
final def Disabled: Receive = commonDisabledBehavior final def Disabled: Receive = commonDisabledBehavior
.orElse { .orElse {
case _ => ; case _ => ()
} }
def commonDeleteBehavior: Receive = checkBehavior def commonDeleteBehavior: Receive = checkBehavior
@ -336,7 +330,7 @@ class VehicleControl(vehicle: Vehicle)
final def ReadyToDelete: Receive = commonDeleteBehavior final def ReadyToDelete: Receive = commonDeleteBehavior
.orElse { .orElse {
case _ => ; case _ => ()
} }
override protected def mountTest( override protected def mountTest(
@ -354,39 +348,32 @@ class VehicleControl(vehicle: Vehicle)
super.mountTest(obj, seatNumber, user) super.mountTest(obj, seatNumber, user)
} }
def mountCleanup(mount_point: Int, user: Player): Unit = { override def mountActionResponse(user: Player, @unused mountPoint: Int, seatNumber: Int): Unit = {
vehicle.PassengerInSeat(user) match { super.mountActionResponse(user, mountPoint, seatNumber)
case Some(0) => //driver seat val vsrc = VehicleSource(vehicle)
val vsrc = VehicleSource(vehicle) user.LogActivity(MountingActivity(vsrc, PlayerSource.inSeat(user, vsrc, seatNumber), vehicle.Zone.Number))
user.LogActivity(VehicleMountActivity(vsrc, PlayerSource.inSeat(user, vsrc, seatNumber = 0), vehicle.Zone.Number)) if (seatNumber == 0) {
//if the driver mount, change ownership if that is permissible for this vehicle //if the driver mount, change ownership if that is permissible for this vehicle
if (!vehicle.OwnerName.contains(user.Name) && vehicle.Definition.CanBeOwned.nonEmpty) { if (!vehicle.OwnerName.contains(user.Name) && vehicle.Definition.CanBeOwned.nonEmpty) {
//whatever vehicle was previously owned //whatever vehicle was previously owned
vehicle.Zone.GUID(user.avatar.vehicle) match { vehicle.Zone.GUID(user.avatar.vehicle) match {
case Some(v: Vehicle) => case Some(v: Vehicle) =>
v.Actor ! Vehicle.Ownership(None) v.Actor ! Vehicle.Ownership(None)
case _ => case _ =>
user.avatar.vehicle = None user.avatar.vehicle = None
}
GainOwnership(user) //gain new ownership
} else {
decaying = false
decayTimer.cancel()
} }
passengerRadiationCloudTimer.cancel() GainOwnership(user) //gain new ownership
updateZoneInteractionProgressUI(user) } else {
case Some(seatNumber) => //literally any other seat
val vsrc = VehicleSource(vehicle)
user.LogActivity(VehicleMountActivity(vsrc, PlayerSource.inSeat(user, vsrc, seatNumber), vehicle.Zone.Number))
decaying = false decaying = false
decayTimer.cancel() decayTimer.cancel()
if (!vehicle.Seats(0).isOccupied && passengerRadiationCloudTimer.isCancelled) { }
StartRadiationSelfReporting() TryStopInteractionSelfReporting()
} updateZoneInteractionProgressUI(user)
updateZoneInteractionProgressUI(user) } else {
decaying = false
case None => () decayTimer.cancel()
StopInteractionSelfReporting()
updateZoneInteractionProgressUI(user)
} }
} }
@ -398,48 +385,37 @@ class VehicleControl(vehicle: Vehicle)
vehicle.DeploymentState == DriveState.Deployed || super.dismountTest(obj, seatNumber, user) 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 obj = MountableObject
val allSeatsUnoccupied = !obj.Seats.values.exists(_.isOccupied) if (seatBeingDismounted == 0) {
// Reset velocity to zero when driver dismounts, to allow jacking/repair if vehicle was moving slightly before dismount
if (!obj.Seats(0).isOccupied) {
obj.Velocity = Some(Vector3.Zero) obj.Velocity = Some(Vector3.Zero)
} }
if (allSeatsUnoccupied) { if (TestToStartSelfReporting()) {
passengerRadiationCloudTimer.cancel() StartInteractionSelfReporting()
} else if (seatBeingDismounted == 0) {
StartRadiationSelfReporting()
} }
if (!obj.Seats(seatBeingDismounted).isOccupied) { //this seat really was vacated //we were only owning the vehicle while we sat in its driver seat
user.LogActivity(VehicleDismountActivity(VehicleSource(vehicle), PlayerSource(user), vehicle.Zone.Number)) val canBeOwned = obj.Definition.CanBeOwned
//we were only owning the vehicle while we sat in its driver seat if (canBeOwned.contains(false) && seatBeingDismounted == 0) {
val canBeOwned = obj.Definition.CanBeOwned LoseOwnership()
if (canBeOwned.contains(false) && seatBeingDismounted == 0) { }
LoseOwnership() //are we already decaying? are we unowned? is no one seated anywhere?
} if (!decaying &&
//are we already decaying? are we unowned? is no one seated anywhere? obj.Definition.undergoesDecay &&
if (!decaying && obj.OwnerGuid.isEmpty &&
obj.Definition.undergoesDecay && !vehicle.Seats.values.exists(_.isOccupied)) {
obj.OwnerGuid.isEmpty && decaying = true
allSeatsUnoccupied) { decayTimer = context.system.scheduler.scheduleOnce(
decaying = true MountableObject.Definition.DeconstructionTime.getOrElse(5 minutes),
decayTimer = context.system.scheduler.scheduleOnce( self,
MountableObject.Definition.DeconstructionTime.getOrElse(5 minutes), VehicleControl.PrepareForDeletion()
self, )
VehicleControl.PrepareForDeletion()
)
}
} }
} }
private def StartRadiationSelfReporting(): Unit = { def TestToStartSelfReporting(): Boolean = {
passengerRadiationCloudTimer.cancel() vehicle.MountedIn.isEmpty && !vehicle.Seats.values.exists(_.isOccupied)
passengerRadiationCloudTimer = context.system.scheduler.scheduleWithFixedDelay(
250.milliseconds,
250.milliseconds,
self,
VehicleControl.RadiationTick
)
} }
def PrepareForDisabled(kickPassengers: Boolean) : Unit = { def PrepareForDisabled(kickPassengers: Boolean) : Unit = {
@ -568,7 +544,7 @@ class VehicleControl(vehicle: Vehicle)
VehicleAction.InventoryState2(Service.defaultPlayerGUID, box.GUID, iguid, box.Capacity) 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) VehicleAction.KickPassenger(tplayer.GUID, 4, unk2 = false, vguid)
) )
} }
case _ => ; // No player seated case _ => () // No player seated
} }
} }
vehicle.CargoHolds.foreach { vehicle.CargoHolds.foreach {
@ -724,11 +700,11 @@ class VehicleControl(vehicle: Vehicle)
// Instruct client to start bail dismount procedure // Instruct client to start bail dismount procedure
self ! DismountVehicleCargoMsg(dguid, cargo.GUID, bailed = true, requestedByPassenger = false, kicked = false) 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 { } else {
log.warn( log.warn(
@ -766,10 +742,39 @@ class VehicleControl(vehicle: Vehicle)
} }
} }
override protected def canChangeVulnerability(state: Damageable.PersonalVulnerability): Boolean = {
state match {
case Damageable.MakeInvulnerable =>
vehicle.LastDamage.exists { d => System.currentTimeMillis() - d.interaction.hitTime > 2500L }
case _ =>
true
}
}
override protected def DestructionAwareness(target: Target, cause: DamageResult): Unit = { override protected def DestructionAwareness(target: Target, cause: DamageResult): Unit = {
passengerRadiationCloudTimer.cancel() StopInteractionSelfReportingNoReset()
super.DestructionAwareness(target, cause) 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 { object VehicleControl {
@ -779,7 +784,5 @@ object VehicleControl {
private case class Deletion() private case class Deletion()
private case object RadiationTick
final case class AssignOwnership(player: Option[Player]) 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 // Copyright (c) 2021 PSForever
package net.psforever.objects.vehicles package net.psforever.objects.vehicles.interaction
import net.psforever.objects.Vehicle 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.blockmap.SectorPopulation
import net.psforever.objects.zones.interaction.InteractsWithZone 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.PlanetSideGameObject
import net.psforever.objects.definition.{EquipmentDefinition, KitDefinition, ToolDefinition} import net.psforever.objects.definition.{EquipmentDefinition, KitDefinition, ToolDefinition}
import net.psforever.objects.serverobject.affinity.FactionAffinity 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.environment.EnvironmentReason
import net.psforever.objects.vital.etc.{ExplodingEntityReason, PainboxReason, SuicideReason} import net.psforever.objects.vital.etc.{ExplodingEntityReason, PainboxReason, SuicideReason}
import net.psforever.objects.vital.interaction.{DamageInteraction, DamageResult} 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]) final case class ShieldCharge(amount: Int, cause: Option[SourceEntry])
extends GeneralActivity extends GeneralActivity
trait TerminalUse {
def terminal: AmenitySource
}
final case class TerminalUsedActivity(terminal: AmenitySource, transaction: TransactionType.Value) 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) final case class TelepadUseActivity(router: VehicleSource, telepad: DeployableSource, player: PlayerSource)
extends GeneralActivity extends GeneralActivity
sealed trait VehicleMountChange extends GeneralActivity { sealed trait MountChange extends GeneralActivity {
def vehicle: VehicleSource def mount: SourceEntry with MountableEntry
def zoneNumber: Int def zoneNumber: Int
} }
sealed trait VehiclePassengerMountChange extends VehicleMountChange { sealed trait PassengerMountChange extends MountChange {
def player: PlayerSource def player: PlayerSource
} }
sealed trait VehicleCargoMountChange extends VehicleMountChange { sealed trait CargoMountChange extends MountChange {
def cargo: VehicleSource def cargo: VehicleSource
} }
final case class VehicleMountActivity(vehicle: VehicleSource, player: PlayerSource, zoneNumber: Int) final case class MountingActivity(mount: SourceEntry with MountableEntry, player: PlayerSource, zoneNumber: Int)
extends VehiclePassengerMountChange extends PassengerMountChange
final case class VehicleDismountActivity( final case class DismountingActivity(
vehicle: VehicleSource, mount: SourceEntry with MountableEntry,
player: PlayerSource, player: PlayerSource,
zoneNumber: Int, zoneNumber: Int,
pairedEvent: Option[VehicleMountActivity] = None pairedEvent: Option[MountingActivity] = None
) extends VehiclePassengerMountChange ) extends PassengerMountChange
final case class VehicleCargoMountActivity(vehicle: VehicleSource, cargo: VehicleSource, zoneNumber: Int) final case class VehicleCargoMountActivity(mount: VehicleSource, cargo: VehicleSource, zoneNumber: Int)
extends VehicleCargoMountChange extends CargoMountChange
final case class VehicleCargoDismountActivity( final case class VehicleCargoDismountActivity(
vehicle: VehicleSource, mount: VehicleSource,
cargo: VehicleSource, cargo: VehicleSource,
zoneNumber: Int, zoneNumber: Int,
pairedEvent: Option[VehicleCargoMountActivity] = None pairedEvent: Option[VehicleCargoMountActivity] = None
) extends VehicleCargoMountChange ) extends CargoMountChange
final case class Contribution(src: SourceUniqueness, entries: List[InGameActivity]) final case class Contribution(src: SourceUniqueness, entries: List[InGameActivity])
extends GeneralActivity { 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) final case class HealFromEquipment(user: PlayerSource, equipment_def: EquipmentDefinition, amount: Int)
extends HealingActivity with SupportActivityCausedByAnother extends HealingActivity with SupportActivityCausedByAnother
final case class HealFromTerminal(term: AmenitySource, amount: Int) final case class HealFromTerminal(terminal: AmenitySource, amount: Int)
extends HealingActivity extends HealingActivity with TerminalUse
final case class HealFromImplant(implant: ImplantType, amount: Int) final case class HealFromImplant(implant: ImplantType, amount: Int)
extends HealingActivity 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) final case class RepairFromEquipment(user: PlayerSource, equipment_def: EquipmentDefinition, amount: Int)
extends RepairingActivity with SupportActivityCausedByAnother 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) final case class RepairFromArmorSiphon(siphon_def: ToolDefinition, vehicle: VehicleSource, amount: Int)
extends RepairingActivity extends RepairingActivity
@ -251,24 +256,24 @@ trait InGameHistory {
*/ */
def LogActivity(action: Option[InGameActivity]): List[InGameActivity] = { def LogActivity(action: Option[InGameActivity]): List[InGameActivity] = {
action match { action match {
case Some(act: VehicleDismountActivity) if act.pairedEvent.isEmpty => case Some(act: DismountingActivity) if act.pairedEvent.isEmpty =>
history history
.findLast(_.isInstanceOf[VehicleMountActivity]) .findLast(_.isInstanceOf[MountingActivity])
.collect { .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) history = history :+ InGameActivity.ShareTime(act.copy(pairedEvent = Some(event)), act)
} }
.orElse { .orElse {
history = history :+ act history = history :+ act
None None
} }
case Some(act: VehicleDismountActivity) => case Some(act: DismountingActivity) =>
history = history :+ act history = history :+ act
case Some(act: VehicleCargoDismountActivity) => case Some(act: VehicleCargoDismountActivity) =>
history history
.findLast(_.isInstanceOf[VehicleCargoMountActivity]) .findLast(_.isInstanceOf[VehicleCargoMountActivity])
.collect { .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) history = history :+ InGameActivity.ShareTime(act.copy(pairedEvent = Some(event)), act)
} }
.orElse { .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. eventually, they stop logging in.
Anyway, this has nothing to do with that. 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 def source: DamageProperties = SuicideReason.damageProperties

View file

@ -1809,6 +1809,29 @@ object Zone {
/* explosions */ /* 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 * Allocates `Damageable` targets within the vicinity of server-prepared damage dealing
* and informs those entities that they have affected by the aforementioned damage. * 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 zone the zone in which the damage should occur
* @param source the entity that embodies the damage (information) * @param source the entity that embodies the damage (information)
* @param createInteraction how the interaction for this damage is to prepared * @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 testTargetsFromZone a custom test for determining whether the allocated targets are affected by the damage;
* @param acquireTargetsFromZone the main target-collecting algorithm * filters targets from the existing selection
* @param acquireTargetsFromZone the main target-collecting algorithm;
* collects targets from sector information
* @return a list of affected entities; * @return a list of affected entities;
* only mostly complete due to the exclusion of objects whose damage resolution is different than usual * only mostly complete due to the exclusion of objects whose damage resolution is different than usual
*/ */
@ -1825,8 +1850,8 @@ object Zone {
zone: Zone, zone: Zone,
source: PlanetSideGameObject with FactionAffinity with Vitality, source: PlanetSideGameObject with FactionAffinity with Vitality,
createInteraction: (PlanetSideGameObject with FactionAffinity with Vitality, PlanetSideGameObject with FactionAffinity with Vitality) => DamageInteraction, createInteraction: (PlanetSideGameObject with FactionAffinity with Vitality, PlanetSideGameObject with FactionAffinity with Vitality) => DamageInteraction,
testTargetsFromZone: (PlanetSideGameObject, PlanetSideGameObject, Float) => Boolean = distanceCheck, testTargetsFromZone: (PlanetSideGameObject, PlanetSideGameObject, Float) => Boolean,
acquireTargetsFromZone: (Zone, PlanetSideGameObject with FactionAffinity with Vitality, DamageWithPosition) => List[PlanetSideServerObject with Vitality] = findAllTargets acquireTargetsFromZone: (Zone, PlanetSideGameObject with FactionAffinity with Vitality, DamageWithPosition) => List[PlanetSideServerObject with Vitality]
): List[PlanetSideServerObject] = { ): List[PlanetSideServerObject] = {
source.Definition.innateDamage match { source.Definition.innateDamage match {
case Some(damage) => case Some(damage) =>
@ -1851,8 +1876,10 @@ object Zone {
* @param zone the zone in which the damage should occur * @param zone the zone in which the damage should occur
* @param source the entity that embodies the damage (information) * @param source the entity that embodies the damage (information)
* @param createInteraction how the interaction for this damage is to prepared * @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 testTargetsFromZone a custom test for determining whether the allocated targets are affected by the damage;
* @param acquireTargetsFromZone the main target-collecting algorithm * filters targets from the existing selection
* @param acquireTargetsFromZone the main target-collecting algorithm;
* collects targets from sector information
* @return a list of affected entities; * @return a list of affected entities;
* only mostly complete due to the exclusion of objects whose damage resolution is different than usual * 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 akka.actor.ActorRef
import net.psforever.objects.GlobalDefinitions import net.psforever.objects.GlobalDefinitions
import net.psforever.objects.avatar.scoring.{Kill, SupportActivity} import net.psforever.objects.avatar.scoring.{Kill, SupportActivity}
import net.psforever.objects.sourcing.{BuildingSource, PlayerSource, SourceEntry, SourceUniqueness, TurretSource, VehicleSource} import net.psforever.objects.sourcing.{BuildingSource, MountableEntry, PlayerSource, SourceEntry, SourceUniqueness, TurretSource, UniquePlayer, VehicleSource}
import net.psforever.objects.vital.{Contribution, HealFromTerminal, InGameActivity, RepairFromTerminal, RevivingActivity, TelepadUseActivity, TerminalUsedActivity, VehicleCargoDismountActivity, VehicleCargoMountActivity, VehicleDismountActivity, VehicleMountActivity} 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.vital.projectile.ProjectileReason
import net.psforever.objects.zones.exp.rec.{CombinedHealthAndArmorContributionProcess, MachineRecoveryExperienceContributionProcess} import net.psforever.objects.zones.exp.rec.{CombinedHealthAndArmorContributionProcess, MachineRecoveryExperienceContributionProcess}
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} 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 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 there are considerations of time and distance traveled before the kill as well
*/ */
case out: VehicleDismountActivity case out: DismountingActivity
if !out.vehicle.owner.contains(out.player.unique) && out.pairedEvent.nonEmpty => (out.pairedEvent.get, out) if out.mount.isInstanceOf[VehicleSource] &&
!ownershipFromMount(out.mount).contains(out.player.unique) &&
out.pairedEvent.nonEmpty => (out.pairedEvent.get, out)
} }
.collect { .collect {
case (in: VehicleMountActivity, out: VehicleDismountActivity) case (in: MountingActivity, out: DismountingActivity)
if in.vehicle.unique == out.vehicle.unique && if in.mount.unique == out.mount.unique &&
out.vehicle.Faction == out.player.Faction && out.mount.Faction == out.player.Faction &&
/* /*
considerations of time and distance transported before the kill considerations of time and distance transported before the kill
*/ */
@ -407,7 +409,7 @@ object KillContributions {
} }
} || { } || {
val sameZone = in.zoneNumber == out.zoneNumber 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 distanceMoved = {
val killLocation = killerOpt.map(_.Position.xy).getOrElse(Vector3.Zero) val killLocation = killerOpt.map(_.Position.xy).getOrElse(Vector3.Zero)
Vector3.DistanceSquared(killLocation, out.player.Position.xy) Vector3.DistanceSquared(killLocation, out.player.Position.xy)
@ -423,9 +425,9 @@ object KillContributions {
} }
//apply //apply
dismountActivity dismountActivity
.groupBy { _.vehicle } .groupBy { _.mount }
.collect { case (mount, dismountsFromVehicle) if mount.owner.nonEmpty => .collect { case (mount, dismountsFromVehicle) if ownershipFromMount(mount).nonEmpty =>
val promotedOwner = PlayerSource(mount.owner.get, mount.Position) val promotedOwner = PlayerSource(ownershipFromMount(mount).get, mount.Position)
val size = dismountsFromVehicle.size val size = dismountsFromVehicle.size
val time = dismountsFromVehicle.maxBy(_.time).time val time = dismountsFromVehicle.maxBy(_.time).time
List((HotDropKillAssist(mount.Definition.ObjectId, 0), "hotdrop", promotedOwner)) 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> * Gather and reward specific in-game equipment use activity.<br>
* na * na
@ -486,14 +506,14 @@ object KillContributions {
val dismountActivity = history val dismountActivity = history
.collect { .collect {
case out: VehicleCargoDismountActivity 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 { .collect {
case (in: VehicleCargoMountActivity, out: VehicleCargoDismountActivity) case (in: VehicleCargoMountActivity, out: VehicleCargoDismountActivity)
if in.vehicle.unique == out.vehicle.unique && if in.mount.unique == out.mount.unique &&
out.vehicle.Faction == out.cargo.Faction && out.mount.Faction == out.cargo.Faction &&
(in.vehicle.Definition == GlobalDefinitions.router || { (in.mount.Definition == GlobalDefinitions.router || {
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 distanceMoved = {
val killLocation = kill.info.adversarial val killLocation = kill.info.adversarial
.collect { adversarial => adversarial.attacker.Position.xy } .collect { adversarial => adversarial.attacker.Position.xy }
@ -513,7 +533,7 @@ object KillContributions {
val promotedOwner = PlayerSource(mount.owner.get, mount.Position) val promotedOwner = PlayerSource(mount.owner.get, mount.Position)
val mountId = mount.Definition.ObjectId val mountId = mount.Definition.ObjectId
dismountsFromVehicle dismountsFromVehicle
.groupBy(_.vehicle) .groupBy(_.mount)
.map { case (vehicle, events) => .map { case (vehicle, events) =>
val size = events.size val size = events.size
val time = events.maxBy(_.time).time val time = events.maxBy(_.time).time
@ -673,8 +693,6 @@ object KillContributions {
): Unit = { ): Unit = {
history history
.collect { .collect {
case h: HealFromTerminal => (h.term, h)
case r: RepairFromTerminal => (r.term, r)
case t: TerminalUsedActivity => (t.terminal, t) case t: TerminalUsedActivity => (t.terminal, t)
} }
.groupBy(_._1.unique) .groupBy(_._1.unique)

View file

@ -2,7 +2,7 @@
package net.psforever.objects.zones.exp package net.psforever.objects.zones.exp
import net.psforever.objects.sourcing.PlayerSource 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.types.{ExoSuitType, PlanetSideEmpire}
import net.psforever.util.{Config, DefinitionUtil, ThreatAssessment, ThreatLevel} import net.psforever.util.{Config, DefinitionUtil, ThreatAssessment, ThreatLevel}
@ -82,7 +82,7 @@ object Support {
val wornTime: mutable.HashMap[Int, Long] = mutable.HashMap[Int, Long]() val wornTime: mutable.HashMap[Int, Long] = mutable.HashMap[Int, Long]()
var currentSuit: Int = initialExosuit.id var currentSuit: Int = initialExosuit.id
var lastActTime: Long = history.head.time 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 //collect history events that encompass changes to exo-suits and to mounting conditions
history.collect { history.collect {
case suitChange: ExoSuitChange => case suitChange: ExoSuitChange =>
@ -93,7 +93,7 @@ object Support {
) )
currentSuit = suitChange.exosuit.id currentSuit = suitChange.exosuit.id
lastActTime = suitChange.time lastActTime = suitChange.time
case mount: VehicleMountActivity => case mount: MountingActivity =>
updateEquippedEntry( updateEquippedEntry(
currentSuit, currentSuit,
mount.time - lastActTime, mount.time - lastActTime,
@ -101,18 +101,18 @@ object Support {
) )
lastActTime = mount.time lastActTime = mount.time
lastMountAct = Some(mount) lastMountAct = Some(mount)
case dismount: VehicleDismountActivity case dismount: DismountingActivity
if dismount.pairedEvent.isEmpty => if dismount.pairedEvent.isEmpty =>
updateEquippedEntry( updateEquippedEntry(
dismount.vehicle.Definition.ObjectId, dismount.mount.Definition.ObjectId,
dismount.time - lastActTime, dismount.time - lastActTime,
wornTime wornTime
) )
lastActTime = dismount.time lastActTime = dismount.time
lastMountAct = None lastMountAct = None
case dismount: VehicleDismountActivity => case dismount: DismountingActivity =>
updateEquippedEntry( updateEquippedEntry(
dismount.vehicle.Definition.ObjectId, dismount.mount.Definition.ObjectId,
dismount.time - dismount.pairedEvent.get.time, dismount.time - dismount.pairedEvent.get.time,
wornTime wornTime
) )
@ -125,7 +125,7 @@ object Support {
.collect { mount => .collect { mount =>
//dying in a vehicle is a reason to care about the last mount activity //dying in a vehicle is a reason to care about the last mount activity
updateEquippedEntry( updateEquippedEntry(
mount.vehicle.Definition.ObjectId, mount.mount.Definition.ObjectId,
lastTime - mount.time, lastTime - mount.time,
wornTime 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.{GlobalDefinitions, LocalLockerItem, LocalProjectile}
import net.psforever.objects.definition.BasicDefinition import net.psforever.objects.definition.BasicDefinition
import net.psforever.objects.guid.selector.{NumberSelector, RandomSelector, SpecificSelector} 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.doors.{Door, DoorDefinition, SpawnTubeDoor}
import net.psforever.objects.serverobject.generator.Generator import net.psforever.objects.serverobject.generator.Generator
import net.psforever.objects.serverobject.llu.{CaptureFlagSocket, CaptureFlagSocketDefinition} import net.psforever.objects.serverobject.llu.{CaptureFlagSocket, CaptureFlagSocketDefinition}
@ -100,17 +101,9 @@ object Zones {
"PathPoints" "PathPoints"
)(ZipLinePath.apply) )(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) // 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 // 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( private val ignoredEntities = Seq("monolith")
"monolith",
"force_dome_dsp_physics",
"force_dome_comm_physics",
"force_dome_cryo_physics",
"force_dome_tech_physics",
"force_dome_amp_physics"
)
private val towerTypes = Seq("tower_a", "tower_b", "tower_c") 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") private val facilityTypes = Seq("amp_station", "cryo_facility", "comm_station", "comm_station_dsp", "tech_plant")
@ -129,6 +122,13 @@ object Zones {
"vt_spawn", "vt_spawn",
"vt_vehicle" "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( private val cavernBuildingTypes = Seq(
"ceiling_bldg_a", "ceiling_bldg_a",
"ceiling_bldg_b", "ceiling_bldg_b",
@ -382,11 +382,27 @@ object Zones {
createObjects( createObjects(
zoneMap, zoneMap,
zoneObjects.filterNot { _.objectType.startsWith("bfr_") }, zoneObjects.filterNot { obj => obj.objectType.startsWith("bfr_") || forceDomeTypes.contains(obj.objectType) },
ownerGuid = 0, ownerGuid = 0,
None, None,
turretWeaponGuid 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 => lattice.asObject.get(mapid).foreach { obj =>
obj.asArray.get.foreach { entry => obj.asArray.get.foreach { entry =>
@ -716,7 +732,6 @@ object Zones {
case _ => () case _ => ()
} }
} }
} }