From 6f430f8de467941261d455fe52f3a0b5ba0c412a Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Tue, 18 Apr 2023 00:00:55 -0400 Subject: [PATCH] the medical applicator will not long open doors from a distance unless we want it to do that --- src/main/resources/application.conf | 2 + .../actors/session/support/SessionData.scala | 15 +++- .../scala/net/psforever/objects/Doors.scala | 81 +++++++++++++++++ .../serverobject/doors/DoorControl.scala | 90 +++++++++++++------ .../serverobject/doors/DoorDefinition.scala | 5 ++ .../local/support/DoorCloseActor.scala | 54 +++-------- .../scala/net/psforever/util/Config.scala | 11 ++- 7 files changed, 177 insertions(+), 81 deletions(-) create mode 100644 src/main/scala/net/psforever/objects/Doors.scala diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf index 0b676c169..d49f34977 100644 --- a/src/main/resources/application.conf +++ b/src/main/resources/application.conf @@ -177,6 +177,8 @@ game { variable = 30 } } + + doors-can-be-opened-by-med-app-from-this-distance = 5.05 } anti-cheat { 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 aef627f4f..4bbf960eb 100644 --- a/src/main/scala/net/psforever/actors/session/support/SessionData.scala +++ b/src/main/scala/net/psforever/actors/session/support/SessionData.scala @@ -540,7 +540,7 @@ class SessionData( } validObject(pkt.object_guid, decorator = "UseItem") match { case Some(door: Door) => - handleUseDoor(door) + handleUseDoor(door, equipment) case Some(resourceSilo: ResourceSilo) => handleUseResourceSilo(resourceSilo, equipment) case Some(panel: IFFLock) => @@ -1245,8 +1245,17 @@ class SessionData( } } - private def handleUseDoor(door: Door): Unit = { - door.Actor ! CommonMessages.Use(player) + private def handleUseDoor(door: Door, equipment: Option[Equipment]): Unit = { + equipment match { + case Some(tool: Tool) if tool.Definition == GlobalDefinitions.medicalapplicator => + val distance: Float = math.max( + Config.app.game.doorsCanBeOpenedByMedAppFromThisDistance, + door.Definition.initialOpeningDistance + ) + door.Actor ! CommonMessages.Use(player, Some(distance)) + case _ => + door.Actor ! CommonMessages.Use(player) + } } private def handleUseResourceSilo(resourceSilo: ResourceSilo, equipment: Option[Equipment]): Unit = { diff --git a/src/main/scala/net/psforever/objects/Doors.scala b/src/main/scala/net/psforever/objects/Doors.scala new file mode 100644 index 000000000..57cd0d9cf --- /dev/null +++ b/src/main/scala/net/psforever/objects/Doors.scala @@ -0,0 +1,81 @@ +// Copyright (c) 2023 PSForever +package net.psforever.objects + +import net.psforever.objects.serverobject.doors.Door +import net.psforever.types.Vector3 + +object Doors { + /** + * If a player was considered as holding the door open, + * check that if that player is still in the same zone as a door + * and is still standing within a certain distance of that door. + * If the target player no longer qualifies to hold the door open, + * search for a new player to hold the door open. + * If the target player did not initially exist (the door was closed), + * do not search any further. + * @param door door that is installed somewhere + * @param maximumDistance permissible square distance between the player and the door + * @return optional player; + * `None` if the no player can trigger the door + */ + def testForTargetHoldingDoorOpen( + door: Door, + maximumDistance: Float + ): Option[Player] = { + door.Open + .flatMap { + testForSpecificTargetHoldingDoorOpen(_, door, maximumDistance * maximumDistance) + .orElse { testForAnyTargetHoldingDoorOpen(door, maximumDistance) } + } + } + + /** + * Check that a player is in the same zone as a door + * and is standing within a certain distance of that door. + * @see `Vector3.MagnitudeSquared` + * @param player player who is standing somewhere + * @param door door that is installed somewhere + * @param maximumDistance permissible square distance between the player and the door + * before one can not influence the other + * @return optional player (same as parameter); + * `None` if the parameter player can not trigger the door + */ + def testForSpecificTargetHoldingDoorOpen( + player: Player, + door: Door, + maximumDistance: Float + ): Option[Player] = { + if (player.Zone == door.Zone && Vector3.MagnitudeSquared(door.Position - player.Position) <= maximumDistance) { + Some(player) + } else { + None + } + } + + /** + * Find a player, any player, that can hold the door open. + * Prop it open with a dead body if no one is available. + * @param door door that is installed somewhere + * @param maximumDistance permissible square distance between a player and the door + * before one can not influence the other + * @return optional player; + * `None` if no player can trigger the door + */ + private def testForAnyTargetHoldingDoorOpen( + door: Door, + maximumDistance: Float + ): Option[Player] = { + //search for nearby players and nearby former players who would block open the door + val zone = door.Zone + val maximumDistanceSq = maximumDistance * maximumDistance + val bmap = zone.blockMap.sector(door.Position, maximumDistance) + (bmap.livePlayerList ++ bmap.corpseList) + .find { testForSpecificTargetHoldingDoorOpen(_, door, maximumDistanceSq).nonEmpty } match { + case out @ Some(newOpener) => + door.Open = newOpener //another player is near the door, keep it open + out + case _ => + None + } + } +} diff --git a/src/main/scala/net/psforever/objects/serverobject/doors/DoorControl.scala b/src/main/scala/net/psforever/objects/serverobject/doors/DoorControl.scala index 573db81f6..1cd30f116 100644 --- a/src/main/scala/net/psforever/objects/serverobject/doors/DoorControl.scala +++ b/src/main/scala/net/psforever/objects/serverobject/doors/DoorControl.scala @@ -1,7 +1,8 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.serverobject.doors -import net.psforever.objects.Player +import akka.actor.ActorRef +import net.psforever.objects.{Default, Doors, Player} import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior} import net.psforever.objects.serverobject.locks.IFFLock @@ -17,8 +18,9 @@ class DoorControl(door: Door) extends PoweredAmenityControl with FactionAffinityBehavior.Check { def FactionObject: FactionAffinity = door - var isLocked: Boolean = false - var lockingMechanism: Door.LockingMechanismLogic = DoorControl.alwaysOpen + + private var isLocked: Boolean = false + private var lockingMechanism: Door.LockingMechanismLogic = DoorControl.alwaysOpen def commonBehavior: Receive = checkBehavior .orElse { @@ -40,15 +42,16 @@ class DoorControl(door: Door) def poweredStateLogic: Receive = commonBehavior .orElse { + case CommonMessages.Use(player, Some(customDistance: Float)) => + testToOpenDoor(player, door, customDistance, sender()) + case CommonMessages.Use(player, _) => - if (lockingMechanism(player, door) && !isLocked) { - openDoor(player) - } + testToOpenDoor(player, door, door.Definition.initialOpeningDistance, sender()) case IFFLock.DoorOpenResponse(target: Player) if !isLocked => - openDoor(target) + DoorControl.openDoor(target, door) - case _ => ; + case _ => () } def unpoweredStateLogic: Receive = { @@ -56,30 +59,33 @@ class DoorControl(door: Door) .orElse { case CommonMessages.Use(player, _) if !isLocked => //without power, the door opens freely - openDoor(player) + DoorControl.openDoor(player, door) - case _ => ; + case _ => () } } - def openDoor(player: Player): Unit = { - val zone = door.Zone - val doorGUID = door.GUID - if (!door.isOpen) { - //global open - door.Open = player - zone.LocalEvents ! LocalServiceMessage( - zone.id, - LocalAction.DoorOpens(Service.defaultPlayerGUID, zone, door) - ) - } - else { - //the door should already open, but the requesting player does not see it as open - sender() ! LocalServiceResponse( - player.Name, - Service.defaultPlayerGUID, - LocalResponse.DoorOpens(doorGUID) - ) + /** + * If the player is close enough to the door, + * the locking mechanism allows for the door to open, + * and the door is not bolted shut (locked), + * then tell the door that it should open. + * @param player player who is standing somewhere + * @param door door that is installed somewhere + * @param maximumDistance permissible square distance between the player and the door + * @param replyTo the player's session message reference + */ + private def testToOpenDoor( + player: Player, + door: Door, + maximumDistance: Float, + replyTo: ActorRef + ): Unit = { + if ( + Doors.testForSpecificTargetHoldingDoorOpen(player, door, maximumDistance * maximumDistance).contains(player) && + lockingMechanism(player, door) && !isLocked + ) { + DoorControl.openDoor(player, door, replyTo) } } @@ -89,5 +95,33 @@ class DoorControl(door: Door) } object DoorControl { + //noinspection ScalaUnusedSymbol def alwaysOpen(obj: PlanetSideServerObject, door: Door): Boolean = true + + /** + * If the door is not open, open this door, propped open by the given player. + * If the door is considered open, ensure the door is proper visible as open to the player. + * @param player the player + * @param door the door + * @param replyTo the player's session message reference + */ + private def openDoor(player: Player, door: Door, replyTo: ActorRef = Default.Actor): Unit = { + val zone = door.Zone + val doorGUID = door.GUID + if (!door.isOpen) { + //global open + door.Open = player + zone.LocalEvents ! LocalServiceMessage( + zone.id, + LocalAction.DoorOpens(Service.defaultPlayerGUID, zone, door) + ) + } else { + //the door should already open, but the requesting player does not see it as open + replyTo ! LocalServiceResponse( + player.Name, + Service.defaultPlayerGUID, + LocalResponse.DoorOpens(doorGUID) + ) + } + } } diff --git a/src/main/scala/net/psforever/objects/serverobject/doors/DoorDefinition.scala b/src/main/scala/net/psforever/objects/serverobject/doors/DoorDefinition.scala index bb074612c..f2b3936e1 100644 --- a/src/main/scala/net/psforever/objects/serverobject/doors/DoorDefinition.scala +++ b/src/main/scala/net/psforever/objects/serverobject/doors/DoorDefinition.scala @@ -9,4 +9,9 @@ import net.psforever.objects.serverobject.structures.AmenityDefinition class DoorDefinition(objectId: Int) extends AmenityDefinition(objectId) { Name = "door" + /** range wherein the door may first be opened + * (note: intentionally inflated as the initial check on the client occurs further than expected) */ + var initialOpeningDistance: Float = 7.5f + /** range within which the door must detect a target player to remain open */ + var continuousOpenDistance: Float = 5.05f } diff --git a/src/main/scala/net/psforever/services/local/support/DoorCloseActor.scala b/src/main/scala/net/psforever/services/local/support/DoorCloseActor.scala index e6557011f..3d25dcb41 100644 --- a/src/main/scala/net/psforever/services/local/support/DoorCloseActor.scala +++ b/src/main/scala/net/psforever/services/local/support/DoorCloseActor.scala @@ -2,11 +2,10 @@ package net.psforever.services.local.support import akka.actor.{Actor, Cancellable} -import net.psforever.objects.{Default, Player} +import net.psforever.objects.{Default, Doors} import net.psforever.objects.serverobject.doors.Door -import net.psforever.objects.serverobject.structures.Building import net.psforever.objects.zones.Zone -import net.psforever.types.{PlanetSideGUID, Vector3} +import net.psforever.types.PlanetSideGUID import scala.annotation.tailrec import scala.concurrent.duration._ @@ -17,7 +16,6 @@ import scala.concurrent.duration._ * @see `LocalService` */ class DoorCloseActor() extends Actor { - /** The periodic `Executor` that checks for doors to be closed */ private var doorCloserTrigger: Cancellable = Default.Cancellable @@ -36,43 +34,11 @@ class DoorCloseActor() extends Actor { case DoorCloseActor.TryCloseDoors() => doorCloserTrigger.cancel() - val now: Long = System.nanoTime + val now: Long = System.currentTimeMillis() val (doorsToClose1, doorsLeftOpen1) = PartitionEntries(openDoors, now) - val (doorsToClose2, doorsLeftOpen2) = doorsToClose1.partition(entry => { - entry.door.Open match { - case Some(player) => - // If the player that opened the door is far enough away, or they're dead, - var openerIsGone = Vector3.MagnitudeSquared(entry.door.Position - player.Position) > 25.5 || !player.isAlive - - if (openerIsGone) { - // Check nobody else is nearby to hold the door opens - val playersToCheck: List[Player] = - if ( - entry.door.Owner - .isInstanceOf[Building] && entry.door.Owner.asInstanceOf[Building].Definition.SOIRadius > 0 - ) { - entry.door.Owner.asInstanceOf[Building].PlayersInSOI - } else { - entry.zone.LivePlayers - } - - playersToCheck - .filter(x => x.isAlive && Vector3.MagnitudeSquared(entry.door.Position - x.Position) < 25.5) - .headOption match { - case Some(newOpener) => - // Another player is near the door, keep it open - entry.door.Open = newOpener - openerIsGone = false - case _ => ; - } - } - - openerIsGone - case None => - // Door should not be open. Mark it to be closed. - true - } - }) + val (doorsToClose2, doorsLeftOpen2) = doorsToClose1.partition { entry => + Doors.testForTargetHoldingDoorOpen(entry.door, entry.door.Definition.continuousOpenDistance).isEmpty + } openDoors = ( doorsLeftOpen1 ++ doorsLeftOpen2.map(entry => DoorCloseActor.DoorEntry(entry.door, entry.zone, now)) @@ -89,7 +55,7 @@ class DoorCloseActor() extends Actor { doorCloserTrigger = context.system.scheduler.scheduleOnce(short_timeout, self, DoorCloseActor.TryCloseDoors()) } - case _ => ; + case _ => () } /** @@ -144,9 +110,9 @@ class DoorCloseActor() extends Actor { object DoorCloseActor { /** The wait before an open door closes; as a Long for calculation simplicity */ - private final val timeout_time: Long = 5000000000L //nanoseconds (5s) + private final val timeout_time: Long = 5000L //milliseconds (5s) /** The wait before an open door closes; as a `FiniteDuration` for `Executor` simplicity */ - private final val timeout: FiniteDuration = timeout_time nanoseconds + private final val timeout: FiniteDuration = timeout_time milliseconds /** * Message that carries information about a door that has been opened. @@ -155,7 +121,7 @@ object DoorCloseActor { * @param time when the door was opened * @see `DoorEntry` */ - final case class DoorIsOpen(door: Door, zone: Zone, time: Long = System.nanoTime()) + final case class DoorIsOpen(door: Door, zone: Zone, time: Long = System.currentTimeMillis()) /** * Message that carries information about a door that needs to close. diff --git a/src/main/scala/net/psforever/util/Config.scala b/src/main/scala/net/psforever/util/Config.scala index a3e3335f4..1c65c45d0 100644 --- a/src/main/scala/net/psforever/util/Config.scala +++ b/src/main/scala/net/psforever/util/Config.scala @@ -46,7 +46,7 @@ object Config { viaNonEmptyStringOpt[A]( v => e.values.toList.collectFirst { - case e if e.toString.toLowerCase == v.toLowerCase => e.asInstanceOf[A] + case e if e.toString.toLowerCase == v.toLowerCase => e }, _.toString ) @@ -62,25 +62,23 @@ object Config { // Raw config object - prefer app when possible lazy val config: TypesafeConfig = source.config() match { case Right(config) => config - case Left(failures) => { + case Left(failures) => logger.error("Loading config failed") failures.toList.foreach { failure => logger.error(failure.toString) } sys.exit(1) - } } // Typed config object lazy val app: AppConfig = source.load[AppConfig] match { case Right(config) => config - case Left(failures) => { + case Left(failures) => logger.error("Loading config failed") failures.toList.foreach { failure => logger.error(failure.toString) } sys.exit(1) - } } } @@ -159,7 +157,8 @@ case class GameConfig( baseCertifications: Seq[Certification], warpGates: WarpGateConfig, cavernRotation: CavernRotationConfig, - savedMsg: SavedMessageEvents + savedMsg: SavedMessageEvents, + doorsCanBeOpenedByMedAppFromThisDistance: Float ) case class NewAvatar(