From f9beb47073ff2c0277d83360647bf089e27c3fd5 Mon Sep 17 00:00:00 2001 From: FateJH Date: Wed, 29 Nov 2017 22:30:25 -0500 Subject: [PATCH] Implant Terminals: Implant terminals (mech) are now properly mountable and implant terminals (interface) are also properly interactive. Player can select to equip or to remove implants properly. Mountable: Vehicles and implant terminal mechs now use common Mountable logic. home3 Hart C: All doors, save for those to the shuttle, and all implant terminals in this building are now rigged to operate. --- .../psforever/objects/GlobalDefinitions.scala | 5 + .../net/psforever/objects/ImplantSlot.scala | 18 +- .../scala/net/psforever/objects/Player.scala | 97 ++++-- .../scala/net/psforever/objects/Vehicle.scala | 40 +-- .../ImplantTerminalInterfaceConverter.scala | 15 + .../converter/VehicleConverter.scala | 3 +- .../psforever/objects/mount/Mountable.scala | 92 +++++ .../objects/mount/MountableControl.scala | 27 ++ .../ImplantTerminalMechObjectBuilder.scala | 34 ++ .../implantmech/ImplantTerminalMech.scala | 42 +++ .../ImplantTerminalMechControl.scala | 14 + .../ImplantTerminalMechDefinition.scala | 22 ++ .../ImplantTerminalInterfaceDefinition.scala | 50 +++ .../serverobject/terminals/Terminal.scala | 18 + .../objects/vehicles/VehicleControl.scala | 18 +- .../psforever/objects/zones/ZoneActor.scala | 33 +- .../net/psforever/objects/zones/ZoneMap.scala | 7 + .../game/objectcreate/ObjectClass.scala | 2 +- .../test/scala/objects/MountableTest.scala | 87 +++++ .../src/test/scala/objects/PlayerTest.scala | 8 +- .../objects/ServerObjectBuilderTest.scala | 116 +++++++ .../src/test/scala/objects/VehicleTest.scala | 10 +- common/src/test/scala/objects/ZoneTest.scala | 18 + .../ImplantTerminalInterfaceTest.scala | 52 +++ .../terminal/ImplantTerminalMechTest.scala | 110 ++++++ pslogin/src/main/scala/PsLogin.scala | 58 +++- .../src/main/scala/WorldSessionActor.scala | 314 +++++++++++++----- 27 files changed, 1134 insertions(+), 176 deletions(-) create mode 100644 common/src/main/scala/net/psforever/objects/definition/converter/ImplantTerminalInterfaceConverter.scala create mode 100644 common/src/main/scala/net/psforever/objects/mount/Mountable.scala create mode 100644 common/src/main/scala/net/psforever/objects/mount/MountableControl.scala create mode 100644 common/src/main/scala/net/psforever/objects/serverobject/builders/ImplantTerminalMechObjectBuilder.scala create mode 100644 common/src/main/scala/net/psforever/objects/serverobject/implantmech/ImplantTerminalMech.scala create mode 100644 common/src/main/scala/net/psforever/objects/serverobject/implantmech/ImplantTerminalMechControl.scala create mode 100644 common/src/main/scala/net/psforever/objects/serverobject/implantmech/ImplantTerminalMechDefinition.scala create mode 100644 common/src/main/scala/net/psforever/objects/serverobject/terminals/ImplantTerminalInterfaceDefinition.scala create mode 100644 common/src/test/scala/objects/MountableTest.scala create mode 100644 common/src/test/scala/objects/ServerObjectBuilderTest.scala create mode 100644 common/src/test/scala/objects/terminal/ImplantTerminalInterfaceTest.scala create mode 100644 common/src/test/scala/objects/terminal/ImplantTerminalMechTest.scala diff --git a/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala b/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala index f4fe1777..7299d182 100644 --- a/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala +++ b/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala @@ -7,6 +7,7 @@ import net.psforever.objects.serverobject.doors.DoorDefinition import net.psforever.objects.equipment.CItem.DeployedItem import net.psforever.objects.equipment._ import net.psforever.objects.inventory.InventoryTile +import net.psforever.objects.serverobject.implantmech.ImplantTerminalMechDefinition import net.psforever.objects.serverobject.locks.IFFLockDefinition import net.psforever.objects.serverobject.terminals._ import net.psforever.objects.vehicles.SeatArmorRestriction @@ -467,6 +468,10 @@ object GlobalDefinitions { val cert_terminal = new CertTerminalDefinition + val implant_terminal_mech = new ImplantTerminalMechDefinition + + val implant_terminal_interface = new ImplantTerminalInterfaceDefinition + val ground_vehicle_terminal = new GroundVehicleTerminalDefinition val air_vehicle_terminal = new AirVehicleTerminalDefinition diff --git a/common/src/main/scala/net/psforever/objects/ImplantSlot.scala b/common/src/main/scala/net/psforever/objects/ImplantSlot.scala index 736b03f1..eca14edf 100644 --- a/common/src/main/scala/net/psforever/objects/ImplantSlot.scala +++ b/common/src/main/scala/net/psforever/objects/ImplantSlot.scala @@ -26,7 +26,7 @@ class ImplantSlot { def Unlocked : Boolean = unlocked def Unlocked_=(lock : Boolean) : Boolean = { - unlocked = lock || unlocked + unlocked = lock || unlocked //do not let re-lock Unlocked } @@ -45,13 +45,13 @@ class ImplantSlot { Active } - def Implant : ImplantType.Value = if(Installed.isDefined) { - implant.get.Type - } - else { - Active = false - Initialized = false - ImplantType.None + def Implant : ImplantType.Value = Installed match { + case Some(idef) => + idef.Type + case None => + Active = false + Initialized = false + ImplantType.None } def Implant_=(anImplant : ImplantDefinition) : ImplantType.Value = { @@ -64,6 +64,8 @@ class ImplantSlot { case Some(_) => implant = anImplant case None => + Active = false + Initialized = false implant = None } } diff --git a/common/src/main/scala/net/psforever/objects/Player.scala b/common/src/main/scala/net/psforever/objects/Player.scala index 37c6a285..4b718f13 100644 --- a/common/src/main/scala/net/psforever/objects/Player.scala +++ b/common/src/main/scala/net/psforever/objects/Player.scala @@ -37,6 +37,15 @@ class Player(private val name : String, private var bep : Long = 0 private var cep : Long = 0 private val certifications : mutable.Set[CertificationType.Value] = mutable.Set[CertificationType.Value]() + /** + * Unlike other objects, the maximum number of `ImplantSlots` are built into the `Player`. + * Additionally, "implants" do not have tightly-coupled "`Definition` objects" that explain a formal implant object. + * The `ImplantDefinition` objects themselves are moved around as if they were the implants. + * The term internally used for this process is "installed" and "uninstalled." + * @see `ImplantSlot` + * @see `DetailedCharacterData.implants` + * @see `AvatarConverter.MakeImplantEntries` + */ private val implants : Array[ImplantSlot] = Array.fill[ImplantSlot](3)(new ImplantSlot) // private var tosRibbon : MeritCommendation.Value = MeritCommendation.None @@ -330,56 +339,94 @@ class Player(private val name : String, def Certifications : mutable.Set[CertificationType.Value] = certifications + /** + * Retrieve the three implant slots for this player. + * @return an `Array` of `ImplantSlot` objects + */ def Implants : Array[ImplantSlot] = implants + /** + * What kind of implant is installed into the given slot number? + * @see `ImplantType` + * @param slot the slot number + * @return the tye of implant + */ def Implant(slot : Int) : ImplantType.Value = { if(-1 < slot && slot < implants.length) { implants(slot).Implant } else { ImplantType.None } } - def InstallImplant(implant : ImplantDefinition) : Boolean = { + /** + * Given a new implant, assign it into a vacant implant slot on this player.
+ *
+ * The implant must be unique in terms of which implants have already been assigned to this player. + * Multiple of a type of implant being assigned at once is not supported. + * Additionally, the implant is inserted into the earliest yet-unknown but vacant slot. + * Implant slots are vacant by just being unlocked or by having their previous implant uninstalled. + * @param implant the implant being installed + * @return the index of the `ImplantSlot` where the implant was installed + */ + def InstallImplant(implant : ImplantDefinition) : Option[Int] = { implants.find({p => p.Installed.contains(implant)}) match { //try to find the installed implant case None => - //install in a free slot - getAvailableImplantSlot(implants.iterator, implant.Type) match { - case Some(slot) => - slot.Implant = implant - true + recursiveFindImplantInSlot(implants.iterator, ImplantType.None) match { //install in a free slot + case out @ Some(slot) => + implants(slot).Implant = implant + out case None => - false + None } case Some(_) => - false + None } } - @tailrec private def getAvailableImplantSlot(iter : Iterator[ImplantSlot], implantType : ImplantType.Value) : Option[ImplantSlot] = { + /** + * Remove a specific implant from a player's allocated installed implants.
+ *
+ * Due to the exclusiveness of installed implants, + * any implant slot with a matching `Definition` can be uninstalled safely. + * (There will never be any doubles.) + * This operation can lead to an irregular pattern of installed and uninstalled `ImplantSlot` objects. + * Despite that breach of pattern, the logic here is consistent as demonstrated by the client and by packets. + * The client also assigns and removes implants based on slot numbers that only express availability of a "slot." + * @see `AvatarImplantMessage.implantSlot` + * @param implantType the type of implant being uninstalled + * @return the index of the `ImplantSlot` where the implant was found and uninstalled + */ + def UninstallImplant(implantType : ImplantType.Value) : Option[Int] = { + recursiveFindImplantInSlot(implants.iterator, implantType) match { + case out @ Some(slot) => + implants(slot).Implant = None + out + case None => + None + } + } + + /** + * Locate the index of the encountered implant type. + * Functional implants may be exclusive in as far as the input `Iterator`'s source is concerned, + * but any number of `ImplantType.None` values are alway allowed in the source in any order. + * @param iter an `Iterator` of `ImplantSlot` objects + * @param implantType the target implant being sought + * @param index a defaulted index value representing the structure underlying the `Iterator` param + * @return the index where the target implant is installed + */ + @tailrec private def recursiveFindImplantInSlot(iter : Iterator[ImplantSlot], implantType : ImplantType.Value, index : Int = 0) : Option[Int] = { if(!iter.hasNext) { None } else { val slot = iter.next - if(!slot.Unlocked || slot.Implant == implantType) { - None - } - else if(slot.Installed.isEmpty) { - Some(slot) + if(slot.Unlocked && slot.Implant == implantType) { + Some(index) } else { - getAvailableImplantSlot(iter, implantType) + recursiveFindImplantInSlot(iter, implantType, index + 1) } } } - def UninstallImplant(implantType : ImplantType.Value) : Boolean = { - implants.find({slot => slot.Implant == implantType}) match { - case Some(slot) => - slot.Implant = None - true - case None => - false - } - } - def ResetAllImplants() : Unit = { implants.foreach(slot => { slot.Installed match { diff --git a/common/src/main/scala/net/psforever/objects/Vehicle.scala b/common/src/main/scala/net/psforever/objects/Vehicle.scala index 1d6acb11..fd3c0931 100644 --- a/common/src/main/scala/net/psforever/objects/Vehicle.scala +++ b/common/src/main/scala/net/psforever/objects/Vehicle.scala @@ -4,6 +4,7 @@ package net.psforever.objects import net.psforever.objects.definition.VehicleDefinition import net.psforever.objects.equipment.{Equipment, EquipmentSize} import net.psforever.objects.inventory.{GridInventory, InventoryTile} +import net.psforever.objects.mount.Mountable import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.vehicles.{AccessPermissionGroup, Seat, Utility, VehicleLockState} import net.psforever.packet.game.PlanetSideGUID @@ -26,7 +27,7 @@ import scala.collection.mutable * stores and unloads pertinent information about the `Vehicle`'s configuration; * used in the initialization process (`loadVehicleDefinition`) */ -class Vehicle(private val vehicleDef : VehicleDefinition) extends PlanetSideServerObject { +class Vehicle(private val vehicleDef : VehicleDefinition) extends PlanetSideServerObject with Mountable { private var faction : PlanetSideEmpire.Value = PlanetSideEmpire.TR private var owner : Option[PlanetSideGUID] = None private var health : Int = 1 @@ -97,7 +98,7 @@ class Vehicle(private val vehicleDef : VehicleDefinition) extends PlanetSideServ } def MaxHealth : Int = { - this.vehicleDef.MaxHealth + Definition.MaxHealth } def Shields : Int = { @@ -110,7 +111,7 @@ class Vehicle(private val vehicleDef : VehicleDefinition) extends PlanetSideServ } def MaxShields : Int = { - vehicleDef.MaxShields + Definition.MaxShields } def Drive : DriveState.Value = { @@ -118,7 +119,7 @@ class Vehicle(private val vehicleDef : VehicleDefinition) extends PlanetSideServ } def Drive_=(deploy : DriveState.Value) : DriveState.Value = { - if(vehicleDef.Deployment) { + if(Definition.Deployment) { this.deployed = deploy } Drive @@ -153,9 +154,11 @@ class Vehicle(private val vehicleDef : VehicleDefinition) extends PlanetSideServ * @return a seat number, or `None` */ def GetSeatFromMountPoint(mountPoint : Int) : Option[Int] = { - vehicleDef.MountPoints.get(mountPoint) + Definition.MountPoints.get(mountPoint) } + def MountPoints : Map[Int, Int] = Definition.MountPoints.toMap + /** * What are the access permissions for a position on this vehicle, seats or trunk? * @param group the group index @@ -225,8 +228,8 @@ class Vehicle(private val vehicleDef : VehicleDefinition) extends PlanetSideServ } } - def Seats : List[Seat] = { - seats.values.toList + def Seats : Map[Int, Seat] = { + seats } def SeatPermissionGroup(seatNumber : Int) : Option[AccessPermissionGroup.Value] = { @@ -428,27 +431,6 @@ object Vehicle { */ final case class PrepareForDeletion() - /** - * This player wants to sit down in an available(?) seat. - * @param seat_num the seat where the player is trying to occupy; - * this is NOT the entry mount point index; - * make certain to convert! - * @param player the `Player` object - */ - final case class TrySeatPlayer(seat_num : Int, player : Player) - /** - * The recipient player of this packet is being allowed to sit in the assigned seat. - * @param vehicle the `Vehicle` object that generated this message - * @param seat_num the seat that the player will occupy - */ - final case class CanSeatPlayer(vehicle : Vehicle, seat_num : Int) extends Exchange - /** - * The recipient player of this packet is not allowed to sit in the requested seat. - * @param vehicle the `Vehicle` object that generated this message - * @param seat_num the seat that the player can not occupy - */ - final case class CannotSeatPlayer(vehicle : Vehicle, seat_num : Int) extends Exchange - /** * Overloaded constructor. * @param vehicleDef the vehicle's definition entry @@ -494,7 +476,7 @@ object Vehicle { * @return the string output */ def toString(obj : Vehicle) : String = { - val occupancy = obj.Seats.count(seat => seat.isOccupied) + val occupancy = obj.Seats.values.count(seat => seat.isOccupied) s"${obj.Definition.Name}, owned by ${obj.Owner}: (${obj.Health}/${obj.MaxHealth})(${obj.Shields}/${obj.MaxShields}) ($occupancy)" } } diff --git a/common/src/main/scala/net/psforever/objects/definition/converter/ImplantTerminalInterfaceConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/ImplantTerminalInterfaceConverter.scala new file mode 100644 index 00000000..d27d167a --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/definition/converter/ImplantTerminalInterfaceConverter.scala @@ -0,0 +1,15 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.definition.converter + +import net.psforever.objects.serverobject.terminals.Terminal +import net.psforever.packet.game.objectcreate.CommonTerminalData + +import scala.util.{Failure, Success, Try} + +class ImplantTerminalInterfaceConverter extends ObjectCreateConverter[Terminal]() { + override def DetailedConstructorData(obj : Terminal) : Try[CommonTerminalData] = + Failure(new Exception("ImplantTerminalInterfaceConverter should not be used to generate detailed CommonTerminalData")) + + override def ConstructorData(obj : Terminal) : Try[CommonTerminalData] = + Success(CommonTerminalData(net.psforever.types.PlanetSideEmpire.VS)) //TODO shortcut +} diff --git a/common/src/main/scala/net/psforever/objects/definition/converter/VehicleConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/VehicleConverter.scala index 2517e8c8..d6e6398b 100644 --- a/common/src/main/scala/net/psforever/objects/definition/converter/VehicleConverter.scala +++ b/common/src/main/scala/net/psforever/objects/definition/converter/VehicleConverter.scala @@ -9,7 +9,8 @@ import net.psforever.packet.game.objectcreate.{InventoryItemData, _} import scala.util.{Failure, Success, Try} class VehicleConverter extends ObjectCreateConverter[Vehicle]() { - override def DetailedConstructorData(obj : Vehicle) : Try[VehicleData] = Failure(new Exception("VehicleConverter should not be used to generate detailed VehicleData")) + override def DetailedConstructorData(obj : Vehicle) : Try[VehicleData] = + Failure(new Exception("VehicleConverter should not be used to generate detailed VehicleData")) override def ConstructorData(obj : Vehicle) : Try[VehicleData] = { Success( diff --git a/common/src/main/scala/net/psforever/objects/mount/Mountable.scala b/common/src/main/scala/net/psforever/objects/mount/Mountable.scala new file mode 100644 index 00000000..06b63faf --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/mount/Mountable.scala @@ -0,0 +1,92 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.mount + +import akka.actor.ActorRef +import net.psforever.objects.Player +import net.psforever.objects.vehicles.Seat + +/** + * A `Trait` common to all game objects that permit players to + * interact with established spatial locations external to the object ("mount points") and + * attach to the object in internal indices ("seats") for an undefined length of time. + * @see `Seat` + */ +trait Mountable { + /** + * Retrieve a mapping of each seat from its internal index. + * @return the mapping of index to seat + */ + def Seats : Map[Int, Seat] + + /** + * Given a seat's index position, retrieve the internal `Seat` object. + * @return the specific seat + */ + def Seat(seatNum : Int) : Option[Seat] + + /** + * Retrieve a mapping of each seat from its mount point index. + * @return the mapping of mount point to seat + */ + def MountPoints : Map[Int, Int] + + /** + * Given a mount point index, return the associated seat index. + * @param mount the mount point + * @return the seat index + */ + def GetSeatFromMountPoint(mount : Int) : Option[Int] + + /** + * Given a player, determine if that player is seated. + * @param user the player + * @return the seat index + */ + def PassengerInSeat(user : Player) : Option[Int] + + /** + * A reference to an `Actor` that governs the logic of the object to accept `Mountable` messages. + * Specifically, the `Actor` should intercept the logic of `MountableControl.` + * @see `MountableControl` + * @see `PlanetSideServerObject.Actor` + * @return the internal `ActorRef` + */ + def Actor : ActorRef //TODO can we enforce this desired association to MountableControl? +} + +object Mountable { + /** + * Message used by the player to indicate the desire to board a `Mountable` object. + * @param player the player who sent this request message + * @param seat_num the seat index + */ + final case class TryMount(player : Player, seat_num : Int) + + /** + * A basic `Trait` connecting all of the actionable `Mountable` response messages. + */ + sealed trait Exchange + + /** + * Message that carries the result of the processed request message back to the original user (`player`). + * @param player the player who sent this request message + * @param response the result of the processed request + */ + final case class MountMessages(player : Player, response : Exchange) + + /** + * Message sent in response to the player succeeding to access a `Mountable` object. + * The player should be seated at the given index. + * @param obj the `Mountable` object + * @param seat_num the seat index + */ + final case class CanMount(obj : Mountable, seat_num : Int) extends Exchange + + /** + * Message sent in response to the player failing to access a `Mountable` object. + * The player would have been be seated at the given index. + * @param obj the `Mountable` object + * @param seat_num the seat index + */ + final case class CanNotMount(obj : Mountable, seat_num : Int) extends Exchange +} diff --git a/common/src/main/scala/net/psforever/objects/mount/MountableControl.scala b/common/src/main/scala/net/psforever/objects/mount/MountableControl.scala new file mode 100644 index 00000000..2084ad2c --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/mount/MountableControl.scala @@ -0,0 +1,27 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.mount + +import akka.actor.Actor + +/** + * The logic governing `Mountable` objects that use the `TryMount` message. + * @see `Seat` + * @see `Mountable` + * @param obj the `Mountable` object governed beholden to this logic + */ +abstract class MountableControl(obj : Mountable) extends Actor { + def receive : Receive = { + case Mountable.TryMount(user, seat_num) => + obj.Seat(seat_num) match { + case Some(seat) => + if((seat.Occupant = user).contains(user)) { + sender ! Mountable.MountMessages(user, Mountable.CanMount(obj, seat_num)) + } + else { + sender ! Mountable.MountMessages(user, Mountable.CanNotMount(obj, seat_num)) + } + case None => + sender ! Mountable.MountMessages(user, Mountable.CanNotMount(obj, seat_num)) + } + } +} diff --git a/common/src/main/scala/net/psforever/objects/serverobject/builders/ImplantTerminalMechObjectBuilder.scala b/common/src/main/scala/net/psforever/objects/serverobject/builders/ImplantTerminalMechObjectBuilder.scala new file mode 100644 index 00000000..13162944 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/builders/ImplantTerminalMechObjectBuilder.scala @@ -0,0 +1,34 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.serverobject.builders + +import akka.actor.Props +import net.psforever.objects.serverobject.implantmech.{ImplantTerminalMech, ImplantTerminalMechControl, ImplantTerminalMechDefinition} + +/** + * Wrapper `Class` designed to instantiate a `ImplantTerminalMech` server object. + * @param idef a `ImplantTerminalMechDefinition` object, indicating the specific functionality of the resulting `Door` + * @param id the globally unique identifier to which this "tube" will be registered + */ +class ImplantTerminalMechObjectBuilder(private val idef : ImplantTerminalMechDefinition, private val id : Int) extends ServerObjectBuilder[ImplantTerminalMech] { + import akka.actor.ActorContext + import net.psforever.objects.guid.NumberPoolHub + + def Build(implicit context : ActorContext, guid : NumberPoolHub) : ImplantTerminalMech = { + val obj = ImplantTerminalMech(idef) + guid.register(obj, id) //non-Actor GUID registration + obj.Actor = context.actorOf(Props(classOf[ImplantTerminalMechControl], obj), s"${idef.Name}_${obj.GUID.guid}") + obj + } +} + +object ImplantTerminalMechObjectBuilder { + /** + * Overloaded constructor for a `DoorObjectBuilder`. + * @param idef a `DoorDefinition` object + * @param id a globally unique identifier + * @return a `DoorObjectBuilder` object + */ + def apply(idef : ImplantTerminalMechDefinition, id : Int) : ImplantTerminalMechObjectBuilder = { + new ImplantTerminalMechObjectBuilder(idef, id) + } +} diff --git a/common/src/main/scala/net/psforever/objects/serverobject/implantmech/ImplantTerminalMech.scala b/common/src/main/scala/net/psforever/objects/serverobject/implantmech/ImplantTerminalMech.scala new file mode 100644 index 00000000..490c42d6 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/implantmech/ImplantTerminalMech.scala @@ -0,0 +1,42 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.serverobject.implantmech + +import net.psforever.objects.Player +import net.psforever.objects.definition.ObjectDefinition +import net.psforever.objects.mount.Mountable +import net.psforever.objects.serverobject.PlanetSideServerObject +import net.psforever.objects.vehicles.Seat + +/** + * A structure-owned server object that is the visible and `Mountable` component of an implant terminal. + * For the most part, it merely implements the support data structures indicated by `Mountable`. + * @param idef the `ObjectDefinition` that constructs this object and maintains some of its immutable fields + */ +class ImplantTerminalMech(private val idef : ImplantTerminalMechDefinition) extends PlanetSideServerObject with Mountable { + private val seats : Map[Int, Seat] = Map( 0 -> new Seat(idef.Seats(0)) ) + + def Seats : Map[Int, Seat] = seats + + def Seat(seatNum : Int) : Option[Seat] = seats.get(seatNum) + + def MountPoints : Map[Int, Int] = idef.MountPoints + + def GetSeatFromMountPoint(mount : Int) : Option[Int] = idef.MountPoints.get(mount) + + def PassengerInSeat(user : Player) : Option[Int] = { + if(seats(0).Occupant.contains(user)) { + Some(0) + } + else { + None + } + } + + def Definition : ObjectDefinition = idef +} + +object ImplantTerminalMech { + def apply(idef : ImplantTerminalMechDefinition) : ImplantTerminalMech = { + new ImplantTerminalMech(idef) + } +} diff --git a/common/src/main/scala/net/psforever/objects/serverobject/implantmech/ImplantTerminalMechControl.scala b/common/src/main/scala/net/psforever/objects/serverobject/implantmech/ImplantTerminalMechControl.scala new file mode 100644 index 00000000..f418a27e --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/implantmech/ImplantTerminalMechControl.scala @@ -0,0 +1,14 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.serverobject.implantmech + +import net.psforever.objects.mount.MountableControl + +/** + * An `Actor` that handles messages being dispatched to a specific `ImplantTerminalMech`. + * @param mech the "mech" object being governed + */ +class ImplantTerminalMechControl(mech : ImplantTerminalMech) extends MountableControl(mech) { + override def receive : Receive = super[MountableControl].receive.orElse { + case _ => ; + } +} diff --git a/common/src/main/scala/net/psforever/objects/serverobject/implantmech/ImplantTerminalMechDefinition.scala b/common/src/main/scala/net/psforever/objects/serverobject/implantmech/ImplantTerminalMechDefinition.scala new file mode 100644 index 00000000..554a19d3 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/implantmech/ImplantTerminalMechDefinition.scala @@ -0,0 +1,22 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.serverobject.implantmech + +import net.psforever.objects.definition.{ObjectDefinition, SeatDefinition} +import net.psforever.objects.vehicles.SeatArmorRestriction + +/** + * The `Definition` for any `Terminal` that is of a type "implant_terminal_interface." + * Implant terminals are composed of two components. + * This `Definition` constructs the visible mechanical tube component that can be mounted. + */ +class ImplantTerminalMechDefinition extends ObjectDefinition(410) { + /* key - seat index, value - seat object */ + private val seats : Map[Int, SeatDefinition] = Map(0 -> new SeatDefinition) + /* key - entry point index, value - seat index */ + private val mountPoints : Map[Int, Int] = Map(1 -> 0) + Name = "implant_terminal_mech" + + def Seats : Map[Int, SeatDefinition] = seats + + def MountPoints : Map[Int, Int] = mountPoints +} diff --git a/common/src/main/scala/net/psforever/objects/serverobject/terminals/ImplantTerminalInterfaceDefinition.scala b/common/src/main/scala/net/psforever/objects/serverobject/terminals/ImplantTerminalInterfaceDefinition.scala new file mode 100644 index 00000000..82298076 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/serverobject/terminals/ImplantTerminalInterfaceDefinition.scala @@ -0,0 +1,50 @@ +// Copyright (c) 2017 PSForever +package net.psforever.objects.serverobject.terminals + +import net.psforever.objects.{GlobalDefinitions, Player} +import net.psforever.objects.definition.ImplantDefinition +import net.psforever.objects.definition.converter.ImplantTerminalInterfaceConverter +import net.psforever.packet.game.ItemTransactionMessage +import net.psforever.packet.game.objectcreate.ObjectClass + +/** + * The `Definition` for any `Terminal` that is of a type "implant_terminal_interface." + * Implant terminals are composed of two components. + * This `Definition` constructs the invisible interface component (interacted with as a game window). + * Unlike other `Terminal` objects in the game, this one must be constructed on the client and + * attached as a child of the visible implant terminal component - the "implant_terminal_mech." + */ +class ImplantTerminalInterfaceDefinition extends TerminalDefinition(ObjectClass.implant_terminal_interface) { + private val implants : Map[String, ImplantDefinition] = Map ( + "advanced_regen" -> GlobalDefinitions.advanced_regen, + "targeting" -> GlobalDefinitions.targeting, + "audio_amplifier" -> GlobalDefinitions.audio_amplifier, + "darklight_vision" -> GlobalDefinitions.darklight_vision, + "melee_booster" -> GlobalDefinitions.melee_booster, + "personal_shield" -> GlobalDefinitions.personal_shield, + "range_magnifier" -> GlobalDefinitions.range_magnifier, + "second_wind" -> GlobalDefinitions.second_wind, + "silent_run" -> GlobalDefinitions.silent_run, + "surge" -> GlobalDefinitions.surge + ) + Packet = new ImplantTerminalInterfaceConverter + Name = "implante_terminal_interface" + + def Buy(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange = { + implants.get(msg.item_name) match { + case Some(implant) => + Terminal.LearnImplant(implant) + case None => + Terminal.NoDeal() + } + } + + override def Sell(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange = { + implants.get(msg.item_name) match { + case Some(implant) => + Terminal.SellImplant(implant) + case None => + Terminal.NoDeal() + } + } +} 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 554f0960..8a03fb59 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 @@ -2,6 +2,7 @@ package net.psforever.objects.serverobject.terminals import net.psforever.objects.Player +import net.psforever.objects.definition.ImplantDefinition import net.psforever.objects.equipment.Equipment import net.psforever.objects.inventory.InventoryItem import net.psforever.objects.serverobject.PlanetSideServerObject @@ -151,7 +152,24 @@ object Terminal { */ final case class SellCertification(cert : CertificationType.Value, cost : Int) extends Exchange + /** + * Provide the implant type unlocked by the player. + * @param implant the implant (definition) requested + */ + final case class LearnImplant(implant : ImplantDefinition) extends Exchange + + /** + * Provide the implant type freed-up by the player. + * @param implant the implant (definition) returned + */ + final case class SellImplant(implant : ImplantDefinition) extends Exchange + import net.psforever.objects.Vehicle + /** + * Provide a vehicle that was constructed for the player. + * @param vehicle the vehicle + * @param loadout the vehicle's trunk contents + */ final case class BuyVehicle(vehicle : Vehicle, loadout: List[Any]) extends Exchange /** diff --git a/common/src/main/scala/net/psforever/objects/vehicles/VehicleControl.scala b/common/src/main/scala/net/psforever/objects/vehicles/VehicleControl.scala index 5f6be0e3..d41a702e 100644 --- a/common/src/main/scala/net/psforever/objects/vehicles/VehicleControl.scala +++ b/common/src/main/scala/net/psforever/objects/vehicles/VehicleControl.scala @@ -1,8 +1,8 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.vehicles -import akka.actor.Actor import net.psforever.objects.Vehicle +import net.psforever.objects.mount.MountableControl /** * An `Actor` that handles messages being dispatched to a specific `Vehicle`.
@@ -11,23 +11,11 @@ import net.psforever.objects.Vehicle * The latter is applicable only when the specific vehicle is being deconstructed. * @param vehicle the `Vehicle` object being governed */ -class VehicleControl(private val vehicle : Vehicle) extends Actor { - def receive : Receive = { +class VehicleControl(private val vehicle : Vehicle) extends MountableControl(vehicle) { + override def receive : Receive = super[MountableControl].receive.orElse { case Vehicle.PrepareForDeletion => context.become(Disabled) - case Vehicle.TrySeatPlayer(seat_num, player) => - vehicle.Seat(seat_num) match { - case Some(seat) => - if((seat.Occupant = player).contains(player)) { - sender ! Vehicle.VehicleMessages(player, Vehicle.CanSeatPlayer(vehicle, seat_num)) - } - else { - sender ! Vehicle.VehicleMessages(player, Vehicle.CannotSeatPlayer(vehicle, seat_num)) - } - case None => - sender ! Vehicle.VehicleMessages(player, Vehicle.CannotSeatPlayer(vehicle, seat_num)) - } case _ => ; } diff --git a/common/src/main/scala/net/psforever/objects/zones/ZoneActor.scala b/common/src/main/scala/net/psforever/objects/zones/ZoneActor.scala index 3e2a63fc..d72d7db4 100644 --- a/common/src/main/scala/net/psforever/objects/zones/ZoneActor.scala +++ b/common/src/main/scala/net/psforever/objects/zones/ZoneActor.scala @@ -45,16 +45,16 @@ class ZoneActor(zone : Zone) extends Actor { } catch { case _ : Exception => - slog.error(s"expected a door at id $door_guid, but looking for uninitialized object") + slog.error(s"expected a door at id $door_guid but no object is initialized") } try { if(!guid(lock_guid).get.isInstanceOf[IFFLock]) { - slog.error(s"expected id $lock_guid to be an IFF locks, but it was not") + slog.error(s"expected id $lock_guid to be an IFF locks but it was not") } } catch { case _ : Exception => - slog.error(s"expected an IFF locks at id $lock_guid, but looking for uninitialized object") + slog.error(s"expected an IFF locks at id $lock_guid but no object is initialized") } }) @@ -69,7 +69,7 @@ class ZoneActor(zone : Zone) extends Actor { } catch { case _ : Exception => - slog.error(s"expected a terminal at id $term_guid, but looking for uninitialized object") + slog.error(s"expected a terminal at id $term_guid but no object is initialized") } try { if(!guid(pad_guid).get.isInstanceOf[VehicleSpawnPad]) { @@ -78,7 +78,30 @@ class ZoneActor(zone : Zone) extends Actor { } catch { case _ : Exception => - slog.error(s"expected a spawn pad at id $pad_guid, but looking for uninitialized object") + slog.error(s"expected a spawn pad at id $pad_guid but no object is initialized") + } + }) + + //check implant terminal mech to implant terminal interface association + import net.psforever.objects.serverobject.implantmech.ImplantTerminalMech + map.TerminalToInterface.foreach({case ((mech_guid, interface_guid)) => + try { + if(!guid(mech_guid).get.isInstanceOf[ImplantTerminalMech]) { + slog.error(s"expected id $mech_guid to be an implant terminal mech, but it was not") + } + } + catch { + case _ : Exception => + slog.error(s"expected a implant terminal mech at id $mech_guid but no object is initialized") + } + try { + if(!guid(interface_guid).get.isInstanceOf[Terminal]) { //TODO check is implant terminal + slog.error(s"expected id $interface_guid to be an implant terminal interface, but it was not") + } + } + catch { + case _ : Exception => + slog.error(s"expected a implant terminal interface at id $interface_guid but no object is initialized") } }) } diff --git a/common/src/main/scala/net/psforever/objects/zones/ZoneMap.scala b/common/src/main/scala/net/psforever/objects/zones/ZoneMap.scala index 35d38fe9..f5a68998 100644 --- a/common/src/main/scala/net/psforever/objects/zones/ZoneMap.scala +++ b/common/src/main/scala/net/psforever/objects/zones/ZoneMap.scala @@ -26,6 +26,7 @@ import net.psforever.objects.serverobject.builders.ServerObjectBuilder class ZoneMap(private val name : String) { private var localObjects : List[ServerObjectBuilder[_]] = List() private var linkTerminalPad : Map[Int, Int] = Map() + private var linkTerminalInterface : Map[Int, Int] = Map() private var linkDoorLock : Map[Int, Int] = Map() private var linkObjectBase : Map[Int, Int] = Map() private var numBases : Int = 0 @@ -74,4 +75,10 @@ class ZoneMap(private val name : String) { def TerminalToSpawnPad(terminal_guid : Int, pad_guid : Int) : Unit = { linkTerminalPad = linkTerminalPad ++ Map(terminal_guid -> pad_guid) } + + def TerminalToInterface : Map[Int, Int] = linkTerminalInterface + + def TerminalToInterface(interface_guid : Int, terminal_guid : Int) : Unit = { + linkTerminalInterface = linkTerminalInterface ++ Map(interface_guid -> terminal_guid) + } } diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/ObjectClass.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/ObjectClass.scala index e9506fc3..2b1bef29 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/ObjectClass.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/ObjectClass.scala @@ -1268,7 +1268,7 @@ object ObjectClass { case ObjectClass.ams_respawn_tube => DroppedItemData.genericCodec(CommonTerminalData.codec, "terminal") case ObjectClass.avatar => ConstructorData.genericCodec(CharacterData.codec, "avatar") case ObjectClass.capture_flag => ConstructorData.genericCodec(CaptureFlagData.codec, "capture flag") - case ObjectClass.implant_terminal_interface => DroppedItemData.genericCodec(CommonTerminalData.codec, "implant terminal") + case ObjectClass.implant_terminal_interface => ConstructorData.genericCodec(CommonTerminalData.codec, "implant terminal") case ObjectClass.locker_container => ConstructorData.genericCodec(LockerContainerData.codec, "locker container") case ObjectClass.matrix_terminala => DroppedItemData.genericCodec(CommonTerminalData.codec, "terminal") case ObjectClass.matrix_terminalb => DroppedItemData.genericCodec(CommonTerminalData.codec, "terminal") diff --git a/common/src/test/scala/objects/MountableTest.scala b/common/src/test/scala/objects/MountableTest.scala new file mode 100644 index 00000000..88cce0e3 --- /dev/null +++ b/common/src/test/scala/objects/MountableTest.scala @@ -0,0 +1,87 @@ +// Copyright (c) 2017 PSForever +package objects + +import akka.actor.{ActorRef, Props} +import net.psforever.objects.Player +import net.psforever.objects.definition.{ObjectDefinition, SeatDefinition} +import net.psforever.objects.mount.{Mountable, MountableControl} +import net.psforever.objects.serverobject.PlanetSideServerObject +import net.psforever.objects.vehicles.Seat +import net.psforever.types.{CharacterGender, PlanetSideEmpire} + +import scala.concurrent.duration.Duration + +class MountableControl1Test extends ActorTest() { + "MountableControl" should { + "construct" in { + val obj = new MountableTest.MountableTestObject + obj.Actor = system.actorOf(Props(classOf[MountableTest.MountableTestControl], obj), "mech") + assert(obj.Actor != ActorRef.noSender) + } + } +} + +class MountableControl2Test extends ActorTest() { + "MountableControl" should { + "let a player mount" in { + val player = Player("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) + val obj = new MountableTest.MountableTestObject + obj.Actor = system.actorOf(Props(classOf[MountableTest.MountableTestControl], obj), "mountable") + val msg = Mountable.TryMount(player, 0) + + obj.Actor ! msg + val reply = receiveOne(Duration.create(100, "ms")) + assert(reply.isInstanceOf[Mountable.MountMessages]) + val reply2 = reply.asInstanceOf[Mountable.MountMessages] + assert(reply2.player == player) + assert(reply2.response.isInstanceOf[Mountable.CanMount]) + val reply3 = reply2.response.asInstanceOf[Mountable.CanMount] + assert(reply3.obj == obj) + assert(reply3.seat_num == 0) + } + } +} + +class MountableControl3Test extends ActorTest() { + "MountableControl" should { + "block a player from mounting" in { + val player1 = Player("test1", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) + val player2 = Player("test2", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) + val obj = new MountableTest.MountableTestObject + obj.Actor = system.actorOf(Props(classOf[MountableTest.MountableTestControl], obj), "mountable") + obj.Actor ! Mountable.TryMount(player1, 0) + receiveOne(Duration.create(100, "ms")) //consume reply + + obj.Actor ! Mountable.TryMount(player2, 0) + val reply = receiveOne(Duration.create(100, "ms")) + assert(reply.isInstanceOf[Mountable.MountMessages]) + val reply2 = reply.asInstanceOf[Mountable.MountMessages] + assert(reply2.player == player2) + assert(reply2.response.isInstanceOf[Mountable.CanNotMount]) + val reply3 = reply2.response.asInstanceOf[Mountable.CanNotMount] + assert(reply3.obj == obj) + assert(reply3.seat_num == 0) + } + } +} + +object MountableTest { + class MountableTestObject extends PlanetSideServerObject with Mountable { + private val seats : Map[Int, Seat] = Map( 0 -> new Seat(new SeatDefinition()) ) + def Seats : Map[Int, Seat] = seats + def Seat(seatNum : Int) : Option[Seat] = seats.get(seatNum) + def MountPoints : Map[Int, Int] = Map(1 -> 0) + def GetSeatFromMountPoint(mount : Int) : Option[Int] = MountPoints.get(mount) + def PassengerInSeat(user : Player) : Option[Int] = { + if(seats(0).Occupant.contains(user)) { + Some(0) + } + else { + None + } + } + def Definition : ObjectDefinition = null //eh whatever + } + + class MountableTestControl(obj : Mountable) extends MountableControl(obj) +} diff --git a/common/src/test/scala/objects/PlayerTest.scala b/common/src/test/scala/objects/PlayerTest.scala index 2a95f598..8f798070 100644 --- a/common/src/test/scala/objects/PlayerTest.scala +++ b/common/src/test/scala/objects/PlayerTest.scala @@ -110,7 +110,7 @@ class PlayerTest extends Specification { val testplant : ImplantDefinition = ImplantDefinition(1) val obj = new Player("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) obj.Implants(0).Unlocked = true - obj.InstallImplant(testplant) mustEqual true + obj.InstallImplant(testplant) mustEqual Some(0) obj.Implants.find({p => p.Implant == ImplantType(1)}) match { //find the installed implant case Some(slot) => slot.Installed mustEqual Some(testplant) @@ -126,15 +126,15 @@ class PlayerTest extends Specification { val obj = new Player("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) obj.Implants(0).Unlocked = true obj.Implants(1).Unlocked = true - obj.InstallImplant(testplant1) mustEqual true - obj.InstallImplant(testplant2) mustEqual false + obj.InstallImplant(testplant1) mustEqual Some(0) + obj.InstallImplant(testplant2) mustEqual Some(1) } "uninstall implants" in { val testplant : ImplantDefinition = ImplantDefinition(1) val obj = new Player("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) obj.Implants(0).Unlocked = true - obj.InstallImplant(testplant) mustEqual true + obj.InstallImplant(testplant) mustEqual Some(0) obj.Implants(0).Installed mustEqual Some(testplant) obj.UninstallImplant(testplant.Type) diff --git a/common/src/test/scala/objects/ServerObjectBuilderTest.scala b/common/src/test/scala/objects/ServerObjectBuilderTest.scala new file mode 100644 index 00000000..a9aa75ba --- /dev/null +++ b/common/src/test/scala/objects/ServerObjectBuilderTest.scala @@ -0,0 +1,116 @@ +// Copyright (c) 2017 PSForever +package objects + +import akka.actor.{Actor, Props} +import net.psforever.objects.GlobalDefinitions +import net.psforever.objects.guid.NumberPoolHub +import net.psforever.packet.game.PlanetSideGUID +import net.psforever.objects.serverobject.builders.ServerObjectBuilder + +import scala.concurrent.duration.Duration + +class DoorObjectBuilderTest extends ActorTest { + import net.psforever.objects.serverobject.doors.Door + import net.psforever.objects.serverobject.builders.DoorObjectBuilder + "DoorObjectBuilder" should { + "build" in { + val hub = ServerObjectBuilderTest.NumberPoolHub + val actor = system.actorOf(Props(classOf[ServerObjectBuilderTest.BuilderTestActor], DoorObjectBuilder(GlobalDefinitions.door, 1), hub), "door") + actor ! "!" + + val reply = receiveOne(Duration.create(100, "ms")) + assert(reply.isInstanceOf[Door]) + assert(reply.asInstanceOf[Door].HasGUID) + assert(reply.asInstanceOf[Door].GUID == PlanetSideGUID(1)) + assert(reply == hub(1).get) + } + } +} + +class IFFLockObjectBuilderTest extends ActorTest { + import net.psforever.objects.serverobject.locks.IFFLock + import net.psforever.objects.serverobject.builders.IFFLockObjectBuilder + "IFFLockObjectBuilder" should { + "build" in { + val hub = ServerObjectBuilderTest.NumberPoolHub + val actor = system.actorOf(Props(classOf[ServerObjectBuilderTest.BuilderTestActor], IFFLockObjectBuilder(GlobalDefinitions.lock_external, 1), hub), "lock") + actor ! "!" + + val reply = receiveOne(Duration.create(100, "ms")) + assert(reply.isInstanceOf[IFFLock]) + assert(reply.asInstanceOf[IFFLock].HasGUID) + assert(reply.asInstanceOf[IFFLock].GUID == PlanetSideGUID(1)) + assert(reply == hub(1).get) + } + } +} + +class ImplantTerminalMechObjectBuilderTest extends ActorTest { + import net.psforever.objects.serverobject.implantmech.ImplantTerminalMech + import net.psforever.objects.serverobject.builders.ImplantTerminalMechObjectBuilder + "IFFLockObjectBuilder" should { + "build" in { + val hub = ServerObjectBuilderTest.NumberPoolHub + val actor = system.actorOf(Props(classOf[ServerObjectBuilderTest.BuilderTestActor], ImplantTerminalMechObjectBuilder(GlobalDefinitions.implant_terminal_mech, 1), hub), "mech") + actor ! "!" + + val reply = receiveOne(Duration.create(100, "ms")) + assert(reply.isInstanceOf[ImplantTerminalMech]) + assert(reply.asInstanceOf[ImplantTerminalMech].HasGUID) + assert(reply.asInstanceOf[ImplantTerminalMech].GUID == PlanetSideGUID(1)) + assert(reply == hub(1).get) + } + } +} + +class TerminalObjectBuilderTest extends ActorTest { + import net.psforever.objects.serverobject.terminals.Terminal + import net.psforever.objects.serverobject.builders.TerminalObjectBuilder + "TerminalObjectBuilder" should { + "build" in { + val hub = ServerObjectBuilderTest.NumberPoolHub + val actor = system.actorOf(Props(classOf[ServerObjectBuilderTest.BuilderTestActor], TerminalObjectBuilder(GlobalDefinitions.order_terminal, 1), hub), "term") + actor ! "!" + + val reply = receiveOne(Duration.create(100, "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 + import net.psforever.objects.serverobject.builders.VehicleSpawnPadObjectBuilder + "TerminalObjectBuilder" should { + "build" in { + val hub = ServerObjectBuilderTest.NumberPoolHub + val actor = system.actorOf(Props(classOf[ServerObjectBuilderTest.BuilderTestActor], VehicleSpawnPadObjectBuilder(GlobalDefinitions.spawn_pad, 1), hub), "pad") + actor ! "!" + + val reply = receiveOne(Duration.create(100, "ms")) + assert(reply.isInstanceOf[VehicleSpawnPad]) + assert(reply.asInstanceOf[VehicleSpawnPad].HasGUID) + assert(reply.asInstanceOf[VehicleSpawnPad].GUID == PlanetSideGUID(1)) + assert(reply == hub(1).get) + } + } +} + +object ServerObjectBuilderTest { + import net.psforever.objects.guid.source.LimitedNumberSource + def NumberPoolHub : NumberPoolHub = { + val obj = new NumberPoolHub(new LimitedNumberSource(2)) + obj + } + + class BuilderTestActor(builder : ServerObjectBuilder[_], hub : NumberPoolHub) extends Actor { + def receive : Receive = { + case _ => + sender ! builder.Build(context, hub) + } + } +} + diff --git a/common/src/test/scala/objects/VehicleTest.scala b/common/src/test/scala/objects/VehicleTest.scala index a61ad431..eba1463a 100644 --- a/common/src/test/scala/objects/VehicleTest.scala +++ b/common/src/test/scala/objects/VehicleTest.scala @@ -128,11 +128,11 @@ class VehicleTest extends Specification { val fury_vehicle = Vehicle(GlobalDefinitions.fury) fury_vehicle.Owner mustEqual None fury_vehicle.Seats.size mustEqual 1 - fury_vehicle.Seats.head.ArmorRestriction mustEqual SeatArmorRestriction.NoMax - fury_vehicle.Seats.head.isOccupied mustEqual false - fury_vehicle.Seats.head.Occupant mustEqual None - fury_vehicle.Seats.head.Bailable mustEqual true - fury_vehicle.Seats.head.ControlledWeapon mustEqual Some(1) + fury_vehicle.Seats(0).ArmorRestriction mustEqual SeatArmorRestriction.NoMax + fury_vehicle.Seats(0).isOccupied mustEqual false + fury_vehicle.Seats(0).Occupant mustEqual None + fury_vehicle.Seats(0).Bailable mustEqual true + fury_vehicle.Seats(0).ControlledWeapon mustEqual Some(1) fury_vehicle.PermissionGroup(0) mustEqual Some(VehicleLockState.Locked) //driver fury_vehicle.PermissionGroup(1) mustEqual Some(VehicleLockState.Empire) //gunner fury_vehicle.PermissionGroup(2) mustEqual Some(VehicleLockState.Empire) //passenger diff --git a/common/src/test/scala/objects/ZoneTest.scala b/common/src/test/scala/objects/ZoneTest.scala index 268f99da..a16eeb10 100644 --- a/common/src/test/scala/objects/ZoneTest.scala +++ b/common/src/test/scala/objects/ZoneTest.scala @@ -44,6 +44,24 @@ class ZoneTest extends Specification { map.DoorToLock(3, 4) map.DoorToLock mustEqual Map(1 -> 2, 3 -> 4) } + + "associates terminals to spawn pads (doesn't check numbers)" in { + val map = new ZoneMap("map13") + map.TerminalToSpawnPad mustEqual Map.empty + map.TerminalToSpawnPad(1, 2) + map.TerminalToSpawnPad mustEqual Map(1 -> 2) + map.TerminalToSpawnPad(3, 4) + map.TerminalToSpawnPad mustEqual Map(1 -> 2, 3 -> 4) + } + + "associates mechanical components to implant terminals (doesn't check numbers)" in { + val map = new ZoneMap("map13") + map.TerminalToInterface mustEqual Map.empty + map.TerminalToInterface(1, 2) + map.TerminalToInterface mustEqual Map(1 -> 2) + map.TerminalToInterface(3, 4) + map.TerminalToInterface mustEqual Map(1 -> 2, 3 -> 4) + } } val map13 = new ZoneMap("map13") diff --git a/common/src/test/scala/objects/terminal/ImplantTerminalInterfaceTest.scala b/common/src/test/scala/objects/terminal/ImplantTerminalInterfaceTest.scala new file mode 100644 index 00000000..c5893ea0 --- /dev/null +++ b/common/src/test/scala/objects/terminal/ImplantTerminalInterfaceTest.scala @@ -0,0 +1,52 @@ +// Copyright (c) 2017 PSForever +package objects.terminal + +import akka.actor.ActorRef +import net.psforever.objects.{GlobalDefinitions, Player} +import net.psforever.objects.serverobject.terminals.Terminal +import net.psforever.packet.game.{ItemTransactionMessage, PlanetSideGUID} +import net.psforever.types.{CharacterGender, PlanetSideEmpire, TransactionType} +import org.specs2.mutable.Specification + +class ImplantTerminalInterfaceTest extends Specification { + "Implant_Terminal_Interface" should { + val player = Player("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) + + "construct" in { + val terminal = Terminal(GlobalDefinitions.implant_terminal_interface) + terminal.Actor mustEqual ActorRef.noSender + } + + "player can learn an implant ('darklight_vision')" in { + val terminal = Terminal(GlobalDefinitions.implant_terminal_interface) + val msg = ItemTransactionMessage(PlanetSideGUID(1), TransactionType.Buy, 0, "darklight_vision", 0, PlanetSideGUID(0)) + val reply = terminal.Request(player, msg) + reply.isInstanceOf[Terminal.LearnImplant] mustEqual true + val reply2 = reply.asInstanceOf[Terminal.LearnImplant] + reply2.implant mustEqual GlobalDefinitions.darklight_vision + } + + "player can not learn a fake implant ('aimbot')" in { + val terminal = Terminal(GlobalDefinitions.implant_terminal_interface) + val msg = ItemTransactionMessage(PlanetSideGUID(1), TransactionType.Buy, 0, "aimbot", 0, PlanetSideGUID(0)) + + terminal.Request(player, msg) mustEqual Terminal.NoDeal() + } + + "player can surrender an implant ('darklight_vision')" in { + val terminal = Terminal(GlobalDefinitions.implant_terminal_interface) + val msg = ItemTransactionMessage(PlanetSideGUID(1), TransactionType.Sell, 0, "darklight_vision", 0, PlanetSideGUID(0)) + val reply = terminal.Request(player, msg) + reply.isInstanceOf[Terminal.SellImplant] mustEqual true + val reply2 = reply.asInstanceOf[Terminal.SellImplant] + reply2.implant mustEqual GlobalDefinitions.darklight_vision + } + + "player can not surrender a fake implant ('aimbot')" in { + val terminal = Terminal(GlobalDefinitions.implant_terminal_interface) + val msg = ItemTransactionMessage(PlanetSideGUID(1), TransactionType.Sell, 0, "aimbot", 0, PlanetSideGUID(0)) + + terminal.Request(player, msg) mustEqual Terminal.NoDeal() + } + } +} diff --git a/common/src/test/scala/objects/terminal/ImplantTerminalMechTest.scala b/common/src/test/scala/objects/terminal/ImplantTerminalMechTest.scala new file mode 100644 index 00000000..9677a09a --- /dev/null +++ b/common/src/test/scala/objects/terminal/ImplantTerminalMechTest.scala @@ -0,0 +1,110 @@ +// Copyright (c) 2017 PSForever +package objects.terminal + +import akka.actor.{ActorRef, Props} +import net.psforever.objects.definition.SeatDefinition +import net.psforever.objects.mount.Mountable +import net.psforever.objects.serverobject.implantmech.{ImplantTerminalMech, ImplantTerminalMechControl} +import net.psforever.objects.vehicles.Seat +import net.psforever.objects.{GlobalDefinitions, Player} +import net.psforever.types.{CharacterGender, PlanetSideEmpire} +import objects.ActorTest +import org.specs2.mutable.Specification + +import scala.concurrent.duration.Duration + +class ImplantTerminalMechTest extends Specification { + "Implant_Terminal_Mech" should { + "define" in { + val implant_terminal_mech = GlobalDefinitions.implant_terminal_mech + implant_terminal_mech.ObjectId mustEqual 410 + implant_terminal_mech.MountPoints mustEqual Map(1 -> 0) + implant_terminal_mech.Seats.keySet mustEqual Set(0) + implant_terminal_mech.Seats(0).isInstanceOf[SeatDefinition] mustEqual true + implant_terminal_mech.Seats(0).ArmorRestriction mustEqual net.psforever.objects.vehicles.SeatArmorRestriction.NoMax + implant_terminal_mech.Seats(0).Bailable mustEqual false + implant_terminal_mech.Seats(0).ControlledWeapon mustEqual None + } + } + + "VehicleSpawnPad" should { + "construct" in { + val obj = ImplantTerminalMech(GlobalDefinitions.implant_terminal_mech) + obj.Actor mustEqual ActorRef.noSender + obj.Definition mustEqual GlobalDefinitions.implant_terminal_mech + obj.Seats.keySet mustEqual Set(0) + obj.Seats(0).isInstanceOf[Seat] mustEqual true + } + + "get seat from mount points" in { + val obj = ImplantTerminalMech(GlobalDefinitions.implant_terminal_mech) + obj.GetSeatFromMountPoint(0) mustEqual None + obj.GetSeatFromMountPoint(1) mustEqual Some(0) + obj.GetSeatFromMountPoint(2) mustEqual None + } + + "get passenger in a seat" in { + val player = Player("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) + val obj = ImplantTerminalMech(GlobalDefinitions.implant_terminal_mech) + obj.PassengerInSeat(player) mustEqual None + obj.Seats(0).Occupant = player + obj.PassengerInSeat(player) mustEqual Some(0) + obj.Seats(0).Occupant = None + obj.PassengerInSeat(player) mustEqual None + } + } +} + +class ImplantTerminalMechControl1Test extends ActorTest() { + "ImplantTerminalMechControl" should { + "construct" in { + val obj = ImplantTerminalMech(GlobalDefinitions.implant_terminal_mech) + obj.Actor = system.actorOf(Props(classOf[ImplantTerminalMechControl], obj), "mech") + assert(obj.Actor != ActorRef.noSender) + } + } +} + +class ImplantTerminalMechControl2Test extends ActorTest() { + "ImplantTerminalMechControl" should { + "let a player mount" in { + val player = Player("test", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) + val obj = ImplantTerminalMech(GlobalDefinitions.implant_terminal_mech) + obj.Actor = system.actorOf(Props(classOf[ImplantTerminalMechControl], obj), "mech") + val msg = Mountable.TryMount(player, 0) + + obj.Actor ! msg + val reply = receiveOne(Duration.create(100, "ms")) + assert(reply.isInstanceOf[Mountable.MountMessages]) + val reply2 = reply.asInstanceOf[Mountable.MountMessages] + assert(reply2.player == player) + assert(reply2.response.isInstanceOf[Mountable.CanMount]) + val reply3 = reply2.response.asInstanceOf[Mountable.CanMount] + assert(reply3.obj == obj) + assert(reply3.seat_num == 0) + } + } +} + +class ImplantTerminalMechControl3Test extends ActorTest() { + "ImplantTerminalMechControl" should { + "block a player from mounting" in { + val player1 = Player("test1", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) + val player2 = Player("test2", PlanetSideEmpire.TR, CharacterGender.Male, 0, 0) + val obj = ImplantTerminalMech(GlobalDefinitions.implant_terminal_mech) + obj.Actor = system.actorOf(Props(classOf[ImplantTerminalMechControl], obj), "mech") + obj.Actor ! Mountable.TryMount(player1, 0) + receiveOne(Duration.create(100, "ms")) //consume reply + + obj.Actor ! Mountable.TryMount(player2, 0) + val reply = receiveOne(Duration.create(100, "ms")) + assert(reply.isInstanceOf[Mountable.MountMessages]) + val reply2 = reply.asInstanceOf[Mountable.MountMessages] + assert(reply2.player == player2) + assert(reply2.response.isInstanceOf[Mountable.CanNotMount]) + val reply3 = reply2.response.asInstanceOf[Mountable.CanNotMount] + assert(reply3.obj == obj) + assert(reply3.seat_num == 0) + } + } +} diff --git a/pslogin/src/main/scala/PsLogin.scala b/pslogin/src/main/scala/PsLogin.scala index e63ef0a1..262746f4 100644 --- a/pslogin/src/main/scala/PsLogin.scala +++ b/pslogin/src/main/scala/PsLogin.scala @@ -14,7 +14,7 @@ import com.typesafe.config.ConfigFactory import net.psforever.crypto.CryptoInterface import net.psforever.objects.zones._ import net.psforever.objects.guid.TaskResolver -import net.psforever.objects.serverobject.builders.{DoorObjectBuilder, IFFLockObjectBuilder, TerminalObjectBuilder, VehicleSpawnPadObjectBuilder} +import net.psforever.objects.serverobject.builders._ import net.psforever.types.Vector3 import org.slf4j import org.fusesource.jansi.Ansi._ @@ -233,10 +233,29 @@ object PsLogin { LocalObject(DoorObjectBuilder(door, 330)) LocalObject(DoorObjectBuilder(door, 332)) + LocalObject(DoorObjectBuilder(door, 362)) LocalObject(DoorObjectBuilder(door, 370)) LocalObject(DoorObjectBuilder(door, 371)) LocalObject(DoorObjectBuilder(door, 372)) LocalObject(DoorObjectBuilder(door, 373)) + LocalObject(DoorObjectBuilder(door, 374)) + LocalObject(DoorObjectBuilder(door, 375)) + LocalObject(DoorObjectBuilder(door, 394)) + LocalObject(DoorObjectBuilder(door, 395)) + LocalObject(DoorObjectBuilder(door, 396)) + LocalObject(DoorObjectBuilder(door, 397)) + LocalObject(DoorObjectBuilder(door, 398)) + LocalObject(DoorObjectBuilder(door, 462)) + LocalObject(DoorObjectBuilder(door, 463)) + LocalObject(ImplantTerminalMechObjectBuilder(implant_terminal_mech, 520)) //Hart B + LocalObject(ImplantTerminalMechObjectBuilder(implant_terminal_mech, 522)) //Hart C + LocalObject(ImplantTerminalMechObjectBuilder(implant_terminal_mech, 523)) //Hart C + LocalObject(ImplantTerminalMechObjectBuilder(implant_terminal_mech, 524)) //Hart C + LocalObject(ImplantTerminalMechObjectBuilder(implant_terminal_mech, 525)) //Hart C + LocalObject(ImplantTerminalMechObjectBuilder(implant_terminal_mech, 526)) //Hart C + LocalObject(ImplantTerminalMechObjectBuilder(implant_terminal_mech, 527)) //Hart C + LocalObject(ImplantTerminalMechObjectBuilder(implant_terminal_mech, 528)) //Hart C + LocalObject(ImplantTerminalMechObjectBuilder(implant_terminal_mech, 529)) //Hart C LocalObject(IFFLockObjectBuilder(lock_external, 556)) LocalObject(IFFLockObjectBuilder(lock_external, 558)) LocalObject(TerminalObjectBuilder(cert_terminal, 186)) @@ -245,6 +264,15 @@ object PsLogin { LocalObject(TerminalObjectBuilder(order_terminal, 853)) LocalObject(TerminalObjectBuilder(order_terminal, 855)) LocalObject(TerminalObjectBuilder(order_terminal, 860)) + LocalObject(TerminalObjectBuilder(implant_terminal_interface, 1081)) //tube 520 + LocalObject(TerminalObjectBuilder(implant_terminal_interface, 1082)) //TODO guid not correct + LocalObject(TerminalObjectBuilder(implant_terminal_interface, 1083)) //TODO guid not correct + LocalObject(TerminalObjectBuilder(implant_terminal_interface, 1084)) //TODO guid not correct + LocalObject(TerminalObjectBuilder(implant_terminal_interface, 1085)) //TODO guid not correct + LocalObject(TerminalObjectBuilder(implant_terminal_interface, 1086)) //TODO guid not correct + LocalObject(TerminalObjectBuilder(implant_terminal_interface, 1087)) //TODO guid not correct + LocalObject(TerminalObjectBuilder(implant_terminal_interface, 1088)) //TODO guid not correct + LocalObject(TerminalObjectBuilder(implant_terminal_interface, 1089)) //TODO guid not correct LocalObject(TerminalObjectBuilder(ground_vehicle_terminal, 1063)) LocalObject(VehicleSpawnPadObjectBuilder(spawn_pad, 500)) //TODO guid not correct LocalObject(TerminalObjectBuilder(dropship_vehicle_terminal, 304)) @@ -254,16 +282,36 @@ object PsLogin { ObjectToBase(330, 29) ObjectToBase(332, 29) + //ObjectToBase(520, 29) + ObjectToBase(522, 29) + ObjectToBase(523, 29) + ObjectToBase(524, 29) + ObjectToBase(525, 29) + ObjectToBase(526, 29) + ObjectToBase(527, 29) + ObjectToBase(528, 29) + ObjectToBase(529, 29) ObjectToBase(556, 29) ObjectToBase(558, 29) - ObjectToBase(1063, 29) //TODO unowned courtyard terminal? - ObjectToBase(500, 29) //TODO unowned courtyard spawnpad? - ObjectToBase(304, 29) //TODO unowned courtyard terminal? - ObjectToBase(501, 29) //TODO unowned courtyard spawnpad? + ObjectToBase(1081, 29) + ObjectToBase(1063, 2) //TODO unowned courtyard terminal? + ObjectToBase(500, 2) //TODO unowned courtyard spawnpad? + ObjectToBase(304, 2) //TODO unowned courtyard terminal? + ObjectToBase(501, 2) //TODO unowned courtyard spawnpad? + DoorToLock(330, 558) DoorToLock(332, 556) TerminalToSpawnPad(1063, 500) TerminalToSpawnPad(304, 501) + TerminalToInterface(520, 1081) + TerminalToInterface(522, 1082) + TerminalToInterface(523, 1083) + TerminalToInterface(524, 1084) + TerminalToInterface(525, 1085) + TerminalToInterface(526, 1086) + TerminalToInterface(527, 1087) + TerminalToInterface(528, 1088) + TerminalToInterface(529, 1089) } val home3 = new Zone("home3", map13, 13) { override def Init(implicit context : ActorContext) : Unit = { diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index 5c650542..4b8ca23d 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -14,14 +14,16 @@ import net.psforever.objects._ import net.psforever.objects.equipment._ import net.psforever.objects.guid.{GUIDTask, Task, TaskResolver} import net.psforever.objects.inventory.{GridInventory, InventoryItem} +import net.psforever.objects.mount.Mountable import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} import net.psforever.objects.serverobject.doors.Door +import net.psforever.objects.serverobject.implantmech.ImplantTerminalMech import net.psforever.objects.serverobject.locks.IFFLock import net.psforever.objects.serverobject.pad.VehicleSpawnPad import net.psforever.objects.serverobject.terminals.Terminal import net.psforever.objects.vehicles.{AccessPermissionGroup, VehicleLockState} import net.psforever.objects.zones.{InterstellarCluster, Zone} -import net.psforever.packet.game.objectcreate._ +import net.psforever.packet.game.objectcreate.{DetailedCharacterData, _} import net.psforever.types._ import services._ import services.avatar._ @@ -342,6 +344,62 @@ class WorldSessionActor extends Actor with MDCContextAware { case Door.NoEvent() => ; } + case Mountable.MountMessages(tplayer, reply) => + reply match { + case Mountable.CanMount(obj : ImplantTerminalMech, seat_num) => + val player_guid : PlanetSideGUID = tplayer.GUID + val obj_guid : PlanetSideGUID = obj.GUID + log.info(s"MountVehicleMsg: $player_guid mounts $obj @ $seat_num") + tplayer.VehicleSeated = Some(obj_guid) + sendResponse(PacketCoding.CreateGamePacket(0, PlanetsideAttributeMessage(obj_guid, 0, 1000L))) //health of mech + sendResponse(PacketCoding.CreateGamePacket(0, ObjectAttachMessage(obj_guid, player_guid, seat_num))) + vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.MountVehicle(player_guid, obj_guid, seat_num)) + + case Mountable.CanMount(obj : Vehicle, seat_num) => + val obj_guid : PlanetSideGUID = obj.GUID + val player_guid : PlanetSideGUID = tplayer.GUID + log.info(s"MountVehicleMsg: $player_guid mounts $obj_guid @ $seat_num") + vehicleService ! VehicleServiceMessage.UnscheduleDeconstruction(obj_guid) //clear all deconstruction timers + tplayer.VehicleSeated = Some(obj_guid) + if(seat_num == 0) { //simplistic vehicle ownership management + obj.Owner match { + case Some(owner_guid) => + continent.GUID(owner_guid) match { + case Some(previous_owner : Player) => + if(previous_owner.VehicleOwned.contains(obj_guid)) { + previous_owner.VehicleOwned = None //simplistic ownership management, player loses vehicle ownership + } + case _ => ; + } + case None => ; + } + tplayer.VehicleOwned = Some(obj_guid) + obj.Owner = Some(player_guid) + } + obj.WeaponControlledFromSeat(seat_num) match { + case Some(weapon : Tool) => + //update mounted weapon belonging to seat + val magazine = weapon.AmmoSlots(weapon.FireModeIndex).Box //update the magazine in the weapon, specifically + sendResponse(PacketCoding.CreateGamePacket(0, InventoryStateMessage(magazine.GUID, 0, weapon.GUID, weapon.Magazine.toLong))) + //update all related ammunition objects in trunk + obj.Trunk.Items + .filter({ case ((_, item)) => item.obj.isInstanceOf[AmmoBox] && item.obj.asInstanceOf[AmmoBox].AmmoType == weapon.AmmoType }) + .foreach({ case ((_, item)) => + val box = item.obj.asInstanceOf[AmmoBox] + sendResponse(PacketCoding.CreateGamePacket(0, InventoryStateMessage(box.GUID, 0, obj_guid, box.Capacity.toLong))) + }) + case _ => ; //no weapons to update + } + sendResponse(PacketCoding.CreateGamePacket(0, ObjectAttachMessage(obj_guid, player_guid, seat_num))) + vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.MountVehicle(player_guid, obj_guid, seat_num)) + + case Mountable.CanMount(obj : Mountable, seat_num) => + log.warn(s"MountVehicleMsg: $obj is some generic mountable object and nothing will happen") + + case Mountable.CanNotMount(obj, seat_num) => + log.warn(s"MountVehicleMsg: $tplayer attempted to mount $obj's seat $seat_num, but was not allowed") + } + case Terminal.TerminalMessage(tplayer, msg, order) => order match { case Terminal.BuyExosuit(exosuit, subtype) => @@ -545,6 +603,84 @@ class WorldSessionActor extends Actor with MDCContextAware { sendResponse(PacketCoding.CreateGamePacket(0, ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Learn, false))) } + case Terminal.LearnImplant(implant) => + val terminal_guid = msg.terminal_guid + val implant_type = implant.Type + val message = s"Implants: $tplayer wants to learn $implant_type" + val (interface, slotNumber) = tplayer.VehicleSeated match { + case Some(mech_guid) => + ( + continent.Map.TerminalToInterface.get(mech_guid.guid), + if(!tplayer.Implants.exists({slot => slot.Implant == implant_type})) { //no duplicates + tplayer.InstallImplant(implant) + } + else { + None + } + ) + case _ => + (None, None) + } + + if(interface.contains(terminal_guid.guid) && slotNumber.isDefined) { + val slot = slotNumber.get + log.info(s"$message - put in slot $slot") + sendResponse(PacketCoding.CreateGamePacket(0, AvatarImplantMessage(tplayer.GUID, 0, slot, implant_type.id))) + sendResponse(PacketCoding.CreateGamePacket(0, ItemTransactionResultMessage(terminal_guid, TransactionType.Learn, true))) + } + else { + if(interface.isEmpty) { + log.warn(s"$message - not interacting with a terminal") + } + else if(!interface.contains(terminal_guid.guid)) { + log.warn(s"$message - interacting with the wrong terminal, ${interface.get}") + } + else if(slotNumber.isEmpty) { + log.warn(s"$message - already knows that implant") + } + else { + log.warn(s"$message - forgot to sit at a terminal") + } + sendResponse(PacketCoding.CreateGamePacket(0, ItemTransactionResultMessage(terminal_guid, TransactionType.Learn, false))) + } + + case Terminal.SellImplant(implant) => + val terminal_guid = msg.terminal_guid + val implant_type = implant.Type + val (interface, slotNumber) = tplayer.VehicleSeated match { + case Some(mech_guid) => + ( + continent.Map.TerminalToInterface.get(mech_guid.guid), + tplayer.UninstallImplant(implant_type) + ) + case None => + (None, None) + } + + if(interface.contains(terminal_guid.guid) && slotNumber.isDefined) { + val slot = slotNumber.get + log.info(s"$tplayer is selling $implant_type - take from slot $slot") + sendResponse(PacketCoding.CreateGamePacket(0, AvatarImplantMessage(tplayer.GUID, 1, slot, 0))) + sendResponse(PacketCoding.CreateGamePacket(0, ItemTransactionResultMessage(terminal_guid, TransactionType.Sell, true))) + } + else { + val message = s"$tplayer can not sell $implant_type" + if(interface.isEmpty) { + log.warn(s"$message - not interacting with a terminal") + } + else if(!interface.contains(terminal_guid.guid)) { + log.warn(s"$message - interacting with the wrong terminal, ${interface.get}") + } + else if(slotNumber.isEmpty) { + log.warn(s"$message - does not know that implant") + } + else { + log.warn(s"$message - forgot to sit at a terminal") + } + sendResponse(PacketCoding.CreateGamePacket(0, ItemTransactionResultMessage(terminal_guid, TransactionType.Sell, false))) + } + + case Terminal.BuyVehicle(vehicle, loadout) => continent.Map.TerminalToSpawnPad.get(msg.terminal_guid.guid) match { case Some(pad_guid) => @@ -563,52 +699,6 @@ class WorldSessionActor extends Actor with MDCContextAware { sendResponse(PacketCoding.CreateGamePacket(0, ItemTransactionResultMessage(msg.terminal_guid, msg.transaction_type, false))) } - case Vehicle.VehicleMessages(tplayer, reply) => - reply match { - case Vehicle.CanSeatPlayer(vehicle, seat_num) => - log.info(s"MountVehicleMsg: ${player.GUID} mounts ${vehicle.GUID} @ $seat_num") - vehicleService ! VehicleServiceMessage.UnscheduleDeconstruction(vehicle.GUID) //clear all deconstruction timers - val vehicle_guid : PlanetSideGUID = vehicle.GUID - tplayer.VehicleSeated = Some(vehicle_guid) - if(seat_num == 0) { //simplistic vehicle ownership management - vehicle.Owner match { - case Some(owner_guid) => - continent.GUID(owner_guid) match { - case Some(previous_owner : Player) => - if(previous_owner.VehicleOwned.contains(vehicle_guid)) { - previous_owner.VehicleOwned = None //simplistic ownership management, player loses vehicle ownership - } - case _ => ; - } - case None => ; - } - player.VehicleOwned = Some(vehicle_guid) - vehicle.Owner = Some(player.GUID) - } - vehicle.WeaponControlledFromSeat(seat_num) match { - case Some(weapon : Tool) => - //update mounted weapon belonging to seat - val magazine = weapon.AmmoSlots(weapon.FireModeIndex).Box //update the magazine in the weapon, specifically - sendResponse(PacketCoding.CreateGamePacket(0, InventoryStateMessage(magazine.GUID, 0, weapon.GUID, weapon.Magazine.toLong))) - //update all related ammunition objects in trunk - vehicle.Trunk.Items - .filter({ case ((_, item)) => item.obj.isInstanceOf[AmmoBox] && item.obj.asInstanceOf[AmmoBox].AmmoType == weapon.AmmoType }) - .foreach({ case ((_, item)) => - val box = item.obj.asInstanceOf[AmmoBox] - sendResponse(PacketCoding.CreateGamePacket(0, InventoryStateMessage(box.GUID, 0, vehicle_guid, box.Capacity.toLong))) - }) - case _ => ; //no weapons to update - } - val player_guid : PlanetSideGUID = tplayer.GUID - sendResponse(PacketCoding.CreateGamePacket(0, ObjectAttachMessage(vehicle_guid, player_guid, seat_num))) - vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.MountVehicle(player_guid, vehicle_guid, seat_num)) - - case Vehicle.CannotSeatPlayer(vehicle, seat_num) => - log.warn(s"MountVehicleMsg: player $tplayer attempted to board vehicle ${vehicle.GUID}'s seat $seat_num, but was not allowed") - - case _ => ; - } - case VehicleSpawnPad.ConcealPlayer => sendResponse(PacketCoding.CreateGamePacket(0, GenericObjectActionMessage(player.GUID, 36))) avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ConcealPlayer(player.GUID)) @@ -627,7 +717,7 @@ class WorldSessionActor extends Actor with MDCContextAware { //sendResponse(PacketCoding.CreateGamePacket(0, ObjectAttachMessage(vehicle_guid, player_guid, 0))) vehicleService ! VehicleServiceMessage.UnscheduleDeconstruction(vehicle_guid) //cancel queue timeout delay vehicleService ! VehicleServiceMessage.DelayedVehicleDeconstruction(vehicle, continent, 21L) //temporary drive away from pad delay - vehicle.Actor ! Vehicle.TrySeatPlayer(0, player) + vehicle.Actor ! Mountable.TryMount(player, 0) case VehicleSpawnPad.PlayerSeatedInVehicle(vehicle) => vehicleService ! VehicleServiceMessage.DelayedVehicleDeconstruction(vehicle, continent, 21L) //sitting in the vehicle clears the drive away delay @@ -906,7 +996,8 @@ class WorldSessionActor extends Actor with MDCContextAware { player.Certifications += CertificationType.AirSupport player.Certifications += CertificationType.GalaxyGunship player.Certifications += CertificationType.Phantasm - //player.ExoSuit = ExoSuitType.Infiltrator + AwardBattleExperiencePoints(player, 1000000L) +// player.ExoSuit = ExoSuitType.MAX //TODO strange issue; divide number above by 10 when uncommenting player.Slot(0).Equipment = Tool(beamer) player.Slot(2).Equipment = Tool(suppressor) player.Slot(4).Equipment = Tool(forceblade) @@ -919,7 +1010,6 @@ class WorldSessionActor extends Actor with MDCContextAware { player.Slot(5).Equipment.get.asInstanceOf[LockerContainer].Inventory += 0 -> SimpleItem(remote_electronics_kit) //TODO end temp player character auto-loading self ! ListAccountCharacters - import scala.concurrent.duration._ import scala.concurrent.ExecutionContext.Implicits.global clientKeepAlive.cancel @@ -982,25 +1072,58 @@ class WorldSessionActor extends Actor with MDCContextAware { //load active vehicles in zone continent.Vehicles.foreach(vehicle => { val definition = vehicle.Definition - sendResponse( - PacketCoding.CreateGamePacket(0, - ObjectCreateMessage( - definition.ObjectId, - vehicle.GUID, - definition.Packet.ConstructorData(vehicle).get - ) + sendResponse(PacketCoding.CreateGamePacket(0, + ObjectCreateMessage( + definition.ObjectId, + vehicle.GUID, + definition.Packet.ConstructorData(vehicle).get ) - ) + )) //seat vehicle occupants vehicle.Definition.MountPoints.values.foreach(seat_num => { vehicle.Seat(seat_num).get.Occupant match { case Some(tplayer) => - sendResponse(PacketCoding.CreateGamePacket(0, ObjectAttachMessage(vehicle.GUID, tplayer.GUID, seat_num))) + if(tplayer.HasGUID) { + sendResponse(PacketCoding.CreateGamePacket(0, ObjectAttachMessage(vehicle.GUID, tplayer.GUID, seat_num))) + } case None => ; } }) ReloadVehicleAccessPermissions(vehicle) }) + //implant terminals + continent.Map.TerminalToInterface.foreach({ case((terminal_guid, interface_guid)) => + val parent_guid = PlanetSideGUID(terminal_guid) + continent.GUID(interface_guid) match { + case Some(obj : Terminal) => + val obj_def = obj.Definition + val obj_uid = obj_def.ObjectId + val obj_data = obj_def.Packet.ConstructorData(obj).get + sendResponse(PacketCoding.CreateGamePacket(0, + ObjectCreateMessage( + obj_uid, + PlanetSideGUID(interface_guid), + ObjectCreateMessageParent(parent_guid, 1), + obj_data + ) + )) + case _ => ; + } + //seat terminal occupants + continent.GUID(terminal_guid) match { + case Some(obj : Mountable) => + obj.MountPoints.foreach({ case((_, seat_num)) => + obj.Seat(seat_num).get.Occupant match { + case Some(tplayer) => + if(tplayer.HasGUID) { + sendResponse(PacketCoding.CreateGamePacket(0, ObjectAttachMessage(parent_guid, tplayer.GUID, seat_num))) + } + case None => ; + } + }) + case _ => ; + } + }) avatarService ! Service.Join(player.Continent) localService ! Service.Join(player.Continent) vehicleService ! Service.Join(player.Continent) @@ -1423,18 +1546,18 @@ class WorldSessionActor extends Actor with MDCContextAware { case msg @ WarpgateRequest(continent_guid, building_guid, dest_building_guid, dest_continent_guid, unk1, unk2) => log.info("WarpgateRequest: " + msg) - case msg @ MountVehicleMsg(player_guid, vehicle_guid, unk) => - //log.info("MountVehicleMsg: "+msg) - continent.GUID(vehicle_guid) match { - case Some(obj : Vehicle) => + case msg @ MountVehicleMsg(player_guid, mountable_guid, unk) => + log.info("MountVehicleMsg: "+msg) + continent.GUID(mountable_guid) match { + case Some(obj : Mountable) => obj.GetSeatFromMountPoint(unk) match { case Some(seat_num) => - obj.Actor ! Vehicle.TrySeatPlayer(seat_num, player) + obj.Actor ! Mountable.TryMount(player, seat_num) case None => - log.warn(s"MountVehicleMsg: attempted to board vehicle $vehicle_guid's seat $unk, but no seat exists there") + log.warn(s"MountVehicleMsg: attempted to board mountable $mountable_guid's seat $unk, but no seat exists there") } case None | Some(_) => - log.warn(s"MountVehicleMsg: not a vehicle") + log.warn(s"MountVehicleMsg: not a mountable thing") } case msg @ DismountVehicleMsg(player_guid, unk1, unk2) => @@ -1443,31 +1566,37 @@ class WorldSessionActor extends Actor with MDCContextAware { if(player.GUID == player_guid) { //common warning for this section def dismountWarning(msg : String) : Unit = { - log.warn(s"$msg; a vehicle may not know that a player is no longer sitting it in") + log.warn(s"$msg; some vehicle might not know that a player is no longer sitting in it") } //normally disembarking from a seat player.VehicleSeated match { - case Some(vehicle_guid) => - continent.GUID(vehicle_guid) match { - case Some(obj : Vehicle) => - obj.Seats.find(seat => seat.Occupant.contains(player)) match { + case Some(obj_guid) => + continent.GUID(obj_guid) match { + case Some(obj : Mountable) => + val seats = obj.Seats.values + seats.find(seat => seat.Occupant.contains(player)) match { case Some(seat) => val vel = obj.Velocity.getOrElse(Vector3(0f, 0f, 0f)) val has_vel : Int = math.abs(vel.x * vel.y * vel.z).toInt if(seat.Bailable || obj.Velocity.isEmpty || has_vel == 0) { //ugh, float comparison seat.Occupant = None - if(obj.Seats.count(seat => seat.isOccupied) == 0) { - vehicleService ! VehicleServiceMessage.DelayedVehicleDeconstruction(obj, continent, 600L) //start vehicle decay (10m) + //special actions + obj match { + case (veh : Vehicle) => + if(seats.count(seat => seat.isOccupied) == 0) { + vehicleService ! VehicleServiceMessage.DelayedVehicleDeconstruction(veh, continent, 600L) //start vehicle decay (10m) + } + case _ => ; } } case None => - dismountWarning(s"DismountVehicleMsg: can not find where player $player_guid is seated in vehicle $vehicle_guid") + dismountWarning(s"DismountVehicleMsg: can not find where player $player_guid is seated in mountable $obj_guid") } case _ => - dismountWarning(s"DismountVehicleMsg: can not find vehicle $vehicle_guid") + dismountWarning(s"DismountVehicleMsg: can not find mountable entity $obj_guid") } case None => - dismountWarning(s"DismountVehicleMsg: player $player_guid not considered seated in a vehicle") + dismountWarning(s"DismountVehicleMsg: player $player_guid not considered seated in a mountable entity") } //should be safe player.VehicleSeated = None @@ -1483,12 +1612,13 @@ class WorldSessionActor extends Actor with MDCContextAware { if(tplayer.VehicleSeated.contains(vehicle_guid)) { continent.GUID(vehicle_guid) match { case Some(obj : Vehicle) => - obj.Seats.find(seat => seat.Occupant.contains(tplayer)) match { + val seats = obj.Seats.values + seats.find(seat => seat.Occupant.contains(tplayer)) match { case Some(seat) => seat.Occupant = None tplayer.VehicleSeated = None vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.KickPassenger(player_guid, unk1, unk2)) - if(obj.Seats.count(seat => seat.isOccupied) == 0) { + if(seats.count(seat => seat.isOccupied) == 0) { vehicleService ! VehicleServiceMessage.DelayedVehicleDeconstruction(obj, continent, 600L) //start vehicle decay (10m) } case None => @@ -1984,6 +2114,34 @@ class WorldSessionActor extends Actor with MDCContextAware { }) } + /** + * Gives a target player positive battle experience points only. + * If the player has access to more implant slots as a result of changing battle experience points, unlock those slots. + * @param tplayer the player + * @param bep the change in experience points, positive by assertion + * @return the player's current battle experience points + */ + def AwardBattleExperiencePoints(tplayer : Player, bep : Long) : Long = { + val oldBep = tplayer.BEP + if(bep <= 0) { + log.error(s"trying to set $bep battle experience points on $tplayer; value can not be negative") + oldBep + } + else { + val oldSlots = DetailedCharacterData.numberOfImplantSlots(oldBep) + val newBep = oldBep + bep + val newSlots = DetailedCharacterData.numberOfImplantSlots(newBep) + tplayer.BEP = newBep + if(newSlots > oldSlots) { + (oldSlots until newSlots).foreach(slotNumber => { + tplayer.Implants(slotNumber).Unlocked = true + log.info(s"unlocking implant slot $slotNumber for $tplayer") + }) + } + newBep + } + } + def failWithError(error : String) = { log.error(error) sendResponse(PacketCoding.CreateControlPacket(ConnectionClose()))