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 472834d05..d7e2515bf 100644 --- a/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala @@ -51,7 +51,6 @@ import net.psforever.types.{CapacitorStateType, ChatMessageType, Cosmetic, Drive import net.psforever.util.Config import scala.concurrent.duration._ -import scala.util.Success object GeneralLogic { def apply(ops: GeneralOperations): GeneralLogic = { @@ -170,6 +169,9 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex } case None => () } + //llu destruction check + sessionLogic.localResponse.loseFlagViolently(ops.specialItemSlotGuid, player) + // val eagleEye: Boolean = ops.canSeeReallyFar val isNotVisible: Boolean = sessionLogic.zoning.zoningStatus == Zoning.Status.Deconstructing || (player.isAlive && sessionLogic.zoning.spawn.deadState == DeadState.RespawnTime) 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 6a7e41a01..90a60a2ef 100644 --- a/src/main/scala/net/psforever/actors/session/normal/MountHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/MountHandlerLogic.scala @@ -414,6 +414,7 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act case Mountable.CanNotDismount(obj: Vehicle, _, BailType.Bailed) if obj.DeploymentState == DriveState.AutoPilot => + //todo @Vehicle_CannotBailInWarpgateEnvelope sendResponse(ChatMsg(ChatMessageType.UNK_224, "@SA_CannotBailAtThisTime")) log.warn(s"DismountVehicleMsg: ${tplayer.Name} can not bail from $obj's when in autopilot") diff --git a/src/main/scala/net/psforever/actors/session/normal/VehicleHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/normal/VehicleHandlerLogic.scala index 6d8f9c1a4..208173854 100644 --- a/src/main/scala/net/psforever/actors/session/normal/VehicleHandlerLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/VehicleHandlerLogic.scala @@ -4,6 +4,7 @@ package net.psforever.actors.session.normal import akka.actor.{ActorContext, ActorRef, typed} import net.psforever.actors.session.AvatarActor import net.psforever.actors.session.support.{SessionData, SessionVehicleHandlers, VehicleHandlerFunctions} +import net.psforever.objects.avatar.SpecialCarry import net.psforever.objects.{GlobalDefinitions, Player, Tool, Vehicle, Vehicles} import net.psforever.objects.equipment.{Equipment, JammableMountedWeapons, JammableUnit} import net.psforever.objects.guid.{GUIDTask, TaskWorkflow} @@ -62,6 +63,13 @@ class VehicleHandlerLogic(val ops: SessionVehicleHandlers, implicit val context: player.Orientation = orient player.Velocity = vel sessionLogic.updateLocalBlockMap(pos) + if (player.Carrying.contains(SpecialCarry.CaptureFlag)) { + continent + .GUID(player.VehicleSeated) + .collect { case vehicle: Vehicle => + sessionLogic.localResponse.loseFlagViolently(sessionLogic.general.specialItemSlotGuid, vehicle) + } + } case VehicleResponse.VehicleState( vehicleGuid, 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 6b293ed75..a6fee9c12 100644 --- a/src/main/scala/net/psforever/actors/session/normal/VehicleLogic.scala +++ b/src/main/scala/net/psforever/actors/session/normal/VehicleLogic.scala @@ -70,6 +70,7 @@ class VehicleLogic(val ops: VehicleOperations, implicit val context: ActorContex obj.Velocity = None obj.Flying = None } + // continent.VehicleEvents ! VehicleServiceMessage( continent.id, VehicleAction.VehicleState( diff --git a/src/main/scala/net/psforever/actors/session/support/SessionLocalHandlers.scala b/src/main/scala/net/psforever/actors/session/support/SessionLocalHandlers.scala index 7fc6a893b..299fb750e 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionLocalHandlers.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionLocalHandlers.scala @@ -2,12 +2,17 @@ package net.psforever.actors.session.support import akka.actor.ActorContext -import net.psforever.objects.{Players, TurretDeployable} +import net.psforever.objects.{PlanetSideGameObject, Players, TurretDeployable} import net.psforever.objects.ce.Deployable import net.psforever.objects.guid.{GUIDTask, TaskWorkflow} +import net.psforever.objects.serverobject.environment.EnvironmentAttribute +import net.psforever.objects.serverobject.environment.interaction.InteractWithEnvironment +import net.psforever.objects.serverobject.environment.interaction.common.Watery import net.psforever.objects.serverobject.interior.Sidedness +import net.psforever.objects.serverobject.llu.CaptureFlag +import net.psforever.objects.zones.InteractsWithZone import net.psforever.packet.game.GenericObjectActionMessage -import net.psforever.services.local.LocalResponse +import net.psforever.services.local.{LocalAction, LocalResponse, LocalServiceMessage} import net.psforever.types.PlanetSideGUID trait LocalHandlerFunctions extends CommonSessionInterfacingFunctionality { @@ -47,4 +52,40 @@ class SessionLocalHandlers( else 400f } + + /** + * na + * @param target evaluate this to determine if to continue with this loss + * @return whether or not we are sufficiently submerged in water + */ + def wadingInWater(target: PlanetSideGameObject with InteractsWithZone): Boolean = { + target + .interaction() + .collectFirst { + case env: InteractWithEnvironment => + env + .Interactions + .get(EnvironmentAttribute.Water) + .collectFirst { + case water: Watery => water.Depth > 0f + } + } + .flatten + .contains(true) + } + + /** + * na + * @param flagGuid flag that may exist + * @param target evaluate this to determine if to continue with this loss + */ + def loseFlagViolently(flagGuid: Option[PlanetSideGUID], target: PlanetSideGameObject with InteractsWithZone): Unit = { + continent + .GUID(flagGuid) + .collect { + case flag: CaptureFlag if wadingInWater(target) => + flag.Destroyed = true + continent.LocalEvents ! LocalServiceMessage("", LocalAction.LluLost(flag)) + } + } } diff --git a/src/main/scala/net/psforever/objects/avatar/interaction/WithWater.scala b/src/main/scala/net/psforever/objects/avatar/interaction/WithWater.scala index 333ae6cb1..cb3aeb448 100644 --- a/src/main/scala/net/psforever/objects/avatar/interaction/WithWater.scala +++ b/src/main/scala/net/psforever/objects/avatar/interaction/WithWater.scala @@ -1,11 +1,11 @@ // Copyright (c) 2024 PSForever package net.psforever.objects.avatar.interaction -import net.psforever.objects.Player +import net.psforever.objects.{GlobalDefinitions, PlanetSideGameObject, Player} import net.psforever.objects.serverobject.environment.interaction.{InteractionWith, RespondsToZoneEnvironment} import net.psforever.objects.serverobject.environment.interaction.common.Watery import net.psforever.objects.serverobject.environment.interaction.common.Watery.OxygenStateTarget -import net.psforever.objects.serverobject.environment.{PieceOfEnvironment, interaction} +import net.psforever.objects.serverobject.environment.{EnvironmentTrait, PieceOfEnvironment, interaction} import net.psforever.objects.zones.InteractsWithZone import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.types.OxygenState @@ -15,9 +15,11 @@ import scala.concurrent.duration._ class WithWater(val channel: String) extends InteractionWith with Watery { + /** do this every time we're in sufficient contact with water */ + private var doInteractingWithBehavior: (InteractsWithZone, PieceOfEnvironment, Option[Any]) => Unit = wadingBeforeDrowning + /** - * Water causes players to slowly suffocate. - * When they (finally) drown, they will die. + * Water is wet. * @param obj the target * @param body the environment */ @@ -26,22 +28,84 @@ class WithWater(val channel: String) body: PieceOfEnvironment, data: Option[Any] ): Unit = { - val extra = data.collect { - case t: OxygenStateTarget => Some(t) - case w: Watery => w.Condition - }.flatten - if (extra.isDefined) { - //inform the player that their mounted vehicle is in trouble (that they are in trouble (but not from drowning (yet))) - stopInteractingWith(obj, body, data) + if (getExtra(data).nonEmpty) { + inheritAndPushExtraData(obj, body, data) } else { - val (effect, time, percentage) = Watery.drowningInWateryConditions(obj, condition.map(_.state), waterInteractionTime) - if (effect) { - val cond = OxygenStateTarget(obj.GUID, body, OxygenState.Suffocation, percentage) - waterInteractionTime = System.currentTimeMillis() + time - condition = Some(cond) - obj.Actor ! RespondsToZoneEnvironment.Timer(attribute, delay = time milliseconds, obj.Actor, Player.Die()) - //inform the player that they are in trouble - obj.Zone.AvatarEvents ! AvatarServiceMessage(channel, AvatarAction.OxygenState(cond, extra)) + depth = math.max(0f, body.collision.altitude - obj.Position.z) + doInteractingWithBehavior(obj, body, data) + obj.Actor ! RespondsToZoneEnvironment.Timer(attribute, delay = 500 milliseconds, obj.Actor, interaction.InteractingWithEnvironment(body, Some("wading"))) + } + } + + /** + * Wading only happens while the player's head is above the water. + * @param obj the target + * @param body the environment + */ + private def wadingBeforeDrowning( + obj: InteractsWithZone, + body: PieceOfEnvironment, + data: Option[Any] + ): Unit = { + //we're already "wading", let's see if we're drowning + if (depth >= GlobalDefinitions.MaxDepth(obj)) { + //drowning + beginDrowning(obj, body, data) + } else { + //inform the player that their mounted vehicle is in trouble (that they are in trouble (but not from drowning (yet))) + val extra = getExtra(data) + if (extra.nonEmpty) { + displayOxygenState( + obj, + condition.getOrElse(OxygenStateTarget(obj.GUID, body, OxygenState.Recovery, 95f)), + extra + ) + } + } + } + + /** + * Too much water causes players to slowly suffocate. + * When they (finally) drown, they will die. + * @param obj the target + * @param body the environment + */ + private def beginDrowning( + obj: InteractsWithZone, + body: PieceOfEnvironment, + data: Option[Any] + ): Unit = { + val (effect, time, percentage) = Watery.drowningInWateryConditions(obj, condition.map(_.state), waterInteractionTime) + if (effect) { + val cond = OxygenStateTarget(obj.GUID, body, OxygenState.Suffocation, percentage) + waterInteractionTime = System.currentTimeMillis() + time + condition = Some(cond) + obj.Actor ! RespondsToZoneEnvironment.StopTimer(WithWater.WaterAction) + obj.Actor ! RespondsToZoneEnvironment.Timer(WithWater.WaterAction, delay = time milliseconds, obj.Actor, Player.Die()) + //inform the player that they are in trouble + displayOxygenState(obj, cond, getExtra(data)) + doInteractingWithBehavior = drowning + } + } + + /** + * Too much water causes players to slowly suffocate. + * When they (finally) drown, they will die. + * @param obj the target + * @param body the environment + */ + private def drowning( + obj: InteractsWithZone, + body: PieceOfEnvironment, + data: Option[Any] + ): Unit = { + //test if player ever gets head above the water level + if (depth < GlobalDefinitions.MaxDepth(obj)) { + val (_, _, percentage) = Watery.recoveringFromWateryConditions(obj, condition.map(_.state), waterInteractionTime) + //switch to recovery + if (percentage > 0) { + recoverFromDrowning(obj, body, data) + doInteractingWithBehavior = recoverFromDrowning } } } @@ -52,16 +116,22 @@ class WithWater(val channel: String) * @param obj the target * @param body the environment */ - override def stopInteractingWith( - obj: InteractsWithZone, - body: PieceOfEnvironment, - data: Option[Any] - ): Unit = { - val (effect, time, percentage) = Watery.recoveringFromWateryConditions(obj, condition.map(_.state), waterInteractionTime) - if (percentage > 99f) { - recoverFromInteracting(obj) + private def recoverFromDrowning( + obj: InteractsWithZone, + body: PieceOfEnvironment, + data: Option[Any] + ): Unit = { + val state = condition.map(_.state) + if (state.contains(OxygenState.Suffocation)) { + //set up for recovery + val (effect, time, percentage) = Watery.recoveringFromWateryConditions(obj, state, waterInteractionTime) + if (percentage < 99f) { + //we're not too far gone + recoverFromDrowning(obj, body, data, effect, time, percentage) + } + doInteractingWithBehavior = recovering } else { - stopInteractingAction(obj, body, data, effect, time, percentage) + doInteractingWithBehavior = wadingBeforeDrowning } } @@ -74,38 +144,169 @@ class WithWater(val channel: String) * @param time current time until completion of the next effect * @param percentage value to display in the drowning UI progress bar */ - private def stopInteractingAction( - obj: InteractsWithZone, - body: PieceOfEnvironment, - data: Option[Any], - effect: Boolean, - time: Long, - percentage: Float - ): Unit = { + private def recoverFromDrowning( + obj: InteractsWithZone, + body: PieceOfEnvironment, + data: Option[Any], + effect: Boolean, + time: Long, + percentage: Float + ): Unit = { val cond = OxygenStateTarget(obj.GUID, body, OxygenState.Recovery, percentage) val extra = data.collect { case t: OxygenStateTarget => Some(t) case w: Watery => w.Condition }.flatten - if (effect) { + if (effect) { condition = Some(cond) waterInteractionTime = System.currentTimeMillis() + time - obj.Actor ! RespondsToZoneEnvironment.Timer(attribute, delay = time milliseconds, obj.Actor, interaction.RecoveredFromEnvironmentInteraction(attribute)) + obj.Actor ! RespondsToZoneEnvironment.StopTimer(WithWater.WaterAction) + obj.Actor ! RespondsToZoneEnvironment.Timer(WithWater.WaterAction, delay = time milliseconds, obj.Actor, interaction.RecoveredFromEnvironmentInteraction(attribute)) //inform the player - obj.Zone.AvatarEvents ! AvatarServiceMessage(channel, AvatarAction.OxygenState(cond, extra)) + displayOxygenState(obj, cond, extra) } else if (extra.isDefined) { //inform the player - obj.Zone.AvatarEvents ! AvatarServiceMessage(channel, AvatarAction.OxygenState(cond, extra)) + displayOxygenState(obj, cond, extra) + } + } + + /** + * The recovery period is much faster than the drowning process. + * Check for when the player fully recovers, + * and that the player does not regress back to drowning. + * @param obj the target + * @param body the environment + */ + def recovering( + obj: InteractsWithZone, + body: PieceOfEnvironment, + data: Option[Any] + ): Unit = { + lazy val state = condition.map(_.state) + if (depth >= GlobalDefinitions.MaxDepth(obj)) { + //go back to drowning + beginDrowning(obj, body, data) + } else if (state.contains(OxygenState.Recovery)) { + //check recovery conditions + val (_, _, percentage) = Watery.recoveringFromWateryConditions(obj, state, waterInteractionTime) + if (percentage < 1f) { + doInteractingWithBehavior = wadingBeforeDrowning + } + } + } + + /** + * When out of water, the player is no longer suffocating. + * He's even stopped wading. + * The only thing we should let complete now is recovery. + * @param obj the target + * @param body the environment + */ + override def stopInteractingWith( + obj: InteractsWithZone, + body: PieceOfEnvironment, + data: Option[Any] + ): Unit = { + if (getExtra(data).nonEmpty) { + inheritAndPushExtraData(obj, body, data) + } else { + stopInteractingWithAction(obj, body, data) + } + } + + /** + * When out of water, the player is no longer suffocating. + * He's even stopped wading. + * The only thing we should let complete now is recovery. + * @param obj the target + * @param body the environment + */ + private def stopInteractingWithAction( + obj: InteractsWithZone, + body: PieceOfEnvironment, + data: Option[Any] + ): Unit = { + val cond = condition.map(_.state) + if (cond.contains(OxygenState.Suffocation)) { + //go from suffocating to recovery + recoverFromDrowning(obj, body, data) + } else if (cond.isEmpty) { + //neither suffocating nor recovering, so just reset everything + recoverFromInteracting(obj) + obj.Actor ! RespondsToZoneEnvironment.StopTimer(attribute) + waterInteractionTime = 0L + depth = 0f + condition = None + doInteractingWithBehavior = wadingBeforeDrowning } } override def recoverFromInteracting(obj: InteractsWithZone): Unit = { super.recoverFromInteracting(obj) - if (condition.exists(_.state == OxygenState.Suffocation)) { - val (effect, time, percentage) = Watery.recoveringFromWateryConditions(obj, condition.map(_.state), waterInteractionTime) - stopInteractingAction(obj, condition.map(_.body).get, None, effect, time, percentage) + val cond = condition.map(_.state) + //whether or not we were suffocating or recovering, we need to undo the visuals for that + if (cond.nonEmpty) { + obj.Actor ! RespondsToZoneEnvironment.StopTimer(WithWater.WaterAction) + displayOxygenState( + obj, + OxygenStateTarget(obj.GUID, condition.map(_.body).get, OxygenState.Recovery, 100f), + None + ) } - waterInteractionTime = 0L condition = None } + + /** + * From the "condition" of someone else's drowning status, + * extract target information and progress. + * @param data any information + * @return target information and drowning progress + */ + private def getExtra(data: Option[Any]): Option[OxygenStateTarget] = { + data.collect { + case t: OxygenStateTarget => Some(t) + case w: Watery => w.Condition + }.flatten + } + + /** + * Send the message regarding drowning and recovery + * that includes additional information about a related target that is drowning or recovering. + * @param obj the target + * @param body the environment + * @param data essential information about someone else's interaction with water + */ + private def inheritAndPushExtraData( + obj: InteractsWithZone, + body: PieceOfEnvironment, + data: Option[Any] + ): Unit = { + val state = condition.map(_.state).getOrElse(OxygenState.Recovery) + val Some((_, _, percentage)) = state match { + case OxygenState.Suffocation => Some(Watery.drowningInWateryConditions(obj, Some(state), waterInteractionTime)) + case OxygenState.Recovery => Some(Watery.recoveringFromWateryConditions(obj, Some(state), waterInteractionTime)) + } + displayOxygenState(obj, OxygenStateTarget(obj.GUID, body, state, percentage), getExtra(data)) + } + + /** + * Send the message regarding drowning and recovery. + * @param obj the target + * @param cond the environment + */ + private def displayOxygenState( + obj: InteractsWithZone, + cond: OxygenStateTarget, + data: Option[OxygenStateTarget] + ): Unit = { + obj.Zone.AvatarEvents ! AvatarServiceMessage(channel, AvatarAction.OxygenState(cond, data)) + } +} + +object WithWater { + /** special environmental trait to queue actions independent from the primary wading test */ + case object WaterAction extends EnvironmentTrait { + override def canInteractWith(obj: PlanetSideGameObject): Boolean = false + override def testingDepth: Float = Float.PositiveInfinity + } } diff --git a/src/main/scala/net/psforever/objects/serverobject/environment/EnvironmentAttribute.scala b/src/main/scala/net/psforever/objects/serverobject/environment/EnvironmentAttribute.scala index 2ad7d1e87..e7e88c7dc 100644 --- a/src/main/scala/net/psforever/objects/serverobject/environment/EnvironmentAttribute.scala +++ b/src/main/scala/net/psforever/objects/serverobject/environment/EnvironmentAttribute.scala @@ -10,6 +10,8 @@ import net.psforever.types.Vector3 */ abstract class EnvironmentTrait { def canInteractWith(obj: PlanetSideGameObject): Boolean + + def testingDepth: Float } object EnvironmentAttribute { @@ -25,16 +27,22 @@ object EnvironmentAttribute { case _ => false }) } + + def testingDepth: Float = 0.2f } case object Lava extends EnvironmentTrait { /** lava can only interact with anything capable of registering damage */ def canInteractWith(obj: PlanetSideGameObject): Boolean = canInteractWithDamagingFields(obj) + + val testingDepth: Float = 0f } case object Death extends EnvironmentTrait { /** death can only interact with anything capable of registering damage */ def canInteractWith(obj: PlanetSideGameObject): Boolean = canInteractWithDamagingFields(obj) + + val testingDepth: Float = 0f } case object GantryDenialField @@ -46,18 +54,24 @@ object EnvironmentAttribute { case _ => false } } + + val testingDepth: Float = 0f } case object MovementFieldTrigger extends EnvironmentTrait { /** only interact with living player characters or vehicles */ def canInteractWith(obj: PlanetSideGameObject): Boolean = canInteractWithPlayersAndVehicles(obj) + + val testingDepth: Float = 0f } case object InteriorField extends EnvironmentTrait { /** only interact with living player characters or vehicles */ def canInteractWith(obj: PlanetSideGameObject): Boolean = canInteractWithPlayersAndVehicles(obj) + + val testingDepth: Float = 0f } /** diff --git a/src/main/scala/net/psforever/objects/serverobject/environment/interaction/InteractWithEnvironment.scala b/src/main/scala/net/psforever/objects/serverobject/environment/interaction/InteractWithEnvironment.scala index aacd261b9..5e889fcba 100644 --- a/src/main/scala/net/psforever/objects/serverobject/environment/interaction/InteractWithEnvironment.scala +++ b/src/main/scala/net/psforever/objects/serverobject/environment/interaction/InteractWithEnvironment.scala @@ -1,7 +1,6 @@ // Copyright (c) 2021 PSForever package net.psforever.objects.serverobject.environment.interaction -import net.psforever.objects.GlobalDefinitions import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.serverobject.environment.{EnvironmentTrait, PieceOfEnvironment} import net.psforever.objects.zones._ @@ -117,9 +116,8 @@ object InteractWithEnvironment { obj: PlanetSideServerObject, sector: SectorPopulation ): Set[PieceOfEnvironment] = { - val depth = GlobalDefinitions.MaxDepth(obj) sector.environmentList - .filter(body => body.attribute.canInteractWith(obj) && body.testInteraction(obj, depth)) + .filter(body => body.attribute.canInteractWith(obj) && body.testInteraction(obj, body.attribute.testingDepth)) .distinctBy(_.attribute) .toSet } @@ -136,7 +134,7 @@ object InteractWithEnvironment { body: PieceOfEnvironment, obj: PlanetSideServerObject ): Option[PieceOfEnvironment] = { - if ((obj.Zone eq zone) && body.testInteraction(obj, GlobalDefinitions.MaxDepth(obj))) { + if ((obj.Zone eq zone) && body.testInteraction(obj, body.attribute.testingDepth)) { Some(body) } else { None diff --git a/src/main/scala/net/psforever/objects/serverobject/environment/interaction/common/Watery.scala b/src/main/scala/net/psforever/objects/serverobject/environment/interaction/common/Watery.scala index 799c166c8..d52011c02 100644 --- a/src/main/scala/net/psforever/objects/serverobject/environment/interaction/common/Watery.scala +++ b/src/main/scala/net/psforever/objects/serverobject/environment/interaction/common/Watery.scala @@ -8,13 +8,16 @@ import net.psforever.types.{OxygenState, PlanetSideGUID} trait Watery { val attribute: EnvironmentTrait = EnvironmentAttribute.Water - /** how long the current interaction has been progressing in the current way */ protected var waterInteractionTime: Long = 0 /** information regarding the drowning state */ protected var condition: Option[OxygenStateTarget] = None /** information regarding the drowning state */ def Condition: Option[OxygenStateTarget] = condition + /** how far the player's feet are below the surface of the water */ + protected var depth: Float = 0f + /** how far the player's feet are below the surface of the water */ + def Depth: Float = depth } object Watery { diff --git a/src/main/scala/net/psforever/objects/serverobject/llu/CaptureFlag.scala b/src/main/scala/net/psforever/objects/serverobject/llu/CaptureFlag.scala index d13627e9f..bb02a6970 100644 --- a/src/main/scala/net/psforever/objects/serverobject/llu/CaptureFlag.scala +++ b/src/main/scala/net/psforever/objects/serverobject/llu/CaptureFlag.scala @@ -34,6 +34,7 @@ class CaptureFlag(private val tDef: CaptureFlagDefinition) extends Amenity { private var faction: PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL private var carrier: Option[Player] = None private var lastTimeCollected: Long = System.currentTimeMillis() + private val spawnedTime: Long = lastTimeCollected def Target: Building = target def Target_=(newTarget: Building): Building = { @@ -56,10 +57,11 @@ class CaptureFlag(private val tDef: CaptureFlagDefinition) extends Amenity { * When the flag is carried by a player, the position returned should be that of the carrier not the flag. * @return the position of the carrier, if there is a player carrying the flag, or the flag itself */ - override def Position: Vector3 = if (Carrier.nonEmpty) { - carrier.get.Position - } else { - super.Position + override def Position: Vector3 = { + carrier match { + case Some(player) => player.Position + case None => super.Position + } } def Carrier: Option[Player] = carrier @@ -70,6 +72,8 @@ class CaptureFlag(private val tDef: CaptureFlagDefinition) extends Amenity { } def LastCollectionTime: Long = carrier.map { _ => lastTimeCollected }.getOrElse { System.currentTimeMillis() } + + def InitialSpawnTime: Long = spawnedTime } object CaptureFlag { 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 6821e79a4..66b6f2700 100644 --- a/src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala +++ b/src/main/scala/net/psforever/objects/vehicles/control/VehicleControl.scala @@ -606,9 +606,9 @@ class VehicleControl(vehicle: Vehicle) c } } - watery.doInteractingWithTargets(player, percentage, watery.Condition.map(_.body).get, List(player)) + WithWater.doInteractingWithTargets(player, percentage, watery.Condition.map(_.body).get, List(player)) case watery: WithWater if watery.Condition.map(_.state).contains(OxygenState.Recovery) => - watery.stopInteractingWithTargets( + WithWater.stopInteractingWithTargets( player, Watery.recoveringFromWater(vehicle, watery)._3, watery.Condition.map(_.body).get, diff --git a/src/main/scala/net/psforever/objects/vehicles/interaction/WithWater.scala b/src/main/scala/net/psforever/objects/vehicles/interaction/WithWater.scala index 39fd899bf..dd4dc299d 100644 --- a/src/main/scala/net/psforever/objects/vehicles/interaction/WithWater.scala +++ b/src/main/scala/net/psforever/objects/vehicles/interaction/WithWater.scala @@ -1,37 +1,71 @@ // Copyright (c) 2024 PSForever package net.psforever.objects.vehicles.interaction -import net.psforever.objects.{GlobalDefinitions, Vehicle} +import net.psforever.objects.{GlobalDefinitions, PlanetSideGameObject, Vehicle} import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.serverobject.environment.interaction.{InteractionWith, RespondsToZoneEnvironment} import net.psforever.objects.serverobject.environment.interaction.common.Watery import net.psforever.objects.serverobject.environment.interaction.common.Watery.OxygenStateTarget -import net.psforever.objects.serverobject.environment.{PieceOfEnvironment, interaction} +import net.psforever.objects.serverobject.environment.{EnvironmentTrait, PieceOfEnvironment, interaction} import net.psforever.objects.vehicles.control.VehicleControl import net.psforever.objects.zones.InteractsWithZone import net.psforever.types.OxygenState +import scala.annotation.unused import scala.concurrent.duration._ class WithWater() extends InteractionWith with Watery { + /** do this every time we're in sufficient contact with water */ + private var doInteractingWithBehavior: (InteractsWithZone, PieceOfEnvironment, Option[Any]) => Unit = wadingBeforeDrowning + /** - * Water causes vehicles to become disabled if they dive off too far, too deep. - * Flying vehicles do not display progress towards being waterlogged. - * They just disable outright. - * @param obj the target - * @param body the environment - * @param data additional interaction information, if applicable - */ + * Water is wet. + * @param obj the target + * @param body the environment + */ def doInteractingWith( obj: InteractsWithZone, body: PieceOfEnvironment, data: Option[Any] ): Unit = { + depth = math.max(0f, body.collision.altitude - obj.Position.z) + doInteractingWithBehavior(obj, body, data) + obj.Actor ! RespondsToZoneEnvironment.Timer(attribute, delay = 500 milliseconds, obj.Actor, interaction.InteractingWithEnvironment(body, Some("wading"))) + } + + /** + * Wading only happens while the vehicle's wheels are mostly above the water. + * @param obj the target + * @param body the environment + */ + private def wadingBeforeDrowning( + obj: InteractsWithZone, + body: PieceOfEnvironment, + data: Option[Any] + ): Unit = { + //we're already "wading", let's see if we're drowning + if (depth >= GlobalDefinitions.MaxDepth(obj)) { + //drowning + beginDrowning(obj, body, data) + } + } + + /** + * Too much water causes players to slowly suffocate. + * When they (finally) drown, they will die. + * @param obj the target + * @param body the environment + */ + private def beginDrowning( + obj: InteractsWithZone, + body: PieceOfEnvironment, + @unused data: Option[Any] + ): Unit = { obj match { case vehicle: Vehicle => - val (effect: Boolean, time: Long, percentage: Float) = { + val (effect, time, percentage): (Boolean, Long, Float) = { val (a, b, c) = Watery.drowningInWateryConditions(obj, condition.map(_.state), waterInteractionTime) if (a && GlobalDefinitions.isFlightVehicle(vehicle.Definition)) { (true, 0L, 0f) //no progress bar @@ -42,8 +76,94 @@ class WithWater() if (effect) { condition = Some(OxygenStateTarget(obj.GUID, body, OxygenState.Suffocation, percentage)) waterInteractionTime = System.currentTimeMillis() + time - obj.Actor ! RespondsToZoneEnvironment.Timer(attribute, delay = time milliseconds, obj.Actor, VehicleControl.Disable(true)) - doInteractingWithTargets( + obj.Actor ! RespondsToZoneEnvironment.StopTimer(WithWater.WaterAction) + obj.Actor ! RespondsToZoneEnvironment.Timer(WithWater.WaterAction, delay = time milliseconds, obj.Actor, VehicleControl.Disable(true)) + WithWater.doInteractingWithTargets( + obj, + percentage, + body, + vehicle.Seats.values.flatMap(_.occupants).filter(p => p.isAlive && (p.Zone eq obj.Zone)) + ) + doInteractingWithBehavior = drowning + } + case _ => () + } + } + + /** + * Too much water causes vehicles to slowly disable. + * When fully waterlogged, the vehicle is completely immobile. + * @param obj the target + * @param body the environment + */ + private def drowning( + obj: InteractsWithZone, + body: PieceOfEnvironment, + data: Option[Any] + ): Unit = { + //test if player ever gets head above the water level + if (depth < GlobalDefinitions.MaxDepth(obj)) { + val (effect, time, percentage) = Watery.recoveringFromWateryConditions(obj, condition.map(_.state), waterInteractionTime) + //switch to recovery + if (percentage > 0) { + recoverFromDrowning(obj, body, data, effect, time, percentage) + doInteractingWithBehavior = recovering + } + } + } + + /** + * When out of water, the vehicle is no longer being waterlogged. + * It does have to endure a recovery period to get back to normal, though. + * @param obj the target + * @param body the environment + */ + private def recoverFromDrowning( + obj: InteractsWithZone, + body: PieceOfEnvironment, + data: Option[Any] + ): Unit = { + val state = condition.map(_.state) + if (state.contains(OxygenState.Suffocation)) { + //set up for recovery + val (effect, time, percentage) = Watery.recoveringFromWateryConditions(obj, state, waterInteractionTime) + if (percentage < 99f) { + //we're not too far gone + recoverFromDrowning(obj, body, data, effect, time, percentage) + } + doInteractingWithBehavior = recovering + } else { + doInteractingWithBehavior = wadingBeforeDrowning + } + } + + /** + * When out of water, the vehicle is no longer being waterlogged. + * It does have to endure a recovery period to get back to normal, though. + * @param obj the target + * @param body the environment + * @param effect na + * @param time current time until completion of the next effect + * @param percentage value to display in the drowning UI progress bar + */ + private def recoverFromDrowning( + obj: InteractsWithZone, + body: PieceOfEnvironment, + @unused data: Option[Any], + effect: Boolean, + time: Long, + percentage: Float + ): Unit = { + obj match { + case vehicle: Vehicle => + val cond = OxygenStateTarget(obj.GUID, body, OxygenState.Recovery, percentage) + if (effect) { + condition = Some(cond) + waterInteractionTime = System.currentTimeMillis() + time + obj.Actor ! RespondsToZoneEnvironment.StopTimer(WithWater.WaterAction) + obj.Actor ! RespondsToZoneEnvironment.Timer(WithWater.WaterAction, delay = time milliseconds, obj.Actor, interaction.RecoveredFromEnvironmentInteraction(attribute)) + //inform the players + WithWater.stopInteractingWithTargets( obj, percentage, body, @@ -55,34 +175,52 @@ class WithWater() } /** - * When out of water, the vehicle no longer risks becoming disabled. - * It does have to endure a recovery period to get back to full dehydration - * Flying vehicles are exempt from this process due to the abrupt disability they experience. - * @param obj the target - * @param body the environment - * @param data additional interaction information, if applicable - */ + * The recovery period is much faster than the waterlogging process. + * Check for when the vehicle fully recovers, + * and that the vehicle does not regress back to waterlogging. + * @param obj the target + * @param body the environment + */ + def recovering( + obj: InteractsWithZone, + body: PieceOfEnvironment, + data: Option[Any] + ): Unit = { + lazy val state = condition.map(_.state) + if (depth >= GlobalDefinitions.MaxDepth(obj)) { + //go back to drowning + beginDrowning(obj, body, data) + } else if (state.contains(OxygenState.Recovery)) { + //check recovery conditions + val (_, _, percentage) = Watery.recoveringFromWateryConditions(obj, state, waterInteractionTime) + if (percentage < 1f) { + doInteractingWithBehavior = wadingBeforeDrowning + } + } + }/** + * When out of water, the vehicle no longer risks becoming disabled. + * It does have to endure a recovery period to get back to full dehydration + * Flying vehicles are exempt from this process due to the abrupt disability they experience. + * @param obj the target + * @param body the environment + */ override def stopInteractingWith( obj: InteractsWithZone, body: PieceOfEnvironment, data: Option[Any] ): Unit = { - obj match { - case vehicle: Vehicle => - val (effect: Boolean, time: Long, percentage: Float) = - Watery.recoveringFromWateryConditions(obj, condition.map(_.state), waterInteractionTime) - if (effect) { - condition = Some(OxygenStateTarget(obj.GUID, body, OxygenState.Recovery, percentage)) - waterInteractionTime = System.currentTimeMillis() + time - obj.Actor ! RespondsToZoneEnvironment.Timer(attribute, delay = time milliseconds, obj.Actor, interaction.RecoveredFromEnvironmentInteraction(attribute)) - stopInteractingWithTargets( - obj, - percentage, - body, - vehicle.Seats.values.flatMap(_.occupants).filter(p => p.isAlive && (p.Zone eq obj.Zone)) - ) - } - case _ => () + val cond = condition.map(_.state) + if (cond.contains(OxygenState.Suffocation)) { + //go from suffocating to recovery + recoverFromDrowning(obj, body, data) + } else if (cond.isEmpty) { + //neither suffocating nor recovering, so just reset everything + recoverFromInteracting(obj) + obj.Actor ! RespondsToZoneEnvironment.StopTimer(attribute) + waterInteractionTime = 0L + depth = 0f + condition = None + doInteractingWithBehavior = wadingBeforeDrowning } } @@ -91,46 +229,53 @@ class WithWater() if (condition.exists(_.state == OxygenState.Suffocation)) { stopInteractingWith(obj, condition.map(_.body).get, None) } - waterInteractionTime = 0L condition = None } +} + +object WithWater { + /** special environmental trait to queue actions independent from the primary wading test */ + case object WaterAction extends EnvironmentTrait { + override def canInteractWith(obj: PlanetSideGameObject): Boolean = false + override def testingDepth: Float = Float.PositiveInfinity + } /** - * Tell the given targets that water causes vehicles to become disabled if they dive off too far, too deep. - * @see `InteractingWithEnvironment` - * @see `OxygenState` - * @see `OxygenStateTarget` - * @param obj the target - * @param percentage the progress bar completion state - * @param body the environment - * @param targets recipients of the information - */ + * Tell the given targets that water causes vehicles to become disabled if they dive off too far, too deep. + * @see `InteractingWithEnvironment` + * @see `OxygenState` + * @see `OxygenStateTarget` + * @param obj the target + * @param percentage the progress bar completion state + * @param body the environment + * @param targets recipients of the information + */ def doInteractingWithTargets( - obj: PlanetSideServerObject, - percentage: Float, - body: PieceOfEnvironment, - targets: Iterable[PlanetSideServerObject] - ): Unit = { + obj: PlanetSideServerObject, + percentage: Float, + body: PieceOfEnvironment, + targets: Iterable[PlanetSideServerObject] + ): Unit = { val state = Some(OxygenStateTarget(obj.GUID, body, OxygenState.Suffocation, percentage)) targets.foreach(_.Actor ! interaction.InteractingWithEnvironment(body, state)) } /** - * Tell the given targets that, when out of water, the vehicle no longer risks becoming disabled. - * @see `EscapeFromEnvironment` - * @see `OxygenState` - * @see `OxygenStateTarget` - * @param obj the target - * @param percentage the progress bar completion state - * @param body the environment - * @param targets recipients of the information - */ + * Tell the given targets that, when out of water, the vehicle no longer risks becoming disabled. + * @see `EscapeFromEnvironment` + * @see `OxygenState` + * @see `OxygenStateTarget` + * @param obj the target + * @param percentage the progress bar completion state + * @param body the environment + * @param targets recipients of the information + */ def stopInteractingWithTargets( - obj: PlanetSideServerObject, - percentage: Float, - body: PieceOfEnvironment, - targets: Iterable[PlanetSideServerObject] - ): Unit = { + obj: PlanetSideServerObject, + percentage: Float, + body: PieceOfEnvironment, + targets: Iterable[PlanetSideServerObject] + ): Unit = { val state = Some(OxygenStateTarget(obj.GUID, body, OxygenState.Recovery, percentage)) targets.foreach(_.Actor ! interaction.EscapeFromEnvironment(body, state)) } diff --git a/src/main/scala/net/psforever/packet/game/OxygenStateMessage.scala b/src/main/scala/net/psforever/packet/game/OxygenStateMessage.scala index d80f16d77..0a7629a70 100644 --- a/src/main/scala/net/psforever/packet/game/OxygenStateMessage.scala +++ b/src/main/scala/net/psforever/packet/game/OxygenStateMessage.scala @@ -134,7 +134,17 @@ object OxygenStateMessage extends Marshallable[OxygenStateMessage] { 204.8f, 11 ) :: //hackish: 2^11 == 2047, so it should be 204.7; but, 204.8 allows decode == encode - OxygenState.codec + bool.xmap[OxygenState]( + { + case false => OxygenState.Recovery + case true => OxygenState.Suffocation + }, + { + case OxygenState.Recovery => false + case OxygenState.Suffocation => true + case _ => false + } + ) ).as[DrowningTarget] implicit val codec: Codec[OxygenStateMessage] = ( diff --git a/src/main/scala/net/psforever/services/local/LocalService.scala b/src/main/scala/net/psforever/services/local/LocalService.scala index 72aa05aa8..91d900d3b 100644 --- a/src/main/scala/net/psforever/services/local/LocalService.scala +++ b/src/main/scala/net/psforever/services/local/LocalService.scala @@ -120,6 +120,8 @@ class LocalService(zone: Zone) extends Actor { hackCapturer ! HackCaptureActor.StartCaptureTerminalHack(target, zone, 0, 8L) case LocalAction.LluCaptured(llu) => hackCapturer ! HackCaptureActor.FlagCaptured(llu) + case LocalAction.LluLost(llu) => + hackCapturer ! HackCaptureActor.FlagLost(llu) case LocalAction.LluSpawned(player_guid, llu) => // Forward to all clients to create object locally diff --git a/src/main/scala/net/psforever/services/local/LocalServiceMessage.scala b/src/main/scala/net/psforever/services/local/LocalServiceMessage.scala index e6d70f3af..de4545cc5 100644 --- a/src/main/scala/net/psforever/services/local/LocalServiceMessage.scala +++ b/src/main/scala/net/psforever/services/local/LocalServiceMessage.scala @@ -61,6 +61,7 @@ object LocalAction { final case class ResecureCaptureTerminal(target: CaptureTerminal, hacker: PlayerSource) extends Action final case class StartCaptureTerminalHack(target: CaptureTerminal) extends Action final case class LluCaptured(llu: CaptureFlag) extends Action + final case class LluLost(llu: CaptureFlag) extends Action final case class LluSpawned(player_guid: PlanetSideGUID, llu: CaptureFlag) extends Action final case class LluDespawned(player_guid: PlanetSideGUID, guid: PlanetSideGUID, position: Vector3) extends Action diff --git a/src/main/scala/net/psforever/services/local/support/CaptureFlagManager.scala b/src/main/scala/net/psforever/services/local/support/CaptureFlagManager.scala index 91bf2499f..46127229c 100644 --- a/src/main/scala/net/psforever/services/local/support/CaptureFlagManager.scala +++ b/src/main/scala/net/psforever/services/local/support/CaptureFlagManager.scala @@ -98,6 +98,12 @@ class CaptureFlagManager(zone: Zone) extends Actor { flag.Zone, CaptureFlagChatMessageStrings.CTF_Failed_TimedOut(building.Name, flag.Target.Name, flag.Faction) ) + case CaptureFlagLostReasonEnum.FlagLost => + val building = flag.Owner.asInstanceOf[Building] + ChatBroadcast( + flag.Zone, + CaptureFlagChatMessageStrings.CTF_Failed_FlagLost(building.Name, flag.Faction) + ) case CaptureFlagLostReasonEnum.Ended => () } @@ -197,6 +203,7 @@ class CaptureFlagManager(zone: Zone) extends Actor { } private def ChatBroadcast(zone: Zone, message: String, fanfare: Boolean = true): Unit = { + //todo use UNK_222 sometimes val messageType: ChatMessageType = if (fanfare) { ChatMessageType.UNK_223 } else { @@ -224,15 +231,6 @@ object CaptureFlagManager { } object CaptureFlagChatMessageStrings { - /* - @CTF_Failed_TargetLost=%1's LLU target facility %2 was lost!\nHack canceled! - @CTF_Failed_FlagLost=The %1 lost %2's LLU!\nHack canceled! - @CTF_Warning_Carrier=%1 of the %2 has %3's LLU.\nIt must be taken to %4 within %5 minutes! - @CTF_Warning_NoCarrier=%1's LLU is in the field.\nThe %2 must take it to %3 within %4 minutes! - @CTF_Warning_Carrier1Min=%1 of the %2 has %3's LLU.\nIt must be taken to %4 within the next minute! - @CTF_Warning_NoCarrier1Min=%1's LLU is in the field.\nThe %2 must take it to %3 within the next minute! - */ - // @CTF_Success=%1 captured %2's LLU for the %3! /** {player.Name} captured {ownerName}'s LLU for the {player.Faction}! */ private[support] def CTF_Success(playerName:String, playerFaction: PlanetSideEmpire.Value, ownerName: String): String = @@ -243,10 +241,20 @@ object CaptureFlagChatMessageStrings { private[support] def CTF_Failed_TimedOut(ownerName: String, name: String, faction: PlanetSideEmpire.Value): String = s"@CTF_Failed_TimedOut^@${GetFactionString(faction)}~^@$ownerName~^@$name~" + // @CTF_Failed_Lost=The %1 lost %2's LLU!\nHack canceled! + /** The {faction} lost {ownerName}'s LLU!\nHack canceled! */ + private[support] def CTF_Failed_FlagLost(ownerName: String, faction: PlanetSideEmpire.Value): String = + s"@CTF_Failed_FlagLost^@${GetFactionString(faction)}~^@$ownerName~" + + // @CTF_Failed_TargetLost=%1's LLU target facility %2 was lost!\nHack canceled! + /** {hackFacility}'s LLU target facility {targetFacility} was lost!\nHack canceled! */ + private[support] def CTF_Failed_TargetLost(hackFacility: String, targetFacility: String): String = + s"@CTF_Failed_TargetLost^@$hackFacility~^@$targetFacility~" + // @CTF_Failed_SourceResecured=The %1 resecured %2!\nThe LLU was lost! /** The {faction} resecured {name}!\nThe LLU was lost! */ private[support] def CTF_Failed_SourceResecured(name: String, faction: PlanetSideEmpire.Value): String = - s"@CTF_Failed_SourceResecured^@${CaptureFlagChatMessageStrings.GetFactionString(faction)}~^@$name~" + s"@CTF_Failed_SourceResecured^@${GetFactionString(faction)}~^@$name~" // @CTF_FlagSpawned=%1 %2 has spawned a LLU.\nIt must be taken to %3 %4's Control Console within %5 minutes or the hack will fail! /** {facilityType} {facilityName} has spawned a LLU.\nIt must be taken to {targetFacilityType} {targetFacilityName}'s Control Console within 15 minutes or the hack will fail! */ @@ -256,12 +264,56 @@ object CaptureFlagChatMessageStrings { // @CTF_FlagPickedUp=%1 of the %2 picked up %3's LLU /** {player.Name} of the {player.Faction} picked up {ownerName}'s LLU */ def CTF_FlagPickedUp(playerName: String, playerFaction: PlanetSideEmpire.Value, ownerName: String): String = - s"@CTF_FlagPickedUp^$playerName~^@${CaptureFlagChatMessageStrings.GetFactionString(playerFaction)}~^@$ownerName~" + s"@CTF_FlagPickedUp^$playerName~^@${GetFactionString(playerFaction)}~^@$ownerName~" // @CTF_FlagDropped=%1 of the %2 dropped %3's LLU /** {playerName} of the {faction} dropped {facilityName}'s LLU */ def CTF_FlagDropped(playerName: String, playerFaction: PlanetSideEmpire.Value, ownerName: String): String = - s"@CTF_FlagDropped^$playerName~^@${CaptureFlagChatMessageStrings.GetFactionString(playerFaction)}~^@$ownerName~" + s"@CTF_FlagDropped^$playerName~^@${GetFactionString(playerFaction)}~^@$ownerName~" + + // @CTF_Warning_Carrier=%1's LLU is in the field.\nThe %2 must take it to %3 within %4 minutes! + /** {facilityName}'s LLU is in the field.\nThe {faction} must take it to {targetFacilityName} within {time} minutes! */ + def CTF_Warning_Carrier( + playerName:String, + playerFaction: PlanetSideEmpire.Value, + facilityName: String, + targetFacilityName: String, + time: Int + ): String = { + s"@CTF_Warning_Carrier^$playerName~^@${GetFactionString(playerFaction)}~^@$facilityName~^@$targetFacilityName~^@$time~" + } + + // @CTF_Warning_Carrier1Min=%1 of the %2 has %3's LLU.\nIt must be taken to %4 within the next minute! + /** {playerName} of the {faction} has {facilityName}'s LLU.\nIt must be taken to {targetFacilityName} within the next minute! */ + def CTF_Warning_Carrier1Min( + playerName:String, + playerFaction: PlanetSideEmpire.Value, + facilityName: String, + targetFacilityName: String + ): String = { + s"@CTF_Warning_Carrier1Min^$playerName~^@${GetFactionString(playerFaction)}~^@$facilityName~^@$targetFacilityName~" + } + + // @CTF_Warning_NoCarrier=%1's LLU is in the field.\nThe %2 must take it to %3 within %4 minutes! + /** {facilityName}'s LLU is in the field.\nThe {faction} must take it to {targetFacilityName} within {time} minute! */ + def CTF_Warning_NoCarrier( + facilityName: String, + playerFaction: PlanetSideEmpire.Value, + targetFacilityName: String, + time: Int + ): String = { + s"@CTF_Warning_NoCarrier^@$facilityName~^@${GetFactionString(playerFaction)}~^@$targetFacilityName~^$time~" + } + + // @CTF_Warning_NoCarrier1Min=%1's LLU is in the field.\nThe %2 must take it to %3 within the next minute! + /** {facilityName}'s LLU is in the field.\nThe {faction} must take it to {targetFacilityName} within the next minute! */ + def CTF_Warning_NoCarrier1Min( + facilityName: String, + playerFaction: PlanetSideEmpire.Value, + targetFacilityName: String + ): String = { + s"@CTF_Warning_NoCarrier1Min^@$facilityName~^@${GetFactionString(playerFaction)}~^@$targetFacilityName~" + } private def GetFactionString: PlanetSideEmpire.Value=>String = { case PlanetSideEmpire.TR => "TerranRepublic" @@ -277,4 +329,5 @@ object CaptureFlagLostReasonEnum { final case object Resecured extends CaptureFlagLostReasonEnum final case object TimedOut extends CaptureFlagLostReasonEnum final case object Ended extends CaptureFlagLostReasonEnum + final case object FlagLost extends CaptureFlagLostReasonEnum } diff --git a/src/main/scala/net/psforever/services/local/support/HackCaptureActor.scala b/src/main/scala/net/psforever/services/local/support/HackCaptureActor.scala index b9a4f4700..2423677d1 100644 --- a/src/main/scala/net/psforever/services/local/support/HackCaptureActor.scala +++ b/src/main/scala/net/psforever/services/local/support/HackCaptureActor.scala @@ -62,9 +62,14 @@ class HackCaptureActor extends Actor { // If the base has a socket, but no flag spawned it means the hacked base is neutral with no friendly neighbouring bases to deliver to, making it a timed hack. val building = terminal.Owner.asInstanceOf[Building] building.GetFlag match { + case Some(llu) if llu.LastCollectionTime == llu.InitialSpawnTime => + // LLU was never once collected. Send resecured notifications + terminal.Zone.LocalEvents ! CaptureFlagManager.Lost(llu, CaptureFlagLostReasonEnum.TimedOut) + NotifyHackStateChange(terminal, isResecured = true) + case Some(llu) => // LLU was not delivered in time. Send resecured notifications - terminal.Zone.LocalEvents ! CaptureFlagManager.Lost(llu, CaptureFlagLostReasonEnum.TimedOut) + terminal.Zone.LocalEvents ! CaptureFlagManager.Lost(llu, CaptureFlagLostReasonEnum.FlagLost) NotifyHackStateChange(terminal, isResecured = true) case _ => @@ -138,6 +143,26 @@ class HackCaptureActor extends Actor { log.error(s"Attempted LLU capture for ${flag.Owner.asInstanceOf[Building].Name} but CC GUID ${flag.Owner.asInstanceOf[Building].CaptureTerminal.get.GUID} was not in list of hacked objects") } + case HackCaptureActor.FlagLost(flag) => + val terminalOpt = flag.Owner.asInstanceOf[Building].CaptureTerminal + hackedObjects + .find(entry => terminalOpt.contains(entry.target)) + .collect { entry => + val terminal = terminalOpt.get + hackedObjects = hackedObjects.filterNot(x => x == entry) + log.info(s"FlagLost: ${flag.Carrier.map(_.Name).getOrElse("")} the flag carrier screwed up the capture for ${flag.Target.Name} and the LLU has been lost") + terminal.Actor ! CommonMessages.ClearHack() + NotifyHackStateChange(terminal, isResecured = true) + // If there's hacked objects left in the list restart the timer with the shortest hack time left + RestartTimer() + } + .orElse{ + log.warn(s"FlagLost: flag data does not match to an entry in the hacked objects list") + None + } + context.parent ! CaptureFlagManager.Lost(flag, CaptureFlagLostReasonEnum.FlagLost) + + case _ => () } @@ -256,6 +281,7 @@ object HackCaptureActor { final case class ResecureCaptureTerminal(target: CaptureTerminal, zone: Zone, hacker: PlayerSource) final case class FlagCaptured(flag: CaptureFlag) + final case class FlagLost(flag: CaptureFlag) private final case class ProcessCompleteHacks() diff --git a/src/main/scala/net/psforever/types/OxygenState.scala b/src/main/scala/net/psforever/types/OxygenState.scala index 56f27166a..c588a2ac7 100644 --- a/src/main/scala/net/psforever/types/OxygenState.scala +++ b/src/main/scala/net/psforever/types/OxygenState.scala @@ -1,9 +1,6 @@ package net.psforever.types import enumeratum.{Enum, EnumEntry} -import net.psforever.packet.PacketHelpers -import scodec.Codec -import scodec.codecs.uint /** * The progress state of being a drowning victim. @@ -22,6 +19,4 @@ object OxygenState extends Enum[OxygenState] { case object Recovery extends OxygenState case object Suffocation extends OxygenState - - implicit val codec: Codec[OxygenState] = PacketHelpers.createEnumCodec(e = this, uint(bits = 1)) }