From 5b0203850d09f3eb1e4af81fe2130bdc91fcf74b Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Tue, 18 Apr 2023 20:43:02 -0400 Subject: [PATCH] Not Really a Door Opener (#1063) * the medical applicator will not long open doors from a distance unless we want it to do that * fixing tests --- 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 +-- src/test/scala/objects/DoorTest.scala | 91 +++++++++---------- 8 files changed, 222 insertions(+), 127 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 0b676c16..d49f3497 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 aef627f4..4bbf960e 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 00000000..57cd0d9c --- /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 573db81f..1cd30f11 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 bb074612..f2b3936e 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 e6557011..3d25dcb4 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 a3e3335f..1c65c45d 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( diff --git a/src/test/scala/objects/DoorTest.scala b/src/test/scala/objects/DoorTest.scala index 72e08daf..15d9db03 100644 --- a/src/test/scala/objects/DoorTest.scala +++ b/src/test/scala/objects/DoorTest.scala @@ -1,7 +1,7 @@ // Copyright (c) 2017 PSForever package objects -import akka.actor.{ActorSystem, Props} +import akka.actor.{ActorRef, ActorSystem, Props} import akka.testkit.TestProbe import base.ActorTest import net.psforever.objects.avatar.Avatar @@ -12,15 +12,14 @@ import net.psforever.objects.{Default, GlobalDefinitions, Player} import net.psforever.objects.serverobject.doors.{Door, DoorControl} import net.psforever.objects.serverobject.structures.{Building, StructureType} import net.psforever.objects.zones.{Zone, ZoneMap} -import net.psforever.packet.game.UseItemMessage -import net.psforever.services.local.{LocalAction, LocalServiceMessage} +import net.psforever.services.local.{LocalAction, LocalResponse, LocalServiceMessage, LocalServiceResponse} import net.psforever.types._ import org.specs2.mutable.Specification import scala.concurrent.duration._ class DoorTest extends Specification { - val player = Player(Avatar(0, "test", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) + private val player: Player = Player(Avatar(0, "test", PlanetSideEmpire.TR, CharacterSex.Male, 0, CharacterVoice.Mute)) "Door" should { "construct" in { @@ -49,19 +48,6 @@ class DoorTest extends Specification { } "be opened and closed (2; toggle)" in { - val msg = UseItemMessage( - PlanetSideGUID(6585), - PlanetSideGUID(0), - PlanetSideGUID(372), - 4294967295L, - false, - Vector3(5.0f, 0.0f, 0.0f), - Vector3(0.0f, 0.0f, 0.0f), - 11, - 25, - 0, - 364 - ) val door = Door(GlobalDefinitions.door) door.Open.isEmpty mustEqual true door.Open = player @@ -74,7 +60,7 @@ class DoorTest extends Specification { } } -class DoorControl1Test extends ActorTest { +class DoorControlConstructTest extends ActorTest { "DoorControl" should { "construct" in { val door = Door(GlobalDefinitions.door) @@ -84,28 +70,11 @@ class DoorControl1Test extends ActorTest { } } -class DoorControl2Test extends ActorTest { +class DoorControlOpenTest extends ActorTest { "DoorControl" should { "open on use" in { - val (player, door) = DoorControlTest.SetUpAgents(PlanetSideEmpire.TR) - val probe = new TestProbe(system) - door.Zone.LocalEvents = probe.ref - val msg = UseItemMessage( - PlanetSideGUID(1), - PlanetSideGUID(0), - PlanetSideGUID(2), - 0L, - false, - Vector3(0f, 0f, 0f), - Vector3(0f, 0f, 0f), - 0, - 0, - 0, - 0L - ) //faked - assert(door.Open.isEmpty) - - door.Actor ! CommonMessages.Use(player, Some(msg)) + val (player, door, probe) = DoorControlTest.SetUpAgents(PlanetSideEmpire.TR) + door.Actor ! CommonMessages.Use(player) val reply = probe.receiveOne(1000 milliseconds) assert(reply match { case LocalServiceMessage("test", LocalAction.DoorOpens(PlanetSideGUID(0), _, d)) => d eq door @@ -116,26 +85,55 @@ class DoorControl2Test extends ActorTest { } } -class DoorControl3Test extends ActorTest { +class DoorControlTooFarTest extends ActorTest { "DoorControl" should { - "do nothing if given garbage" in { - val (_, door) = DoorControlTest.SetUpAgents(PlanetSideEmpire.TR) + "do not open if the player is too far away" in { + val (player, door, probe) = DoorControlTest.SetUpAgents(PlanetSideEmpire.TR) + player.Position = Vector3(10,0,0) + door.Actor ! CommonMessages.Use(player) + probe.expectNoMessage(Duration.create(500, "ms")) + assert(door.Open.isEmpty) + } + } +} + +class DoorControlAlreadyOpenTest extends ActorTest { + "DoorControl" should { + "is already open" in { + val (player, door, probe) = DoorControlTest.SetUpAgents(PlanetSideEmpire.TR) + door.Open = player //door thinks it is open + door.Actor.tell(CommonMessages.Use(player), probe.ref) + val reply = probe.receiveOne(1000 milliseconds) + assert(reply match { + case LocalServiceResponse("test", _, LocalResponse.DoorOpens(guid)) => guid == door.GUID + case _ => false + }) + } + } +} + +class DoorControlGarbageDataTest extends ActorTest { + "DoorControl" should { + "do nothing if given garbage data" in { + val (_, door, probe) = DoorControlTest.SetUpAgents(PlanetSideEmpire.TR) assert(door.Open.isEmpty) door.Actor ! "trash" - expectNoMessage(Duration.create(500, "ms")) + probe.expectNoMessage(Duration.create(500, "ms")) assert(door.Open.isEmpty) } } } object DoorControlTest { - def SetUpAgents(faction: PlanetSideEmpire.Value)(implicit system: ActorSystem): (Player, Door) = { + def SetUpAgents(faction: PlanetSideEmpire.Value)(implicit system: ActorSystem): (Player, Door, TestProbe) = { + val eventsProbe = new TestProbe(system) val door = Door(GlobalDefinitions.door) val guid = new NumberPoolHub(new MaxNumberSource(5)) - val zone = new Zone("test", new ZoneMap("test"), 0) { - override def SetupNumberPools() = {} + val zone = new Zone(id = "test", new ZoneMap(name = "test"), zoneNumber = 0) { + override def SetupNumberPools(): Unit = {} GUID(guid) + override def LocalEvents: ActorRef = eventsProbe.ref } guid.register(door, 1) door.Actor = system.actorOf(Props(classOf[DoorControl], door), "door") @@ -149,7 +147,8 @@ object DoorControlTest { ) door.Owner.Faction = faction val player = Player(Avatar(0, "test", faction, CharacterSex.Male, 0, CharacterVoice.Mute)) + player.Zone = zone guid.register(player, 2) - (player, door) + (player, door, eventsProbe) } }