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()))