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