diff --git a/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala b/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala index db4c14fef..0e542f53b 100644 --- a/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala +++ b/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala @@ -515,6 +515,14 @@ object GlobalDefinitions { val respawn_tube_tower = new SpawnTubeDefinition(733) + val adv_med_terminal = new MedicalTerminalDefinition(38) + + val crystals_health_a = new MedicalTerminalDefinition(225) + + val crystals_health_b = new MedicalTerminalDefinition(226) + + val medical_terminal = new MedicalTerminalDefinition(529) + val spawn_pad = new VehicleSpawnPadDefinition val mb_locker = new LockerDefinition diff --git a/common/src/main/scala/net/psforever/objects/serverobject/CommonMessages.scala b/common/src/main/scala/net/psforever/objects/serverobject/CommonMessages.scala index c73b7a235..ad7a26b61 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/CommonMessages.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/CommonMessages.scala @@ -5,6 +5,8 @@ import net.psforever.objects.Player //temporary location for these messages object CommonMessages { + final case class Use(player : Player) + final case class Unuse(player : Player) final case class Hack(player : Player) final case class ClearHack() } diff --git a/common/src/main/scala/net/psforever/objects/serverobject/terminals/MedicalTerminalDefinition.scala b/common/src/main/scala/net/psforever/objects/serverobject/terminals/MedicalTerminalDefinition.scala new file mode 100644 index 000000000..fbd7f0129 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/terminals/MedicalTerminalDefinition.scala @@ -0,0 +1,36 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.serverobject.terminals + +import net.psforever.objects.Player +import net.psforever.packet.game.ItemTransactionMessage + +/** + * The definition for any `Terminal` that is of a type "medical_terminal". + * This includes the limited proximity-based functionality of the formal medical terminals + * and the actual proximity-based functionality of the cavern crystals.
+ *
+ * Do not confuse the "medical_terminal" category and the actual `medical_terminal` object (529). + * Objects created by this definition being linked by their use of `ProximityTerminalUseMessage` is more accurate. + */ +class MedicalTerminalDefinition(objectId : Int) extends TerminalDefinition(objectId) { + Name = if(objectId == 38) { + "adv_med_terminal" + } + else if(objectId == 225) { + "crystals_health_a" + } + else if(objectId == 226) { + "crystals_health_b" + } + else if(objectId == 529) { + "medical_terminal" + } + else if(objectId == 689) { + "portable_med_terminal" + } + else { + throw new IllegalArgumentException("medical terminal must be either object id 38, 225, 226, 529, or 689") + } + + def Buy(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange = Terminal.NoDeal() +} diff --git a/common/src/main/scala/net/psforever/objects/serverobject/terminals/ProximityTerminal.scala b/common/src/main/scala/net/psforever/objects/serverobject/terminals/ProximityTerminal.scala new file mode 100644 index 000000000..c98e7da50 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/terminals/ProximityTerminal.scala @@ -0,0 +1,54 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.serverobject.terminals + +import net.psforever.packet.game.PlanetSideGUID + +/** + * A server object that is a "terminal" that can be accessed for amenities and services, + * triggered when a certain distance from the unit itself (proximity-based).
+ *
+ * Unlike conventional terminals, this structure is not necessarily structure-owned. + * For example, the cavern crystals are considered owner-neutral elements that are not attached to a `Building` object. + * @param tdef the `ObjectDefinition` that constructs this object and maintains some of its immutable fields + */ +class ProximityTerminal(tdef : MedicalTerminalDefinition) extends Terminal(tdef) { + private var users : Set[PlanetSideGUID] = Set.empty + + def NumberUsers : Int = users.size + + def AddUser(player_guid : PlanetSideGUID) : Int = { + users += player_guid + NumberUsers + } + + def RemoveUser(player_guid : PlanetSideGUID) : Int = { + users -= player_guid + NumberUsers + } +} + +object ProximityTerminal { + /** + * Overloaded constructor. + * @param tdef the `ObjectDefinition` that constructs this object and maintains some of its immutable fields + */ + def apply(tdef : MedicalTerminalDefinition) : ProximityTerminal = { + new ProximityTerminal(tdef) + } + + import akka.actor.ActorContext + + /** + * Instantiate an configure a `Terminal` object + * @param tdef the `ObjectDefinition` that constructs this object and maintains some of its immutable fields + * @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 `Terminal` object + */ + def Constructor(tdef : MedicalTerminalDefinition)(id : Int, context : ActorContext) : Terminal = { + import akka.actor.Props + val obj = ProximityTerminal(tdef) + obj.Actor = context.actorOf(Props(classOf[ProximityTerminalControl], obj), s"${tdef.Name}_$id") + obj + } +} diff --git a/common/src/main/scala/net/psforever/objects/serverobject/terminals/ProximityTerminalControl.scala b/common/src/main/scala/net/psforever/objects/serverobject/terminals/ProximityTerminalControl.scala new file mode 100644 index 000000000..407fd0cbf --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/terminals/ProximityTerminalControl.scala @@ -0,0 +1,37 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.serverobject.terminals + +import akka.actor.Actor +import net.psforever.objects.serverobject.CommonMessages +import net.psforever.objects.serverobject.affinity.{FactionAffinity, FactionAffinityBehavior} +import net.psforever.objects.serverobject.terminals.Terminal.TerminalMessage + +/** + * + * An `Actor` that handles messages being dispatched to a specific `ProximityTerminal`. + * Although this "terminal" itself does not accept the same messages as a normal `Terminal` object, + * it returns the same type of messages - wrapped in a `TerminalMessage` - to the `sender`. + * @param term the proximity unit (terminal) + */ +class ProximityTerminalControl(term : ProximityTerminal) extends Actor with FactionAffinityBehavior.Check { + def FactionObject : FactionAffinity = term + + def receive : Receive = checkBehavior.orElse { + case CommonMessages.Use(player) => + val hadNoUsers = term.NumberUsers == 0 + if(term.AddUser(player.GUID) == 1 && hadNoUsers) { + sender ! TerminalMessage(player, null, Terminal.StartProximityEffect(term)) + } + + case CommonMessages.Unuse(player) => + val hadUsers = term.NumberUsers > 0 + if(term.RemoveUser(player.GUID) == 0 && hadUsers) { + sender ! TerminalMessage(player, null, Terminal.StopProximityEffect(term)) + } + + case _ => + sender ! Terminal.NoDeal() + } + + override def toString : String = term.Definition.Name +} diff --git a/common/src/main/scala/net/psforever/objects/serverobject/terminals/Terminal.scala b/common/src/main/scala/net/psforever/objects/serverobject/terminals/Terminal.scala index 3f6f6389d..b031a9905 100644 --- a/common/src/main/scala/net/psforever/objects/serverobject/terminals/Terminal.scala +++ b/common/src/main/scala/net/psforever/objects/serverobject/terminals/Terminal.scala @@ -190,6 +190,18 @@ object Terminal { */ final case class InfantryLoadout(exosuit : ExoSuitType.Value, subtype : Int = 0, holsters : List[InventoryItem], inventory : List[InventoryItem]) extends Exchange + /** + * Start the special effects caused by a proximity-base service. + * @param terminal the proximity-based unit + */ + final case class StartProximityEffect(terminal : ProximityTerminal) extends Exchange + + /** + * Stop the special effects caused by a proximity-base service. + * @param terminal the proximity-based unit + */ + final case class StopProximityEffect(terminal : ProximityTerminal) extends Exchange + /** * Overloaded constructor. * @param tdef the `ObjectDefinition` that constructs this object and maintains some of its immutable fields diff --git a/common/src/test/scala/objects/ServerObjectBuilderTest.scala b/common/src/test/scala/objects/ServerObjectBuilderTest.scala index 813dbe36c..49678d5f3 100644 --- a/common/src/test/scala/objects/ServerObjectBuilderTest.scala +++ b/common/src/test/scala/objects/ServerObjectBuilderTest.scala @@ -6,6 +6,7 @@ import net.psforever.objects.guid.NumberPoolHub import net.psforever.packet.game.PlanetSideGUID import net.psforever.objects.serverobject.ServerObjectBuilder import net.psforever.objects.serverobject.structures.{Building, FoundationBuilder, StructureType, WarpGate} +import net.psforever.objects.serverobject.terminals.ProximityTerminal import net.psforever.objects.zones.Zone import net.psforever.types.Vector3 @@ -130,6 +131,24 @@ class TerminalObjectBuilderTest extends ActorTest { } } +class ProximityTerminalObjectBuilderTest extends ActorTest { + import net.psforever.objects.GlobalDefinitions.medical_terminal + import net.psforever.objects.serverobject.terminals.Terminal + "Terminal object" should { + "build" in { + val hub = ServerObjectBuilderTest.NumberPoolHub + val actor = system.actorOf(Props(classOf[ServerObjectBuilderTest.BuilderTestActor], ServerObjectBuilder(1, ProximityTerminal.Constructor(medical_terminal)), hub), "term") + actor ! "!" + + val reply = receiveOne(Duration.create(1000, "ms")) + assert(reply.isInstanceOf[Terminal]) + assert(reply.asInstanceOf[Terminal].HasGUID) + assert(reply.asInstanceOf[Terminal].GUID == PlanetSideGUID(1)) + assert(reply == hub(1).get) + } + } +} + class VehicleSpawnPadObjectBuilderTest extends ActorTest { import net.psforever.objects.serverobject.pad.VehicleSpawnPad "Vehicle spawn pad object" should { diff --git a/common/src/test/scala/objects/terminal/MedicalTerminalTest.scala b/common/src/test/scala/objects/terminal/MedicalTerminalTest.scala new file mode 100644 index 000000000..86b39fe3f --- /dev/null +++ b/common/src/test/scala/objects/terminal/MedicalTerminalTest.scala @@ -0,0 +1,90 @@ +// Copyright (c) 2017 PSForever +package objects.terminal + +import akka.actor.ActorRef +import net.psforever.objects.serverobject.terminals.{MedicalTerminalDefinition, ProximityTerminal, Terminal} +import net.psforever.objects.{Avatar, GlobalDefinitions, Player} +import net.psforever.packet.game.{ItemTransactionMessage, PlanetSideGUID} +import net.psforever.types.{CharacterGender, PlanetSideEmpire, TransactionType} +import org.specs2.mutable.Specification + +class MedicalTerminalTest extends Specification { + "MedicalTerminal" should { + "define (a)" in { + val a = new MedicalTerminalDefinition(38) + a.ObjectId mustEqual 38 + a.Name mustEqual "adv_med_terminal" + } + + "define (b)" in { + val b = new MedicalTerminalDefinition(225) + b.ObjectId mustEqual 225 + b.Name mustEqual "crystals_health_a" + } + + "define (c)" in { + val c = new MedicalTerminalDefinition(226) + c.ObjectId mustEqual 226 + c.Name mustEqual "crystals_health_b" + } + + "define (d)" in { + val d = new MedicalTerminalDefinition(529) + d.ObjectId mustEqual 529 + d.Name mustEqual "medical_terminal" + } + + "define (e)" in { + val e = new MedicalTerminalDefinition(689) + e.ObjectId mustEqual 689 + e.Name mustEqual "portable_med_terminal" + } + + "define (invalid)" in { + var id : Int = (math.random * Int.MaxValue).toInt + if(id == 224) { + id += 2 + } + else if(id == 37) { + id += 1 + } + else if(id == 528) { + id += 1 + } + else if(id == 688) { + id += 1 + } + + new MedicalTerminalDefinition(id) must throwA[IllegalArgumentException] + } + } + + "Medical_Terminal" should { + "construct" in { + ProximityTerminal(GlobalDefinitions.medical_terminal).Actor mustEqual ActorRef.noSender + } + + "can add a player to a list of users" in { + val terminal = ProximityTerminal(GlobalDefinitions.medical_terminal) + terminal.NumberUsers mustEqual 0 + terminal.AddUser(PlanetSideGUID(10)) + terminal.NumberUsers mustEqual 1 + } + + "can remove a player from a list of users" in { + val terminal = ProximityTerminal(GlobalDefinitions.medical_terminal) + terminal.AddUser(PlanetSideGUID(10)) + terminal.NumberUsers mustEqual 1 + terminal.RemoveUser(PlanetSideGUID(10)) + terminal.NumberUsers mustEqual 0 + } + + "player can not interact with the proximity terminal normally (buy)" in { + val terminal = ProximityTerminal(GlobalDefinitions.medical_terminal) + val player = Player(Avatar("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) + val msg = ItemTransactionMessage(PlanetSideGUID(1), TransactionType.Buy, 1, "lite_armor", 0, PlanetSideGUID(0)) + + terminal.Request(player, msg) mustEqual Terminal.NoDeal() + } + } +} diff --git a/common/src/test/scala/objects/terminal/ProximityTerminalControlTest.scala b/common/src/test/scala/objects/terminal/ProximityTerminalControlTest.scala new file mode 100644 index 000000000..f817be6a6 --- /dev/null +++ b/common/src/test/scala/objects/terminal/ProximityTerminalControlTest.scala @@ -0,0 +1,121 @@ +// Copyright (c) 2017 PSForever +package objects.terminal + +import akka.actor.{ActorSystem, Props} +import net.psforever.objects.serverobject.CommonMessages +import net.psforever.objects.{Avatar, GlobalDefinitions, Player} +import net.psforever.objects.serverobject.terminals._ +import net.psforever.packet.game.PlanetSideGUID +import net.psforever.types.{CharacterGender, PlanetSideEmpire} +import objects.ActorTest + +import scala.concurrent.duration.Duration + +class ProximityTerminalControl1Test extends ActorTest() { + "ProximityTerminalControl" should { + "construct (medical terminal)" in { + val terminal = ProximityTerminal(GlobalDefinitions.medical_terminal) + terminal.Actor = system.actorOf(Props(classOf[ProximityTerminalControl], terminal), "test-term") + } + } +} + +class ProximityTerminalControl2Test extends ActorTest() { + "ProximityTerminalControl can not process wrong messages" in { + val (_, terminal) = TerminalControlTest.SetUpAgents(GlobalDefinitions.medical_terminal, PlanetSideEmpire.TR) + + terminal.Actor !"hello" + val reply = receiveOne(Duration.create(500, "ms")) + assert(reply.isInstanceOf[Terminal.NoDeal]) + } +} + +//terminal control is mostly a pass-through actor for Terminal.Exchange messages, wrapped in Terminal.TerminalMessage protocol +class MedicalTerminalControl1Test extends ActorTest() { + "ProximityTerminalControl sends a message to the first new user only" in { + val (player, terminal) = ProximityTerminalControlTest.SetUpAgents(GlobalDefinitions.medical_terminal, PlanetSideEmpire.TR) + player.GUID = PlanetSideGUID(10) + val player2 = Player(Avatar("someothertest", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) + player2.GUID = PlanetSideGUID(11) + + terminal.Actor ! CommonMessages.Use(player) + val reply = receiveOne(Duration.create(500, "ms")) + assert(reply.isInstanceOf[Terminal.TerminalMessage]) + val reply2 = reply.asInstanceOf[Terminal.TerminalMessage] + assert(reply2.player == player) + assert(reply2.msg == null) + assert(reply2.response.isInstanceOf[Terminal.StartProximityEffect]) + assert(reply2.response.asInstanceOf[Terminal.StartProximityEffect].terminal == terminal) + assert(terminal.NumberUsers == 1) + + terminal.Actor ! CommonMessages.Use(player2) + expectNoMsg(Duration.create(500, "ms")) + assert(terminal.NumberUsers == 2) + } +} + +class MedicalTerminalControl2Test extends ActorTest() { + "ProximityTerminalControl sends a message to the last user only" in { + val (player, terminal) : (Player, ProximityTerminal) = ProximityTerminalControlTest.SetUpAgents(GlobalDefinitions.medical_terminal, PlanetSideEmpire.TR) + player.GUID = PlanetSideGUID(10) + val player2 = Player(Avatar("someothertest", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) + player2.GUID = PlanetSideGUID(11) + + terminal.Actor ! CommonMessages.Use(player) + receiveOne(Duration.create(500, "ms")) + terminal.Actor ! CommonMessages.Use(player2) + expectNoMsg(Duration.create(500, "ms")) + assert(terminal.NumberUsers == 2) + + terminal.Actor ! CommonMessages.Unuse(player) + expectNoMsg(Duration.create(500, "ms")) + assert(terminal.NumberUsers == 1) + + terminal.Actor ! CommonMessages.Unuse(player2) + val reply = receiveOne(Duration.create(500, "ms")) + assert(reply.isInstanceOf[Terminal.TerminalMessage]) + val reply2 = reply.asInstanceOf[Terminal.TerminalMessage] + assert(reply2.player == player2) + assert(reply2.msg == null) + assert(reply2.response.isInstanceOf[Terminal.StopProximityEffect]) + assert(reply2.response.asInstanceOf[Terminal.StopProximityEffect].terminal == terminal) + assert(terminal.NumberUsers == 0) + } +} + +class MedicalTerminalControl3Test extends ActorTest() { + "ProximityTerminalControl sends a message to the last user only (confirmation of test #2)" in { + val (player, terminal) : (Player, ProximityTerminal) = ProximityTerminalControlTest.SetUpAgents(GlobalDefinitions.medical_terminal, PlanetSideEmpire.TR) + player.GUID = PlanetSideGUID(10) + val player2 = Player(Avatar("someothertest", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0)) + player2.GUID = PlanetSideGUID(11) + + terminal.Actor ! CommonMessages.Use(player) + receiveOne(Duration.create(500, "ms")) + terminal.Actor ! CommonMessages.Use(player2) + expectNoMsg(Duration.create(500, "ms")) + assert(terminal.NumberUsers == 2) + + terminal.Actor ! CommonMessages.Unuse(player2) + expectNoMsg(Duration.create(500, "ms")) + assert(terminal.NumberUsers == 1) + + terminal.Actor ! CommonMessages.Unuse(player) + val reply = receiveOne(Duration.create(500, "ms")) + assert(reply.isInstanceOf[Terminal.TerminalMessage]) + val reply2 = reply.asInstanceOf[Terminal.TerminalMessage] + assert(reply2.player == player) //important! + assert(reply2.msg == null) + assert(reply2.response.isInstanceOf[Terminal.StopProximityEffect]) + assert(reply2.response.asInstanceOf[Terminal.StopProximityEffect].terminal == terminal) + assert(terminal.NumberUsers == 0) + } +} + +object ProximityTerminalControlTest { + def SetUpAgents(tdef : MedicalTerminalDefinition, faction : PlanetSideEmpire.Value)(implicit system : ActorSystem) : (Player, ProximityTerminal) = { + val terminal = ProximityTerminal(tdef) + terminal.Actor = system.actorOf(Props(classOf[ProximityTerminalControl], terminal), "test-term") + (Player(Avatar("test", faction, CharacterGender.Male, 0, 0)), terminal) + } +} diff --git a/pslogin/src/main/scala/Maps.scala b/pslogin/src/main/scala/Maps.scala index a6f8cd0ae..8b9351b12 100644 --- a/pslogin/src/main/scala/Maps.scala +++ b/pslogin/src/main/scala/Maps.scala @@ -7,7 +7,7 @@ import net.psforever.objects.serverobject.locks.IFFLock import net.psforever.objects.serverobject.mblocker.Locker import net.psforever.objects.serverobject.pad.VehicleSpawnPad import net.psforever.objects.serverobject.structures.{Building, FoundationBuilder, StructureType, WarpGate} -import net.psforever.objects.serverobject.terminals.Terminal +import net.psforever.objects.serverobject.terminals.{ProximityTerminal, Terminal} import net.psforever.objects.serverobject.tube.SpawnTube import net.psforever.types.Vector3 @@ -100,6 +100,8 @@ object Maps { LocalObject(1186, Locker.Constructor) LocalObject(1187, Locker.Constructor) LocalObject(1188, Locker.Constructor) + LocalObject(1492, ProximityTerminal.Constructor(medical_terminal)) //lobby + LocalObject(1494, ProximityTerminal.Constructor(medical_terminal)) //kitchen LocalObject(1564, Terminal.Constructor(order_terminal)) LocalObject(1568, Terminal.Constructor(order_terminal)) LocalObject(1569, Terminal.Constructor(order_terminal)) @@ -200,6 +202,8 @@ object Maps { ObjectToBuilding(1186, 2) ObjectToBuilding(1187, 2) ObjectToBuilding(1188, 2) + ObjectToBuilding(1492, 2) + ObjectToBuilding(1494, 2) ObjectToBuilding(1564, 2) ObjectToBuilding(1568, 2) ObjectToBuilding(1569, 2) @@ -452,6 +456,10 @@ object Maps { LocalObject(691, Locker.Constructor) LocalObject(692, Locker.Constructor) LocalObject(693, Locker.Constructor) + LocalObject(778, ProximityTerminal.Constructor(medical_terminal)) + LocalObject(779, ProximityTerminal.Constructor(medical_terminal)) + LocalObject(780, ProximityTerminal.Constructor(medical_terminal)) + LocalObject(781, ProximityTerminal.Constructor(medical_terminal)) LocalObject(842, Terminal.Constructor(order_terminal)) LocalObject(843, Terminal.Constructor(order_terminal)) LocalObject(844, Terminal.Constructor(order_terminal)) @@ -495,6 +503,10 @@ object Maps { ObjectToBuilding(691, 2) ObjectToBuilding(692, 2) ObjectToBuilding(693, 2) + ObjectToBuilding(778, 2) + ObjectToBuilding(779, 2) + ObjectToBuilding(780, 2) + ObjectToBuilding(781, 2) ObjectToBuilding(842, 2) ObjectToBuilding(843, 2) ObjectToBuilding(844, 2) diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index d6c597d96..46fe9aef6 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -26,7 +26,7 @@ import net.psforever.objects.serverobject.implantmech.ImplantTerminalMech import net.psforever.objects.serverobject.locks.IFFLock import net.psforever.objects.serverobject.mblocker.Locker import net.psforever.objects.serverobject.pad.VehicleSpawnPad -import net.psforever.objects.serverobject.terminals.{MatrixTerminalDefinition, Terminal} +import net.psforever.objects.serverobject.terminals.{MatrixTerminalDefinition, ProximityTerminal, Terminal} import net.psforever.objects.serverobject.terminals.Terminal.TerminalMessage import net.psforever.objects.vehicles.{AccessPermissionGroup, Utility, VehicleLockState} import net.psforever.objects.serverobject.structures.{Building, StructureType, WarpGate} @@ -66,6 +66,9 @@ class WorldSessionActor extends Actor with MDCContextAware { var speed : Float = 1.0f var spectator : Boolean = false var admin : Boolean = false + var usingMedicalTerminal : Option[PlanetSideGUID] = None + var usingProximityTerminal : Set[PlanetSideGUID] = Set.empty + var delayedProximityTerminalResets : Map[PlanetSideGUID, Cancellable] = Map.empty var clientKeepAlive : Cancellable = DefaultCancellable.obj var progressBarUpdate : Cancellable = DefaultCancellable.obj @@ -82,6 +85,18 @@ class WorldSessionActor extends Actor with MDCContextAware { LivePlayerList.Remove(sessionId) if(player != null && player.HasGUID) { val player_guid = player.GUID + //proximity vehicle terminals must be considered too + delayedProximityTerminalResets.foreach({case(_, task) => task.cancel}) + usingProximityTerminal.foreach(term_guid => { + continent.GUID(term_guid) match { + case Some(obj : ProximityTerminal) => + if(obj.NumberUsers > 0 && obj.RemoveUser(player_guid) == 0) { //refer to ProximityTerminalControl when modernizng + localService ! LocalServiceMessage(continent.Id, LocalAction.ProximityTerminalEffect(player_guid, term_guid, false)) + } + case _ => ; + } + }) + if(player.isAlive) { //actually being alive or manually deconstructing player.VehicleSeated match { @@ -371,6 +386,11 @@ class WorldSessionActor extends Actor with MDCContextAware { sendResponse(HackMessage(0, target_guid, guid, 100, unk1, HackState.Hacked, unk2)) } + case LocalResponse.ProximityTerminalEffect(object_guid, effectState) => + if(player.GUID != guid) { + sendResponse(ProximityTerminalUseMessage(PlanetSideGUID(0), object_guid, effectState)) + } + case LocalResponse.TriggerSound(sound, pos, unk, volume) => sendResponse(TriggerSoundMessage(sound, pos, unk, volume)) @@ -955,8 +975,28 @@ class WorldSessionActor extends Actor with MDCContextAware { log.error(s"$tplayer wanted to spawn a vehicle, but there was no spawn pad associated with terminal ${msg.terminal_guid} to accept it") } + case Terminal.StartProximityEffect(term) => + val player_guid = player.GUID + val term_guid = term.GUID + StartUsingProximityUnit(term) //redundant but cautious + sendResponse(ProximityTerminalUseMessage(player_guid, term_guid, true)) + localService ! LocalServiceMessage(continent.Id, LocalAction.ProximityTerminalEffect(player_guid, term_guid, true)) + + case Terminal.StopProximityEffect(term) => + val player_guid = player.GUID + val term_guid = term.GUID + StopUsingProximityUnit(term) //redundant but cautious + sendResponse(ProximityTerminalUseMessage(player_guid, term_guid, false)) + localService ! LocalServiceMessage(continent.Id, LocalAction.ProximityTerminalEffect(player_guid, term_guid, false)) + case Terminal.NoDeal() => - log.warn(s"$tplayer made a request but the terminal rejected the order $msg") + val order : String = if(msg == null) { + s"order $msg" + } + else { + "missing order" + } + log.warn(s"${tplayer.Name} made a request but the terminal rejected the $order") sendResponse(ItemTransactionResultMessage(msg.terminal_guid, msg.transaction_type, false)) } @@ -1244,6 +1284,9 @@ class WorldSessionActor extends Actor with MDCContextAware { } } + case DelayedProximityUnitStop(terminal) => + StopUsingProximityUnit(terminal) + case ResponseToSelf(pkt) => log.info(s"Received a direct message: $pkt") sendResponse(pkt) @@ -1437,6 +1480,10 @@ class WorldSessionActor extends Actor with MDCContextAware { player.FacingYawUpper = yaw_upper player.Crouching = is_crouching player.Jumping = is_jumping + + if(vel.isDefined && usingMedicalTerminal.isDefined) { + StopUsingProximityUnit(continent.GUID(usingMedicalTerminal.get).get.asInstanceOf[ProximityTerminal]) + } val wepInHand : Boolean = player.Slot(player.DrawnSlot).Equipment match { case Some(item) => item.Definition == GlobalDefinitions.bolt_driver case None => false @@ -1885,6 +1932,13 @@ class WorldSessionActor extends Actor with MDCContextAware { //TODO remove this kludge; explore how to stop BuyExoSuit(Max) sending a tardy ObjectHeldMessage(me, 255) if(player.ExoSuit != ExoSuitType.MAX && (player.DrawnSlot = held_holsters) != before) { avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.ObjectHeld(player.GUID, player.LastDrawnSlot)) + if(player.VisibleSlots.contains(held_holsters)) { + usingMedicalTerminal match { + case Some(term_guid) => + StopUsingProximityUnit(continent.GUID(term_guid).get.asInstanceOf[ProximityTerminal]) + case None => ; + } + } } case msg @ AvatarJumpMessage(state) => @@ -2156,6 +2210,7 @@ class WorldSessionActor extends Actor with MDCContextAware { case Some(obj : SpawnTube) => //deconstruction PlayerActionsToCancel() + CancelAllProximityUnits() player.Release sendResponse(AvatarDeadStateMessage(DeadState.Release, 0, 0, player.Position, player.Faction, true)) continent.Population ! Zone.Population.Release(avatar) @@ -2173,6 +2228,22 @@ class WorldSessionActor extends Actor with MDCContextAware { case None => ; } + case msg @ ProximityTerminalUseMessage(player_guid, object_guid, _) => + log.info(s"ProximityTerminal: $msg") + continent.GUID(object_guid) match { + case Some(obj : ProximityTerminal) => + if(usingProximityTerminal.contains(object_guid)) { + SelectProximityUnit(obj) + } + else { + StartUsingProximityUnit(obj) + } + case Some(obj) => ; + log.warn(s"ProximityTerminal: object is not a terminal - $obj") + case None => + log.warn(s"ProximityTerminal: no object with guid $object_guid found") + } + case msg @ UnuseItemMessage(player_guid, object_guid) => log.info("UnuseItem: " + msg) continent.GUID(object_guid) match { @@ -3429,6 +3500,7 @@ class WorldSessionActor extends Actor with MDCContextAware { avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(player_guid, 29, 1)) } PlayerActionsToCancel() + CancelAllProximityUnits() import scala.concurrent.duration._ import scala.concurrent.ExecutionContext.Implicits.global @@ -3472,7 +3544,7 @@ class WorldSessionActor extends Actor with MDCContextAware { } if(speed > 1) { sendResponse(ChatMsg(ChatMessageType.CMT_SPEED, false, "", "1.000", None)) - speed = 1f + speed = 1f } } @@ -3484,6 +3556,7 @@ class WorldSessionActor extends Actor with MDCContextAware { def AvatarCreate() : Unit = { player.Spawn player.Health = 50 //TODO temp + player.Armor = 25 val packet = player.Definition.Packet val dcdata = packet.DetailedConstructorData(player).get sendResponse(ObjectCreateDetailedMessage(ObjectClass.avatar, player.GUID, dcdata)) @@ -3599,6 +3672,186 @@ class WorldSessionActor extends Actor with MDCContextAware { } } + /** + * Start using a proximity-base service. + * Special note is warranted in the case of a medical terminal or an advanced medical terminal. + * @param terminal the proximity-based unit + */ + def StartUsingProximityUnit(terminal : ProximityTerminal) : Unit = { + val term_guid = terminal.GUID + if(!usingProximityTerminal.contains(term_guid)) { + usingProximityTerminal += term_guid + terminal.Definition match { + case GlobalDefinitions.adv_med_terminal | GlobalDefinitions.medical_terminal => + usingMedicalTerminal = Some(term_guid) + case _ => + SetDelayedProximityUnitReset(terminal) + } + terminal.Actor ! CommonMessages.Use(player) + } + } + + /** + * Stop using a proximity-base service. + * Special note is warranted when determining the identity of the proximity terminal. + * Medical terminals of both varieties can be cancelled by movement. + * Other sorts of proximity-based units are put on a timer. + * @param terminal the proximity-based unit + */ + def StopUsingProximityUnit(terminal : ProximityTerminal) : Unit = { + val term_guid = terminal.GUID + if(usingProximityTerminal.contains(term_guid)) { + usingProximityTerminal -= term_guid + ClearDelayedProximityUnitReset(term_guid) + if(usingMedicalTerminal.contains(term_guid)) { + usingMedicalTerminal = None + } + terminal.Actor ! CommonMessages.Unuse(player) + } + } + + /** + * For pure proximity-based units and services, a manual attempt at cutting off the functionality. + * First, if an existing timer can be found, cancel it. + * Then, create a new timer. + * If this timer completes, a message will be sent that will attempt to disassociate from the target proximity unit. + * @param terminal the proximity-based unit + */ + def SetDelayedProximityUnitReset(terminal : ProximityTerminal) : Unit = { + val terminal_guid = terminal.GUID + ClearDelayedProximityUnitReset(terminal_guid) + import scala.concurrent.duration._ + import scala.concurrent.ExecutionContext.Implicits.global + delayedProximityTerminalResets += terminal_guid -> + context.system.scheduler.scheduleOnce(3000 milliseconds, self, DelayedProximityUnitStop(terminal)) + } + + /** + * For pure proximity-based units and services, disable any manual attempt at cutting off the functionality. + * If an existing timer can be found, cancel it. + * @param terminal the proximity-based unit + */ + def ClearDelayedProximityUnitReset(terminal_guid : PlanetSideGUID) : Unit = { + delayedProximityTerminalResets.get(terminal_guid) match { + case Some(task) => + task.cancel + delayedProximityTerminalResets -= terminal_guid + case None => ; + } + } + + /** + * Cease all current interactions with proximity-based units. + * Pair with `PlayerActionsToCancel`, except when logging out (stopping). + * This operations may invoke callback messages. + * @see `postStop`
+ * `Terminal.StopProximityEffects` + */ + def CancelAllProximityUnits() : Unit = { + delayedProximityTerminalResets.foreach({case(term_guid, task) => + task.cancel + delayedProximityTerminalResets -= term_guid + }) + usingProximityTerminal.foreach(term_guid => { + StopUsingProximityUnit(continent.GUID(term_guid).get.asInstanceOf[ProximityTerminal]) + }) + } + + /** + * Determine which functionality to pursue, by being given a generic proximity-functional unit + * and determinig which kind of unit is being utilized. + * @param terminal the proximity-based unit + */ + def SelectProximityUnit(terminal : ProximityTerminal) : Unit = { + terminal.Definition match { + case GlobalDefinitions.adv_med_terminal | GlobalDefinitions.medical_terminal => + ProximityMedicalTerminal(terminal) + + case GlobalDefinitions.crystals_health_a | GlobalDefinitions.crystals_health_b => + SetDelayedProximityUnitReset(terminal) + ProximityHealCrystal(terminal) + + case _ => ; + } + } + + /** + * When standing on the platform of a(n advanced) medical terminal, + * resotre the player's health and armor points (when they need their health and armor points restored). + * If the player is both fully healed and fully repaired, stop using the terminal. + * @param unit the medical terminal + */ + def ProximityMedicalTerminal(unit : ProximityTerminal) : Unit = { + val healthFull : Boolean = if(player.Health < player.MaxHealth) { + HealAction(player) + } + else { + true + } + val armorFull : Boolean = if(player.Armor < player.MaxArmor) { + ArmorRepairAction(player) + } + else { + true + } + if(healthFull && armorFull) { + log.info(s"${player.Name} is all fixed up") + StopUsingProximityUnit(unit) + } + } + + /** + * When near a red cavern crystal, resotre the player's health (when they need their health restored). + * If the player is fully healed, stop using the crystal. + * @param unit the healing crystal + */ + def ProximityHealCrystal(unit : ProximityTerminal) : Unit = { + val healthFull : Boolean = if(player.Health < player.MaxHealth) { + HealAction(player) + } + else { + true + } + if(healthFull) { + log.info(s"${player.Name} is all healed up") + StopUsingProximityUnit(unit) + } + } + + /** + * Restore, at most, a specific amount of health points on a player. + * Send messages to connected client and to events system. + * @param tplayer the player + * @param repairValue the amount to heal; + * 10 by default + * @return whether the player can be repaired for any more health points + */ + def HealAction(tplayer : Player, healValue : Int = 10) : Boolean = { + log.info(s"Dispensing health to ${tplayer.Name} - <3") + val player_guid = tplayer.GUID + tplayer.Health = tplayer.Health + healValue + sendResponse(PlanetsideAttributeMessage(player_guid, 0, tplayer.Health)) + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(player_guid, 0, tplayer.Health)) + tplayer.Health == tplayer.MaxHealth + } + + /** + * Restore, at most, a specific amount of personal armor points on a player. + * Send messages to connected client and to events system. + * @param tplayer the player + * @param repairValue the amount to repair; + * 10 by default + * @return whether the player can be repaired for any more armor points + */ + def ArmorRepairAction(tplayer : Player, repairValue : Int = 10) : Boolean = { + log.info(s"Dispensing armor to ${tplayer.Name} - c[=") + val player_guid = tplayer.GUID + tplayer.Armor = tplayer.Armor + repairValue + sendResponse(PlanetsideAttributeMessage(player_guid, 4, tplayer.Armor)) + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(player_guid, 4, tplayer.Armor)) + tplayer.Armor == tplayer.MaxArmor + } + def failWithError(error : String) = { log.error(error) sendResponse(ConnectionClose()) @@ -3644,6 +3897,7 @@ object WorldSessionActor { private final case class ListAccountCharacters() private final case class SetCurrentAvatar(tplayer : Player) private final case class VehicleLoaded(vehicle : Vehicle) + private final case class DelayedProximityUnitStop(unit : ProximityTerminal) /** * A message that indicates the user is using a remote electronics kit to hack some server object. diff --git a/pslogin/src/main/scala/services/local/LocalAction.scala b/pslogin/src/main/scala/services/local/LocalAction.scala index 4003fd9b0..1c04f2e7b 100644 --- a/pslogin/src/main/scala/services/local/LocalAction.scala +++ b/pslogin/src/main/scala/services/local/LocalAction.scala @@ -14,5 +14,6 @@ object LocalAction { final case class DoorCloses(player_guid : PlanetSideGUID, door_guid : PlanetSideGUID) extends Action final case class HackClear(player_guid : PlanetSideGUID, target : PlanetSideServerObject, unk1 : Long, unk2 : Long = 8L) extends Action final case class HackTemporarily(player_guid : PlanetSideGUID, continent : Zone, target : PlanetSideServerObject, unk1 : Long, unk2 : Long = 8L) extends Action + final case class ProximityTerminalEffect(player_guid : PlanetSideGUID, object_guid : PlanetSideGUID, effectState : Boolean) extends Action final case class TriggerSound(player_guid : PlanetSideGUID, sound : TriggeredSound.Value, pos : Vector3, unk : Int, volume : Float) extends Action } diff --git a/pslogin/src/main/scala/services/local/LocalResponse.scala b/pslogin/src/main/scala/services/local/LocalResponse.scala index 72bd523f7..fdc2aa37c 100644 --- a/pslogin/src/main/scala/services/local/LocalResponse.scala +++ b/pslogin/src/main/scala/services/local/LocalResponse.scala @@ -11,5 +11,6 @@ object LocalResponse { final case class DoorCloses(door_guid : PlanetSideGUID) extends Response final case class HackClear(target_guid : PlanetSideGUID, unk1 : Long, unk2 : Long) extends Response final case class HackObject(target_guid : PlanetSideGUID, unk1 : Long, unk2 : Long) extends Response + final case class ProximityTerminalEffect(object_guid : PlanetSideGUID, effectState : Boolean) extends Response final case class TriggerSound(sound : TriggeredSound.Value, pos : Vector3, unk : Int, volume : Float) extends Response } diff --git a/pslogin/src/main/scala/services/local/LocalService.scala b/pslogin/src/main/scala/services/local/LocalService.scala index b11eef4e4..5c01ee1a4 100644 --- a/pslogin/src/main/scala/services/local/LocalService.scala +++ b/pslogin/src/main/scala/services/local/LocalService.scala @@ -55,6 +55,10 @@ class LocalService extends Actor { LocalEvents.publish( LocalServiceResponse(s"/$forChannel/Local", player_guid, LocalResponse.HackObject(target.GUID, unk1, unk2)) ) + case LocalAction.ProximityTerminalEffect(player_guid, object_guid, effectState) => + LocalEvents.publish( + LocalServiceResponse(s"/$forChannel/Local", player_guid, LocalResponse.ProximityTerminalEffect(object_guid, effectState)) + ) case LocalAction.TriggerSound(player_guid, sound, pos, unk, volume) => LocalEvents.publish( LocalServiceResponse(s"/$forChannel/Local", player_guid, LocalResponse.TriggerSound(sound, pos, unk, volume)) diff --git a/pslogin/src/test/scala/LocalServiceTest.scala b/pslogin/src/test/scala/LocalServiceTest.scala new file mode 100644 index 000000000..b6cbd6960 --- /dev/null +++ b/pslogin/src/test/scala/LocalServiceTest.scala @@ -0,0 +1,115 @@ +// Copyright (c) 2017 PSForever +import akka.actor.Props +import net.psforever.objects.serverobject.PlanetSideServerObject +import net.psforever.packet.game.PlanetSideGUID +import net.psforever.types.{PlanetSideEmpire, Vector3} +import services.Service +import services.local._ + +class LocalService1Test extends ActorTest { + "LocalService" should { + "construct" in { + system.actorOf(Props[LocalService], "service") + assert(true) + } + } +} + +class LocalService2Test extends ActorTest { + "LocalService" should { + "subscribe" in { + val service = system.actorOf(Props[LocalService], "service") + service ! Service.Join("test") + assert(true) + } + } +} + +class LocalService3Test extends ActorTest { + "LocalService" should { + "subscribe to a specific channel" in { + val service = system.actorOf(Props[LocalService], "service") + service ! Service.Join("test") + service ! Service.Leave() + assert(true) + } + } +} + +class LocalService4Test extends ActorTest { + "LocalService" should { + "subscribe" in { + val service = system.actorOf(Props[LocalService], "service") + service ! Service.Join("test") + service ! Service.LeaveAll() + assert(true) + } + } +} + +class LocalService5Test extends ActorTest { + "LocalService" should { + "pass an unhandled message" in { + val service = system.actorOf(Props[LocalService], "service") + service ! Service.Join("test") + service ! "hello" + expectNoMsg() + } + } +} + +class DoorClosesTest extends ActorTest { + "LocalService" should { + "pass DoorCloses" in { + val service = system.actorOf(Props[LocalService], "service") + service ! Service.Join("test") + service ! LocalServiceMessage("test", LocalAction.DoorCloses(PlanetSideGUID(10), PlanetSideGUID(40))) + expectMsg(LocalServiceResponse("/test/Local", PlanetSideGUID(10), LocalResponse.DoorCloses(PlanetSideGUID(40)))) + } + } +} + +class HackClearTest extends ActorTest { + val obj = new PlanetSideServerObject() { + def Faction = PlanetSideEmpire.NEUTRAL + def Definition = null + GUID = PlanetSideGUID(40) + } + + "LocalService" should { + "pass HackClear" in { + val service = system.actorOf(Props[LocalService], "service") + service ! Service.Join("test") + service ! LocalServiceMessage("test", LocalAction.HackClear(PlanetSideGUID(10), obj, 0L, 1000L)) + expectMsg(LocalServiceResponse("/test/Local", PlanetSideGUID(10), LocalResponse.HackClear(PlanetSideGUID(40), 0L, 1000L))) + } + } +} + +class ProximityTerminalEffectTest extends ActorTest { + "LocalService" should { + "pass ProximityTerminalEffect" in { + val service = system.actorOf(Props[LocalService], "service") + service ! Service.Join("test") + service ! LocalServiceMessage("test", LocalAction.ProximityTerminalEffect(PlanetSideGUID(10), PlanetSideGUID(40), true)) + expectMsg(LocalServiceResponse("/test/Local", PlanetSideGUID(10), LocalResponse.ProximityTerminalEffect(PlanetSideGUID(40), true))) + } + } +} + +class TriggerSoundTest extends ActorTest { + import net.psforever.packet.game.TriggeredSound + + "LocalService" should { + "pass TriggerSound" in { + val service = system.actorOf(Props[LocalService], "service") + service ! Service.Join("test") + service ! LocalServiceMessage("test", LocalAction.TriggerSound(PlanetSideGUID(10), TriggeredSound.LockedOut, Vector3(1.1f, 2.2f, 3.3f), 0, 0.75f)) + expectMsg(LocalServiceResponse("/test/Local", PlanetSideGUID(10), LocalResponse.TriggerSound(TriggeredSound.LockedOut, Vector3(1.1f, 2.2f, 3.3f), 0, 0.75f))) + } + } +} + +object LocalServiceTest { + //decoy +}