diff --git a/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala b/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala
index 21bfbd07d..6d961e8fc 100644
--- a/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala
+++ b/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala
@@ -3,11 +3,12 @@ package net.psforever.objects
import net.psforever.objects.definition._
import net.psforever.objects.definition.converter.{CommandDetonaterConverter, LockerContainerConverter, REKConverter}
+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.terminals.OrderTerminalDefinition
-import net.psforever.packet.game.objectcreate.ObjectClass
+import net.psforever.objects.serverobject.locks.IFFLockDefinition
+import net.psforever.objects.serverobject.terminals.{CertTerminalDefinition, OrderTerminalDefinition}
import net.psforever.types.PlanetSideEmpire
object GlobalDefinitions {
@@ -1239,5 +1240,12 @@ object GlobalDefinitions {
fury.TrunkOffset = 30
val
- orderTerminal = new OrderTerminalDefinition
+ order_terminal = new OrderTerminalDefinition
+ val
+ cert_terminal = new CertTerminalDefinition
+
+ val
+ lock_external = new IFFLockDefinition
+ val
+ door = new DoorDefinition
}
diff --git a/common/src/main/scala/net/psforever/objects/ObjectType.scala b/common/src/main/scala/net/psforever/objects/ObjectType.scala
new file mode 100644
index 000000000..b7df4a729
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/ObjectType.scala
@@ -0,0 +1,96 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects
+
+object ObjectType extends Enumeration {
+ type Value = String
+
+ val AmbientSoundSource = "ambient_sound_source"
+ val Ammunition = "ammunition"
+ val AnimatedBarrier = "animated_barrier"
+ val Applicator = "applicator"
+ val Armor = "armor"
+ val ArmorSiphon = "armor_siphon"
+ val AwardStatistic = "award_statistic"
+ val Avatar = "avatar"
+ val AvatarBot = "avatar_bot"
+ val Ball = "ball"
+ val Bank = "bank"
+ val Barrier = "barrier"
+ val BfrTerminal = "bfr_terminal"
+ val Billboard = "billboard"
+ val Boomer = "boomer"
+ val BoomerTrigger = "boomer_trigger"
+ val Building = "building"
+ val CaptureFlag = "capture_flag"
+ val CaptureFlagSocket = "capture_flag_socket"
+ val CaptureTerminal = "capture_terminal"
+ val CertTerminal = "cert_terminal"
+ val ChainLashDamager = "chain_lash_damager"
+ val Dispenser = "dispenser"
+ val Door = "door"
+ val EmpBlast = "emp_blast"
+ val FrameVehicle = "framevehicle"
+ val Flag = "flag"
+ val FlightVehicle = "flightvehicle"
+ val ForceDome = "forcedome"
+ val ForceDomeGenerator = "forcedomegenerator"
+ val Game = "game"
+ val Generic = "generic"
+ val GenericTeleportion = "generic_teleportation"
+ val GeneratorTerminal = "generator_terminal"
+ val GsGenbase = "GS_genbase"
+ val HandGrenade = "hand_grenade"
+ val HeMine = "he_mine"
+ val HeavyWeapon = "heavy_weapon"
+ val HoverVehicle = "hovervehicle"
+ val Implant = "implant"
+ val ImplantInterfaceTerminal = "implant_terminal_interface"
+ val Lazer = "lazer"
+ val Locker = "locker"
+ val LockerContainer = "locker_container"
+ val LockExternal = "lock_external"
+ val LockSmall = "lock_small"
+ val MainTerminal = "main_terminal"
+ val Map = "map"
+ val MedicalTerminal = "medical_terminal"
+ val Medkit = "medkit"
+ val Monolith = "monolith"
+ val MonolithUnit = "monolith_unit"
+ val MotionAlarmSensorDest = "motion_alarm_sensor_dest"
+ val NanoDispenser = "nano_dispenser"
+ val NtuSipon = "ntu_siphon"
+ val OrbitalShuttlePad = "orbital_shuttle_pad"
+ val OrbitalStrike = "orbital_strike"
+ val OrderTerminal = "order_terminal"
+ val PainTerminal = "pain_terminal"
+ val Projectile = "projectile"
+ val RadiationCloud = "radiation_cloud"
+ val RearmTerminal = "rearm_terminal"
+ val RechargeTerminal = "recharge_terminal"
+ val Rek = "rek"
+ val RepairTerminal = "repair_terminal"
+ val ResourceSilo = "resource_silo"
+ val RespawnTube = "respawn_tube"
+ val SensorShield = "sensor_shield"
+ val ShieldGenerator = "shield_generator"
+ val Shifter = "shifter"
+ val SkyDome = "skydome"
+ val SpawnPlayer = "spawn_player"
+ val SpawnPoint = "spawn_point"
+ val SpawnTerminal = "spawn_terminal"
+ val TeleportPad = "teleport_pad"
+ val Terminal = "terminal"
+ val TradeContainer = "trade_container"
+ val UplinkDevice = "uplink_device"
+ val VanuCradleClass = "vanu_cradle_class"
+ val VanuModuleClass = "vanu_module_class"
+ val VanuModuleFactory = "vanu_module_factory"
+ val VanuReceptacleClass = "vanu_receptacle_class"
+ val Vehicle = "vehicle"
+ val VehicleCreationPad = "vehicle_creation_pad"
+ val VehicleLandingPad = "vehicle_landing_pad"
+ val VehicleTerminal = "vehicle_terminal"
+ val Warpgate = "waprgate"
+ val WarpZone = "warp_zone"
+ val Weapon = "weapon"
+}
diff --git a/common/src/main/scala/net/psforever/objects/Player.scala b/common/src/main/scala/net/psforever/objects/Player.scala
index 9d59f733d..15572f514 100644
--- a/common/src/main/scala/net/psforever/objects/Player.scala
+++ b/common/src/main/scala/net/psforever/objects/Player.scala
@@ -13,8 +13,8 @@ import scala.collection.mutable
class Player(private val name : String,
private val faction : PlanetSideEmpire.Value,
private val sex : CharacterGender.Value,
- private val voice : Int,
- private val head : Int
+ private val head : Int,
+ private val voice : Int
) extends PlanetSideGameObject {
private var alive : Boolean = false
private var backpack : Boolean = false
@@ -64,8 +64,6 @@ class Player(private val name : String,
/** Last medkituse. */
var lastMedkit : Long = 0
var death_by : Int = 0
- var doors : Array[Int] = Array.ofDim(120)
- var doorsTime : Array[Long] = Array.ofDim(120)
var lastSeenStreamMessage : Array[Long] = Array.fill[Long](65535)(0L)
var lastShotSeq_time : Int = -1
/** The player is shooting. */
@@ -521,11 +519,11 @@ object Player {
final val FreeHandSlot : Int = 250
final val HandsDownSlot : Int = 255
- def apply(name : String, faction : PlanetSideEmpire.Value, sex : CharacterGender.Value, voice : Int, head : Int) : Player = {
- new Player(name, faction, sex, voice, head)
+ def apply(name : String, faction : PlanetSideEmpire.Value, sex : CharacterGender.Value, head : Int, voice : Int) : Player = {
+ new Player(name, faction, sex, head, voice)
}
- def apply(guid : PlanetSideGUID, name : String, faction : PlanetSideEmpire.Value, sex : CharacterGender.Value, voice : Int, head : Int) : Player = {
+ def apply(guid : PlanetSideGUID, name : String, faction : PlanetSideEmpire.Value, sex : CharacterGender.Value, head : Int, voice : Int) : Player = {
val obj = new Player(name, faction, sex, voice, head)
obj.GUID = guid
obj
diff --git a/common/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala
index b20812fdc..9ceb021bf 100644
--- a/common/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala
+++ b/common/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala
@@ -3,7 +3,7 @@ package net.psforever.objects.definition.converter
import net.psforever.objects.{EquipmentSlot, GlobalDefinitions, ImplantSlot, Player}
import net.psforever.objects.equipment.Equipment
-import net.psforever.packet.game.objectcreate.{BasicCharacterData, CharacterAppearanceData, CharacterData, DetailedCharacterData, DrawnSlot, ImplantEffects, ImplantEntry, InternalSlot, InventoryData, PlacementData, RibbonBars, UniformStyle}
+import net.psforever.packet.game.objectcreate.{BasicCharacterData, CharacterAppearanceData, CharacterData, Cosmetics, DetailedCharacterData, DrawnSlot, ImplantEffects, ImplantEntry, InternalSlot, InventoryData, PlacementData, RibbonBars, UniformStyle}
import net.psforever.types.{GrenadeState, ImplantType}
import scala.annotation.tailrec
@@ -42,6 +42,7 @@ class AvatarConverter extends ObjectCreateConverter[Player]() {
MakeImplantEntries(obj),
List.empty[String], //TODO fte list
List.empty[String], //TODO tutorial list
+ MakeCosmetics(obj.BEP),
InventoryData((MakeHolsters(obj, BuildDetailedEquipment) ++ MakeFifthSlot(obj) ++ MakeInventory(obj)).sortBy(_.parentSlot)),
GetDrawnSlot(obj)
)
@@ -56,7 +57,7 @@ class AvatarConverter extends ObjectCreateConverter[Player]() {
private def MakeAppearanceData(obj : Player) : CharacterAppearanceData = {
CharacterAppearanceData(
PlacementData(obj.Position, obj.Orientation, obj.Velocity),
- BasicCharacterData(obj.Name, obj.Faction, obj.Sex, obj.Voice, obj.Head),
+ BasicCharacterData(obj.Name, obj.Faction, obj.Sex, obj.Head, obj.Voice),
0,
false,
false,
@@ -132,7 +133,10 @@ class AvatarConverter extends ObjectCreateConverter[Player]() {
* @see `ImplantEntry` in `DetailedCharacterData`
*/
private def MakeImplantEntries(obj : Player) : List[ImplantEntry] = {
- obj.Implants.map(slot => {
+ val numImplants : Int = DetailedCharacterData.numberOfImplantSlots(obj.BEP)
+ val implants = obj.Implants
+ (0 until numImplants).map(index => {
+ val slot = implants(index)
slot.Installed match {
case Some(_) =>
if(slot.Initialized) {
@@ -176,6 +180,20 @@ class AvatarConverter extends ObjectCreateConverter[Player]() {
}
}
+ /**
+ * Should this player be of battle rank 24 or higher, they will have a mandatory cosmetics object.
+ * @param bep battle experience points
+ * @see `Cosmetics`
+ * @return the `Cosmetics` options
+ */
+ protected def MakeCosmetics(bep : Long) : Option[Cosmetics] =
+ if(DetailedCharacterData.isBR24(bep)) {
+ Some(Cosmetics(false, false, false, false, false))
+ }
+ else {
+ None
+ }
+
/**
* Given a player with an inventory, convert the contents of that inventory into converted-decoded packet data.
* The inventory is not represented in a `0x17` `Player`, so the conversion is only valid for `0x18` avatars.
@@ -236,7 +254,7 @@ class AvatarConverter extends ObjectCreateConverter[Player]() {
* @param equip the game object
* @return the game object in decoded packet form
*/
- private def BuildDetailedEquipment(index : Int, equip : Equipment) : InternalSlot = {
+ protected def BuildDetailedEquipment(index : Int, equip : Equipment) : InternalSlot = {
InternalSlot(equip.Definition.ObjectId, equip.GUID, index, equip.Definition.Packet.DetailedConstructorData(equip).get)
}
@@ -274,7 +292,7 @@ class AvatarConverter extends ObjectCreateConverter[Player]() {
* @param obj the `Player` game object
* @return the holster's Enumeration value
*/
- private def GetDrawnSlot(obj : Player) : DrawnSlot.Value = {
+ protected def GetDrawnSlot(obj : Player) : DrawnSlot.Value = {
try { DrawnSlot(obj.DrawnSlot) } catch { case _ : Exception => DrawnSlot.None }
}
}
diff --git a/common/src/main/scala/net/psforever/objects/definition/converter/CharacterSelectConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/CharacterSelectConverter.scala
new file mode 100644
index 000000000..293c71ced
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/definition/converter/CharacterSelectConverter.scala
@@ -0,0 +1,102 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.definition.converter
+
+import net.psforever.objects.{EquipmentSlot, Player}
+import net.psforever.objects.equipment.Equipment
+import net.psforever.packet.game.objectcreate.{BasicCharacterData, CharacterAppearanceData, CharacterData, DetailedCharacterData, ImplantEntry, InternalSlot, InventoryData, PlacementData, RibbonBars}
+import net.psforever.types.{GrenadeState, ImplantType}
+
+import scala.annotation.tailrec
+import scala.util.{Failure, Success, Try}
+
+/**
+ * `CharacterSelectConverter` is a simplified `AvatarConverter`
+ * that is tailored for appearance of the player character on the character selection screen.
+ * Details that would not be apparent on that screen such as implants or certifications are ignored.
+ */
+class CharacterSelectConverter extends AvatarConverter {
+ override def ConstructorData(obj : Player) : Try[CharacterData] = Failure(new Exception("CharacterSelectConverter should not be used to generate CharacterData"))
+
+ override def DetailedConstructorData(obj : Player) : Try[DetailedCharacterData] = {
+ Success(
+ DetailedCharacterData(
+ MakeAppearanceData(obj),
+ obj.BEP,
+ obj.CEP,
+ 1, 1, 0, 1, 1,
+ Nil,
+ MakeImplantEntries(obj), //necessary for correct stream length
+ Nil, Nil,
+ MakeCosmetics(obj.BEP),
+ InventoryData(recursiveMakeHolsters(obj.Holsters().iterator)),
+ GetDrawnSlot(obj)
+ )
+ )
+ }
+
+ /**
+ * Compose some data from a `Player` into a representation common to both `CharacterData` and `DetailedCharacterData`.
+ * @param obj the `Player` game object
+ * @see `AvatarConverter.MakeAppearanceData`
+ * @return the resulting `CharacterAppearanceData`
+ */
+ private def MakeAppearanceData(obj : Player) : CharacterAppearanceData = {
+ CharacterAppearanceData(
+ PlacementData(0f, 0f, 0f),
+ BasicCharacterData(obj.Name, obj.Faction, obj.Sex, obj.Head, 1),
+ 0,
+ false,
+ false,
+ obj.ExoSuit,
+ "",
+ 0,
+ false,
+ 0f,
+ 0f,
+ true,
+ GrenadeState.None,
+ false,
+ false,
+ false,
+ RibbonBars()
+ )
+ }
+
+ /**
+ * Transform an `Array` of `Implant` objects into a `List` of `ImplantEntry` objects suitable as packet data.
+ * @param obj the `Player` game object
+ * @return the resulting implant `List`
+ * @see `ImplantEntry` in `DetailedCharacterData`
+ */
+ private def MakeImplantEntries(obj : Player) : List[ImplantEntry] = {
+ List.fill[ImplantEntry](DetailedCharacterData.numberOfImplantSlots(obj.BEP))(ImplantEntry(ImplantType.None, None))
+ }
+
+ /**
+ * Given some equipment holsters, convert the contents of those holsters into converted-decoded packet data.
+ * @param iter an `Iterator` of `EquipmentSlot` objects that are a part of the player's holsters
+ * @param list the current `List` of transformed data
+ * @param index which holster is currently being explored
+ * @see `AvatarConverter.recursiveMakeHolsters`
+ * @return the `List` of inventory data created from the holsters
+ */
+ @tailrec private def recursiveMakeHolsters(iter : Iterator[EquipmentSlot], list : List[InternalSlot] = Nil, index : Int = 0) : List[InternalSlot] = {
+ if(!iter.hasNext) {
+ list
+ }
+ else {
+ val slot : EquipmentSlot = iter.next
+ if(slot.Equipment.isDefined) {
+ val equip : Equipment = slot.Equipment.get
+ recursiveMakeHolsters(
+ iter,
+ list :+ BuildDetailedEquipment(index, equip),
+ index + 1
+ )
+ }
+ else {
+ recursiveMakeHolsters(iter, list, index + 1)
+ }
+ }
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/guid/NumberPoolHub.scala b/common/src/main/scala/net/psforever/objects/guid/NumberPoolHub.scala
index e360df533..39054335c 100644
--- a/common/src/main/scala/net/psforever/objects/guid/NumberPoolHub.scala
+++ b/common/src/main/scala/net/psforever/objects/guid/NumberPoolHub.scala
@@ -60,14 +60,18 @@ class NumberPoolHub(private val source : NumberSource) {
* @param name the name of the pool
* @param pool the `List` of numbers that will belong to the pool
* @return the newly-created number pool
- * @throws IllegalArgumentException if the pool is already defined;
- * if the pool contains numbers the source does not
+ * @throws IllegalArgumentException if the pool's name is already defined;
+ * if the pool is (already) empty;
+ * if the pool contains numbers the source does not;
* if the pool contains numbers from already existing pools
*/
def AddPool(name : String, pool : List[Int]) : NumberPool = {
if(hash.get(name).isDefined) {
throw new IllegalArgumentException(s"can not add pool $name - name already known to this hub?")
}
+ if(pool.isEmpty) {
+ throw new IllegalArgumentException(s"can not add empty pool $name")
+ }
if(source.Size <= pool.max) {
throw new IllegalArgumentException(s"can not add pool $name - max(pool) is greater than source.size")
}
@@ -203,8 +207,8 @@ class NumberPoolHub(private val source : NumberSource) {
val slctr = pool.Selector
import net.psforever.objects.guid.selector.SpecificSelector
val specific = new SpecificSelector
- specific.SelectionIndex = number
pool.Selector = specific
+ specific.SelectionIndex = number
pool.Get()
pool.Selector = slctr
register_GetAvailableNumberFromSource(number)
diff --git a/common/src/main/scala/net/psforever/objects/guid/actor/IsRegistered.scala b/common/src/main/scala/net/psforever/objects/guid/actor/IsRegistered.scala
deleted file mode 100644
index b680c4698..000000000
--- a/common/src/main/scala/net/psforever/objects/guid/actor/IsRegistered.scala
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright (c) 2017 PSForever
-package net.psforever.objects.guid.actor
-
-import net.psforever.objects.entity.IdentifiableEntity
-
-/**
- * A message for requesting information about the registration status of an object or a number.
- * @param obj the optional object
- * @param number the optional number
- */
-final case class IsRegistered(obj : Option[IdentifiableEntity], number : Option[Int])
-
-object IsRegistered {
- /**
- * Overloaded constructor for querying an object's status.
- * @param obj the object
- * @return an `IsRegistered` object
- */
- def apply(obj : IdentifiableEntity) : IsRegistered = {
- new IsRegistered(Some(obj), None)
- }
-
- /**
- * Overloaded constructor for querying a number's status.
- * @param number the number
- * @return an `IsRegistered` object
- */
- def apply(number : Int) : IsRegistered = {
- new IsRegistered(None, Some(number))
- }
-}
diff --git a/common/src/main/scala/net/psforever/objects/guid/actor/NumberPoolAccessorActor.scala b/common/src/main/scala/net/psforever/objects/guid/actor/NumberPoolAccessorActor.scala
deleted file mode 100644
index ec35632c2..000000000
--- a/common/src/main/scala/net/psforever/objects/guid/actor/NumberPoolAccessorActor.scala
+++ /dev/null
@@ -1,216 +0,0 @@
-// Copyright (c) 2017 PSForever
-package net.psforever.objects.guid.actor
-
-import akka.actor.{Actor, ActorRef}
-import akka.pattern.ask
-import akka.util.Timeout
-import net.psforever.objects.entity.IdentifiableEntity
-import net.psforever.objects.guid.NumberPoolHub
-import net.psforever.objects.guid.pool.NumberPool
-
-import scala.concurrent.duration._
-import scala.util.{Failure, Success}
-
-/**
- * An `Actor` that wraps around the `Actor` for a `NumberPool` and automates a portion of the number registration process.
- *
- * The `NumberPoolActor` that is created is used as the synchronized "gate" through which the number selection process occurs.
- * This `Actor` `ask`s the internal `Actor` and then waits on that `Future` to resolve.
- * For the registration process, once it resolves, a number for the accompanying object has been chosen.
- * The last part involves configuring the `NumberSource` of the hub so that it knows.
- * For the process of revoking registration, the number from the object is returned to the pool.
- * Like during the registration process, the `NumberSource` is then also updated.
- *
- * The object is always registered using the underlying governed `NumberPool`.
- * The object will not unregister if the object or its number are not recognized as members previously registered to the `NumberPool`.
- * Whether or not an object or a specific number has been registered is always possible.
- * The scope encompasses the whole of the associated `NumberSource` as opposed to just this `NumberPool`.
- * @param hub the `NumberPoolHub` this `Actor` manipulates
- * @param pool the specific `NumberPool` this `Actor` maintains
- * @param poolActor a shared `Actor` that governs this `NumberPool`
- */
-class NumberPoolAccessorActor(private val hub : NumberPoolHub, private val pool : NumberPool, private val poolActor : ActorRef) extends Actor {
- //the timeout is for when we ask the poolActor
- private implicit val timeout = Timeout(50 milliseconds)
- private[this] val log = org.log4s.getLogger
-
- private final case class GUIDRequest(obj : IdentifiableEntity, replyTo : ActorRef)
- private val requestQueue : collection.mutable.LongMap[GUIDRequest] = new collection.mutable.LongMap()
- private var index : Long = Long.MinValue
-
- def receive : Receive = {
- //register
- case Register(obj, _, None, call) =>
- try {
- obj.GUID //stop if object has a GUID; sometimes this happens
- log.warn(s"$obj already registered")
- }
- catch {
- case _ : Exception =>
- val id : Long = index
- index += 1
- requestQueue += id -> GUIDRequest(obj, call.getOrElse(sender()))
- poolActor ! NumberPoolActor.GetAnyNumber(Some(id))
- }
-
- case Register(obj, _, Some(number), call) =>
- try {
- obj.GUID //stop if object has a GUID; sometimes this happens
- log.warn(s"$obj already registered")
- }
- catch {
- case _ : Exception =>
- val id : Long = index
- index += 1
- requestQueue += id -> GUIDRequest(obj, call.getOrElse(sender()))
- poolActor ! NumberPoolActor.GetSpecificNumber(number, Some(id))
- }
-
- case NumberPoolActor.GiveNumber(number, id) =>
- id match {
- case Some(nid : Long) =>
- Register(nid, requestQueue.remove(nid), number)
- case _ =>
- pool.Return(number) //recovery?
- log.warn(s"received a number but there is no request to process it; returning number to pool")
- }
-
- case NumberPoolActor.NoNumber(ex, id) =>
- val req = id match {
- case Some(nid : Long) =>
- val req = requestQueue.remove(nid)
- if(req.isDefined) { s"$req" } else { s"a corresponding request $nid was not found;" }
- case _ =>
- "generic request;" //should be unreachable
- }
- log.warn(s"a number was not drawn from the pool; $req $ex")
-
- //unregister
- case Unregister(obj, call) =>
- val callback = call.getOrElse(sender())
- try {
- val number = obj.GUID.guid
- if(pool.Numbers.contains(number) && hub.WhichPool(obj).isDefined) {
- val id : Long = index
- index += 1
- requestQueue += id -> GUIDRequest(obj, callback)
- poolActor ! NumberPoolActor.ReturnNumber(number, Some(id))
- }
- else {
- callback ! Failure(new Exception(s"the GUID of object $obj - $number - is not a part of this number pool"))
- }
- }
- catch {
- case msg : Exception =>
- callback ! Failure(msg)
- }
-
- case NumberPoolActor.ReturnNumberResult(number, None, id) =>
- id match {
- case Some(nid : Long) =>
- Unregister(nid, requestQueue.remove(nid), number)
- case _ =>
- NumberPoolActor.GetSpecificNumber(pool, number) //recovery?
- log.error(s"returned a number but there is no request to process it; recovering the number from pool")
- }
-
- case NumberPoolActor.ReturnNumberResult(number, ex, id) =>
- val req = id match {
- case Some(nid : Long) =>
- val req = requestQueue.remove(nid)
- if(req.isDefined) { s"$req" } else { s"a corresponding request $nid was not found;" }
- case _ =>
- "generic request;" //should be unreachable
- }
- log.warn(s"a number $number was not returned to the pool; $req $ex")
-
- //common
- case IsRegistered(Some(obj), None) =>
- sender ! hub.isRegistered(obj)
-
- case IsRegistered(None, Some(number)) =>
- sender ! hub.isRegistered(number)
-
- case NumberPoolActor.ReturnNumber(number, _) =>
- sender ! (poolActor ? NumberPoolActor.ReturnNumber(number))
-
- case msg =>
- log.warn(s"unexpected message received - $msg")
- }
-
- /**
- * A step of the object registration process.
- * If there is a successful request object to be found, complete the registration request.
- * @param id the identifier of this request
- * @param request the request data
- * @param number the number that was drawn from the `NumberPool`
- */
- private def Register(id : Long, request : Option[GUIDRequest], number : Int) : Unit = {
- request match {
- case Some(GUIDRequest(obj, replyTo)) =>
- processRegisterResult(obj, number, replyTo)
- case None =>
- pool.Return(number) //recovery?
- log.warn(s"received a number but the request for it is missing; returning number to pool")
- }
- }
-
- /**
- * A step of the object registration process.
- * This step completes the registration by consulting the `NumberSource`.
- * @param obj the object
- * @param number the number to use
- * @param callback an optional callback `ActorRef`
- */
- private def processRegisterResult(obj : IdentifiableEntity, number : Int, callback : ActorRef) : Unit = {
- try {
- obj.GUID
- pool.Return(number) //recovery?
- callback ! Success(obj)
- }
- catch {
- case _ : Exception =>
- hub.latterPartRegister(obj, number) match {
- case Success(_) =>
- callback ! Success(obj)
- case Failure(ex) =>
- pool.Return(number) //recovery?
- callback ! Failure(ex)
- }
- }
- }
-
- /**
- * A step of the object un-registration process.
- * If there is a successful request object to be found, complete the registration request.
- * @param id the identifier of this request
- * @param request the request data
- * @param number the number that was drawn from the `NumberPool`
- */
- private def Unregister(id : Long, request : Option[GUIDRequest], number : Int) : Unit = {
- request match {
- case Some(GUIDRequest(obj, replyTo)) =>
- processUnregisterResult(obj, obj.GUID.guid, replyTo)
- case None =>
- NumberPoolActor.GetSpecificNumber(pool, number) //recovery?
- log.error(s"returned a number but the rest of the request is missing; recovering the number from pool")
- }
- }
-
- /**
- * A step of the object un-registration process.
- * This step completes revoking the object's registration by consulting the `NumberSource`.
- * @param obj the object
- * @param callback an optional callback `ActorRef`
- */
- private def processUnregisterResult(obj : IdentifiableEntity, number : Int, callback : ActorRef) : Unit = {
- hub.latterPartUnregister(number) match {
- case Some(_) =>
- obj.Invalidate()
- callback ! Success(obj)
- case None =>
- NumberPoolActor.GetSpecificNumber(pool, number) //recovery?
- callback ! Failure(new Exception(s"failed to unregister a number; this may be a critical error"))
- }
- }
-}
diff --git a/common/src/main/scala/net/psforever/objects/guid/actor/NumberPoolHubActor.scala b/common/src/main/scala/net/psforever/objects/guid/actor/NumberPoolHubActor.scala
deleted file mode 100644
index d1a9af128..000000000
--- a/common/src/main/scala/net/psforever/objects/guid/actor/NumberPoolHubActor.scala
+++ /dev/null
@@ -1,212 +0,0 @@
-// Copyright (c) 2017 PSForever
-package net.psforever.objects.guid.actor
-
-import akka.pattern.ask
-import akka.util.Timeout
-import akka.actor.{Actor, ActorRef, Props}
-
-import net.psforever.objects.entity.IdentifiableEntity
-import net.psforever.objects.guid.NumberPoolHub
-import net.psforever.objects.guid.pool.NumberPool
-
-import scala.collection.mutable
-import scala.concurrent.Future
-import scala.concurrent.duration._
-import scala.util.{Failure, Success, Try}
-
-/**
- * An incoming message for retrieving a specific `NumberPoolAccessorActor`.
- * @param name the name of the accessor's `NumberPool`
- */
-final case class RequestPoolActor(name : String)
-
-/**
- * An outgoing message for giving a specific `NumberPoolAccessorActor`.
- * @param name the name of the accessor's `NumberPool`, for reference
- * @param actor the accessor
- */
-final case class DeliverPoolActor(name : String, actor : ActorRef)
-
-/**
- * An `Actor` that wraps around the management system for `NumberPools`.
- *
- * By just instantiating, this object builds and stores a `NumberPoolAccessorActor` for each `NumberPool` known to the `hub`.
- * Additional `NumberPool`s created by the `hub` need to be paired with a created accessor manually.
- * Each accessor is the primary entry point to a registration process for the specific `NumberPool` it represents.
- * The `hub` `Actor` itself distribute any registration task it receives out to an applicable accessor of which it is aware.
- * It will attempt to revoke registration on its own, without relying on the functionality from any accessor.
- *
- * In the same way that `NumberPoolHub` is a tool for keeping track of `NumberPool` objects,
- * its `Actor` is a tool for keeping track of accessors created from `NumberPool` objects.
- * It is very, however, for handling unspecific revoke tasks.
- * @param hub the central `NumberPool` management object for an embedded `NumberSource` object
- */
-class NumberPoolHubActor(private val hub : NumberPoolHub) extends Actor {
- private val actorHash : mutable.HashMap[String, ActorRef] = mutable.HashMap[String, ActorRef]()
- hub.Pools.foreach({ case(name, pool) => CreatePoolActor(name, pool) })
- implicit val timeout = Timeout(50 milliseconds)
- private[this] val log = org.log4s.getLogger
-
- def receive : Receive = {
- case RequestPoolActor(name) =>
- sender ! (GetPoolActor(name) match {
- case Success(poolActor) =>
- DeliverPoolActor(name, poolActor)
- case Failure(ex) =>
- Failure(ex)
- })
-
- case Register(obj, name, None, callback) =>
- HubRegister(obj, name, callback)
-
- case Register(obj, name, Some(number), callback) =>
- HubRegister(obj, name, number, callback)
-
- //common
- case IsRegistered(Some(obj), None) =>
- sender ! hub.isRegistered(obj)
-
- case IsRegistered(None, Some(number)) =>
- sender ! hub.isRegistered(number)
-
- case Unregister(obj, callback) =>
- Unregister(obj, if(callback.isEmpty) { sender } else { callback.get })
-
- case msg =>
- log.warn(s"unexpected message received - ${msg.toString}")
- }
-
- /**
- * From a name, find an existing `NumberPoolAccessorActor`.
- * @param name the accessor's name
- * @return the accessor that was requested
- */
- private def GetPoolActor(name : String) : Try[ActorRef] = {
- actorHash.get(name) match {
- case Some(actor) =>
- Success(actor)
- case _ =>
- Failure(new Exception(s"number pool $name not defined"))
- }
- }
-
- /**
- * Create a new `NumberPoolAccessorActor` and add it to the local collection of accessors.
- * @param name the accessor's name
- * @param pool the underlying `NumberPool`
- */
- private def CreatePoolActor(name : String, pool : NumberPool) : Unit = {
- actorHash.get(name) match {
- case None =>
- actorHash += name -> context.actorOf(Props(classOf[NumberPoolAccessorActor], hub, pool), s"${name}Actor")
- case Some(_) =>
- //TODO complain?
- }
- }
-
- /**
- * A step of the object registration process.
- * Select a valid `NumberPoolAccessorActor` and pass a task onto it.
- * @param obj an object
- * @param name a potential accessor pool
- * @param callback an optional callback `ActorRef`
- */
- private def HubRegister(obj : IdentifiableEntity, name : Option[String], callback : Option[ActorRef]) : Unit = {
- val genericPool = actorHash("generic")
- val pool = if(name.isDefined) { actorHash.get(name.get).orElse(Some(genericPool)).get } else { genericPool }
- pool ! Register(obj, None, None, callback)
- }
-
- /**
- * A step of the object registration process.
- * Determine to which `NumberPool` the `number` belongs.
- * @param obj an object
- * @param name a potential accessor pool
- * @param number a potential number
- * @param callback an optional callback `ActorRef`
- */
- private def HubRegister(obj : IdentifiableEntity, name : Option[String], number : Int, callback : Option[ActorRef]) : Unit = {
- hub.WhichPool(number) match {
- case Some(poolname) =>
- HubRegister_GetActor(obj, name, poolname, number, callback)
- case None =>
- self ! Register(obj, name, None, callback)
- }
- }
-
- /**
- * A step of the object registration process.
- * Pass a task onto an accessor or, if the accessor can not be found, attempt to recover.
- * @param obj an object
- * @param name a potential accessor pool
- * @param poolname the suggested accessor pool
- * @param number a potential number
- * @param callback an optional callback `ActorRef`
- */
- private def HubRegister_GetActor(obj : IdentifiableEntity, name : Option[String], poolname : String, number : Int, callback : Option[ActorRef]) : Unit = {
- actorHash.get(poolname) match {
- case Some(pool) =>
- pool ! Register(obj, None, Some(number), callback)
- case None =>
- HubRegister_MissingActor(obj, name, poolname, number, callback)
- }
- }
-
- /**
- * A step of the object registration process.
- * If an accessor could not be found in the last step, attempt to create the accessor.
- * If the accessor can not be created, the `number` can not be used;
- * fall back on the original pool (`name`).
- * @param obj an object
- * @param name a potential accessor pool
- * @param poolname the suggested accessor pool
- * @param number a potential number
- * @param callback an optional callback `ActorRef`
- */
- private def HubRegister_MissingActor(obj : IdentifiableEntity, name : Option[String], poolname : String, number : Int, callback : Option[ActorRef]) : Unit = {
- hub.GetPool(poolname) match {
- case Some(pool) =>
- CreatePoolActor(poolname, pool)
- actorHash(poolname) ! Register(obj, None, Some(number), callback)
- case None =>
- log.error(s"matched number $number to pool $poolname, but could not find $poolname when asked")
- self ! Register(obj, name, None, callback)
- }
- }
-
- /**
- * A step of the object un-registration process.
- * This step locates the `NumberPool` to which this object is a member.
- * If found, it prepares a `Future` to resolve later regarding whether the `NumberPool` accepted the number.
- * @param obj the object
- * @param callback a callback `ActorRef`
- */
- private def Unregister(obj : IdentifiableEntity, callback : ActorRef) : Unit = {
- hub.WhichPool(obj) match {
- case Some(name) =>
- val objToUnregister = obj
- val poolName = name
- processUnregisterResult(objToUnregister, (actorHash(poolName) ? NumberPoolActor.ReturnNumber(objToUnregister.GUID.guid)).mapTo[Boolean], callback)
- case None =>
- callback ! UnregisterFailure(obj, new Exception("could not find pool object is member of"))
- }
- }
-
- /**
- * A step of the object un-registration process.
- * This step completes revoking the object's registration by consulting the `NumberSource`.
- * @param obj the object
- * @param result whether the number was returned in the last step
- * @param callback a callback `ActorRef`
- */
- private def processUnregisterResult(obj : IdentifiableEntity, result : Future[Boolean], callback : ActorRef) : Unit = {
- import scala.concurrent.ExecutionContext.Implicits.global
- result.foreach {
- case true =>
- hub.latterPartUnregister(obj.GUID.guid)
- callback ! UnregisterSuccess(obj)
- case false =>
- callback ! UnregisterFailure(obj, new Exception("could not find object to remove"))
- }
- }
-}
diff --git a/common/src/main/scala/net/psforever/objects/guid/actor/UniqueNumberSystem.scala b/common/src/main/scala/net/psforever/objects/guid/actor/UniqueNumberSystem.scala
new file mode 100644
index 000000000..fcd16c173
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/guid/actor/UniqueNumberSystem.scala
@@ -0,0 +1,348 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.guid.actor
+
+import akka.actor.{Actor, ActorContext, ActorRef, Props}
+import net.psforever.objects.entity.IdentifiableEntity
+import net.psforever.objects.guid.NumberPoolHub
+
+import scala.util.{Failure, Success}
+
+/**
+ * An `Actor` that wraps around converted `NumberPool`s and synchronizes a portion of the number registration process.
+ * The ultimate goal is to manage a coherent group of unique identifiers for a given "region" (`Zone`).
+ * Both parts of the UID system sit atop the `Zone` for easy external access.
+ * The plain part - the `NumberPoolHub` here - is used for low-priority requests such as checking for existing associations.
+ * This `Actor` is the involved portion that paces registration and unregistration.
+ *
+ * A four part process is used for object registration tasks.
+ * First, the requested `NumberPool` is located among the list of known `NumberPool`s.
+ * Second, an asynchronous request is sent to that pool to retrieve a number.
+ * (Only any number. Only a failing case allows for selection of a specific number.)
+ * Third, the asynchronous request returns and the original information about the request is recovered.
+ * Fourth, both sides of the contract are completed by the object being assigned the number and
+ * the underlying "number source" is made to remember an association between the object and the number.
+ * Short circuits and recoveries as available on all steps though reporting is split between logging and callbacks.
+ * The process of removing the association between a number and object (unregistering) is a similar four part process.
+ *
+ * The important relationship between this `Actor` and the `Map` of `NumberPoolActors` is an "gate."
+ * A single `Map` is constructed and shared between multiple entry points to the UID system where requests are messaged.
+ * Multiple entry points send messages to the same `NumberPool`.
+ * That `NumberPool` deals with the messages one at a time and sends reply to each entry point that communicated with it.
+ * This process is almost as fast as the process of the `NumberPool` selecting a number.
+ * (At least, both should be fast.)
+ * @param guid the `NumberPoolHub` that is partially manipulated by this `Actor`
+ * @param poolActors a common mapping created from the `NumberPool`s in `guid`;
+ * there is currently no check for this condition save for requests failing
+ */
+class UniqueNumberSystem(private val guid : NumberPoolHub, private val poolActors : Map[String, ActorRef]) extends Actor {
+ /** Information about Register and Unregister requests that persists between messages to a specific `NumberPool`. */
+ private val requestQueue : collection.mutable.LongMap[UniqueNumberSystem.GUIDRequest] = new collection.mutable.LongMap()
+ /** The current value for the next request entry's index. */
+ private var index : Long = Long.MinValue
+ private[this] val log = org.log4s.getLogger
+
+ def receive : Receive = {
+ case Register(obj, Some(pname), None, call) =>
+ val callback = call.getOrElse(sender())
+ try {
+ obj.GUID //stop if object already has a GUID; sometimes this happens
+ AlreadyRegistered(obj, pname)
+ callback ! Success(obj)
+ }
+ catch {
+ case _ : Exception =>
+ val id : Long = index
+ index += 1
+ requestQueue += id -> UniqueNumberSystem.GUIDRequest(obj, pname, callback)
+ RegistrationProcess(pname, id)
+ }
+
+ //this message is automatically sent by NumberPoolActor
+ case NumberPoolActor.GiveNumber(number, id) =>
+ id match {
+ case Some(nid : Long) =>
+ RegistrationProcess(requestQueue.remove(nid), number, nid)
+ case _ =>
+ log.warn(s"received a number but there is no request to process it; returning number to pool")
+ NoCallbackReturnNumber(number) //recovery?
+ //no callback is possible
+ }
+
+ //this message is automatically sent by NumberPoolActor
+ case NumberPoolActor.NoNumber(ex, id) =>
+ id match {
+ case Some(nid : Long) =>
+ requestQueue.remove(nid) match {
+ case Some(entry) =>
+ entry.replyTo ! Failure(ex) //ONLY callback that is possible
+ case None => ;
+ log.warn(s"failed number request and no record of number request - $ex") //neither a successful request nor an entry of making the request
+ }
+ case None => ;
+ log.warn(s"failed number request and no record of number request - $ex") //neither a successful request nor an entry of making the request
+ case _ => ;
+ log.warn(s"unrecognized request $id accompanying a failed number request - $ex")
+ }
+
+ case Unregister(obj, call) =>
+ val callback = call.getOrElse(sender())
+ try {
+ val number = obj.GUID.guid
+ guid.WhichPool(number) match {
+ case Some(pname) =>
+ val id : Long = index
+ index += 1
+ requestQueue += id -> UniqueNumberSystem.GUIDRequest(obj, pname, callback)
+ UnregistrationProcess(pname, number, id)
+ case None =>
+ callback ! Failure(new Exception(s"the GUID of object $obj - $number - is not a part of this number pool"))
+ }
+ }
+ catch {
+ case _ : Exception =>
+ log.info(s"$obj is already unregistered")
+ callback ! Success(obj)
+ }
+
+ //this message is automatically sent by NumberPoolActor
+ case NumberPoolActor.ReturnNumberResult(number, None, id) =>
+ id match {
+ case Some(nid : Long) =>
+ UnregistrationProcess(requestQueue.remove(nid), number, nid)
+ case _ =>
+ log.error(s"returned a number but there is no request to process it; recovering the number from pool")
+ NoCallbackGetSpecificNumber(number) //recovery?
+ //no callback is possible
+ }
+
+ //this message is automatically sent by NumberPoolActor
+ case NumberPoolActor.ReturnNumberResult(number, Some(ex), id) => //if there is a problem when returning the number
+ id match {
+ case Some(nid : Long) =>
+ requestQueue.remove(nid) match {
+ case Some(entry) =>
+ entry.replyTo ! Failure(new Exception(s"for ${entry.target} with number $number, ${ex.getMessage}"))
+ case None => ;
+ log.error(s"could not find original request $nid that caused error $ex, but pool was $sender")
+ //no callback is possible
+ }
+ case _ => ;
+ log.error(s"could not find original request $id that caused error $ex, but pool was $sender")
+ //no callback is possible
+ }
+
+ case msg =>
+ log.warn(s"unexpected message received - $msg")
+ }
+
+ /**
+ * A step of the object registration process.
+ * Send a message to the `NumberPool` to request a number back.
+ * @param poolName the pool to which the object is trying to register
+ * @param id a potential identifier to associate this request
+ */
+ private def RegistrationProcess(poolName : String, id : Long) : Unit = {
+ poolActors.get(poolName) match {
+ case Some(pool) =>
+ pool ! NumberPoolActor.GetAnyNumber(Some(id))
+ case None =>
+ //do not log; use callback
+ requestQueue.remove(id).get.replyTo ! Failure(new Exception(s"can not find pool $poolName; nothing was registered"))
+ }
+ }
+
+ /**
+ * A step of the object registration process.
+ * If there is a successful request object to be found, continue the registration request.
+ * @param request the original request data
+ * @param number the number that was drawn from a `NumberPool`
+ */
+ private def RegistrationProcess(request : Option[UniqueNumberSystem.GUIDRequest], number : Int, id : Long) : Unit = {
+ request match {
+ case Some(entry) =>
+ processRegisterResult(entry, number)
+ case None =>
+ log.error(s"returned a number but the rest of the request is missing (id:$id)")
+ if(id != Long.MinValue) { //check to ignore endless loop of error-catching
+ log.warn("returning number to pool")
+ NoCallbackReturnNumber(number) //recovery?
+ //no callback is possible
+ }
+ }
+ }
+
+ /**
+ * A step of the object registration process.
+ * This step completes the registration by asking the `NumberPoolHub` to sort out its `NumberSource`.
+ * @param entry the original request data
+ * @param number the number to use
+ */
+ private def processRegisterResult(entry : UniqueNumberSystem.GUIDRequest, number : Int) : Unit = {
+ val obj = entry.target
+ guid.latterPartRegister(obj, number) match {
+ case Success(_) =>
+ entry.replyTo ! Success(obj)
+ case Failure(ex) =>
+ //do not log; use callback
+ NoCallbackReturnNumber(number, entry.targetPool) //recovery?
+ entry.replyTo ! Failure(ex)
+ }
+ }
+
+ /**
+ * A step of the object unregistration process.
+ * Send a message to the `NumberPool` to restore the availability of one of its numbers.
+ * @param poolName the pool to which the number will try to be returned
+ * @param number the number that was previously drawn from the specified `NumberPool`
+ * @param id a potential identifier to associate this request
+ */
+ private def UnregistrationProcess(poolName : String, number : Int, id : Long) : Unit = {
+ poolActors.get(poolName) match {
+ case Some(pool) =>
+ pool ! NumberPoolActor.ReturnNumber(number, Some(id))
+ case None =>
+ //do not log; use callback
+ requestQueue.remove(id).get.replyTo ! Failure(new Exception(s"can not find pool $poolName; nothing was de-registered"))
+ }
+ }
+
+ /**
+ * A step of the object unregistration process.
+ * If there is a successful request object to be found, continue the registration request.
+ * @param request the original request data
+ * @param number the number that was drawn from the `NumberPool`
+ */
+ private def UnregistrationProcess(request : Option[UniqueNumberSystem.GUIDRequest], number : Int, id : Long) : Unit = {
+ request match {
+ case Some(entry) =>
+ processUnregisterResult(entry, number)
+ case None =>
+ log.error(s"returned a number but the rest of the request is missing (id:$id)")
+ if(id != Long.MinValue) { //check to ignore endless loop of error-catching
+ log.error("recovering the number from pool")
+ NoCallbackGetSpecificNumber(number) //recovery?
+ //no callback is possible
+ }
+ }
+ }
+
+ /**
+ * A step of the object unregistration process.
+ * This step completes revoking of the object's registration by consulting the `NumberSource`.
+ * @param entry the original request data
+ * @param number the number to use
+ */
+ private def processUnregisterResult(entry : UniqueNumberSystem.GUIDRequest, number : Int) : Unit = {
+ val obj = entry.target
+ guid.latterPartUnregister(number) match {
+ case Some(_) =>
+ obj.Invalidate()
+ entry.replyTo ! Success(obj)
+ case None =>
+ //do not log; use callback
+ NoCallbackGetSpecificNumber(number, entry.targetPool) //recovery?
+ entry.replyTo ! Failure(new Exception(s"failed to unregister a number; this may be a critical error"))
+ }
+ }
+
+ /**
+ * Generate a relevant logging message for an object that is trying to register is actually already registered.
+ * @param obj the object that was trying to register
+ * @param poolName the pool to which the object was trying to register
+ */
+ private def AlreadyRegistered(obj : IdentifiableEntity, poolName : String) : Unit = {
+ val msg =
+ guid.WhichPool(obj) match {
+ case Some(pname) =>
+ if(poolName.equals(pname)) {
+ s"to pool $poolName"
+ }
+ else {
+ s"but to different pool $pname"
+ }
+ case None =>
+ "but not to any pool known to this system"
+ }
+ log.warn(s"$obj already registered $msg")
+ }
+
+ /**
+ * Access a specific `NumberPool` in a way that doesn't invoke a callback and reset one of its numbers.
+ * @param number the number that was drawn from a `NumberPool`
+ */
+ private def NoCallbackReturnNumber(number : Int) : Unit = {
+ guid.WhichPool(number) match {
+ case Some(pname) =>
+ NoCallbackReturnNumber(number, pname)
+ case None =>
+ log.error(s"critical: tried to return number $number but could not find containing pool")
+ }
+ }
+
+ /**
+ * Access a specific `NumberPool` in a way that doesn't invoke a callback and reset one of its numbers.
+ * To avoid fully processing the callback, an id of `Long.MinValue` is used to short circuit the routine.
+ * @param number the number that was drawn from a `NumberPool`
+ * @param poolName the `NumberPool` from which the `number` was drawn
+ * @see `UniqueNumberSystem.UnregistrationProcess(Option[GUIDRequest], Int, Int)`
+ */
+ private def NoCallbackReturnNumber(number : Int, poolName : String) : Unit = {
+ poolActors.get(poolName) match {
+ case Some(pool) =>
+ pool ! NumberPoolActor.ReturnNumber(number, Some(Long.MinValue))
+ case None =>
+ log.error(s"critical: tried to return number $number but did not find pool $poolName")
+ }
+ }
+
+ /**
+ * Access a specific `NumberPool` in a way that doesn't invoke a callback and claim one of its numbers.
+ * @param number the number to be drawn from a `NumberPool`
+ */
+ private def NoCallbackGetSpecificNumber(number : Int) : Unit = {
+ guid.WhichPool(number) match {
+ case Some(pname) =>
+ NoCallbackGetSpecificNumber(number, pname)
+ case None =>
+ log.error(s"critical: tried to re-register number $number but could not find containing pool")
+ }
+ }
+
+ /**
+ * Access a specific `NumberPool` in a way that doesn't invoke a callback and claim one of its numbers.
+ * To avoid fully processing the callback, an id of `Long.MinValue` is used to short circuit the routine.
+ * @param number the number to be drawn from a `NumberPool`
+ * @param poolName the `NumberPool` from which the `number` is to be drawn
+ * @see `UniqueNumberSystem.RegistrationProcess(Option[GUIDRequest], Int, Int)`
+ */
+ private def NoCallbackGetSpecificNumber(number : Int, poolName : String) : Unit = {
+ poolActors.get(poolName) match {
+ case Some(pool) =>
+ pool ! NumberPoolActor.GetSpecificNumber(number, Some(Long.MinValue))
+ case None =>
+ log.error(s"critical: tried to re-register number $number but did not find pool $poolName")
+ }
+ }
+}
+
+object UniqueNumberSystem {
+ /**
+ * Persistent record of the important information between the time fo request and the time of reply.
+ * @param target the object
+ * @param targetPool the name of the `NumberPool` being used
+ * @param replyTo the callback `ActorRef`
+ */
+ private final case class GUIDRequest(target : IdentifiableEntity, targetPool : String, replyTo : ActorRef)
+
+ /**
+ * Transform `NumberPool`s into `NumberPoolActor`s and pair them with their name.
+ * @param poolSource where the raw `NumberPools` are located
+ * @param context used to create the `NumberPoolActor` instances
+ * @return a `Map` of the pool names to the `ActorRef` created from the `NumberPool`
+ */
+ def AllocateNumberPoolActors(poolSource : NumberPoolHub)(implicit context : ActorContext) : Map[String, ActorRef] = {
+ poolSource.Pools.map({ case ((pname, pool)) =>
+ pname -> context.actorOf(Props(classOf[NumberPoolActor], pool), pname)
+ }).toMap
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/guid/actor/UnregisterFailure.scala b/common/src/main/scala/net/psforever/objects/guid/actor/UnregisterFailure.scala
deleted file mode 100644
index 60eb0ce3c..000000000
--- a/common/src/main/scala/net/psforever/objects/guid/actor/UnregisterFailure.scala
+++ /dev/null
@@ -1,11 +0,0 @@
-// Copyright (c) 2017 PSForever
-package net.psforever.objects.guid.actor
-
-import net.psforever.objects.entity.IdentifiableEntity
-
-/**
- * A message for when an object has failed to be unregistered for some reason.
- * @param obj the object
- * @param ex the reason that the registration process failed
- */
-final case class UnregisterFailure(obj : IdentifiableEntity, ex : Throwable)
diff --git a/common/src/main/scala/net/psforever/objects/guid/actor/UnregisterSuccess.scala b/common/src/main/scala/net/psforever/objects/guid/actor/UnregisterSuccess.scala
deleted file mode 100644
index 603de46ad..000000000
--- a/common/src/main/scala/net/psforever/objects/guid/actor/UnregisterSuccess.scala
+++ /dev/null
@@ -1,11 +0,0 @@
-// Copyright (c) 2017 PSForever
-package net.psforever.objects.guid.actor
-
-import net.psforever.objects.entity.IdentifiableEntity
-
-/**
- * A message for when an object has been unregistered.
- * @param obj the object
- */
-final case class UnregisterSuccess(obj : IdentifiableEntity)
-
diff --git a/common/src/main/scala/net/psforever/objects/guid/misc/AscendingNumberSource.scala b/common/src/main/scala/net/psforever/objects/guid/misc/AscendingNumberSource.scala
deleted file mode 100644
index c4534faba..000000000
--- a/common/src/main/scala/net/psforever/objects/guid/misc/AscendingNumberSource.scala
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright (c) 2017 PSForever
-package net.psforever.objects.guid.misc
-
-/**
- * This class is just a proof of concept model of a self-contained system.
- */
-class AscendingNumberSource {
- val pool : Array[Int] = Array.ofDim[Int](65536)
- (0 to 65535).foreach(x => { pool(x) = x })
- var head : Int = 0
-
- def Get() : Int = {
- val start : Int = head
- if(pool(head) == -1) {
- do {
- head = (head + 1) % pool.length
- }
- while(pool(head) == -1 && head != start)
- }
- if(head == start) {
- import net.psforever.objects.entity.NoGUIDException
- throw NoGUIDException("no unused numbers available")
- }
- val outNumber : Int = head
- pool(head) = -1
- outNumber
- }
-
- def Return(number : Int) : Unit = {
- pool(number) = number
- }
-}
diff --git a/common/src/main/scala/net/psforever/objects/guid/misc/RegistrationTaskResolver.scala b/common/src/main/scala/net/psforever/objects/guid/misc/RegistrationTaskResolver.scala
deleted file mode 100644
index f45d7e942..000000000
--- a/common/src/main/scala/net/psforever/objects/guid/misc/RegistrationTaskResolver.scala
+++ /dev/null
@@ -1,137 +0,0 @@
-// Copyright (c) 2017 PSForever
-package net.psforever.objects.guid.misc
-
-import java.util.concurrent.TimeoutException
-
-import akka.actor.{Actor, ActorRef, Cancellable}
-import net.psforever.objects.entity.IdentifiableEntity
-
-import scala.annotation.tailrec
-import scala.concurrent.ExecutionContext.Implicits.global
-import scala.concurrent.duration._
-import scala.util.{Failure, Success, Try}
-
-/**
- * Accept a task in waiting and series of lesser tasks that complete the provided primary task.
- * Receive periodic updates on the states of the lesser tasks and, when these sub-tasks have been accomplished,
- * declare the primary task accomplished as well.
- *
- * This ia admittedly a simplistic model of task resolution, currently, and is rather specific and limited.
- * Generalizing and expanding on this class in the future might be beneficial.
- * @param obj the primary task
- * @param list a series of sub-tasks that need to be completed before the pimrary task can be completed
- * @param callback where to report about the pirmary task having succeeded or failed
- * @param timeoutDuration a delay during which sub-tasks are permitted to be accomplished;
- * after this grave period is over, the task has failed
- */
-class RegistrationTaskResolver[T <: IdentifiableEntity](private val obj : T, private val list : List[T], callback : ActorRef, timeoutDuration : FiniteDuration) extends Actor {
- /** sub-tasks that contribute to completion of the task */
- private val checklist : Array[Boolean] = Array.fill[Boolean](list.length)(false)
- /** whether or not it matters that sub-tasks are coming in */
- private var valid : Boolean = true
- /** declares when the task has taken too long to complete */
- private val taskTimeout : Cancellable = context.system.scheduler.scheduleOnce(timeoutDuration, self, Failure(new TimeoutException(s"a task for $obj has timed out")))
- private[this] val log = org.log4s.getLogger
- ConfirmTask(Success(true)) //check for auto-completion
-
- def receive : Receive = {
- case Success(objn)=>
- ConfirmTask(ConfirmSubtask(objn.asInstanceOf[T]))
-
- case Failure(ex)=>
- FailedTask(ex)
-
- case msg =>
- log.warn(s"unexpected message received - ${msg.toString}")
- }
-
- /**
- * If this object is still accepting task resolutions, determine if that sub-task can be checked off.
- * @param objn the sub-task entry
- * @return a successful pass or a failure if the task can't be found;
- * a "successful failure" if task resolutions are no longer accepted
- */
- private def ConfirmSubtask(objn : T) : Try[Boolean] = {
- if(valid) {
- if(MatchSubtask(objn, list.iterator)) {
- Success(true)
- }
- else {
- Failure(new Exception(s"can not find a subtask to check off - ${objn.toString}"))
- }
- }
- else {
- Success(false)
- }
- }
-
- /**
- * Find a sub-task from a `List` of sub-tasks and mark it as completed, if found.
- * @param objn the sub-task entry
- * @param iter_list an `Iterator` to the list of sub-tasks
- * @param index the index of this entry;
- * defaults to zero
- * @return whether or not the subtask has been marked as completed
- */
- @tailrec private def MatchSubtask(objn : T, iter_list : Iterator[T], index : Int = 0) : Boolean = {
- if(!iter_list.hasNext) {
- false
- }
- else {
- val subtask = iter_list.next
- if(subtask.equals(objn)) {
- checklist(index) = true
- true
- }
- else {
- MatchSubtask(objn, iter_list, index + 1)
- }
- }
- }
-
- /**
- * Determine whether all sub-tasks have been completed successfully.
- * If so, complete the primary task.
- * @param subtaskComplete the status of the recent sub-task confirmation that triggered this confirmation request
- */
- private def ConfirmTask(subtaskComplete : Try[Boolean]) : Unit = {
- if(valid) {
- subtaskComplete match {
- case Success(true) =>
- if(!checklist.contains(false)) {
- FulfillTask()
- }
- case Success(false) =>
- log.warn(s"when checking a task for ${obj.toString}, arrived at a state where we previously failed a subtask but main task still valid")
- case Failure(ex) =>
- FailedTask(ex)
- }
- }
- }
-
- /**
- * All sub-tasks have been completed; the main task can also be completed.
- * Alert interested parties that the task is performed successfully.
- * Stop as soon as possible.
- */
- private def FulfillTask() : Unit = {
- valid = false
- callback ! Success(obj)
- taskTimeout.cancel()
- context.stop(self)
- }
-
- /**
- * The main task can not be completed.
- * Clean up as much as possible and alert interested parties that the task has been dropped.
- * Let this `Actor` stop gracefully.
- * @param ex why the main task can not be completed
- */
- private def FailedTask(ex : Throwable) : Unit = {
- valid = false
- callback ! Failure(ex)
- taskTimeout.cancel()
- import akka.pattern.gracefulStop
- gracefulStop(self, 2 seconds) //give time for any other messages; avoid dead letters
- }
-}
diff --git a/common/src/main/scala/net/psforever/objects/guid/source/MaxNumberSource.scala b/common/src/main/scala/net/psforever/objects/guid/source/MaxNumberSource.scala
deleted file mode 100644
index ea5c969b6..000000000
--- a/common/src/main/scala/net/psforever/objects/guid/source/MaxNumberSource.scala
+++ /dev/null
@@ -1,119 +0,0 @@
-// Copyright (c) 2017 PSForever
-package net.psforever.objects.guid.source
-
-import net.psforever.objects.entity.IdentifiableEntity
-import net.psforever.objects.guid.key.{LoanedKey, SecureKey}
-import net.psforever.objects.guid.AvailabilityPolicy
-
-/**
- * A `NumberSource` is considered a master "pool" of numbers from which all numbers are available to be drawn.
- * The numbers are considered to be exclusive.
- *
- * This source utilizes all positive integers (to `Int.MaxValue`, anyway) and zero.
- * It allocates number `Monitors` as it needs them.
- * While this allows for a wide range of possible numbers, the internal structure expands and contracts as needed.
- * The underlying flexible structure is a `LongMap` and is subject to constraints regarding `LongMap` growth.
- */
-class MaxNumberSource() extends NumberSource {
- import scala.collection.mutable
- private val hash : mutable.LongMap[Key] = mutable.LongMap[Key]() //TODO consider seeding an initialBufferSize
- private var allowRestrictions : Boolean = true
-
- def Size : Int = Int.MaxValue
-
- def CountAvailable : Int = Size - CountUsed
-
- def CountUsed : Int = hash.size
-
- override def Test(guid : Int) : Boolean = guid > -1
-
- def Get(number : Int) : Option[SecureKey] = {
- if(!Test(number)) {
- None
- }
- else {
- val existing : Option[Key] = hash.get(number).orElse({
- val key : Key = new Key
- key.Policy = AvailabilityPolicy.Available
- hash.put(number, key)
- Some(key)
- })
- Some(new SecureKey(number, existing.get))
- }
- }
-
-// def GetAll(list : List[Int]) : List[SecureKey] = {
-// list.map(number =>
-// hash.get(number) match {
-// case Some(key) =>
-// new SecureKey(number, key)
-// case _ =>
-// new SecureKey(number, new Key { Policy = AvailabilityPolicy.Available })
-// }
-// )
-// }
-//
-// def GetAll( p : Key => Boolean ) : List[SecureKey] = {
-// hash.filter(entry => p.apply(entry._2)).map(entry => new SecureKey(entry._1.toInt, entry._2)).toList
-// }
-
- def Available(number : Int) : Option[LoanedKey] = {
- if(!Test(number)) {
- throw new IndexOutOfBoundsException("number can not be negative")
- }
- hash.get(number) match {
- case Some(_) =>
- None
- case _ =>
- val key : Key = new Key
- key.Policy = AvailabilityPolicy.Leased
- hash.put(number, key)
- Some(new LoanedKey(number, key))
- }
- }
-
- def Return(number : Int) : Option[IdentifiableEntity] = {
- val existing = hash.get(number)
- if(existing.isDefined && existing.get.Policy == AvailabilityPolicy.Leased) {
- hash -= number
- val obj = existing.get.Object
- existing.get.Object = None
- obj
- }
- else {
- None
- }
- }
-
- def Restrict(number : Int) : Option[LoanedKey] = {
- if(allowRestrictions) {
- val existing : Key = hash.get(number).orElse({
- val key : Key = new Key
- hash.put(number, key)
- Some(key)
- }).get
- existing.Policy = AvailabilityPolicy.Restricted
- Some(new LoanedKey(number, existing))
- }
- else {
- None
- }
- }
-
- def FinalizeRestrictions : List[Int] = {
- allowRestrictions = false
- hash.filter(entry => entry._2.Policy == AvailabilityPolicy.Restricted).map(entry => entry._1.toInt).toList
- }
-
- def Clear() : List[IdentifiableEntity] = {
- val list : List[IdentifiableEntity] = hash.values.filter(key => key.Object.isDefined).map(key => key.Object.get).toList
- hash.clear()
- list
- }
-}
-
-object MaxNumberSource {
- def apply() : MaxNumberSource = {
- new MaxNumberSource()
- }
-}
\ No newline at end of file
diff --git a/common/src/main/scala/net/psforever/objects/serverobject/CommonMessages.scala b/common/src/main/scala/net/psforever/objects/serverobject/CommonMessages.scala
new file mode 100644
index 000000000..c73b7a235
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/serverobject/CommonMessages.scala
@@ -0,0 +1,10 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.serverobject
+
+import net.psforever.objects.Player
+
+//temporary location for these messages
+object CommonMessages {
+ final case class Hack(player : Player)
+ final case class ClearHack()
+}
diff --git a/common/src/main/scala/net/psforever/objects/serverobject/PlanetSideServerObject.scala b/common/src/main/scala/net/psforever/objects/serverobject/PlanetSideServerObject.scala
new file mode 100644
index 000000000..f44975444
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/serverobject/PlanetSideServerObject.scala
@@ -0,0 +1,32 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.serverobject
+
+import akka.actor.ActorRef
+import net.psforever.objects.PlanetSideGameObject
+
+/**
+ * An object layered on top of the standard game object class that maintains an internal `ActorRef`.
+ * A measure of synchronization can be managed using this `Actor`.
+ */
+abstract class PlanetSideServerObject extends PlanetSideGameObject {
+ private var actor = ActorRef.noSender
+
+ /**
+ * Retrieve a reference to the internal `Actor`.
+ * @return the internal `ActorRef`
+ */
+ def Actor : ActorRef = actor
+
+ /**
+ * Assign an `Actor` to act for this server object.
+ * This reference is only set once, that is, as long as the internal `ActorRef` directs to `Actor.noSender` (`null`).
+ * @param control the `Actor` whose functionality will govern this server object
+ * @return the current internal `ActorRef`
+ */
+ def Actor_=(control : ActorRef) : ActorRef = {
+ if(actor == ActorRef.noSender) {
+ actor = control
+ }
+ actor
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/serverobject/builders/DoorObjectBuilder.scala b/common/src/main/scala/net/psforever/objects/serverobject/builders/DoorObjectBuilder.scala
new file mode 100644
index 000000000..d40ff8261
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/serverobject/builders/DoorObjectBuilder.scala
@@ -0,0 +1,34 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.serverobject.builders
+
+import akka.actor.Props
+import net.psforever.objects.serverobject.doors.{Door, DoorControl, DoorDefinition}
+
+/**
+ * Wrapper `Class` designed to instantiate a `Door` server object.
+ * @param ddef a `DoorDefinition` object, indicating the specific functionality of the resulting `Door`
+ * @param id the globally unique identifier to which this `Door` will be registered
+ */
+class DoorObjectBuilder(private val ddef : DoorDefinition, private val id : Int) extends ServerObjectBuilder[Door] {
+ import akka.actor.ActorContext
+ import net.psforever.objects.guid.NumberPoolHub
+
+ def Build(implicit context : ActorContext, guid : NumberPoolHub) : Door = {
+ val obj = Door(ddef)
+ guid.register(obj, id) //non-Actor GUID registration
+ obj.Actor = context.actorOf(Props(classOf[DoorControl], obj), s"${ddef.Name}_${obj.GUID.guid}")
+ obj
+ }
+}
+
+object DoorObjectBuilder {
+ /**
+ * Overloaded constructor for a `DoorObjectBuilder`.
+ * @param ddef a `DoorDefinition` object
+ * @param id a globally unique identifier
+ * @return a `DoorObjectBuilder` object
+ */
+ def apply(ddef : DoorDefinition, id : Int) : DoorObjectBuilder = {
+ new DoorObjectBuilder(ddef, id)
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/serverobject/builders/IFFLockObjectBuilder.scala b/common/src/main/scala/net/psforever/objects/serverobject/builders/IFFLockObjectBuilder.scala
new file mode 100644
index 000000000..26834d933
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/serverobject/builders/IFFLockObjectBuilder.scala
@@ -0,0 +1,34 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.serverobject.builders
+
+import akka.actor.Props
+import net.psforever.objects.serverobject.locks.{IFFLock, IFFLockControl, IFFLockDefinition}
+
+/**
+ * Wrapper `Class` designed to instantiate a door lock server object that is sensitive to user faction affiliation.
+ * @param idef a `IFFLockDefinition` object, indicating the specific functionality
+ * @param id the globally unique identifier to which this `IFFLock` will be registered
+ */
+class IFFLockObjectBuilder(private val idef : IFFLockDefinition, private val id : Int) extends ServerObjectBuilder[IFFLock] {
+ import akka.actor.ActorContext
+ import net.psforever.objects.guid.NumberPoolHub
+
+ def Build(implicit context : ActorContext, guid : NumberPoolHub) : IFFLock = {
+ val obj = IFFLock(idef)
+ guid.register(obj, id) //non-Actor GUID registration
+ obj.Actor = context.actorOf(Props(classOf[IFFLockControl], obj), s"${idef.Name}_${obj.GUID.guid}")
+ obj
+ }
+}
+
+object IFFLockObjectBuilder {
+ /**
+ * Overloaded constructor for a `IFFLockObjectBuilder`.
+ * @param idef an `IFFLock` object
+ * @param id a globally unique identifier
+ * @return an `IFFLockObjectBuilder` object
+ */
+ def apply(idef : IFFLockDefinition, id : Int) : IFFLockObjectBuilder = {
+ new IFFLockObjectBuilder(idef, id)
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/zones/ServerObjectBuilder.scala b/common/src/main/scala/net/psforever/objects/serverobject/builders/ServerObjectBuilder.scala
similarity index 81%
rename from common/src/main/scala/net/psforever/objects/zones/ServerObjectBuilder.scala
rename to common/src/main/scala/net/psforever/objects/serverobject/builders/ServerObjectBuilder.scala
index 2635df096..e1bb0ce7b 100644
--- a/common/src/main/scala/net/psforever/objects/zones/ServerObjectBuilder.scala
+++ b/common/src/main/scala/net/psforever/objects/serverobject/builders/ServerObjectBuilder.scala
@@ -1,5 +1,5 @@
// Copyright (c) 2017 PSForever
-package net.psforever.objects.zones
+package net.psforever.objects.serverobject.builders
import akka.actor.ActorContext
import net.psforever.objects.PlanetSideGameObject
@@ -7,9 +7,11 @@ import net.psforever.objects.guid.NumberPoolHub
/**
* Wrapper `Trait` designed to be extended to implement custom object instantiation logic at the `ZoneMap` level.
+ * @tparam A any object that extends from PlanetSideGameObject
* @see `Zone.Init`
*/
-trait ServerObjectBuilder {
+//TODO can we changed PlanetSideGameObject -> PlanetSideServerObject?
+trait ServerObjectBuilder[A <: PlanetSideGameObject] {
/**
* Instantiate and configure the given server object
* (at a later time compared to the construction of the builder class).
@@ -23,5 +25,5 @@ trait ServerObjectBuilder {
* defaults to `null`
* @return the object that was created and integrated into the `Zone`
*/
- def Build(implicit context : ActorContext = null, guid : NumberPoolHub = null) : PlanetSideGameObject
+ def Build(implicit context : ActorContext = null, guid : NumberPoolHub = null) : A
}
diff --git a/common/src/main/scala/net/psforever/objects/zones/TerminalObjectBuilder.scala b/common/src/main/scala/net/psforever/objects/serverobject/builders/TerminalObjectBuilder.scala
similarity index 74%
rename from common/src/main/scala/net/psforever/objects/zones/TerminalObjectBuilder.scala
rename to common/src/main/scala/net/psforever/objects/serverobject/builders/TerminalObjectBuilder.scala
index 2ced4d84d..d01022d56 100644
--- a/common/src/main/scala/net/psforever/objects/zones/TerminalObjectBuilder.scala
+++ b/common/src/main/scala/net/psforever/objects/serverobject/builders/TerminalObjectBuilder.scala
@@ -1,21 +1,22 @@
// Copyright (c) 2017 PSForever
-package net.psforever.objects.zones
+package net.psforever.objects.serverobject.builders
-import net.psforever.objects.terminals.{Terminal, TerminalDefinition}
+import akka.actor.Props
+import net.psforever.objects.serverobject.terminals.{Terminal, TerminalControl, TerminalDefinition}
/**
* Wrapper `Class` designed to instantiate a `Terminal` server object.
* @param tdef a `TerminalDefinition` object, indicating the specific functionality of the resulting `Terminal`
* @param id the globally unique identifier to which this `Terminal` will be registered
*/
-class TerminalObjectBuilder(private val tdef : TerminalDefinition, private val id : Int) extends ServerObjectBuilder {
+class TerminalObjectBuilder(private val tdef : TerminalDefinition, private val id : Int) extends ServerObjectBuilder[Terminal] {
import akka.actor.ActorContext
import net.psforever.objects.guid.NumberPoolHub
def Build(implicit context : ActorContext, guid : NumberPoolHub) : Terminal = {
val obj = Terminal(tdef)
guid.register(obj, id) //non-Actor GUID registration
- obj.Actor //it's necessary to register beforehand because the Actor name utilizes the GUID
+ obj.Actor = context.actorOf(Props(classOf[TerminalControl], obj), s"${tdef.Name}_${obj.GUID.guid}")
obj
}
}
diff --git a/common/src/main/scala/net/psforever/objects/serverobject/doors/Base.scala b/common/src/main/scala/net/psforever/objects/serverobject/doors/Base.scala
new file mode 100644
index 000000000..90e6c490a
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/serverobject/doors/Base.scala
@@ -0,0 +1,21 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.serverobject.doors
+
+import net.psforever.types.PlanetSideEmpire
+
+/**
+ * A temporary class to represent "facilities" and "structures."
+ * @param id the map id of the base
+ */
+class Base(private val id : Int) {
+ private var faction : PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL
+
+ def Id : Int = id
+
+ def Faction : PlanetSideEmpire.Value = faction
+
+ def Faction_=(emp : PlanetSideEmpire.Value) : PlanetSideEmpire.Value = {
+ faction = emp
+ Faction
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/serverobject/doors/Door.scala b/common/src/main/scala/net/psforever/objects/serverobject/doors/Door.scala
new file mode 100644
index 000000000..ae63affdc
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/serverobject/doors/Door.scala
@@ -0,0 +1,90 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.serverobject.doors
+
+import net.psforever.objects.serverobject.PlanetSideServerObject
+import net.psforever.objects.Player
+import net.psforever.packet.game.UseItemMessage
+
+/**
+ * A structure-owned server object that is a "door" that can open and can close.
+ * @param ddef the `ObjectDefinition` that constructs this object and maintains some of its immutable fields
+ */
+class Door(private val ddef : DoorDefinition) extends PlanetSideServerObject {
+ private var openState : Boolean = false
+ private var lockState : Boolean = false
+
+ def Open : Boolean = openState
+
+ def Open_=(open : Boolean) : Boolean = {
+ openState = open
+ Open
+ }
+
+ def Locked : Boolean = lockState
+
+ def Locked_=(lock : Boolean) : Boolean = {
+ lockState = lock
+ Locked
+ }
+
+ def Use(player : Player, msg : UseItemMessage) : Door.Exchange = {
+ if(!lockState && !openState) {
+ openState = true
+ Door.OpenEvent()
+ }
+ else if(openState) {
+ openState = false
+ Door.CloseEvent()
+ }
+ else {
+ Door.NoEvent()
+ }
+ }
+
+ def Definition : DoorDefinition = ddef
+}
+
+object Door {
+ /**
+ * Entry message into this `Door` that carries the request.
+ * @param player the player who sent this request message
+ * @param msg the original packet carrying the request
+ */
+ final case class Use(player : Player, msg : UseItemMessage)
+
+ /**
+ * A basic `Trait` connecting all of the actionable `Door` 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 msg the original packet carrying the request
+ * @param response the result of the processed request
+ */
+ final case class DoorMessage(player : Player, msg : UseItemMessage, response : Exchange)
+
+ /**
+ * This door will open.
+ */
+ final case class OpenEvent() extends Exchange
+
+ /**
+ * This door will close.
+ */
+ final case class CloseEvent() extends Exchange
+
+ /**
+ * This door will do nothing.
+ */
+ final case class NoEvent() extends Exchange
+
+ /**
+ * Overloaded constructor.
+ * @param tdef the `ObjectDefinition` that constructs this object and maintains some of its immutable fields
+ */
+ def apply(tdef : DoorDefinition) : Door = {
+ new Door(tdef)
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/serverobject/doors/DoorControl.scala b/common/src/main/scala/net/psforever/objects/serverobject/doors/DoorControl.scala
new file mode 100644
index 000000000..8f1163316
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/serverobject/doors/DoorControl.scala
@@ -0,0 +1,18 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.serverobject.doors
+
+import akka.actor.Actor
+
+/**
+ * An `Actor` that handles messages being dispatched to a specific `Door`.
+ * @param door the `Door` object being governed
+ */
+class DoorControl(door : Door) extends Actor {
+ def receive : Receive = {
+ case Door.Use(player, msg) =>
+ sender ! Door.DoorMessage(player, msg, door.Use(player, msg))
+
+ case _ =>
+ sender ! Door.NoEvent()
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/serverobject/doors/DoorDefinition.scala b/common/src/main/scala/net/psforever/objects/serverobject/doors/DoorDefinition.scala
new file mode 100644
index 000000000..6a22670cd
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/serverobject/doors/DoorDefinition.scala
@@ -0,0 +1,12 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.serverobject.doors
+
+import net.psforever.objects.definition.ObjectDefinition
+
+/**
+ * The definition for any `Door`.
+ * Object Id 242 is a generic door.
+ */
+class DoorDefinition extends ObjectDefinition(242) {
+ Name = "door"
+}
diff --git a/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLock.scala b/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLock.scala
new file mode 100644
index 000000000..e08587e85
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLock.scala
@@ -0,0 +1,66 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.serverobject.locks
+
+import net.psforever.objects.Player
+import net.psforever.objects.serverobject.PlanetSideServerObject
+import net.psforever.packet.game.PlanetSideGUID
+import net.psforever.types.Vector3
+
+/**
+ * A structure-owned server object that is a "door lock."
+ *
+ * The "door lock" exerts an "identify friend or foe" field that detects the faction affiliation of a target player.
+ * It also indirectly inherits faction affiliation from the structure to which it is connected
+ * or it can be "hacked" whereupon the person exploiting it leaves their "faction" as the aforementioned affiliated faction.
+ * The `IFFLock` is ideally associated with a server map object - a `Door` - to which it acts as a gatekeeper.
+ * @param idef the `ObjectDefinition` that constructs this object and maintains some of its immutable fields
+ */
+class IFFLock(private val idef : IFFLockDefinition) extends PlanetSideServerObject {
+ /**
+ * An entry that maintains a reference to the `Player`, and the player's GUID and location when the message was received.
+ */
+ private var hackedBy : Option[(Player, PlanetSideGUID, Vector3)] = None
+
+ def HackedBy : Option[(Player, PlanetSideGUID, Vector3)] = hackedBy
+
+ def HackedBy_=(agent : Player) : Option[(Player, PlanetSideGUID, Vector3)] = HackedBy_=(Some(agent))
+
+ /**
+ * Set the hack state of this object by recording important information about the player that caused it.
+ * Set the hack state if there is no current hack state.
+ * Override the hack state with a new hack state if the new user has different faction affiliation.
+ * @param agent a `Player`, or no player
+ * @return the player hack entry
+ */
+ def HackedBy_=(agent : Option[Player]) : Option[(Player, PlanetSideGUID, Vector3)] = {
+ hackedBy match {
+ case None =>
+ //set the hack state if there is no current hack state
+ if(agent.isDefined) {
+ hackedBy = Some(agent.get, agent.get.GUID, agent.get.Position)
+ }
+ case Some(_) =>
+ //clear the hack state
+ if(agent.isEmpty) {
+ hackedBy = None
+ }
+ //override the hack state with a new hack state if the new user has different faction affiliation
+ else if(agent.get.Faction != hackedBy.get._1.Faction) {
+ hackedBy = Some(agent.get, agent.get.GUID, agent.get.Position)
+ }
+ }
+ HackedBy
+ }
+
+ def Definition : IFFLockDefinition = idef
+}
+
+object IFFLock {
+ /**
+ * Overloaded constructor.
+ * @param idef the `ObjectDefinition` that constructs this object and maintains some of its immutable fields
+ */
+ def apply(idef : IFFLockDefinition) : IFFLock = {
+ new IFFLock(idef)
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLockControl.scala b/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLockControl.scala
new file mode 100644
index 000000000..bf9d1b81a
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLockControl.scala
@@ -0,0 +1,22 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.serverobject.locks
+
+import akka.actor.Actor
+import net.psforever.objects.serverobject.CommonMessages
+
+/**
+ * An `Actor` that handles messages being dispatched to a specific `IFFLock`.
+ * @param lock the `IFFLock` object being governed
+ * @see `CommonMessages`
+ */
+class IFFLockControl(lock : IFFLock) extends Actor {
+ def receive : Receive = {
+ case CommonMessages.Hack(player) =>
+ lock.HackedBy = player
+
+ case CommonMessages.ClearHack() =>
+ lock.HackedBy = None
+
+ case _ => ; //no default message
+ }
+}
diff --git a/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLockDefinition.scala b/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLockDefinition.scala
new file mode 100644
index 000000000..d8c180d89
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/serverobject/locks/IFFLockDefinition.scala
@@ -0,0 +1,12 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.serverobject.locks
+
+import net.psforever.objects.definition.ObjectDefinition
+
+/**
+ * The definition for any `IFFLock`.
+ * Object Id 451 is a generic external lock.
+ */
+class IFFLockDefinition extends ObjectDefinition(451) {
+ Name = "iff_lock"
+}
diff --git a/common/src/main/scala/net/psforever/objects/serverobject/terminals/CertTerminalDefinition.scala b/common/src/main/scala/net/psforever/objects/serverobject/terminals/CertTerminalDefinition.scala
new file mode 100644
index 000000000..dbab72d77
--- /dev/null
+++ b/common/src/main/scala/net/psforever/objects/serverobject/terminals/CertTerminalDefinition.scala
@@ -0,0 +1,100 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.objects.serverobject.terminals
+
+import net.psforever.objects.Player
+import net.psforever.packet.game.ItemTransactionMessage
+import net.psforever.types.CertificationType
+
+/**
+ * The definition for any `Terminal` that is of a type "cert_terminal" (certification terminal).
+ * `Learn` and `Sell` `CertificationType` entries, gaining access to different `Equipment` and `Vehicles`.
+ */
+class CertTerminalDefinition extends TerminalDefinition(171) {
+ Name = "cert_terminal"
+
+ /**
+ * The certifications available.
+ * All entries are listed on page (tab) number 0.
+ */
+ private val certificationList : Map[String, (CertificationType.Value, Int)] = Map(
+ "medium_assault" -> (CertificationType.MediumAssault, 2),
+ "reinforced_armo" -> (CertificationType.ReinforcedExoSuit, 3),
+ "quad_all" -> (CertificationType.ATV, 1),
+ "switchblade" -> (CertificationType.Switchblade, 1),
+ "harasser" -> (CertificationType.Harasser, 1),
+ "anti_vehicular" -> (CertificationType.AntiVehicular, 3),
+ "heavy_assault" -> (CertificationType.HeavyAssault, 4),
+ "sniper" -> (CertificationType.Sniping, 3),
+ "special_assault" -> (CertificationType.SpecialAssault, 3),
+ "special_assault_2" -> (CertificationType.EliteAssault, 1),
+ "infiltration_suit" -> (CertificationType.InfiltrationSuit, 2),
+ "max_anti_personnel" -> (CertificationType.AIMAX, 3),
+ "max_anti_vehicular" -> (CertificationType.AVMAX, 3),
+ "max_anti_aircraft" -> (CertificationType.AAMAX, 2),
+ "max_all" -> (CertificationType.UniMAX, 6),
+ "air_cavalry_scout" -> (CertificationType.AirCavalryScout, 3),
+ "air_cavalry_assault" -> (CertificationType.AirCavalryAssault, 2),
+ "air_cavalry_interceptor" -> (CertificationType.AirCavalryInterceptor, 2),
+ "air_support" -> (CertificationType.AirSupport, 3),
+ "gunship" -> (CertificationType.GalaxyGunship, 2),
+ "phantasm" -> (CertificationType.Phantasm, 3),
+ "armored_assault1" -> (CertificationType.ArmoredAssault1, 2),
+ "armored_assault2" -> (CertificationType.ArmoredAssault2, 1),
+ "flail" -> (CertificationType.Flail, 1),
+ "assault_buggy" -> (CertificationType.AssaultBuggy, 3),
+ "ground_support" -> (CertificationType.GroundSupport, 2),
+ "ground_transport" -> (CertificationType.GroundTransport, 2),
+ "light_scout" -> (CertificationType.LightScout, 5),
+ "Repair" -> (CertificationType.Engineering, 3),
+ "combat_engineering" -> (CertificationType.CombatEngineering, 2),
+ "ce_offense" -> (CertificationType.AssaultEngineering, 3),
+ "ce_defense" -> (CertificationType.FortificationEngineering, 3),
+ "ce_advanced" -> (CertificationType.AdvancedEngineering, 5),
+ "Hacking" -> (CertificationType.Hacking, 3),
+ "advanced_hacking" -> (CertificationType.AdvancedHacking, 2),
+ "expert_hacking" -> (CertificationType.ExpertHacking, 2),
+ "virus_hacking" -> (CertificationType.DataCorruption, 3),
+ "electronics_expert" -> (CertificationType.ElectronicsExpert, 4),
+ "Medical" -> (CertificationType.Medical, 3),
+ "advanced_medical" -> (CertificationType.AdvancedMedical, 2)
+ //TODO bfr certification entries
+ )
+
+ /**
+ * Process a `TransactionType.Learn` action by the user.
+ * @param player the player
+ * @param msg the original packet carrying the request
+ * @return an actionable message that explains how to process the request
+ */
+ def Buy(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange = { //Learn
+ certificationList.get(msg.item_name) match {
+ case Some((cert, cost)) =>
+ Terminal.LearnCertification(cert, cost)
+ case None =>
+ Terminal.NoDeal()
+ }
+ }
+
+ /**
+ * Process a `TransactionType.Sell` action by the user.
+ * @param player the player
+ * @param msg the original packet carrying the request
+ * @return an actionable message that explains how to process the request
+ */
+ def Sell(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange = {
+ certificationList.get(msg.item_name) match {
+ case Some((cert, cost)) =>
+ Terminal.SellCertification(cert, cost)
+ case None =>
+ Terminal.NoDeal()
+ }
+ }
+
+ /**
+ * This action is not supported by this type of `Terminal`.
+ * @param player the player
+ * @param msg the original packet carrying the request
+ * @return `Terminal.NoEvent` always
+ */
+ def Loadout(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange = Terminal.NoDeal()
+}
diff --git a/common/src/main/scala/net/psforever/objects/terminals/OrderTerminalDefinition.scala b/common/src/main/scala/net/psforever/objects/serverobject/terminals/OrderTerminalDefinition.scala
similarity index 90%
rename from common/src/main/scala/net/psforever/objects/terminals/OrderTerminalDefinition.scala
rename to common/src/main/scala/net/psforever/objects/serverobject/terminals/OrderTerminalDefinition.scala
index ee431ff77..cea418f0f 100644
--- a/common/src/main/scala/net/psforever/objects/terminals/OrderTerminalDefinition.scala
+++ b/common/src/main/scala/net/psforever/objects/serverobject/terminals/OrderTerminalDefinition.scala
@@ -1,5 +1,5 @@
// Copyright (c) 2017 PSForever
-package net.psforever.objects.terminals
+package net.psforever.objects.serverobject.terminals
import net.psforever.objects.InfantryLoadout.Simplification
import net.psforever.objects.{Player, Tool}
@@ -10,6 +10,11 @@ import net.psforever.packet.game.ItemTransactionMessage
import scala.annotation.switch
+/**
+ * The definition for any `Terminal` that is of a type "order_terminal".
+ * `Buy` and `Sell` `Equipment` items and `AmmoBox` items.
+ * Change `ExoSuitType` and retrieve `InfantryLoadout` entries.
+ */
class OrderTerminalDefinition extends TerminalDefinition(612) {
Name = "order_terminal"
@@ -21,10 +26,10 @@ class OrderTerminalDefinition extends TerminalDefinition(612) {
/**
* Process a `TransactionType.Buy` action by the user.
+ * Either attempt to purchase equipment or attempt to switch directly to a different exo-suit.
* @param player the player
* @param msg the original packet carrying the request
- * @return an actionable message that explains how to process the request;
- * either you attempt to purchase equipment or attempt to switch directly to a different exo-suit
+ * @return an actionable message that explains how to process the request
*/
def Buy(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange = {
(msg.item_page : @switch) match {
@@ -64,6 +69,7 @@ class OrderTerminalDefinition extends TerminalDefinition(612) {
/**
* Process a `TransactionType.Sell` action by the user.
* There is no specific `order_terminal` tab associated with this action.
+ * Additionally, the equipment to be sold ia almost always in the player's `FreeHand` slot.
* Selling `Equipment` is always permitted.
* @param player the player
* @param msg the original packet carrying the request
@@ -81,7 +87,7 @@ class OrderTerminalDefinition extends TerminalDefinition(612) {
* @param msg the original packet carrying the request
* @return an actionable message that explains how to process the request
*/
- def InfantryLoadout(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange = {
+ def Loadout(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange = {
if(msg.item_page == 4) { //Favorites tab
player.LoadLoadout(msg.unk1) match {
case Some(loadout) =>
diff --git a/common/src/main/scala/net/psforever/objects/terminals/Terminal.scala b/common/src/main/scala/net/psforever/objects/serverobject/terminals/Terminal.scala
similarity index 61%
rename from common/src/main/scala/net/psforever/objects/terminals/Terminal.scala
rename to common/src/main/scala/net/psforever/objects/serverobject/terminals/Terminal.scala
index 81f464fe6..57532c54c 100644
--- a/common/src/main/scala/net/psforever/objects/terminals/Terminal.scala
+++ b/common/src/main/scala/net/psforever/objects/serverobject/terminals/Terminal.scala
@@ -1,55 +1,59 @@
// Copyright (c) 2017 PSForever
-package net.psforever.objects.terminals
+package net.psforever.objects.serverobject.terminals
-import akka.actor.{ActorContext, ActorRef, Props}
-import net.psforever.objects.{PlanetSideGameObject, Player}
+import net.psforever.objects.Player
import net.psforever.objects.equipment.Equipment
import net.psforever.objects.inventory.InventoryItem
-import net.psforever.packet.game.ItemTransactionMessage
-import net.psforever.types.{ExoSuitType, PlanetSideEmpire, TransactionType}
+import net.psforever.objects.serverobject.PlanetSideServerObject
+import net.psforever.packet.game.{ItemTransactionMessage, PlanetSideGUID}
+import net.psforever.types.{ExoSuitType, TransactionType, Vector3}
/**
- * na
+ * A structure-owned server object that is a "terminal" that can be accessed for amenities and services.
* @param tdef the `ObjectDefinition` that constructs this object and maintains some of its immutable fields
*/
-class Terminal(tdef : TerminalDefinition) extends PlanetSideGameObject {
- /** Internal reference to the `Actor` for this `Terminal`, sets up by this `Terminal`. */
- private var actor = ActorRef.noSender
+class Terminal(tdef : TerminalDefinition) extends PlanetSideServerObject {
+ /**
+ * An entry that maintains a reference to the `Player`, and the player's GUID and location when the message was received.
+ */
+ private var hackedBy : Option[(Player, PlanetSideGUID, Vector3)] = None
+
+ def HackedBy : Option[(Player, PlanetSideGUID, Vector3)] = hackedBy
+
+ def HackedBy_=(agent : Player) : Option[(Player, PlanetSideGUID, Vector3)] = HackedBy_=(Some(agent))
/**
- * Get access to the internal `TerminalControl` `Actor` for this `Terminal`.
- * If called for the first time, create the said `Actor`.
- * Must be called only after the globally unique identifier has been set.
- * @param context the `ActorContext` under which this `Terminal`'s `Actor` will be created
- * @return the `Terminal`'s `Actor`
+ * Set the hack state of this object by recording important information about the player that caused it.
+ * Set the hack state if there is no current hack state.
+ * Override the hack state with a new hack state if the new user has different faction affiliation.
+ * @param agent a `Player`, or no player
+ * @return the player hack entry
*/
- def Actor(implicit context : ActorContext) : ActorRef = {
- if(actor == ActorRef.noSender) {
- actor = context.actorOf(Props(classOf[TerminalControl], this), s"${tdef.Name}_${GUID.guid}")
+ def HackedBy_=(agent : Option[Player]) : Option[(Player, PlanetSideGUID, Vector3)] = {
+ hackedBy match {
+ case None =>
+ //set the hack state if there is no current hack state
+ if(agent.isDefined) {
+ hackedBy = Some(agent.get, agent.get.GUID, agent.get.Position)
+ }
+ case Some(_) =>
+ //clear the hack state
+ if(agent.isEmpty) {
+ hackedBy = None
+ }
+ //override the hack state with a new hack state if the new user has different faction affiliation
+ else if(agent.get.Faction != hackedBy.get._1.Faction) {
+ hackedBy = Some(agent.get, agent.get.GUID, agent.get.Position)
+ }
}
- actor
+ HackedBy
}
- //the following fields and related methods are neither finalized no integrated; GOTO Request
- private var faction : PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL
- private var hackedBy : Option[PlanetSideEmpire.Value] = None
+ //the following fields and related methods are neither finalized nor integrated; GOTO Request
private var health : Int = 100 //TODO not real health value
- def Faction : PlanetSideEmpire.Value = faction
-
- def HackedBy : Option[PlanetSideEmpire.Value] = hackedBy
-
def Health : Int = health
- def Convert(toFaction : PlanetSideEmpire.Value) : Unit = {
- hackedBy = None
- faction = toFaction
- }
-
- def HackedBy(toFaction : Option[PlanetSideEmpire.Value]) : Unit = {
- hackedBy = if(toFaction.contains(faction)) { None } else { toFaction }
- }
-
def Damaged(dam : Int) : Unit = {
health = Math.max(0, Health - dam)
}
@@ -66,14 +70,14 @@ class Terminal(tdef : TerminalDefinition) extends PlanetSideGameObject {
*/
def Request(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange = {
msg.transaction_type match {
- case TransactionType.Buy =>
+ case TransactionType.Buy | TransactionType.Learn =>
tdef.Buy(player, msg)
case TransactionType.Sell =>
tdef.Sell(player, msg)
case TransactionType.InfantryLoadout =>
- tdef.InfantryLoadout(player, msg)
+ tdef.Loadout(player, msg)
case _ =>
Terminal.NoDeal()
@@ -132,6 +136,12 @@ object Terminal {
*/
//TODO if there are exceptions, find them
final case class SellEquipment() extends Exchange
+
+ import net.psforever.types.CertificationType
+ final case class LearnCertification(cert : CertificationType.Value, cost : Int) extends Exchange
+
+ final case class SellCertification(cert : CertificationType.Value, cost : Int) extends Exchange
+
/**
* Recover a former exo-suit and `Equipment` configuration that the `Player` possessed.
* A result of a processed request.
@@ -142,14 +152,11 @@ object Terminal {
*/
final case class InfantryLoadout(exosuit : ExoSuitType.Value, subtype : Int = 0, holsters : List[InventoryItem], inventory : List[InventoryItem]) extends Exchange
+ /**
+ * Overloaded constructor.
+ * @param tdef the `ObjectDefinition` that constructs this object and maintains some of its immutable fields
+ */
def apply(tdef : TerminalDefinition) : Terminal = {
new Terminal(tdef)
}
-
- import net.psforever.packet.game.PlanetSideGUID
- def apply(guid : PlanetSideGUID, tdef : TerminalDefinition) : Terminal = {
- val obj = new Terminal(tdef)
- obj.GUID = guid
- obj
- }
}
diff --git a/common/src/main/scala/net/psforever/objects/terminals/TerminalControl.scala b/common/src/main/scala/net/psforever/objects/serverobject/terminals/TerminalControl.scala
similarity index 52%
rename from common/src/main/scala/net/psforever/objects/terminals/TerminalControl.scala
rename to common/src/main/scala/net/psforever/objects/serverobject/terminals/TerminalControl.scala
index 7e9a71cfb..c74ada798 100644
--- a/common/src/main/scala/net/psforever/objects/terminals/TerminalControl.scala
+++ b/common/src/main/scala/net/psforever/objects/serverobject/terminals/TerminalControl.scala
@@ -1,12 +1,10 @@
// Copyright (c) 2017 PSForever
-package net.psforever.objects.terminals
+package net.psforever.objects.serverobject.terminals
import akka.actor.Actor
/**
- * An `Actor` that handles messages being dispatched to a specific `Terminal`.
- *
- * For now, the only important message being managed is `Terminal.Request`.
+ * An `Actor` that handles messages being dispatched to a specific `Terminal`.
* @param term the `Terminal` object being governed
*/
class TerminalControl(term : Terminal) extends Actor {
@@ -14,18 +12,6 @@ class TerminalControl(term : Terminal) extends Actor {
case Terminal.Request(player, msg) =>
sender ! Terminal.TerminalMessage(player, msg, term.Request(player, msg))
- case TemporaryTerminalMessages.Convert(fact) =>
- term.Convert(fact)
-
- case TemporaryTerminalMessages.Hacked(fact) =>
- term.HackedBy(fact)
-
- case TemporaryTerminalMessages.Damaged(dam) =>
- term.Damaged(dam)
-
- case TemporaryTerminalMessages.Repaired(rep) =>
- term.Repair(rep)
-
case _ =>
sender ! Terminal.NoDeal()
}
diff --git a/common/src/main/scala/net/psforever/objects/terminals/TerminalDefinition.scala b/common/src/main/scala/net/psforever/objects/serverobject/terminals/TerminalDefinition.scala
similarity index 95%
rename from common/src/main/scala/net/psforever/objects/terminals/TerminalDefinition.scala
rename to common/src/main/scala/net/psforever/objects/serverobject/terminals/TerminalDefinition.scala
index a82fb739d..5cf8ff3d4 100644
--- a/common/src/main/scala/net/psforever/objects/terminals/TerminalDefinition.scala
+++ b/common/src/main/scala/net/psforever/objects/serverobject/terminals/TerminalDefinition.scala
@@ -1,5 +1,5 @@
// Copyright (c) 2017 PSForever
-package net.psforever.objects.terminals
+package net.psforever.objects.serverobject.terminals
import net.psforever.objects._
import net.psforever.objects.definition._
@@ -7,8 +7,6 @@ import net.psforever.objects.equipment.Equipment
import net.psforever.packet.game.ItemTransactionMessage
import net.psforever.types.ExoSuitType
-import scala.collection.immutable.HashMap
-
/**
* The definition for any `Terminal`.
* @param objectId the object's identifier number
@@ -17,7 +15,7 @@ abstract class TerminalDefinition(objectId : Int) extends ObjectDefinition(objec
Name = "terminal"
/**
- * The unimplemented functionality for this `Terminal`'s `TransactionType.Buy` activity.
+ * The unimplemented functionality for this `Terminal`'s `TransactionType.Buy` and `TransactionType.Learn` activity.
*/
def Buy(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange
@@ -29,7 +27,7 @@ abstract class TerminalDefinition(objectId : Int) extends ObjectDefinition(objec
/**
* The unimplemented functionality for this `Terminal`'s `TransactionType.InfantryLoadout` activity.
*/
- def InfantryLoadout(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange
+ def Loadout(player : Player, msg : ItemTransactionMessage) : Terminal.Exchange
/**
* A `Map` of information for changing exo-suits.
@@ -49,7 +47,7 @@ abstract class TerminalDefinition(objectId : Int) extends ObjectDefinition(objec
* key - an identification string sent by the client
* value - a curried function that builds the object
*/
- protected val infantryAmmunition : HashMap[String, ()=>Equipment] = HashMap(
+ protected val infantryAmmunition : Map[String, ()=>Equipment] = Map(
"9mmbullet" -> MakeAmmoBox(bullet_9mm),
"9mmbullet_AP" -> MakeAmmoBox(bullet_9mm_AP),
"shotgun_shell" -> MakeAmmoBox(shotgun_shell),
@@ -75,7 +73,7 @@ abstract class TerminalDefinition(objectId : Int) extends ObjectDefinition(objec
* key - an identification string sent by the client
* value - a curried function that builds the object
*/
- protected val supportAmmunition : HashMap[String, ()=>Equipment] = HashMap(
+ protected val supportAmmunition : Map[String, ()=>Equipment] = Map(
"health_canister" -> MakeAmmoBox(health_canister),
"armor_canister" -> MakeAmmoBox(armor_canister),
"upgrade_canister" -> MakeAmmoBox(upgrade_canister)
@@ -86,7 +84,7 @@ abstract class TerminalDefinition(objectId : Int) extends ObjectDefinition(objec
* key - an identification string sent by the client
* value - a curried function that builds the object
*/
- protected val vehicleAmmunition : HashMap[String, ()=>Equipment] = HashMap(
+ protected val vehicleAmmunition : Map[String, ()=>Equipment] = Map(
"35mmbullet" -> MakeAmmoBox(bullet_35mm),
"hellfire_ammo" -> MakeAmmoBox(hellfire_ammo),
"liberator_bomb" -> MakeAmmoBox(liberator_bomb),
@@ -129,7 +127,7 @@ abstract class TerminalDefinition(objectId : Int) extends ObjectDefinition(objec
* key - an identification string sent by the client
* value - a curried function that builds the object
*/
- protected val infantryWeapons : HashMap[String, ()=>Equipment] = HashMap(
+ protected val infantryWeapons : Map[String, ()=>Equipment] = Map(
"ilc9" -> MakeTool(ilc9, bullet_9mm),
"repeater" -> MakeTool(repeater, bullet_9mm),
"isp" -> MakeTool(isp, shotgun_shell), //amp
@@ -173,7 +171,7 @@ abstract class TerminalDefinition(objectId : Int) extends ObjectDefinition(objec
* key - an identification string sent by the client
* value - a curried function that builds the object
*/
- protected val supportWeapons : HashMap[String, ()=>Equipment] = HashMap(
+ protected val supportWeapons : Map[String, ()=>Equipment] = Map(
"medkit" -> MakeKit(medkit),
"super_medkit" -> MakeKit(super_medkit),
"super_armorkit" -> MakeKit(super_armorkit),
diff --git a/common/src/main/scala/net/psforever/objects/terminals/TemporaryTerminalMessages.scala b/common/src/main/scala/net/psforever/objects/terminals/TemporaryTerminalMessages.scala
deleted file mode 100644
index 85ae33758..000000000
--- a/common/src/main/scala/net/psforever/objects/terminals/TemporaryTerminalMessages.scala
+++ /dev/null
@@ -1,13 +0,0 @@
-// Copyright (c) 2017 PSForever
-package net.psforever.objects.terminals
-
-import net.psforever.types.PlanetSideEmpire
-
-//temporary location for these temporary messages
-object TemporaryTerminalMessages {
- //TODO send original packets along with these messages
- final case class Convert(faction : PlanetSideEmpire.Value)
- final case class Hacked(faction : Option[PlanetSideEmpire.Value])
- final case class Damaged(dm : Int)
- final case class Repaired(rep : Int)
-}
diff --git a/common/src/main/scala/net/psforever/objects/zones/Zone.scala b/common/src/main/scala/net/psforever/objects/zones/Zone.scala
index 21497a74d..68415c50a 100644
--- a/common/src/main/scala/net/psforever/objects/zones/Zone.scala
+++ b/common/src/main/scala/net/psforever/objects/zones/Zone.scala
@@ -2,10 +2,11 @@
package net.psforever.objects.zones
import akka.actor.{ActorContext, ActorRef, Props}
+import net.psforever.objects.serverobject.doors.Base
import net.psforever.objects.{PlanetSideGameObject, Player}
import net.psforever.objects.equipment.Equipment
import net.psforever.objects.guid.NumberPoolHub
-import net.psforever.objects.guid.actor.{NumberPoolAccessorActor, NumberPoolActor}
+import net.psforever.objects.guid.actor.UniqueNumberSystem
import net.psforever.objects.guid.selector.RandomSelector
import net.psforever.objects.guid.source.LimitedNumberSource
import net.psforever.packet.GamePacket
@@ -41,11 +42,15 @@ class Zone(private val zoneId : String, zoneMap : ZoneMap, zoneNumber : Int) {
private var accessor : ActorRef = ActorRef.noSender
/** The basic support structure for the globally unique number system used by this `Zone`. */
private var guid : NumberPoolHub = new NumberPoolHub(new LimitedNumberSource(65536))
+ guid.AddPool("environment", (0 to 2000).toList)
+ guid.AddPool("dynamic", (2001 to 10000).toList).Selector = new RandomSelector //TODO unlump pools later; do not make too big
/** A synchronized `List` of items (`Equipment`) dropped by players on the ground and can be collected again. */
private val equipmentOnGround : ListBuffer[Equipment] = ListBuffer[Equipment]()
/** Used by the `Zone` to coordinate `Equipment` dropping and collection requests. */
private var ground : ActorRef = ActorRef.noSender
+ private var bases : List[Base] = List()
+
/**
* Establish the basic accessible conditions necessary for a functional `Zone`.
*
@@ -60,18 +65,15 @@ class Zone(private val zoneId : String, zoneMap : ZoneMap, zoneNumber : Int) {
*/
def Init(implicit context : ActorContext) : Unit = {
if(accessor == ActorRef.noSender) {
- //TODO wrong initialization for GUID
- implicit val guid = this.guid
- //passed into builderObject.Build implicitly
- val pool = guid.AddPool("pool", (200 to 1000).toList)
- pool.Selector = new RandomSelector
- val poolActor = context.actorOf(Props(classOf[NumberPoolActor], pool), name = s"$Id-poolActor")
- accessor = context.actorOf(Props(classOf[NumberPoolAccessorActor], guid, pool, poolActor), s"$Id-accessor")
+ implicit val guid : NumberPoolHub = this.guid //passed into builderObject.Build implicitly
+ accessor = context.actorOf(Props(classOf[UniqueNumberSystem], guid, UniqueNumberSystem.AllocateNumberPoolActors(guid)), s"$Id-uns")
ground = context.actorOf(Props(classOf[ZoneGroundActor], equipmentOnGround), s"$Id-ground")
Map.LocalObjects.foreach({ builderObject =>
builderObject.Build
})
+
+ MakeBases(Map.LocalBases)
}
}
@@ -172,6 +174,15 @@ class Zone(private val zoneId : String, zoneMap : ZoneMap, zoneNumber : Int) {
*/
def Ground : ActorRef = ground
+ def MakeBases(num : Int) : List[Base] = {
+ bases = (0 to num).map(id => new Base(id)).toList
+ bases
+ }
+
+ def Base(id : Int) : Option[Base] = {
+ bases.lift(id)
+ }
+
/**
* Provide bulk correspondence on all map entities that can be composed into packet messages and reported to a client.
* These messages are sent in this fashion at the time of joining the server:
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 811b6ef9f..022aa53d7 100644
--- a/common/src/main/scala/net/psforever/objects/zones/ZoneActor.scala
+++ b/common/src/main/scala/net/psforever/objects/zones/ZoneActor.scala
@@ -2,6 +2,7 @@
package net.psforever.objects.zones
import akka.actor.Actor
+import net.psforever.objects.serverobject.locks.IFFLock
/**
* na
@@ -13,8 +14,48 @@ class ZoneActor(zone : Zone) extends Actor {
def receive : Receive = {
case Zone.Init() =>
zone.Init
+ ZoneSetupCheck()
case msg =>
log.warn(s"Received unexpected message - $msg")
}
+
+ def ZoneSetupCheck(): Unit = {
+ def guid(id : Int) = zone.GUID(id)
+ val map = zone.Map
+ val slog = org.log4s.getLogger(s"zone/${zone.Id}/sanity")
+
+ //check base to object associations
+ map.ObjectToBase.foreach({ case((object_guid, base_id)) =>
+ if(zone.Base(base_id).isEmpty) {
+ slog.error(s"expected a base #$base_id")
+ }
+ if(guid(object_guid).isEmpty) {
+ slog.error(s"expected object id $object_guid to exist, but it did not")
+ }
+ })
+
+ //check door to locks association
+ import net.psforever.objects.serverobject.doors.Door
+ map.DoorToLock.foreach({ case((door_guid, lock_guid)) =>
+ try {
+ if(!guid(door_guid).get.isInstanceOf[Door]) {
+ slog.error(s"expected id $door_guid to be a door, but it was not")
+ }
+ }
+ catch {
+ case _ : Exception =>
+ slog.error(s"expected a door, but looking for uninitialized object $door_guid")
+ }
+ try {
+ if(!guid(lock_guid).get.isInstanceOf[IFFLock]) {
+ 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, but looking for uninitialized object $lock_guid")
+ }
+ })
+ }
}
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 b1cf2e9a3..96cc09284 100644
--- a/common/src/main/scala/net/psforever/objects/zones/ZoneMap.scala
+++ b/common/src/main/scala/net/psforever/objects/zones/ZoneMap.scala
@@ -1,6 +1,8 @@
// Copyright (c) 2017 PSForever
package net.psforever.objects.zones
+import net.psforever.objects.serverobject.builders.ServerObjectBuilder
+
/**
* The fixed instantiation and relation of a series of server objects.
*
@@ -12,32 +14,55 @@ package net.psforever.objects.zones
* Use it as a blueprint.
*
* The "training zones" are the best example of the difference between a `ZoneMap` and a `Zone.`
+ * ("Course" will be used as an unofficial location and layout descriptor.)
* `tzdrtr` is the Terran Republic driving course.
* `tzdrvs` is the Vanu Sovereignty driving course.
- * While each course can have different objects and object states (`Zone`),
- * both courses have the same basic server objects because they are built from the same blueprint (`ZoneMap`).
+ * While each course can have different objects and object states, i.e., a `Zone`,
+ * both of these courses utilize the same basic server object layout because they are built from the same blueprint, i.e., a `ZoneMap`.
* @param name the privileged name that can be used as the first parameter in the packet `LoadMapMessage`
* @see `ServerObjectBuilder`
* `LoadMapMessage`
*/
class ZoneMap(private val name : String) {
- private var localObjects : List[ServerObjectBuilder] = List()
+ private var localObjects : List[ServerObjectBuilder[_]] = List()
+ private var linkDoorLock : Map[Int, Int] = Map()
+ private var linkObjectBase : Map[Int, Int] = Map()
+ private var numBases : Int = 0
def Name : String = name
- /**
- * Append the builder for a server object to the list of builders known to this `ZoneMap`.
- * @param obj the builder for a server object
- */
- def LocalObject(obj : ServerObjectBuilder) : Unit = {
- localObjects = localObjects :+ obj
- }
-
/**
* The list of all server object builder wrappers that have been assigned to this `ZoneMap`.
* @return the `List` of all `ServerObjectBuilders` known to this `ZoneMap`
*/
- def LocalObjects : List[ServerObjectBuilder] = {
+ def LocalObjects : List[ServerObjectBuilder[_]] = {
localObjects
}
+
+ /**
+ * Append the builder for a server object to the list of builders known to this `ZoneMap`.
+ * @param obj the builder for a server object
+ */
+ def LocalObject(obj : ServerObjectBuilder[_]) : Unit = {
+ localObjects = localObjects :+ obj
+ }
+
+ def LocalBases : Int = numBases
+
+ def LocalBases_=(num : Int) : Int = {
+ numBases = math.max(0, num)
+ LocalBases
+ }
+
+ def ObjectToBase : Map[Int, Int] = linkObjectBase
+
+ def ObjectToBase(object_guid : Int, base_id : Int) : Unit = {
+ linkObjectBase = linkObjectBase ++ Map(object_guid -> base_id)
+ }
+
+ def DoorToLock : Map[Int, Int] = linkDoorLock
+
+ def DoorToLock(door_guid : Int, lock_guid : Int) = {
+ linkDoorLock = linkDoorLock ++ Map(door_guid -> lock_guid)
+ }
}
diff --git a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
index 27a300a20..bec753054 100644
--- a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
+++ b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
@@ -358,8 +358,8 @@ object GamePacketOpcode extends Enumeration {
// OPCODES 0x20-2f
case 0x20 => noDecoder(UnknownMessage32)
- case 0x21 => noDecoder(ActionProgressMessage)
- case 0x22 => noDecoder(ActionCancelMessage)
+ case 0x21 => game.ActionProgressMessage.decode
+ case 0x22 => game.ActionCancelMessage.decode
case 0x23 => noDecoder(ActionCancelAcknowledgeMessage)
case 0x24 => game.SetEmpireMessage.decode
case 0x25 => game.EmoteMsg.decode
@@ -418,7 +418,7 @@ object GamePacketOpcode extends Enumeration {
case 0x51 => game.TriggerEffectMessage.decode
case 0x52 => game.WeaponDryFireMessage.decode
case 0x53 => noDecoder(DroppodLaunchRequestMessage)
- case 0x54 => noDecoder(HackMessage)
+ case 0x54 => game.HackMessage.decode
case 0x55 => noDecoder(DroppodLaunchResponseMessage)
case 0x56 => noDecoder(GenericObjectActionMessage)
case 0x57 => game.AvatarVehicleTimerMessage.decode
@@ -445,7 +445,7 @@ object GamePacketOpcode extends Enumeration {
case 0x68 => noDecoder(DroppodFreefallingMessage)
case 0x69 => game.AvatarFirstTimeEventMessage.decode
case 0x6a => noDecoder(AggravatedDamageMessage)
- case 0x6b => noDecoder(TriggerSoundMessage)
+ case 0x6b => game.TriggerSoundMessage.decode
case 0x6c => noDecoder(LootItemMessage)
case 0x6d => noDecoder(VehicleSubStateMessage)
case 0x6e => noDecoder(SquadMembershipRequest)
@@ -515,7 +515,7 @@ object GamePacketOpcode extends Enumeration {
case 0xa3 => noDecoder(UplinkResponse)
case 0xa4 => game.WarpgateRequest.decode
case 0xa5 => noDecoder(WarpgateResponse)
- case 0xa6 => noDecoder(DamageWithPositionMessage)
+ case 0xa6 => game.DamageWithPositionMessage.decode
case 0xa7 => game.GenericActionMessage.decode
// 0xa8
case 0xa8 => game.ContinentalLockUpdateMessage.decode
diff --git a/common/src/main/scala/net/psforever/packet/game/ActionCancelMessage.scala b/common/src/main/scala/net/psforever/packet/game/ActionCancelMessage.scala
new file mode 100644
index 000000000..127e2b604
--- /dev/null
+++ b/common/src/main/scala/net/psforever/packet/game/ActionCancelMessage.scala
@@ -0,0 +1,29 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.packet.game
+
+import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
+import scodec.Codec
+import scodec.codecs._
+
+/**
+ * na
+ * @param player_guid na
+ * @param object_guid na
+ * @param unk na
+ */
+final case class ActionCancelMessage(player_guid : PlanetSideGUID,
+ object_guid : PlanetSideGUID,
+ unk : Int)
+ extends PlanetSideGamePacket {
+ type Packet = ActionCancelMessage
+ def opcode = GamePacketOpcode.ActionCancelMessage
+ def encode = ActionCancelMessage.encode(this)
+}
+
+object ActionCancelMessage extends Marshallable[ActionCancelMessage] {
+ implicit val codec : Codec[ActionCancelMessage] = (
+ ("player_guid" | PlanetSideGUID.codec) ::
+ ("object_guid" | PlanetSideGUID.codec) ::
+ ("unk" | uint4L)
+ ).as[ActionCancelMessage]
+}
diff --git a/common/src/main/scala/net/psforever/packet/game/ActionProgressMessage.scala b/common/src/main/scala/net/psforever/packet/game/ActionProgressMessage.scala
new file mode 100644
index 000000000..f9d3f6d97
--- /dev/null
+++ b/common/src/main/scala/net/psforever/packet/game/ActionProgressMessage.scala
@@ -0,0 +1,24 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.packet.game
+
+import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
+import scodec.Codec
+import scodec.codecs._
+
+/**
+ *
+ */
+final case class ActionProgressMessage(unk1 : Int,
+ unk2 : Long)
+ extends PlanetSideGamePacket {
+ type Packet = ActionProgressMessage
+ def opcode = GamePacketOpcode.ActionProgressMessage
+ def encode = ActionProgressMessage.encode(this)
+}
+
+object ActionProgressMessage extends Marshallable[ActionProgressMessage] {
+ implicit val codec : Codec[ActionProgressMessage] = (
+ ("unk1" | uint4L) ::
+ ("unk2" | uint32L)
+ ).as[ActionProgressMessage]
+}
\ No newline at end of file
diff --git a/common/src/main/scala/net/psforever/packet/game/DamageWithPositionMessage.scala b/common/src/main/scala/net/psforever/packet/game/DamageWithPositionMessage.scala
new file mode 100644
index 000000000..9dfe2c41e
--- /dev/null
+++ b/common/src/main/scala/net/psforever/packet/game/DamageWithPositionMessage.scala
@@ -0,0 +1,33 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.packet.game
+
+import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
+import net.psforever.types.Vector3
+import scodec.Codec
+import scodec.codecs._
+
+/**
+ * Dispatched by the server to indicate a source of damage affecting the player.
+ * Unlike `HitHint` the damage source is defined by an actual coordinate location rather than a physical target.
+ *
+ * The player will be shown a fading, outwards drifting, red tick mark.
+ * The location will indicate a general direction towards the source.
+ * If the option `Game/Show Damage Flash` is set, the player's screen will flash red briefly when a mark is displayed.
+ * @param unk the intensity of the damage tick marks
+ * @param pos the position
+ * @see `HitHint`
+ */
+final case class DamageWithPositionMessage(unk : Int,
+ pos : Vector3)
+ extends PlanetSideGamePacket {
+ type Packet = DamageWithPositionMessage
+ def opcode = GamePacketOpcode.DamageWithPositionMessage
+ def encode = DamageWithPositionMessage.encode(this)
+}
+
+object DamageWithPositionMessage extends Marshallable[DamageWithPositionMessage] {
+ implicit val codec : Codec[DamageWithPositionMessage] = (
+ ("unk" | uint8L) ::
+ ("pos" | Vector3.codec_pos)
+ ).as[DamageWithPositionMessage]
+}
diff --git a/common/src/main/scala/net/psforever/packet/game/HackMessage.scala b/common/src/main/scala/net/psforever/packet/game/HackMessage.scala
new file mode 100644
index 000000000..2225033e3
--- /dev/null
+++ b/common/src/main/scala/net/psforever/packet/game/HackMessage.scala
@@ -0,0 +1,85 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.packet.game
+
+import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
+import scodec.Codec
+import scodec.codecs._
+
+/**
+ * An `Enumeration` of the various states and activities of the hacking process.
+ * These values are closely tied to the condition of the hacking progress bar and/or the condition of the hacked object.
+ *
+ * `Start` initially displays the hacking progress bar.
+ * `Ongoing` is a neutral state that keeps the progress bar displayed while its value updates. (unconfirmed?)
+ * `Finished` disposes of the hacking progress bar. It does not, by itself, mean the hack was successful.
+ * `Hacked` modifies the target of the hack.
+ * `HackCleared` modifies the target of the hack, opposite of `Hacked`.
+ */
+object HackState extends Enumeration {
+ type Type = Value
+
+ val
+ Unknown0,
+ Start,
+ Unknown2,
+ Ongoing,
+ Finished,
+ Unknown5,
+ Hacked,
+ HackCleared
+ = Value
+
+ implicit val codec = PacketHelpers.createEnumerationCodec(this, uint8L)
+}
+
+/**
+ * Dispatched by the server to control the process of hacking.
+ *
+ * Part of the hacking process is regulated by the server while another part of it is automatically reset by the client.
+ * The visibility, update, and closing of the hacking progress bar must be handled manually, for each tick.
+ * When hacking is complete, using the appropriate `HackState` will cue the target to be affected by the hack.
+ * Terminals and door IFF panels will temporarily expose their functionality;
+ * the faction association of vehicles will be converted permanently;
+ * a protracted process of a base conversion will be enacted; etc..
+ * This transfer of faction association occurs to align the target with the faction of the hacking player (as indicated).
+ * The client will select the faction without needing to be explicitly told
+ * and will select the appropriate action to enact upon the target.
+ * Upon the hack's completion, the target on the client will automatically revert back to its original state, if possible.
+ * (It will still be necessary to alert this change from the server's perspective.)
+ * @param unk1 na;
+ * hack type?
+ * @param target_guid the target of the hack
+ * @param player_guid the player
+ * @param progress the amount of progress visible;
+ * visible range is 0 - 100
+ * @param unk5 na;
+ * often a large number;
+ * doesn't seem to be `char_id`?
+ * @param hack_state hack state
+ * @param unk7 na;
+ * usually, 8?
+ */
+final case class HackMessage(unk1 : Int,
+ target_guid : PlanetSideGUID,
+ player_guid : PlanetSideGUID,
+ progress : Int,
+ unk5 : Long,
+ hack_state : HackState.Value,
+ unk7 : Long)
+ extends PlanetSideGamePacket {
+ type Packet = HackMessage
+ def opcode = GamePacketOpcode.HackMessage
+ def encode = HackMessage.encode(this)
+}
+
+object HackMessage extends Marshallable[HackMessage] {
+ implicit val codec : Codec[HackMessage] = (
+ ("unk1" | uint2L) ::
+ ("object_guid" | PlanetSideGUID.codec) ::
+ ("player_guid" | PlanetSideGUID.codec) ::
+ ("progress" | uint8L) ::
+ ("unk5" | uint32L) ::
+ ("hack_state" | HackState.codec) ::
+ ("unk7" | uint32L)
+ ).as[HackMessage]
+}
diff --git a/common/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala b/common/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala
index 125e521de..0b640c685 100644
--- a/common/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala
+++ b/common/src/main/scala/net/psforever/packet/game/PlanetsideAttributeMessage.scala
@@ -20,7 +20,7 @@ import scodec.codecs._
* `17 - BEP. Value seems to be the same as BattleExperienceMessage`
* `18 - CEP.`
* `19 - Anchors. Value is 0 to disengage, 1 to engage.`
- * `24 - Certifications with value :`
+ * `24 - Learn certifications with value :`
* 01 : Medium Assault
* 02 : Heavy Assault
* 03 : Special Assault
@@ -66,6 +66,7 @@ import scodec.codecs._
* 43 : Fortification Engineering
* 44 : Assault Engineering
* 45 : Advanced Engineering (= Fortification Engineering + Assault Engineering) Must have Combat Engineering
+ * `25 - Forget certifications (same order as 24)`
* `29 - Visible ?! That's not the cloaked effect, Maybe for spectator mode ?. Value is 0 to visible, 1 to invisible.`
* `31 - Info under avatar name : 0 = LFS, 1 = Looking For Squad Members`
* `32 - Info under avatar name : 0 = Looking For Squad Members, 1 = LFS`
diff --git a/common/src/main/scala/net/psforever/packet/game/TriggerSoundMessage.scala b/common/src/main/scala/net/psforever/packet/game/TriggerSoundMessage.scala
new file mode 100644
index 000000000..24cc5fa92
--- /dev/null
+++ b/common/src/main/scala/net/psforever/packet/game/TriggerSoundMessage.scala
@@ -0,0 +1,77 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.packet.game
+
+import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
+import net.psforever.types.Vector3
+import scodec.Codec
+import scodec.codecs._
+import shapeless.{::, HNil}
+
+/**
+ * An `Enumeration` of the sounds triggered by this packet.
+ * Twenty-one possible sounds are available for playback.
+ */
+object TriggeredSound extends Enumeration {
+ type Type = Value
+
+ val
+ SpawnInTube,
+ Unknown1,
+ Hack,
+ HackDoor,
+ Unknown4,
+ LockedOut,
+ Unknown6,
+ Unknown7,
+ Unknown8,
+ Unknown9,
+ Unknown10,
+ Unknown11,
+ Unknown12,
+ Unknown13,
+ Unknown14,
+ Unknown15,
+ Unknown16,
+ Unknown17,
+ Unknown18,
+ Unknown19,
+ Unknown20 = Value
+
+ implicit val codec = PacketHelpers.createEnumerationCodec(this, uintL(5))
+}
+
+/**
+ * Dispatched by the server to cause a sound to be played at a certain location in the world.
+ * @param sound the kind of sound
+ * @param pos the location where the sound gets played
+ * @param unk na;
+ * may be radius
+ * @param volume the volume of the sound at the origin (0.0f - 1.0f)
+ */
+final case class TriggerSoundMessage(sound : TriggeredSound.Value,
+ pos : Vector3,
+ unk : Int,
+ volume : Float)
+ extends PlanetSideGamePacket {
+ type Packet = TriggerSoundMessage
+ def opcode = GamePacketOpcode.TriggerSoundMessage
+ def encode = TriggerSoundMessage.encode(this)
+}
+
+object TriggerSoundMessage extends Marshallable[TriggerSoundMessage] {
+ implicit val codec : Codec[TriggerSoundMessage] = (
+ ("sound" | TriggeredSound.codec) ::
+ ("pos" | Vector3.codec_pos) ::
+ ("unk" | uintL(9)) ::
+ ("volume" | uint8L)
+ ).xmap[TriggerSoundMessage] (
+ {
+ case a :: b :: c :: d :: HNil =>
+ TriggerSoundMessage(a, b, c, d.toFloat * 0.0039215689f)
+ },
+ {
+ case TriggerSoundMessage(a, b, c, d) =>
+ a :: b :: c :: (d * 255f).toInt :: HNil
+ }
+ )
+}
diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterData.scala
index a3aa0009e..51d08d63c 100644
--- a/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterData.scala
+++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterData.scala
@@ -41,24 +41,6 @@ object UniformStyle extends Enumeration {
implicit val codec = PacketHelpers.createEnumerationCodec(this, uintL(3))
}
-/**
- * The different cosmetics that a player can apply to their model's head.
- *
- * The player gets the ability to apply these minor modifications at battle rank twenty-four, just one rank before the third uniform upgrade.
- * @param no_helmet removes the current helmet on the reinforced exo-suit and the agile exo-suit;
- * all other cosmetics require `no_helmet` to be `true` before they can be seen
- * @param beret player dons a beret
- * @param sunglasses player dons sunglasses
- * @param earpiece player dons an earpiece on the left
- * @param brimmed_cap player dons a cap;
- * the cap overrides the beret, if both are selected
- */
-final case class Cosmetics(no_helmet : Boolean,
- beret : Boolean,
- sunglasses : Boolean,
- earpiece : Boolean,
- brimmed_cap : Boolean)
-
/**
* A part of a representation of the avatar portion of `ObjectCreateMessage` packet data.
* This densely-packed information outlines most of the specifics of depicting some other character.
@@ -118,7 +100,7 @@ final case class CharacterData(appearance : CharacterAppearanceData,
//factor guard bool values into the base size, not its corresponding optional field
val appearanceSize : Long = appearance.bitsize
val effectsSize : Long = if(implant_effects.isDefined) { 4L } else { 0L }
- val cosmeticsSize : Long = if(cosmetics.isDefined) { 5L } else { 0L }
+ val cosmeticsSize : Long = if(cosmetics.isDefined) { cosmetics.get.bitsize } else { 0L }
val inventorySize : Long = if(inventory.isDefined) { inventory.get.bitsize } else { 0L }
32L + appearanceSize + effectsSize + cosmeticsSize + inventorySize
}
@@ -141,19 +123,6 @@ object CharacterData extends Marshallable[CharacterData] {
def apply(appearance : CharacterAppearanceData, health : Int, armor : Int, uniform : UniformStyle.Value, cr : Int, implant_effects : Option[ImplantEffects.Value], cosmetics : Option[Cosmetics], inv : InventoryData, drawn_slot : DrawnSlot.Value) : CharacterData =
new CharacterData(appearance, health, armor, uniform, cr, implant_effects, cosmetics, Some(inv), drawn_slot)
- /**
- * Check for the bit flags for the cosmetic items.
- * These flags are only valid if the player has acquired their third uniform upgrade.
- * @see `UniformStyle.ThirdUpgrade`
- */
- private val cosmeticsCodec : Codec[Cosmetics] = (
- ("no_helmet" | bool) ::
- ("beret" | bool) ::
- ("sunglasses" | bool) ::
- ("earpiece" | bool) ::
- ("brimmed_cap" | bool)
- ).as[Cosmetics]
-
implicit val codec : Codec[CharacterData] = (
("app" | CharacterAppearanceData.codec) ::
("health" | uint8L) :: //dead state when health == 0
@@ -163,7 +132,7 @@ object CharacterData extends Marshallable[CharacterData] {
("command_rank" | uintL(3)) ::
bool :: //stream misalignment when != 1
optional(bool, "implant_effects" | ImplantEffects.codec) ::
- conditional(style == UniformStyle.ThirdUpgrade, "cosmetics" | cosmeticsCodec) ::
+ conditional(style == UniformStyle.ThirdUpgrade, "cosmetics" | Cosmetics.codec) ::
optional(bool, "inventory" | InventoryData.codec) ::
("drawn_slot" | DrawnSlot.codec) ::
bool //usually false
diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/Cosmetics.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/Cosmetics.scala
new file mode 100644
index 000000000..da3f85b25
--- /dev/null
+++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/Cosmetics.scala
@@ -0,0 +1,41 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.packet.game.objectcreate
+
+import scodec.codecs._
+import scodec.Codec
+
+/**
+ * The different cosmetics that a player can apply to their character model's head.
+ *
+ * The player gets the ability to apply these minor modifications at battle rank twenty-four, just one rank before the third uniform upgrade.
+ * These flags are only valid if the player has:
+ * for `DetailedCharacterData`, achieved at least battle rank twenty-four (battle experience points greater than 2286230),
+ * or, for `CharacterData`, achieved at least battle rank twenty-five (acquired their third uniform upgrade).
+ * `CharacterData`, as implied, will not display these options until one battle rank after they would have become available.
+ * @param no_helmet removes the current helmet on the reinforced exo-suit and the agile exo-suit;
+ * all other cosmetics require `no_helmet` to be `true` before they can be seen
+ * @param beret player dons a beret
+ * @param sunglasses player dons sunglasses
+ * @param earpiece player dons an earpiece on the left
+ * @param brimmed_cap player dons a cap;
+ * the cap overrides the beret, if both are selected
+ * @see `UniformStyle.ThirdUpgrade`
+ */
+final case class Cosmetics(no_helmet : Boolean,
+ beret : Boolean,
+ sunglasses : Boolean,
+ earpiece : Boolean,
+ brimmed_cap : Boolean
+ ) extends StreamBitSize {
+ override def bitsize : Long = 5L
+}
+
+object Cosmetics {
+ implicit val codec : Codec[Cosmetics] = (
+ ("no_helmet" | bool) ::
+ ("beret" | bool) ::
+ ("sunglasses" | bool) ::
+ ("earpiece" | bool) ::
+ ("brimmed_cap" | bool)
+ ).as[Cosmetics]
+}
diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/DetailedCharacterData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/DetailedCharacterData.scala
index 13ccf6ee0..c2fa2d747 100644
--- a/common/src/main/scala/net/psforever/packet/game/objectcreate/DetailedCharacterData.scala
+++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/DetailedCharacterData.scala
@@ -70,6 +70,9 @@ final case class ImplantEntry(implant : ImplantType.Value,
* @param tutorials the `List` of tutorials completed by this avatar;
* the size field is a 32-bit number;
* the first entry may be padded
+ * @param cosmetics optional decorative features that are added to the player's head model by console/chat commands;
+ * they become available at battle rank 24;
+ * these flags do not exist if they are not applicable
* @param inventory the avatar's inventory
* @param drawn_slot the holster that is initially drawn
* @see `CharacterAppearanceData`
@@ -93,6 +96,7 @@ final case class DetailedCharacterData(appearance : CharacterAppearanceData,
implants : List[ImplantEntry],
firstTimeEvents : List[String],
tutorials : List[String],
+ cosmetics : Option[Cosmetics],
inventory : Option[InventoryData],
drawn_slot : DrawnSlot.Value = DrawnSlot.None
) extends ConstructorData {
@@ -116,13 +120,16 @@ final case class DetailedCharacterData(appearance : CharacterAppearanceData,
for(str <- tutorials) {
tutorialListSize += StreamBitSize.stringBitSize(str)
}
+ val br24 = DetailedCharacterData.isBR24(bep) //character is at least BR24
+ val extraBitSize : Long = if(br24) { 33L } else { 46L }
+ val cosmeticsSize : Long = if(br24) { cosmetics.get.bitsize } else { 0L }
val inventorySize : Long = if(inventory.isDefined) { //inventory
inventory.get.bitsize
}
else {
0L
}
- 649L + appearanceSize + certSize + implantSize + eventListSize + tutorialListSize + inventorySize
+ 603L + appearanceSize + certSize + implantSize + eventListSize + extraBitSize + cosmeticsSize + tutorialListSize + inventorySize
}
}
@@ -145,8 +152,8 @@ object DetailedCharacterData extends Marshallable[DetailedCharacterData] {
* @param drawn_slot the holster that is initially drawn
* @return a `DetailedCharacterData` object
*/
- def apply(appearance : CharacterAppearanceData, bep : Long, cep : Long, healthMax : Int, health : Int, armor : Int, staminaMax : Int, stamina : Int, certs : List[CertificationType.Value], implants : List[ImplantEntry], firstTimeEvents : List[String], tutorials : List[String], inventory : InventoryData, drawn_slot : DrawnSlot.Value) : DetailedCharacterData =
- new DetailedCharacterData(appearance, bep, cep, healthMax, health, armor, 1, 7, 7, staminaMax, stamina, certs, implants, firstTimeEvents, tutorials, Some(inventory), drawn_slot)
+ def apply(appearance : CharacterAppearanceData, bep : Long, cep : Long, healthMax : Int, health : Int, armor : Int, staminaMax : Int, stamina : Int, certs : List[CertificationType.Value], implants : List[ImplantEntry], firstTimeEvents : List[String], tutorials : List[String], cosmetics : Option[Cosmetics], inventory : InventoryData, drawn_slot : DrawnSlot.Value) : DetailedCharacterData =
+ new DetailedCharacterData(appearance, bep, cep, healthMax, health, armor, 1, 7, 7, staminaMax, stamina, certs, implants, firstTimeEvents, tutorials, cosmetics, Some(inventory), drawn_slot)
/**
* `Codec` for entries in the `List` of implants.
@@ -179,7 +186,7 @@ object DetailedCharacterData extends Marshallable[DetailedCharacterData] {
* @param bep battle experience points
* @return the number of accessible implant slots
*/
- private def numberOfImplantSlots(bep : Long) : Int = {
+ def numberOfImplantSlots(bep : Long) : Int = {
if(bep > 754370) { //BR18+
3
}
@@ -209,7 +216,7 @@ object DetailedCharacterData extends Marshallable[DetailedCharacterData] {
implantOffset += entry.bitsize.toInt
})
val resultB : Int = resultA - (implantOffset % 8)
- if(resultB < 0) { 8 - resultB } else { resultB }
+ if(resultB < 0) { 8 + resultB } else { resultB }
}
/**
@@ -269,6 +276,8 @@ object DetailedCharacterData extends Marshallable[DetailedCharacterData] {
}
}
+ def isBR24(bep : Long) : Boolean = bep > 2286230
+
implicit val codec : Codec[DetailedCharacterData] = (
("appearance" | CharacterAppearanceData.codec) >>:~ { app =>
("bep" | uint32L) >>:~ { bep =>
@@ -297,10 +306,14 @@ object DetailedCharacterData extends Marshallable[DetailedCharacterData] {
(("tutorial_length" | uint32L) >>:~ { len2 =>
conditional(len2 > 0, "tutorial_firstEntry" | PacketHelpers.encodedStringAligned(tutPadding(len, len2, implantFieldPadding(implants, CharacterAppearanceData.altModelBit(app))))) ::
("tutorial_list" | PacketHelpers.listOfNSized(len2 - 1, PacketHelpers.encodedString)) ::
- ignore(207) ::
- optional(bool, "inventory" | InventoryData.codec_detailed) ::
- ("drawn_slot" | DrawnSlot.codec) ::
- bool //usually false
+ ignore(160) ::
+ (bool >>:~ { br24 => //BR24+
+ newcodecs.binary_choice(br24, ignore(33), ignore(46)) ::
+ conditional(br24, Cosmetics.codec) ::
+ optional(bool, "inventory" | InventoryData.codec_detailed) ::
+ ("drawn_slot" | DrawnSlot.codec) ::
+ bool //usually false
+ })
})
})
})
@@ -308,14 +321,14 @@ object DetailedCharacterData extends Marshallable[DetailedCharacterData] {
}
).exmap[DetailedCharacterData] (
{
- case app :: bep :: cep :: _ :: hpmax :: hp :: _ :: armor :: _ :: u1 :: _ :: u2 :: u3 :: stamax :: stam :: _ :: certs :: _ :: _ :: implants :: _ :: _ :: fte0 :: fte1 :: _ :: tut0 :: tut1 :: _ :: inv :: drawn :: false :: HNil =>
+ case app :: bep :: cep :: _ :: hpmax :: hp :: _ :: armor :: _ :: u1 :: _ :: u2 :: u3 :: stamax :: stam :: _ :: certs :: _ :: _ :: implants :: _ :: _ :: fte0 :: fte1 :: _ :: tut0 :: tut1 :: _ :: _ :: _ :: cosmetics :: inv :: drawn :: false :: HNil =>
//prepend the displaced first elements to their lists
- val fteList : List[String] = if(fte0.isDefined) { fte0.get +: fte1 } else fte1
- val tutList : List[String] = if(tut0.isDefined) { tut0.get +: tut1 } else tut1
- Attempt.successful(DetailedCharacterData(app, bep, cep, hpmax, hp, armor, u1, u2, u3, stamax, stam, certs, implants, fteList, tutList, inv, drawn))
+ val fteList : List[String] = if(fte0.isDefined) { fte0.get +: fte1 } else { fte1 }
+ val tutList : List[String] = if(tut0.isDefined) { tut0.get +: tut1 } else { tut1 }
+ Attempt.successful(DetailedCharacterData(app, bep, cep, hpmax, hp, armor, u1, u2, u3, stamax, stam, certs, implants, fteList, tutList, cosmetics, inv, drawn))
},
{
- case DetailedCharacterData(app, bep, cep, hpmax, hp, armor, u1, u2, u3, stamax, stam, certs, implants, fteList, tutList, inv, drawn) =>
+ case DetailedCharacterData(app, bep, cep, hpmax, hp, armor, u1, u2, u3, stamax, stam, certs, implants, fteList, tutList, cos, inv, drawn) =>
val implantCapacity : Int = numberOfImplantSlots(bep)
val implantList = if(implants.length > implantCapacity) {
implants.slice(0, implantCapacity)
@@ -334,7 +347,9 @@ object DetailedCharacterData extends Marshallable[DetailedCharacterData] {
case ((f : String) +: (rest : List[String])) => (Some(f), rest)
case Nil => (None, Nil)
}
- Attempt.successful(app :: bep :: cep :: () :: hpmax :: hp :: () :: armor :: () :: u1 :: () :: u2 :: u3 :: stamax :: stam :: () :: certs :: None :: () :: implantList :: () :: fteList.size.toLong :: firstEvent :: fteListCopy :: tutList.size.toLong :: firstTutorial :: tutListCopy :: () :: inv :: drawn :: false :: HNil)
+ val br24 : Boolean = isBR24(bep)
+ val cosmetics : Option[Cosmetics] = if(br24) { cos } else { None }
+ Attempt.successful(app :: bep :: cep :: () :: hpmax :: hp :: () :: armor :: () :: u1 :: () :: u2 :: u3 :: stamax :: stam :: () :: certs :: None :: () :: implantList :: () :: fteList.size.toLong :: firstEvent :: fteListCopy :: tutList.size.toLong :: firstTutorial :: tutListCopy :: () :: br24 :: () :: cosmetics :: inv :: drawn :: false :: HNil)
}
)
}
diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/DetailedREKData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/DetailedREKData.scala
index de0912c72..c4c19e625 100644
--- a/common/src/main/scala/net/psforever/packet/game/objectcreate/DetailedREKData.scala
+++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/DetailedREKData.scala
@@ -11,9 +11,12 @@ import shapeless.{::, HNil}
* This data will help construct the "tool" called a Remote Electronics Kit.
*
* Of note is the first portion of the data which resembles the `DetailedWeaponData` format.
- * @param unk na
+ * @param unk1 na
+ * @param unk2 na
*/
-final case class DetailedREKData(unk : Int) extends ConstructorData {
+final case class DetailedREKData(unk1 : Int,
+ unk2 : Int = 0
+ ) extends ConstructorData {
override def bitsize : Long = 67L
}
@@ -25,17 +28,17 @@ object DetailedREKData extends Marshallable[DetailedREKData] {
uint4L ::
uint16L ::
uint4L ::
- uintL(15)
+ ("unk2" | uintL(15))
).exmap[DetailedREKData] (
{
- case code :: 8 :: 0 :: 2 :: 0 :: 8 :: 0 :: HNil =>
- Attempt.successful(DetailedREKData(code))
+ case code :: 8 :: 0 :: 2 :: 0 :: 8 :: unk2 :: HNil =>
+ Attempt.successful(DetailedREKData(code, unk2))
case _ =>
Attempt.failure(Err("invalid rek data format"))
},
{
- case DetailedREKData(code) =>
- Attempt.successful(code :: 8 :: 0 :: 2 :: 0 :: 8 :: 0 :: HNil)
+ case DetailedREKData(code, unk2) =>
+ Attempt.successful(code :: 8 :: 0 :: 2 :: 0 :: 8 :: unk2 :: HNil)
}
)
}
diff --git a/common/src/main/scala/net/psforever/types/CertificationType.scala b/common/src/main/scala/net/psforever/types/CertificationType.scala
index d06e2d198..ea4ca3ab5 100644
--- a/common/src/main/scala/net/psforever/types/CertificationType.scala
+++ b/common/src/main/scala/net/psforever/types/CertificationType.scala
@@ -30,9 +30,9 @@ object CertificationType extends Enumeration {
AntiVehicular,
Sniping,
EliteAssault,
- AirCalvaryScout,
- AirCalvaryInterceptor,
- AirCalvaryAssault,
+ AirCavalryScout,
+ AirCavalryInterceptor,
+ AirCavalryAssault,
//10
AirSupport,
ATV,
diff --git a/common/src/main/scala/net/psforever/types/ImplantType.scala b/common/src/main/scala/net/psforever/types/ImplantType.scala
index acd47b5b3..d1c96eae3 100644
--- a/common/src/main/scala/net/psforever/types/ImplantType.scala
+++ b/common/src/main/scala/net/psforever/types/ImplantType.scala
@@ -23,7 +23,9 @@ import scodec.codecs._
*/
object ImplantType extends Enumeration {
type Type = Value
- val AdvancedRegen,
+
+ val
+ AdvancedRegen,
Targeting,
AudioAmplifier,
DarklightVision,
diff --git a/common/src/test/scala/game/ActionCancelMessageTest.scala b/common/src/test/scala/game/ActionCancelMessageTest.scala
new file mode 100644
index 000000000..2c8ee068c
--- /dev/null
+++ b/common/src/test/scala/game/ActionCancelMessageTest.scala
@@ -0,0 +1,29 @@
+// Copyright (c) 2017 PSForever
+package game
+
+import org.specs2.mutable._
+import net.psforever.packet._
+import net.psforever.packet.game._
+import scodec.bits._
+
+class ActionCancelMessageTest extends Specification {
+ val string = hex"22 201ee01a10"
+
+ "decode" in {
+ PacketCoding.DecodePacket(string).require match {
+ case ActionCancelMessage(player_guid, object_guid, unk) =>
+ player_guid mustEqual PlanetSideGUID(7712)
+ object_guid mustEqual PlanetSideGUID(6880)
+ unk mustEqual 1
+ case _ =>
+ ko
+ }
+ }
+
+ "encode" in {
+ val msg = ActionCancelMessage(PlanetSideGUID(7712), PlanetSideGUID(6880), 1)
+ val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+ pkt mustEqual string
+ }
+}
diff --git a/common/src/test/scala/game/ActionProgressMessageTest.scala b/common/src/test/scala/game/ActionProgressMessageTest.scala
new file mode 100644
index 000000000..acbf7eb57
--- /dev/null
+++ b/common/src/test/scala/game/ActionProgressMessageTest.scala
@@ -0,0 +1,29 @@
+// Copyright (c) 2017 PSForever
+package game
+
+import org.specs2.mutable._
+import net.psforever.packet._
+import net.psforever.packet.game._
+import net.psforever.types.ExoSuitType
+import scodec.bits._
+
+class ActionProgressMessageTest extends Specification {
+ val string = hex"216000000000"
+
+ "decode" in {
+ PacketCoding.DecodePacket(string).require match {
+ case ActionProgressMessage(unk1, unk2) =>
+ unk1 mustEqual 6
+ unk2 mustEqual 0
+ case _ =>
+ ko
+ }
+ }
+
+ "encode" in {
+ val msg = ActionProgressMessage(6, 0L)
+ val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+ pkt mustEqual string
+ }
+}
diff --git a/common/src/test/scala/game/DamageWithPositionMessageTest.scala b/common/src/test/scala/game/DamageWithPositionMessageTest.scala
new file mode 100644
index 000000000..8da7c35bb
--- /dev/null
+++ b/common/src/test/scala/game/DamageWithPositionMessageTest.scala
@@ -0,0 +1,31 @@
+// Copyright (c) 2017 PSForever
+package game
+
+import net.psforever.types.{MeritCommendation, Vector3}
+import org.specs2.mutable._
+import net.psforever.packet._
+import net.psforever.packet.game._
+import scodec.bits._
+
+class DamageWithPositionMessageTest extends Specification {
+ val string = hex"A6 11 6C2D7 65535 CA16"
+
+ "decode" in {
+ PacketCoding.DecodePacket(string).require match {
+ case DamageWithPositionMessage(unk, pos) =>
+ unk mustEqual 17
+ pos.x mustEqual 3674.8438f
+ pos.y mustEqual 2726.789f
+ pos.z mustEqual 91.15625f
+ case _ =>
+ ko
+ }
+ }
+
+ "encode" in {
+ val msg = DamageWithPositionMessage(17, Vector3(3674.8438f, 2726.789f, 91.15625f))
+ val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+ pkt mustEqual string
+ }
+}
diff --git a/common/src/test/scala/game/HackMessageTest.scala b/common/src/test/scala/game/HackMessageTest.scala
new file mode 100644
index 000000000..469950d12
--- /dev/null
+++ b/common/src/test/scala/game/HackMessageTest.scala
@@ -0,0 +1,33 @@
+// Copyright (c) 2017 PSForever
+package game
+
+import org.specs2.mutable._
+import net.psforever.packet._
+import net.psforever.packet.game._
+import scodec.bits._
+
+class HackMessageTest extends Specification {
+ // Record 62 in PSCap-hack-door-tower.gcap
+ val string = hex"54 000105c3800000202fc04200000000"
+
+ "decode" in {
+ PacketCoding.DecodePacket(string).require match {
+ case HackMessage(unk1, target_guid, player_guid, progress, unk5, hack_state, unk7) =>
+ unk1 mustEqual 0
+ target_guid mustEqual PlanetSideGUID(1024)
+ player_guid mustEqual PlanetSideGUID(3607)
+ progress mustEqual 0
+ unk5 mustEqual 3212836864L
+ hack_state mustEqual HackState.Start
+ unk7 mustEqual 8L
+ case _ =>
+ ko
+ }
+ }
+
+ "encode" in {
+ val msg = HackMessage(0, PlanetSideGUID(1024), PlanetSideGUID(3607), 0, 3212836864L, HackState.Start, 8L)
+ val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+ pkt mustEqual string
+ }
+}
diff --git a/common/src/test/scala/game/ObjectCreateDetailedMessageTest.scala b/common/src/test/scala/game/ObjectCreateDetailedMessageTest.scala
index f61635200..545a5fdb1 100644
--- a/common/src/test/scala/game/ObjectCreateDetailedMessageTest.scala
+++ b/common/src/test/scala/game/ObjectCreateDetailedMessageTest.scala
@@ -3,7 +3,7 @@ package game
import org.specs2.mutable._
import net.psforever.packet._
-import net.psforever.packet.game._
+import net.psforever.packet.game.{ObjectCreateDetailedMessage, _}
import net.psforever.packet.game.objectcreate._
import net.psforever.types._
import scodec.bits._
@@ -21,6 +21,7 @@ class ObjectCreateDetailedMessageTest extends Specification {
val string_rek = hex"18 97000000 2580 6C2 9F05 81 48000002000080000"
val string_boomer_trigger = hex"18 87000000 6304CA8760B 80 C800000200008"
val string_testchar = hex"18 570C0000 BC8 4B00 6C2D7 65535 CA16 0 00 01 34 40 00 0970 49006C006C006C004900490049006C006C006C0049006C0049006C006C0049006C006C006C0049006C006C004900 84 52 70 76 1E 80 80 00 00 00 00 00 3FFFC 0 00 00 00 20 00 00 0F F6 A7 03 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FC 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 90 01 90 00 64 00 00 01 00 7E C8 00 C8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 C0 00 42 C5 46 86 C7 00 00 00 80 00 00 12 40 78 70 65 5F 73 61 6E 63 74 75 61 72 79 5F 68 65 6C 70 90 78 70 65 5F 74 68 5F 66 69 72 65 6D 6F 64 65 73 8B 75 73 65 64 5F 62 65 61 6D 65 72 85 6D 61 70 31 33 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 0A 23 02 60 04 04 40 00 00 10 00 06 02 08 14 D0 08 0C 80 00 02 00 02 6B 4E 00 82 88 00 00 02 00 00 C0 41 C0 9E 01 01 90 00 00 64 00 44 2A 00 10 91 00 00 00 40 00 18 08 38 94 40 20 32 00 00 00 80 19 05 48 02 17 20 00 00 08 00 70 29 80 43 64 00 00 32 00 0E 05 40 08 9C 80 00 06 40 01 C0 AA 01 19 90 00 00 C8 00 3A 15 80 28 72 00 00 19 00 04 0A B8 05 26 40 00 03 20 06 C2 58 00 A7 88 00 00 02 00 00 80 00 00"
+ val string_testchar_br32 = hex"18 2c e0 00 00 bc 84 B0 00 0b ea 00 6c 7d f1 10 00 00 02 40 00 08 60 4b 00 69 00 43 00 6b 00 4a 00 72 00 02 31 3a cc 82 c0 00 00 00 00 00 00 00 00 3e df 42 00 20 00 0e 00 40 43 40 4c 04 00 02 e8 00 00 03 a8 00 00 01 9c 04 00 00 b8 99 84 00 0e 68 28 00 00 00 00 00 00 00 00 00 00 00 00 01 90 01 90 00 c8 00 00 01 00 7e c8 00 5c 00 00 01 29 c1 cc 80 00 00 00 00 00 00 00 00 00 00 00 00 03 c0 00 40 81 01 c4 45 46 86 c8 88 c9 09 4a 4a 80 50 0c 13 00 00 15 00 80 00 48 00 7870655f6f766572686561645f6d6170 8d7870655f776172705f676174658f7870655f666f726d5f6f75746669748c7870655f626c61636b6f7073927870655f636f6d6d616e645f72616e6b5f35927870655f636f6d6d616e645f72616e6b5f33927870655f73616e6374756172795f68656c70927870655f626174746c655f72616e6b5f3133927870655f626174746c655f72616e6b5f3132927870655f626174746c655f72616e6b5f3130927870655f626174746c655f72616e6b5f3134927870655f626174746c655f72616e6b5f3135937870655f6f72626974616c5f73687574746c658c7870655f64726f705f706f64917870655f62696e645f666163696c697479917870655f626174746c655f72616e6b5f33917870655f626174746c655f72616e6b5f35917870655f626174746c655f72616e6b5f348e7870655f6a6f696e5f73717561648e7870655f666f726d5f7371756164927870655f696e7374616e745f616374696f6e917870655f626174746c655f72616e6b5f32937870655f776172705f676174655f7573616765917870655f626174746c655f72616e6b5f38927870655f626174746c655f72616e6b5f3131917870655f626174746c655f72616e6b5f368e7870655f6d61696c5f616c657274927870655f636f6d6d616e645f72616e6b5f31927870655f626174746c655f72616e6b5f3230927870655f626174746c655f72616e6b5f3138927870655f626174746c655f72616e6b5f3139907870655f6a6f696e5f706c61746f6f6e927870655f626174746c655f72616e6b5f3137927870655f626174746c655f72616e6b5f31368f7870655f6a6f696e5f6f7574666974927870655f626174746c655f72616e6b5f3235927870655f626174746c655f72616e6b5f3234927870655f636f6d6d616e645f72616e6b5f34907870655f666f726d5f706c61746f6f6e8c7870655f62696e645f616d73917870655f626174746c655f72616e6b5f39917870655f626174746c655f72616e6b5f378d7870655f74685f726f757465728c7870655f74685f666c61696c8a7870655f74685f616e748a7870655f74685f616d738f7870655f74685f67726f756e645f708c7870655f74685f6169725f708c7870655f74685f686f7665728d7870655f74685f67726f756e648a7870655f74685f626672927870655f74685f61667465726275726e65728a7870655f74685f6169728c7870655f74685f636c6f616b89757365645f6f69637791757365645f616476616e6365645f61636597766973697465645f73706974666972655f74757272657498766973697465645f73706974666972655f636c6f616b656493766973697465645f73706974666972655f616192766973697465645f74616e6b5f7472617073a1766973697465645f706f727461626c655f6d616e6e65645f7475727265745f6e63a1766973697465645f706f727461626c655f6d616e6e65645f7475727265745f74728e757365645f6d61676375747465728f757365645f636861696e626c6164658f757365645f666f726365626c61646593766973697465645f77616c6c5f74757272657498766973697465645f616e6369656e745f7465726d696e616c8b766973697465645f616d738b766973697465645f616e7490766973697465645f64726f707368697091766973697465645f6c6962657261746f7294766973697465645f6c6967687467756e7368697091766973697465645f6c696768746e696e6790766973697465645f6d616772696465728f766973697465645f70726f776c657293766973697465645f71756164737465616c746890766973697465645f736b7967756172649a766973697465645f74687265656d616e686561767962756767799d766973697465645f74776f5f6d616e5f61737361756c745f627567677998766973697465645f74776f6d616e6865617679627567677998766973697465645f74776f6d616e686f766572627567677990766973697465645f76616e67756172648d766973697465645f666c61696c8e766973697465645f726f7574657293766973697465645f737769746368626c6164658e766973697465645f6175726f726193766973697465645f626174746c657761676f6e8c766973697465645f6675727993766973697465645f7175616461737361756c7496766973697465645f67616c6178795f67756e736869708e766973697465645f6170635f74728e766973697465645f6170635f767390766973697465645f6c6f64657374617290766973697465645f7068616e7461736d91766973697465645f7468756e64657265728e766973697465645f6170635f6e638f766973697465645f76756c747572658c766973697465645f7761737090766973697465645f6d6f73717569746f97766973697465645f617068656c696f6e5f666c6967687497766973697465645f617068656c696f6e5f67756e6e657297766973697465645f636f6c6f737375735f666c6967687497766973697465645f636f6c6f737375735f67756e6e657298766973697465645f706572656772696e655f666c6967687498766973697465645f706572656772696e655f67756e6e657289757365645f62616e6b95766973697465645f7265736f757263655f73696c6f9e766973697465645f63657274696669636174696f6e5f7465726d696e616c94766973697465645f6d65645f7465726d696e616c93757365645f6e616e6f5f64697370656e73657295766973697465645f73656e736f725f736869656c649a766973697465645f62726f6164636173745f77617270676174658c757365645f7068616c616e7894757365645f7068616c616e785f6176636f6d626f96757365645f7068616c616e785f666c616b636f6d626f96766973697465645f77617270676174655f736d616c6c91757365645f666c616d657468726f7765729a757365645f616e6369656e745f7475727265745f776561706f6e92766973697465645f4c4c555f736f636b657492757365645f656e657267795f67756e5f6e6397766973697465645f6d656469756d7472616e73706f72749f757365645f617068656c696f6e5f696d6d6f6c6174696f6e5f63616e6e6f6e93757365645f6772656e6164655f706c61736d6193757365645f6772656e6164655f6a616d6d657298766973697465645f736869656c645f67656e657261746f7295766973697465645f6d6f74696f6e5f73656e736f7296766973697465645f6865616c74685f6372797374616c96766973697465645f7265706169725f6372797374616c97766973697465645f76656869636c655f6372797374616c91757365645f6772656e6164655f6672616788757365645f61636598766973697465645f6164765f6d65645f7465726d696e616c8b757365645f6265616d657290757365645f626f6c745f6472697665728b757365645f6379636c65728a757365645f676175737391757365645f68756e7465727365656b657288757365645f6973708b757365645f6c616e6365728b757365645f6c61736865728e757365645f6d61656c7374726f6d8c757365645f70686f656e69788b757365645f70756c7361728d757365645f70756e69736865728e757365645f725f73686f7467756e8d757365645f7261646961746f7288757365645f72656b8d757365645f72657065617465728c757365645f726f636b6c65748c757365645f737472696b65728f757365645f73757070726573736f728c757365645f7468756d7065729c766973697465645f76616e755f636f6e74726f6c5f636f6e736f6c6598766973697465645f636170747572655f7465726d696e616c92757365645f6d696e695f636861696e67756e91757365645f6c617a655f706f696e7465728c757365645f74656c657061648b757365645f7370696b657291757365645f68656176795f736e6970657293757365645f636f6d6d616e645f75706c696e6b8d757365645f66697265626972648e757365645f666c6563686574746594757365645f68656176795f7261696c5f6265616d89757365645f696c63399a766973697465645f67656e657261746f725f7465726d696e616c8e766973697465645f6c6f636b65729a766973697465645f65787465726e616c5f646f6f725f6c6f636b9c766973697465645f6169725f76656869636c655f7465726d696e616c97766973697465645f67616c6178795f7465726d696e616c98766973697465645f696d706c616e745f7465726d696e616c99766973697465645f7365636f6e646172795f6361707475726590757365645f32356d6d5f63616e6e6f6e99757365645f6c6962657261746f725f626f6d6261726469657293766973697465645f7265706169725f73696c6f93766973697465645f76616e755f6d6f64756c6591757365645f666c61696c5f776561706f6e8b757365645f73637974686598766973697465645f7265737061776e5f7465726d696e616c8c757365645f62616c6c67756e92757365645f656e657267795f67756e5f747295757365645f616e6e69766572736172795f67756e6195757365645f616e6e69766572736172795f67756e6294757365645f616e6e69766572736172795f67756e90757365645f37356d6d5f63616e6e6f6e92757365645f6170635f6e635f776561706f6e92757365645f6170635f74725f776561706f6e92757365645f6170635f76735f776561706f6e90757365645f666c75785f63616e6e6f6e9f757365645f617068656c696f6e5f706c61736d615f726f636b65745f706f6491757365645f617068656c696f6e5f7070618c757365645f666c7578706f6494766973697465645f6266725f7465726d696e616c9e757365645f636f6c6f737375735f636c75737465725f626f6d625f706f64a0757365645f636f6c6f737375735f6475616c5f3130306d6d5f63616e6e6f6e7399757365645f636f6c6f737375735f74616e6b5f63616e6e6f6e96766973697465645f656e657267795f6372797374616c9b757365645f68656176795f6772656e6164655f6c61756e6368657298757365645f33356d6d5f726f74617279636861696e67756e8b757365645f6b6174616e6190757365645f33356d6d5f63616e6e6f6e93757365645f7265617665725f776561706f6e7396757365645f6c696768746e696e675f776561706f6e738c757365645f6d65645f61707090757365645f32306d6d5f63616e6e6f6e98766973697465645f6d6f6e6f6c6974685f616d657269736899766973697465645f6d6f6e6f6c6974685f636572797368656e97766973697465645f6d6f6e6f6c6974685f637973736f7297766973697465645f6d6f6e6f6c6974685f6573616d697299766973697465645f6d6f6e6f6c6974685f666f72736572616c99766973697465645f6d6f6e6f6c6974685f697368756e64617298766973697465645f6d6f6e6f6c6974685f7365617268757397766973697465645f6d6f6e6f6c6974685f736f6c73617292757365645f6e635f6865765f66616c636f6e99757365645f6e635f6865765f7363617474657263616e6e6f6e93757365645f6e635f6865765f73706172726f7791757365645f61726d6f725f736970686f6e9f757365645f706572656772696e655f6475616c5f6d616368696e655f67756e9f757365645f706572656772696e655f6475616c5f726f636b65745f706f647399757365645f706572656772696e655f6d65636868616d6d65729e757365645f706572656772696e655f7061727469636c655f63616e6e6f6e96757365645f706572656772696e655f73706172726f7791757365645f3130356d6d5f63616e6e6f6e92757365645f31356d6d5f636861696e67756ea0757365645f70756c7365645f7061727469636c655f616363656c657261746f7293757365645f726f74617279636861696e67756e9f766973697465645f6465636f6e737472756374696f6e5f7465726d696e616c95757365645f736b7967756172645f776561706f6e7391766973697465645f67656e657261746f7291757365645f67617573735f63616e6e6f6e89757365645f7472656b95757365645f76616e67756172645f776561706f6e73a4766973697465645f616e6369656e745f6169725f76656869636c655f7465726d696e616ca2766973697465645f616e6369656e745f65717569706d656e745f7465726d696e616c96766973697465645f6f726465725f7465726d696e616ca7766973697465645f616e6369656e745f67726f756e645f76656869636c655f7465726d696e616c9f766973697465645f67726f756e645f76656869636c655f7465726d696e616c97757365645f76756c747572655f626f6d6261726469657298757365645f76756c747572655f6e6f73655f63616e6e6f6e98757365645f76756c747572655f7461696c5f63616e6e6f6e97757365645f776173705f776561706f6e5f73797374656d91766973697465645f636861726c6965303191766973697465645f636861726c6965303291766973697465645f636861726c6965303391766973697465645f636861726c6965303491766973697465645f636861726c6965303591766973697465645f636861726c6965303691766973697465645f636861726c6965303791766973697465645f636861726c6965303891766973697465645f636861726c6965303996766973697465645f67696e6765726d616e5f6174617298766973697465645f67696e6765726d616e5f646168616b6196766973697465645f67696e6765726d616e5f6876617296766973697465645f67696e6765726d616e5f697a686199766973697465645f67696e6765726d616e5f6a616d7368696498766973697465645f67696e6765726d616e5f6d697468726198766973697465645f67696e6765726d616e5f726173686e7599766973697465645f67696e6765726d616e5f7372616f73686198766973697465645f67696e6765726d616e5f79617a61746195766973697465645f67696e6765726d616e5f7a616c8e766973697465645f736c656430318e766973697465645f736c656430328e766973697465645f736c656430348e766973697465645f736c656430358e766973697465645f736c656430368e766973697465645f736c656430378e766973697465645f736c6564303897766973697465645f736e6f776d616e5f616d657269736898766973697465645f736e6f776d616e5f636572797368656e96766973697465645f736e6f776d616e5f637973736f7296766973697465645f736e6f776d616e5f6573616d697298766973697465645f736e6f776d616e5f666f72736572616c96766973697465645f736e6f776d616e5f686f7373696e98766973697465645f736e6f776d616e5f697368756e64617297766973697465645f736e6f776d616e5f7365617268757396766973697465645f736e6f776d616e5f736f6c736172857567643036857567643035857567643034857567643033857567643032857567643031856d61703939856d61703938856d61703937856d61703936856d61703135856d61703134856d61703131856d61703038856d61703034856d61703035856d61703033856d61703031856d61703036856d61703032856d61703039856d61703037856d617031300300000091747261696e696e675f73746172745f6e638b747261696e696e675f75698c747261696e696e675f6d61700000000000000000000000000000000000000000800000003d0c04d350840240000010000602429660f80c80000c8004200c1b81480000020000c046f18a47019000019000ca4644304900000040001809e6bb052032000008001a84787211200000080003010714889c06400000100320ff0a42e4000001009e95a7342e03200000080003010408c914064000000001198990c4e4000001000060223b9b2180c800000a00081c20c92c800003600414ec172d900000040001808de1284a0320000320008ef1c336b20000078011d830e6f6400000600569c417e2c80000020000c04102502f019000008c00ce31027d99000000400018099e6146203200004b0015a7d44002f720000008000301040c18dc064000023000b1240800636400000100006020e0e92280c80000c800081650c00cfc800006400ce32a1801a59000000400018099e6fc3e03200004b00058b14680463200000080003010742610c064000043000b16c8880916400000100006020e0d01580c80000c8006714e24012cc80000020000c04cf25c190190000258001032e240307900000c8019c74470061b2000000800030133ced8fc0640000960012d9a8d00f0640000010025b9c1401e4c8000002004b6b23c03d1900000040098f585007b3200000080131a58c00f864000001002536f1c01f4c8000002004a64e2a03f190000004015e1b4580873200000080003010711f8a406400000100110a00c010ee400000100006020e2a51380c8000002002218d21021ec80000020000c041c40249019000000400af18a44043f90000004000180838b44760320000008015e38c80088320000008000301071490cc064000001002bc35890110e400000100006020e2052180c800000200221f90d0222c80000020000c041c5e447019000000400442e62e044790000004000180838af032032000000800886d08c089320000008000301071738740640000010011098898112e400000100006020e2361c80c8000002002212a1b0226c80000020000c041c512170190000004004420a32044f900000040001808389104a0320000008008874c8808a3200000080003010715907c06400000100110c0898114e400000100006020e2771a80c800000200578bd13022ac80000020000c041c424330190000004004423848045790000004000180838bfc32032000000801a86506008b320000008000301071030dc06400000100129f68a0117640000010026353110232c8000002004b69438046d90000004015e2887008eb200000080003010715909406400000100350fb8e011de400000100006020e2881980c8000002005786d0f023cc80000020000c041c4cc3b019000000400af1ba1c047b90000004000180838af872032000000800886344408fb20000008000301071620d406400000100110c10b011fe400000100006020e2870d80c800000200578f30c0240c80000020000c041c5863b019000000400442ee300483900000040001808388605e032000000801a86f03c090b200000080003010712a8fc064000001002bc0d858121e400000100006020e2521c80c800000200578b7230244c80000020000c041c49629019000000400d434026048b90000004000180838afc42032000000801a86d864091b200000080003010711989c064000001003508c8c8123e400000100006020e2a82280c8000002006a14f110248c80000020000c041c4be21019000000400af12640049390000004000180838a54720320000008015e33430092b20000008000301071228cc064000001003546e8d432400000100004f34a631139000004001b0834723120000008000204000c2ed0fa1c800000200a8432234a90000004000180952b248a0320000018004024c569d20000008000250a4d0ebc480000020000c04a24bc43019000000c00e0"
"decode (2)" in {
//an invalid bit representation will fail to turn into an object
@@ -146,7 +147,8 @@ class ObjectCreateDetailedMessageTest extends Specification {
parent.get.guid mustEqual PlanetSideGUID(75)
parent.get.slot mustEqual 1
data.isDefined mustEqual true
- data.get.asInstanceOf[DetailedREKData].unk mustEqual 4
+ data.get.asInstanceOf[DetailedREKData].unk1 mustEqual 4
+ data.get.asInstanceOf[DetailedREKData].unk2 mustEqual 0
case _ =>
ko
}
@@ -231,6 +233,7 @@ class ObjectCreateDetailedMessageTest extends Specification {
char.firstTimeEvents(2) mustEqual "used_beamer"
char.firstTimeEvents(3) mustEqual "map13"
char.tutorials.size mustEqual 0
+ char.cosmetics.isDefined mustEqual false
char.inventory.isDefined mustEqual true
val inventory = char.inventory.get.contents
inventory.size mustEqual 10
@@ -303,6 +306,76 @@ class ObjectCreateDetailedMessageTest extends Specification {
}
}
+ "decode (character, BR32)" in {
+ PacketCoding.DecodePacket(string_testchar_br32).require match {
+ case ObjectCreateDetailedMessage(len, cls, guid, parent, data) =>
+ //this test is mainly for an alternate bitstream parsing order
+ //the object produced is massive and most of it is already covered in other tests
+ //only certain details towards the end of the stream will be checked
+ data.isDefined mustEqual true
+ val char = data.get.asInstanceOf[DetailedCharacterData]
+ DetailedCharacterData.isBR24(char.bep) mustEqual true
+ char.certs.size mustEqual 15
+ char.certs.head mustEqual CertificationType.StandardAssault
+ char.certs(14) mustEqual CertificationType.CombatEngineering
+ char.implants.size mustEqual 3
+ char.implants.head.implant mustEqual ImplantType.AudioAmplifier
+ char.implants.head.activation mustEqual None
+ char.implants(1).implant mustEqual ImplantType.Targeting
+ char.implants(1).activation mustEqual None
+ char.implants(2).implant mustEqual ImplantType.Surge
+ char.implants(2).activation mustEqual None
+ char.firstTimeEvents.size mustEqual 298
+ char.firstTimeEvents.head mustEqual "xpe_overhead_map"
+ char.firstTimeEvents(297) mustEqual "map10"
+ char.tutorials.size mustEqual 3
+ char.tutorials.head mustEqual "training_start_nc"
+ char.tutorials(1) mustEqual "training_ui"
+ char.tutorials(2) mustEqual "training_map"
+ char.cosmetics.isDefined mustEqual true
+ char.cosmetics.get.no_helmet mustEqual true
+ char.cosmetics.get.beret mustEqual true
+ char.cosmetics.get.earpiece mustEqual true
+ char.cosmetics.get.sunglasses mustEqual true
+ char.cosmetics.get.brimmed_cap mustEqual false
+ //inventory
+ char.inventory.isDefined mustEqual true
+ char.inventory.get.contents.size mustEqual 12
+ //0
+ char.inventory.get.contents.head.objectClass mustEqual 531
+ char.inventory.get.contents.head.guid mustEqual PlanetSideGUID(4202)
+ char.inventory.get.contents.head.parentSlot mustEqual 0
+ val wep1 = char.inventory.get.contents.head.obj.asInstanceOf[DetailedWeaponData]
+ wep1.unk1 mustEqual 2
+ wep1.unk2 mustEqual 8
+ wep1.ammo.head.objectClass mustEqual 389
+ wep1.ammo.head.guid mustEqual PlanetSideGUID(3942)
+ wep1.ammo.head.parentSlot mustEqual 0
+ wep1.ammo.head.obj.asInstanceOf[DetailedAmmoBoxData].unk mustEqual 8
+ wep1.ammo.head.obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 100
+ //4
+ char.inventory.get.contents(4).objectClass mustEqual 456
+ char.inventory.get.contents(4).guid mustEqual PlanetSideGUID(5374)
+ char.inventory.get.contents(4).parentSlot mustEqual 5
+ char.inventory.get.contents(4).obj.asInstanceOf[DetailedLockerContainerData].inventory.get.contents.size mustEqual 61
+ //11
+ char.inventory.get.contents(11).objectClass mustEqual 673
+ char.inventory.get.contents(11).guid mustEqual PlanetSideGUID(3661)
+ char.inventory.get.contents(11).parentSlot mustEqual 60
+ val wep2 = char.inventory.get.contents(11).obj.asInstanceOf[DetailedWeaponData]
+ wep2.unk1 mustEqual 2
+ wep2.unk2 mustEqual 8
+ wep2.ammo.head.objectClass mustEqual 674
+ wep2.ammo.head.guid mustEqual PlanetSideGUID(8542)
+ wep2.ammo.head.parentSlot mustEqual 0
+ wep2.ammo.head.obj.asInstanceOf[DetailedAmmoBoxData].unk mustEqual 8
+ wep2.ammo.head.obj.asInstanceOf[DetailedAmmoBoxData].magazine mustEqual 3
+ char.drawn_slot mustEqual DrawnSlot.None
+ case _ =>
+ ko
+ }
+ }
+
"encode (2)" in {
//the lack of an object will fail to turn into a bad bitstream
val msg = ObjectCreateDetailedMessage(0L, ObjectClass.avatar, PlanetSideGUID(2497), None, None)
@@ -428,6 +501,7 @@ class ObjectCreateDetailedMessageTest extends Specification {
List(),
"xpe_sanctuary_help" :: "xpe_th_firemodes" :: "used_beamer" :: "map13" :: Nil,
List.empty,
+ None,
Some(InventoryData(inv)),
DrawnSlot.Pistol1
)
@@ -442,4 +516,560 @@ class ObjectCreateDetailedMessageTest extends Specification {
pkt_bitv.drop(732) mustEqual ori_bitv.drop(732)
//TODO work on DetailedCharacterData to make this pass as a single stream
}
+
+ "encode (character, br32)" in {
+ val obj = DetailedCharacterData(
+ CharacterAppearanceData(
+ PlacementData(
+ Vector3(5500.0f, 3800.0f, 71.484375f),
+ Vector3(0.0f, 0.0f, 90.0f),
+ None
+ ),
+ BasicCharacterData("KiCkJr", PlanetSideEmpire.NC, CharacterGender.Male, 24, 4),
+ 3,
+ false, false,
+ ExoSuitType.Agile,
+ "",
+ 14,
+ false,
+ 354.375f, 354.375f,
+ false,
+ GrenadeState.None,
+ false, false, false,
+ RibbonBars(MeritCommendation.Loser4, MeritCommendation.EventNCElite, MeritCommendation.HeavyAssault6, MeritCommendation.SixYearNC)
+ ),
+ 6366766,
+ 694787,
+ 100, 100, 100,
+ 1, 7, 7,
+ 100, 46,
+ List(
+ CertificationType.StandardAssault,
+ CertificationType.MediumAssault,
+ CertificationType.HeavyAssault,
+ CertificationType.AntiVehicular,
+ CertificationType.AirCavalryScout,
+ CertificationType.GroundSupport,
+ CertificationType.Harasser,
+ CertificationType.StandardExoSuit,
+ CertificationType.AgileExoSuit,
+ CertificationType.Medical,
+ CertificationType.AdvancedMedical,
+ CertificationType.Hacking,
+ CertificationType.AdvancedHacking,
+ CertificationType.Engineering,
+ CertificationType.CombatEngineering
+ ),
+ List(
+ ImplantEntry(ImplantType.AudioAmplifier, None),
+ ImplantEntry(ImplantType.Targeting, None),
+ ImplantEntry(ImplantType.Surge, None)
+ ),
+ List(
+ "xpe_overhead_map",
+ "xpe_warp_gate",
+ "xpe_form_outfit",
+ "xpe_blackops",
+ "xpe_command_rank_5",
+ "xpe_command_rank_3",
+ "xpe_sanctuary_help",
+ "xpe_battle_rank_13",
+ "xpe_battle_rank_12",
+ "xpe_battle_rank_10",
+ "xpe_battle_rank_14",
+ "xpe_battle_rank_15",
+ "xpe_orbital_shuttle",
+ "xpe_drop_pod",
+ "xpe_bind_facility",
+ "xpe_battle_rank_3",
+ "xpe_battle_rank_5",
+ "xpe_battle_rank_4",
+ "xpe_join_squad",
+ "xpe_form_squad",
+ "xpe_instant_action",
+ "xpe_battle_rank_2",
+ "xpe_warp_gate_usage",
+ "xpe_battle_rank_8",
+ "xpe_battle_rank_11",
+ "xpe_battle_rank_6",
+ "xpe_mail_alert",
+ "xpe_command_rank_1",
+ "xpe_battle_rank_20",
+ "xpe_battle_rank_18",
+ "xpe_battle_rank_19",
+ "xpe_join_platoon",
+ "xpe_battle_rank_17",
+ "xpe_battle_rank_16",
+ "xpe_join_outfit",
+ "xpe_battle_rank_25",
+ "xpe_battle_rank_24",
+ "xpe_command_rank_4",
+ "xpe_form_platoon",
+ "xpe_bind_ams",
+ "xpe_battle_rank_9",
+ "xpe_battle_rank_7",
+ "xpe_th_router",
+ "xpe_th_flail",
+ "xpe_th_ant",
+ "xpe_th_ams",
+ "xpe_th_ground_p",
+ "xpe_th_air_p",
+ "xpe_th_hover",
+ "xpe_th_ground",
+ "xpe_th_bfr",
+ "xpe_th_afterburner",
+ "xpe_th_air",
+ "xpe_th_cloak",
+ "used_oicw",
+ "used_advanced_ace",
+ "visited_spitfire_turret",
+ "visited_spitfire_cloaked",
+ "visited_spitfire_aa",
+ "visited_tank_traps",
+ "visited_portable_manned_turret_nc",
+ "visited_portable_manned_turret_tr",
+ "used_magcutter",
+ "used_chainblade",
+ "used_forceblade",
+ "visited_wall_turret",
+ "visited_ancient_terminal",
+ "visited_ams",
+ "visited_ant",
+ "visited_dropship",
+ "visited_liberator",
+ "visited_lightgunship",
+ "visited_lightning",
+ "visited_magrider",
+ "visited_prowler",
+ "visited_quadstealth",
+ "visited_skyguard",
+ "visited_threemanheavybuggy",
+ "visited_two_man_assault_buggy",
+ "visited_twomanheavybuggy",
+ "visited_twomanhoverbuggy",
+ "visited_vanguard",
+ "visited_flail",
+ "visited_router",
+ "visited_switchblade",
+ "visited_aurora",
+ "visited_battlewagon",
+ "visited_fury",
+ "visited_quadassault",
+ "visited_galaxy_gunship",
+ "visited_apc_tr",
+ "visited_apc_vs",
+ "visited_lodestar",
+ "visited_phantasm",
+ "visited_thunderer",
+ "visited_apc_nc",
+ "visited_vulture",
+ "visited_wasp",
+ "visited_mosquito",
+ "visited_aphelion_flight",
+ "visited_aphelion_gunner",
+ "visited_colossus_flight",
+ "visited_colossus_gunner",
+ "visited_peregrine_flight",
+ "visited_peregrine_gunner",
+ "used_bank",
+ "visited_resource_silo",
+ "visited_certification_terminal",
+ "visited_med_terminal",
+ "used_nano_dispenser",
+ "visited_sensor_shield",
+ "visited_broadcast_warpgate",
+ "used_phalanx",
+ "used_phalanx_avcombo",
+ "used_phalanx_flakcombo",
+ "visited_warpgate_small",
+ "used_flamethrower",
+ "used_ancient_turret_weapon",
+ "visited_LLU_socket",
+ "used_energy_gun_nc",
+ "visited_mediumtransport",
+ "used_aphelion_immolation_cannon",
+ "used_grenade_plasma",
+ "used_grenade_jammer",
+ "visited_shield_generator",
+ "visited_motion_sensor",
+ "visited_health_crystal",
+ "visited_repair_crystal",
+ "visited_vehicle_crystal",
+ "used_grenade_frag",
+ "used_ace",
+ "visited_adv_med_terminal",
+ "used_beamer",
+ "used_bolt_driver",
+ "used_cycler",
+ "used_gauss",
+ "used_hunterseeker",
+ "used_isp",
+ "used_lancer",
+ "used_lasher",
+ "used_maelstrom",
+ "used_phoenix",
+ "used_pulsar",
+ "used_punisher",
+ "used_r_shotgun",
+ "used_radiator",
+ "used_rek",
+ "used_repeater",
+ "used_rocklet",
+ "used_striker",
+ "used_suppressor",
+ "used_thumper",
+ "visited_vanu_control_console",
+ "visited_capture_terminal",
+ "used_mini_chaingun",
+ "used_laze_pointer",
+ "used_telepad",
+ "used_spiker",
+ "used_heavy_sniper",
+ "used_command_uplink",
+ "used_firebird",
+ "used_flechette",
+ "used_heavy_rail_beam",
+ "used_ilc9",
+ "visited_generator_terminal",
+ "visited_locker",
+ "visited_external_door_lock",
+ "visited_air_vehicle_terminal",
+ "visited_galaxy_terminal",
+ "visited_implant_terminal",
+ "visited_secondary_capture",
+ "used_25mm_cannon",
+ "used_liberator_bombardier",
+ "visited_repair_silo",
+ "visited_vanu_module",
+ "used_flail_weapon",
+ "used_scythe",
+ "visited_respawn_terminal",
+ "used_ballgun",
+ "used_energy_gun_tr",
+ "used_anniversary_guna",
+ "used_anniversary_gunb",
+ "used_anniversary_gun",
+ "used_75mm_cannon",
+ "used_apc_nc_weapon",
+ "used_apc_tr_weapon",
+ "used_apc_vs_weapon",
+ "used_flux_cannon",
+ "used_aphelion_plasma_rocket_pod",
+ "used_aphelion_ppa",
+ "used_fluxpod",
+ "visited_bfr_terminal",
+ "used_colossus_cluster_bomb_pod",
+ "used_colossus_dual_100mm_cannons",
+ "used_colossus_tank_cannon",
+ "visited_energy_crystal",
+ "used_heavy_grenade_launcher",
+ "used_35mm_rotarychaingun",
+ "used_katana",
+ "used_35mm_cannon",
+ "used_reaver_weapons",
+ "used_lightning_weapons",
+ "used_med_app",
+ "used_20mm_cannon",
+ "visited_monolith_amerish",
+ "visited_monolith_ceryshen",
+ "visited_monolith_cyssor",
+ "visited_monolith_esamir",
+ "visited_monolith_forseral",
+ "visited_monolith_ishundar",
+ "visited_monolith_searhus",
+ "visited_monolith_solsar",
+ "used_nc_hev_falcon",
+ "used_nc_hev_scattercannon",
+ "used_nc_hev_sparrow",
+ "used_armor_siphon",
+ "used_peregrine_dual_machine_gun",
+ "used_peregrine_dual_rocket_pods",
+ "used_peregrine_mechhammer",
+ "used_peregrine_particle_cannon",
+ "used_peregrine_sparrow",
+ "used_105mm_cannon",
+ "used_15mm_chaingun",
+ "used_pulsed_particle_accelerator",
+ "used_rotarychaingun",
+ "visited_deconstruction_terminal",
+ "used_skyguard_weapons",
+ "visited_generator",
+ "used_gauss_cannon",
+ "used_trek",
+ "used_vanguard_weapons",
+ "visited_ancient_air_vehicle_terminal",
+ "visited_ancient_equipment_terminal",
+ "visited_order_terminal",
+ "visited_ancient_ground_vehicle_terminal",
+ "visited_ground_vehicle_terminal",
+ "used_vulture_bombardier",
+ "used_vulture_nose_cannon",
+ "used_vulture_tail_cannon",
+ "used_wasp_weapon_system",
+ "visited_charlie01",
+ "visited_charlie02",
+ "visited_charlie03",
+ "visited_charlie04",
+ "visited_charlie05",
+ "visited_charlie06",
+ "visited_charlie07",
+ "visited_charlie08",
+ "visited_charlie09",
+ "visited_gingerman_atar",
+ "visited_gingerman_dahaka",
+ "visited_gingerman_hvar",
+ "visited_gingerman_izha",
+ "visited_gingerman_jamshid",
+ "visited_gingerman_mithra",
+ "visited_gingerman_rashnu",
+ "visited_gingerman_sraosha",
+ "visited_gingerman_yazata",
+ "visited_gingerman_zal",
+ "visited_sled01",
+ "visited_sled02",
+ "visited_sled04",
+ "visited_sled05",
+ "visited_sled06",
+ "visited_sled07",
+ "visited_sled08",
+ "visited_snowman_amerish",
+ "visited_snowman_ceryshen",
+ "visited_snowman_cyssor",
+ "visited_snowman_esamir",
+ "visited_snowman_forseral",
+ "visited_snowman_hossin",
+ "visited_snowman_ishundar",
+ "visited_snowman_searhus",
+ "visited_snowman_solsar",
+ "ugd06",
+ "ugd05",
+ "ugd04",
+ "ugd03",
+ "ugd02",
+ "ugd01",
+ "map99",
+ "map98",
+ "map97",
+ "map96",
+ "map15",
+ "map14",
+ "map11",
+ "map08",
+ "map04",
+ "map05",
+ "map03",
+ "map01",
+ "map06",
+ "map02",
+ "map09",
+ "map07",
+ "map10"
+ ),
+ List(
+ "training_start_nc",
+ "training_ui",
+ "training_map"
+ ),
+ Some(Cosmetics(true, true, true, true, false)),
+ Some(
+ InventoryData(
+ List(
+ InternalSlot(531, PlanetSideGUID(4202), 0,
+ DetailedWeaponData(2, 8, List(InternalSlot(389, PlanetSideGUID(3942), 0,DetailedAmmoBoxData(8, 100))))
+ ),
+ InternalSlot(132, PlanetSideGUID(6924), 1,
+ DetailedWeaponData(2, 8, List(InternalSlot(111, PlanetSideGUID(9157), 0, DetailedAmmoBoxData(8, 100))))
+ ),
+ InternalSlot(714, PlanetSideGUID(8498), 2,
+ DetailedWeaponData(2, 8, List(InternalSlot(755, PlanetSideGUID(5356), 0, DetailedAmmoBoxData(8, 16))))
+ ),
+ InternalSlot(468, PlanetSideGUID(7198), 4,
+ DetailedWeaponData(2, 8, List(InternalSlot(540, PlanetSideGUID(5009), 0, DetailedAmmoBoxData(8, 1))))
+ ),
+ InternalSlot(456, PlanetSideGUID(5374), 5,
+ DetailedLockerContainerData(8, Some(InventoryData(List(
+ InternalSlot(429, PlanetSideGUID(3021), 0,
+ DetailedWeaponData(6, 8, List(InternalSlot(272, PlanetSideGUID(8729), 0, DetailedAmmoBoxData(8, 0))))
+ ),
+ InternalSlot(838, PlanetSideGUID(8467), 9,
+ DetailedWeaponData(6, 8, List(InternalSlot(839, PlanetSideGUID(8603), 0, DetailedAmmoBoxData(8, 5))))
+ ),
+ InternalSlot(272, PlanetSideGUID(3266), 18, DetailedAmmoBoxData(8, 27)),
+ InternalSlot(577, PlanetSideGUID(2934), 22,
+ DetailedWeaponData(6, 8, List(InternalSlot(111, PlanetSideGUID(4682), 0, DetailedAmmoBoxData(8, 100))))
+ ),
+ InternalSlot(839, PlanetSideGUID(3271), 90, DetailedAmmoBoxData(8, 15)),
+ InternalSlot(839, PlanetSideGUID(7174), 94, DetailedAmmoBoxData(8, 6)),
+ InternalSlot(429, PlanetSideGUID(6084), 98,
+ DetailedWeaponData(6, 8, List(InternalSlot(272, PlanetSideGUID(5928), 0, DetailedAmmoBoxData(8, 35))))
+ ),
+ InternalSlot(462, PlanetSideGUID(5000), 108,
+ DetailedWeaponData(6, 8, List(InternalSlot(463, PlanetSideGUID(6277), 0, DetailedAmmoBoxData(8, 150))))
+ ),
+ InternalSlot(429, PlanetSideGUID(4341), 189,
+ DetailedWeaponData(6, 8, List(InternalSlot(272, PlanetSideGUID(7043), 0, DetailedAmmoBoxData(8, 35))))
+ ),
+ InternalSlot(556, PlanetSideGUID(4168), 198,
+ DetailedWeaponData(6, 8, List(InternalSlot(28, PlanetSideGUID(8937), 0, DetailedAmmoBoxData(8, 100))))
+ ),
+ InternalSlot(272, PlanetSideGUID(3173), 207, DetailedAmmoBoxData(8, 50)),
+ InternalSlot(462, PlanetSideGUID(3221), 210,
+ DetailedWeaponData(6, 8, List(InternalSlot(463, PlanetSideGUID(4031), 0, DetailedAmmoBoxData(8, 150))))
+ ),
+ InternalSlot(556, PlanetSideGUID(6853), 280,
+ DetailedWeaponData(6, 8, List(InternalSlot(29, PlanetSideGUID(8524), 0, DetailedAmmoBoxData(8, 67))))
+ ),
+ InternalSlot(556, PlanetSideGUID(4569), 290,
+ DetailedWeaponData(6, 8, List(InternalSlot(28, PlanetSideGUID(5584), 0, DetailedAmmoBoxData(8, 100))))
+ ),
+ InternalSlot(462, PlanetSideGUID(9294), 300,
+ DetailedWeaponData(6, 8, List(InternalSlot(463, PlanetSideGUID(3118), 0, DetailedAmmoBoxData(8, 150))))
+ ),
+ InternalSlot(272, PlanetSideGUID(4759), 387, DetailedAmmoBoxData(8, 50)),
+ InternalSlot(462, PlanetSideGUID(7377), 390,
+ DetailedWeaponData(6, 8, List(InternalSlot(463, PlanetSideGUID(8155), 0, DetailedAmmoBoxData(8, 150))))
+ ),
+ InternalSlot(843, PlanetSideGUID(6709), 480, DetailedAmmoBoxData(8, 1)),
+ InternalSlot(843, PlanetSideGUID(5276), 484, DetailedAmmoBoxData(8, 1)),
+ InternalSlot(843, PlanetSideGUID(7769), 488, DetailedAmmoBoxData(8, 1)),
+ InternalSlot(844, PlanetSideGUID(5334), 492, DetailedAmmoBoxData(8, 1)),
+ InternalSlot(844, PlanetSideGUID(6219), 496, DetailedAmmoBoxData(8, 1)),
+ InternalSlot(842, PlanetSideGUID(7279), 500, DetailedAmmoBoxData(8, 1)),
+ InternalSlot(842, PlanetSideGUID(5415), 504, DetailedAmmoBoxData(8, 1)),
+ InternalSlot(175, PlanetSideGUID(5741), 540,
+ DetailedWeaponData(6, 8, List(InternalSlot(540, PlanetSideGUID(5183), 0, DetailedAmmoBoxData(8, 1))))
+ ),
+ InternalSlot(324, PlanetSideGUID(6208), 541,
+ DetailedWeaponData(6, 8, List(InternalSlot(540, PlanetSideGUID(5029), 0, DetailedAmmoBoxData(8, 1))))
+ ),
+ InternalSlot(324, PlanetSideGUID(8589), 542,
+ DetailedWeaponData(6, 8, List(InternalSlot(540, PlanetSideGUID(9217), 0, DetailedAmmoBoxData(8, 1))))
+ ),
+ InternalSlot(175, PlanetSideGUID(8901), 543,
+ DetailedWeaponData(6, 8, List(InternalSlot(540, PlanetSideGUID(7633), 0, DetailedAmmoBoxData(8, 1))))
+ ),
+ InternalSlot(175, PlanetSideGUID(8419), 544,
+ DetailedWeaponData(6, 8, List(InternalSlot(540, PlanetSideGUID(6546), 0, DetailedAmmoBoxData(8, 1))))
+ ),
+ InternalSlot(175, PlanetSideGUID(4715), 545,
+ DetailedWeaponData(6, 8, List(InternalSlot(540, PlanetSideGUID(8453), 0, DetailedAmmoBoxData(8, 1))))
+ ),
+ InternalSlot(324, PlanetSideGUID(3577), 546,
+ DetailedWeaponData(6, 8, List(InternalSlot(540, PlanetSideGUID(9202), 0, DetailedAmmoBoxData(8, 1))))
+ ),
+ InternalSlot(324, PlanetSideGUID(6003), 547,
+ DetailedWeaponData(6, 8, List(InternalSlot(540, PlanetSideGUID(3260), 0, DetailedAmmoBoxData(8, 1))))
+ ),
+ InternalSlot(324, PlanetSideGUID(9140), 548,
+ DetailedWeaponData(6, 8, List(InternalSlot(540,PlanetSideGUID(3815),0,DetailedAmmoBoxData(8, 1))))
+ ),
+ InternalSlot(324, PlanetSideGUID(4913), 549,
+ DetailedWeaponData(6, 8, List(InternalSlot(540,PlanetSideGUID(7222),0,DetailedAmmoBoxData(8, 1))))
+ ),
+ InternalSlot(324, PlanetSideGUID(6954), 550,
+ DetailedWeaponData(6, 8, List(InternalSlot(540,PlanetSideGUID(2953),0,DetailedAmmoBoxData(8, 1))))
+ ),
+ InternalSlot(324, PlanetSideGUID(6405), 551,
+ DetailedWeaponData(6, 8, List(InternalSlot(540,PlanetSideGUID(4676),0,DetailedAmmoBoxData(8, 1))))
+ ),
+ InternalSlot(324, PlanetSideGUID(8915), 552,
+ DetailedWeaponData(6, 8, List(InternalSlot(540,PlanetSideGUID(4018),0,DetailedAmmoBoxData(8, 1))))
+ ),
+ InternalSlot(324, PlanetSideGUID(4993), 553,
+ DetailedWeaponData(6, 8, List(InternalSlot(540,PlanetSideGUID(6775),0,DetailedAmmoBoxData(8, 1))))
+ ),
+ InternalSlot(175, PlanetSideGUID(5053), 554,
+ DetailedWeaponData(6, 8, List(InternalSlot(540,PlanetSideGUID(6418),0,DetailedAmmoBoxData(8, 1))))
+ ),
+ InternalSlot(324, PlanetSideGUID(9244), 555,
+ DetailedWeaponData(6, 8, List(InternalSlot(540,PlanetSideGUID(3327),0,DetailedAmmoBoxData(8, 1))))
+ ),
+ InternalSlot(468, PlanetSideGUID(6292), 556,
+ DetailedWeaponData(6, 8, List(InternalSlot(540,PlanetSideGUID(6918),0,DetailedAmmoBoxData(8, 1))))
+ ),
+ InternalSlot(842, PlanetSideGUID(5357), 558, DetailedAmmoBoxData(8, 1)),
+ InternalSlot(844, PlanetSideGUID(4435), 562, DetailedAmmoBoxData(8, 1)),
+ InternalSlot(843, PlanetSideGUID(7242), 566, DetailedAmmoBoxData(8, 1)),
+ InternalSlot(175, PlanetSideGUID(7330), 570,
+ DetailedWeaponData(6, 8, List(InternalSlot(540, PlanetSideGUID(4786), 0, DetailedAmmoBoxData(8, 1))))
+ ),
+ InternalSlot(468, PlanetSideGUID(7415), 571,
+ DetailedWeaponData(6, 8, List(InternalSlot(540, PlanetSideGUID(6536), 0, DetailedAmmoBoxData(8, 1))))
+ ),
+ InternalSlot(175, PlanetSideGUID(3949), 572,
+ DetailedWeaponData(6, 8, List(InternalSlot(540, PlanetSideGUID(7526), 0, DetailedAmmoBoxData(8, 1))))
+ ),
+ InternalSlot(175, PlanetSideGUID(3805), 573,
+ DetailedWeaponData(6, 8, List(InternalSlot(540, PlanetSideGUID(7358), 0, DetailedAmmoBoxData(8, 1))))
+ ),
+ InternalSlot(324, PlanetSideGUID(4493), 574,
+ DetailedWeaponData(6, 8, List(InternalSlot(540, PlanetSideGUID(6852), 0, DetailedAmmoBoxData(8, 1))))
+ ),
+ InternalSlot(324, PlanetSideGUID(5762), 575,
+ DetailedWeaponData(6, 8, List(InternalSlot(540, PlanetSideGUID(3463), 0, DetailedAmmoBoxData(8, 1))))
+ ),
+ InternalSlot(175, PlanetSideGUID(3315), 576,
+ DetailedWeaponData(6, 8, List(InternalSlot(540, PlanetSideGUID(7619), 0, DetailedAmmoBoxData(8, 1))))
+ ),
+ InternalSlot(324, PlanetSideGUID(6263), 577,
+ DetailedWeaponData(6, 8, List(InternalSlot(540, PlanetSideGUID(5912), 0, DetailedAmmoBoxData(8, 1))))
+ ),
+ InternalSlot(468, PlanetSideGUID(4028), 578,
+ DetailedWeaponData(6, 8, List(InternalSlot(540, PlanetSideGUID(8021), 0, DetailedAmmoBoxData(8, 1))))
+ ),
+ InternalSlot(175, PlanetSideGUID(2843), 579,
+ DetailedWeaponData(6, 8, List(InternalSlot(540, PlanetSideGUID(7250), 0, DetailedAmmoBoxData(8, 1))))
+ ),
+ InternalSlot(175, PlanetSideGUID(9143), 580,
+ DetailedWeaponData(6, 8, List(InternalSlot(540, PlanetSideGUID(5195), 0, DetailedAmmoBoxData(8, 1))))
+ ),
+ InternalSlot(468, PlanetSideGUID(5024), 581,
+ DetailedWeaponData(6, 8, List(InternalSlot(540, PlanetSideGUID(4287), 0, DetailedAmmoBoxData(8, 1))))
+ ),
+ InternalSlot(468, PlanetSideGUID(6582), 582,
+ DetailedWeaponData(6, 8, List(InternalSlot(540, PlanetSideGUID(4915), 0, DetailedAmmoBoxData(8, 1))))
+ ),
+ InternalSlot(468, PlanetSideGUID(6425), 583,
+ DetailedWeaponData(6, 8, List(InternalSlot(540, PlanetSideGUID(8872), 0, DetailedAmmoBoxData(8, 1))))
+ ),
+ InternalSlot(468, PlanetSideGUID(4431), 584,
+ DetailedWeaponData(6, 8, List(InternalSlot(540, PlanetSideGUID(4191), 0, DetailedAmmoBoxData(8, 1))))
+ ),
+ InternalSlot(175, PlanetSideGUID(8339), 585,
+ DetailedWeaponData(6, 8, List(InternalSlot(540, PlanetSideGUID(7317), 0, DetailedAmmoBoxData(8, 1))))
+ ),
+ InternalSlot(175, PlanetSideGUID(3277), 586,
+ DetailedWeaponData(6, 8, List(InternalSlot(540, PlanetSideGUID(6469), 0, DetailedAmmoBoxData(8, 1))))
+ )
+ ))))
+ ),
+ InternalSlot(213, PlanetSideGUID(6877), 6, DetailedCommandDetonaterData(4, 8)),
+ InternalSlot(755, PlanetSideGUID(6227), 9, DetailedAmmoBoxData(8, 16)),
+ InternalSlot(728, PlanetSideGUID(7181), 12, DetailedREKData(4, 16)),
+ InternalSlot(536, PlanetSideGUID(4077), 33, DetailedAmmoBoxData(8, 1)),
+ InternalSlot(680, PlanetSideGUID(4377), 37,
+ DetailedWeaponData(2, 8, List(InternalSlot(681, PlanetSideGUID(8905), 0, DetailedAmmoBoxData(8, 3))))
+ ),
+ InternalSlot(32, PlanetSideGUID(5523), 39, DetailedACEData(4)),
+ InternalSlot(673, PlanetSideGUID(3661), 60,
+ DetailedWeaponData(2, 8, List(InternalSlot(674, PlanetSideGUID(8542), 0, DetailedAmmoBoxData(8, 3))))
+ )
+ )
+ )
+ ),
+ DrawnSlot.None
+ )
+ val msg = ObjectCreateDetailedMessage(ObjectClass.avatar, PlanetSideGUID(75), obj)
+ val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+ val pkt_bitv = pkt.toBitVector
+ val ori_bitv = string_testchar_br32.toBitVector
+ pkt_bitv.take(153) mustEqual ori_bitv.take(153) //skip 1
+ pkt_bitv.drop(154).take(144) mustEqual ori_bitv.drop(154).take(144) //skip 24
+ pkt_bitv.drop(322).take(72) mustEqual ori_bitv.drop(322).take(72) //skip 24
+ pkt_bitv.drop(418).take(55) mustEqual ori_bitv.drop(418).take(55) //skip 1
+ pkt_bitv.drop(474).take(102) mustEqual ori_bitv.drop(474).take(102) //skip 126
+ pkt_bitv.drop(702).take(192) mustEqual ori_bitv.drop(702).take(192) //skip 36
+ pkt_bitv.drop(930) mustEqual ori_bitv.drop(930) //to end
+ }
}
diff --git a/common/src/test/scala/game/TriggerSoundMessageTest.scala b/common/src/test/scala/game/TriggerSoundMessageTest.scala
new file mode 100644
index 000000000..2db61b456
--- /dev/null
+++ b/common/src/test/scala/game/TriggerSoundMessageTest.scala
@@ -0,0 +1,33 @@
+// Copyright (c) 2017 PSForever
+package game
+
+import org.specs2.mutable._
+import net.psforever.packet._
+import net.psforever.packet.game._
+import net.psforever.types.Vector3
+import scodec.bits._
+
+class TriggerSoundMessageTest extends Specification {
+ val string = hex"6B 1FD5E1B466DB3858F1FC"
+
+ "decode" in {
+ PacketCoding.DecodePacket(string).require match {
+ case TriggerSoundMessage(sound, pos, unk2, volume) =>
+ sound mustEqual TriggeredSound.HackDoor
+ pos.x mustEqual 1913.9531f
+ pos.y mustEqual 6042.8125f
+ pos.z mustEqual 45.609375f
+ unk2 mustEqual 30
+ volume mustEqual 0.49803925f
+ case _ =>
+ ko
+ }
+ }
+
+ "encode" in {
+ val msg = TriggerSoundMessage(TriggeredSound.HackDoor, Vector3(1913.9531f, 6042.8125f, 45.609375f), 30, 0.49803925f)
+ val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+ pkt mustEqual string
+ }
+}
diff --git a/common/src/test/scala/objects/ActorTest.scala b/common/src/test/scala/objects/ActorTest.scala
new file mode 100644
index 000000000..5b08b9202
--- /dev/null
+++ b/common/src/test/scala/objects/ActorTest.scala
@@ -0,0 +1,13 @@
+// Copyright (c) 2017 PSForever
+package objects
+
+import akka.actor.ActorSystem
+import akka.testkit.{ImplicitSender, TestKit}
+import org.scalatest.{BeforeAndAfterAll, Matchers, WordSpecLike}
+import org.specs2.specification.Scope
+
+abstract class ActorTest(sys : ActorSystem) extends TestKit(sys) with Scope with ImplicitSender with WordSpecLike with Matchers with BeforeAndAfterAll {
+ override def afterAll {
+ TestKit.shutdownActorSystem(system)
+ }
+}
diff --git a/common/src/test/scala/objects/ConverterTest.scala b/common/src/test/scala/objects/ConverterTest.scala
index 2bcde21f0..9f53443c1 100644
--- a/common/src/test/scala/objects/ConverterTest.scala
+++ b/common/src/test/scala/objects/ConverterTest.scala
@@ -1,7 +1,7 @@
// Copyright (c) 2017 PSForever
package objects
-import net.psforever.objects.definition.converter.{ACEConverter, REKConverter}
+import net.psforever.objects.definition.converter.{ACEConverter, CharacterSelectConverter, REKConverter}
import net.psforever.objects._
import net.psforever.objects.definition._
import net.psforever.objects.equipment.CItem.{DeployedItem, Unit}
@@ -130,7 +130,7 @@ class ConverterTest extends Specification {
}
"Player" should {
- "convert to packet" in {
+ val obj : Player = {
/*
Create an AmmoBoxDefinition with which to build two AmmoBoxes
Create a ToolDefinition with which to create a Tool
@@ -157,9 +157,64 @@ class ConverterTest extends Specification {
obj.Slot(2).Equipment = tool
obj.Slot(5).Equipment.get.GUID = PlanetSideGUID(94)
obj.Inventory += 8 -> box2
+ obj
+ }
+ val converter = new CharacterSelectConverter
- obj.Definition.Packet.DetailedConstructorData(obj).isSuccess mustEqual true
- ok //TODO write more of this test
+ "convert to packet (BR < 24)" in {
+ obj.BEP = 0
+ obj.Definition.Packet.DetailedConstructorData(obj) match {
+ case Success(pkt) =>
+ ok
+ case _ =>
+ ko
+ }
+ obj.Definition.Packet.ConstructorData(obj) match {
+ case Success(pkt) =>
+ ok
+ case _ =>
+ ko
+ }
+ }
+
+ "convert to packet (BR >= 24)" in {
+ obj.BEP = 10000000
+ obj.Definition.Packet.DetailedConstructorData(obj) match {
+ case Success(pkt) =>
+ ok
+ case _ =>
+ ko
+ }
+ obj.Definition.Packet.ConstructorData(obj) match {
+ case Success(pkt) =>
+ ok
+ case _ =>
+ ko
+ }
+ }
+
+ "convert to simple packet (BR < 24)" in {
+ obj.BEP = 0
+ converter.DetailedConstructorData(obj) match {
+ case Success(pkt) =>
+ ok
+ case _ =>
+ ko
+ }
+ converter.ConstructorData(obj).isFailure mustEqual true
+ converter.ConstructorData(obj).get must throwA[Exception]
+ }
+
+ "convert to simple packet (BR >= 24)" in {
+ obj.BEP = 10000000
+ converter.DetailedConstructorData(obj) match {
+ case Success(pkt) =>
+ ok
+ case _ =>
+ ko
+ }
+ converter.ConstructorData(obj).isFailure mustEqual true
+ converter.ConstructorData(obj).get must throwA[Exception]
}
}
diff --git a/common/src/test/scala/objects/NumberPoolActorTest.scala b/common/src/test/scala/objects/NumberPoolActorTest.scala
index 52787c818..f40eac431 100644
--- a/common/src/test/scala/objects/NumberPoolActorTest.scala
+++ b/common/src/test/scala/objects/NumberPoolActorTest.scala
@@ -2,24 +2,11 @@
package objects
import akka.actor.{ActorSystem, Props}
-import akka.testkit.{ImplicitSender, TestKit, TestProbe}
-import net.psforever.objects.entity.IdentifiableEntity
-import net.psforever.objects.guid.NumberPoolHub
-import org.scalatest.{BeforeAndAfterAll, Matchers, WordSpecLike}
-import net.psforever.objects.guid.actor.{NumberPoolAccessorActor, NumberPoolActor, Register}
+import net.psforever.objects.guid.actor.NumberPoolActor
import net.psforever.objects.guid.pool.ExclusivePool
import net.psforever.objects.guid.selector.RandomSelector
-import net.psforever.objects.guid.source.LimitedNumberSource
-import org.specs2.specification.Scope
import scala.concurrent.duration.Duration
-import scala.util.Success
-
-abstract class ActorTest(sys : ActorSystem) extends TestKit(sys) with Scope with ImplicitSender with WordSpecLike with Matchers with BeforeAndAfterAll {
- override def afterAll {
- TestKit.shutdownActorSystem(system)
- }
-}
class NumberPoolActorTest extends ActorTest(ActorSystem("test")) {
"NumberPoolActor" should {
@@ -61,23 +48,3 @@ class NumberPoolActorTest2 extends ActorTest(ActorSystem("test")) {
}
}
}
-
-class NumberPoolActorTest3 extends ActorTest(ActorSystem("test")) {
- "NumberPoolAccessorActor" should {
- class TestEntity extends IdentifiableEntity
-
- "register" in {
- val hub = new NumberPoolHub(new LimitedNumberSource(51))
- val pool = hub.AddPool("test", (25 to 50).toList)
- pool.Selector = new RandomSelector
- val poolActor = system.actorOf(Props(classOf[NumberPoolActor], pool), name = "poolActor")
- val poolAccessor = system.actorOf(Props(classOf[NumberPoolAccessorActor], hub, pool, poolActor), name = "accessor")
-
- val obj : TestEntity = new TestEntity
- val probe = new TestProbe(system)
- poolAccessor ! Register(obj, probe.ref)
- probe.expectMsg(Success(obj))
- assert({obj.GUID; true}) //NoGUIDException if failure
- }
- }
-}
diff --git a/common/src/test/scala/objects/NumberSourceTest.scala b/common/src/test/scala/objects/NumberSourceTest.scala
index dc4660617..08c9f48a2 100644
--- a/common/src/test/scala/objects/NumberSourceTest.scala
+++ b/common/src/test/scala/objects/NumberSourceTest.scala
@@ -9,180 +9,6 @@ class NumberSourceTest extends Specification {
import net.psforever.objects.entity.IdentifiableEntity
private class TestClass extends IdentifiableEntity
- "MaxNumberSource" should {
- import net.psforever.objects.guid.source.MaxNumberSource
- "construct" in {
- val obj = MaxNumberSource()
- obj.Size mustEqual Int.MaxValue
- obj.CountAvailable mustEqual Int.MaxValue
- obj.CountUsed mustEqual 0
- }
-
- "get a number" in {
- val obj = MaxNumberSource()
- val result : Option[LoanedKey] = obj.Available(5)
- result.isDefined mustEqual true
- result.get.GUID mustEqual 5
- result.get.Policy mustEqual AvailabilityPolicy.Leased
- result.get.Object mustEqual None
- obj.Size mustEqual Int.MaxValue
- obj.CountAvailable mustEqual Int.MaxValue - 1
- obj.CountUsed mustEqual 1
- }
-
- "assign the number" in {
- val obj = MaxNumberSource()
- val result : Option[LoanedKey] = obj.Available(5)
- result.isDefined mustEqual true
- result.get.Object = new TestClass()
- ok
- }
-
- "return a number (unused)" in {
- val obj = MaxNumberSource()
- val result : Option[LoanedKey] = obj.Available(5)
- result.isDefined mustEqual true
- result.get.GUID mustEqual 5
- obj.CountUsed mustEqual 1
- val ret = obj.Return(result.get)
- ret mustEqual None
- obj.CountUsed mustEqual 0
- }
-
- "return a number (assigned)" in {
- val obj = MaxNumberSource()
- val test = new TestClass()
- val result : Option[LoanedKey] = obj.Available(5)
- result.isDefined mustEqual true
- result.get.GUID mustEqual 5
- result.get.Object = test
- obj.CountUsed mustEqual 1
- val ret = obj.Return(result.get)
- ret mustEqual Some(test)
- obj.CountUsed mustEqual 0
- }
-
- "restrict a number (unassigned)" in {
- val obj = MaxNumberSource()
- val result : Option[LoanedKey] = obj.Restrict(5)
- result.isDefined mustEqual true
- result.get.GUID mustEqual 5
- result.get.Policy mustEqual AvailabilityPolicy.Restricted
- result.get.Object mustEqual None
- }
-
- "restrict a number (assigned + multiple assignments)" in {
- val obj = MaxNumberSource()
- val test1 = new TestClass()
- val test2 = new TestClass()
- val result : Option[LoanedKey] = obj.Restrict(5)
- result.get.GUID mustEqual 5
- result.get.Policy mustEqual AvailabilityPolicy.Restricted
- result.get.Object mustEqual None
- result.get.Object = None //assignment 1
- result.get.Object mustEqual None //still unassigned
- result.get.Object = test1 //assignment 2
- result.get.Object mustEqual Some(test1)
- result.get.Object = test2 //assignment 3
- result.get.Object mustEqual Some(test1) //same as above
- }
-
- "return a restricted number (correctly fail)" in {
- val obj = MaxNumberSource()
- val test = new TestClass()
- val result : Option[LoanedKey] = obj.Restrict(5)
- result.get.GUID mustEqual 5
- result.get.Policy mustEqual AvailabilityPolicy.Restricted
- result.get.Object = test
-
- obj.Return(5)
- val result2 : Option[SecureKey] = obj.Get(5)
- result2.get.GUID mustEqual 5
- result2.get.Policy mustEqual AvailabilityPolicy.Restricted
- result2.get.Object mustEqual Some(test)
- }
-
- "restrict a previously-assigned number" in {
- val obj = MaxNumberSource()
- val test = new TestClass()
- val result1 : Option[LoanedKey] = obj.Available(5)
- result1.isDefined mustEqual true
- result1.get.Policy mustEqual AvailabilityPolicy.Leased
- result1.get.Object = test
- val result2 : Option[LoanedKey] = obj.Restrict(5)
- result2.isDefined mustEqual true
- result2.get.Policy mustEqual AvailabilityPolicy.Restricted
- result2.get.Object mustEqual Some(test)
- }
-
- "check a number (not previously gotten)" in {
- val obj = MaxNumberSource()
- val result2 : Option[SecureKey] = obj.Get(5)
- result2.get.GUID mustEqual 5
- result2.get.Policy mustEqual AvailabilityPolicy.Available
- result2.get.Object mustEqual None
- }
-
- "check a number (previously gotten)" in {
- val obj = MaxNumberSource()
- val result : Option[LoanedKey] = obj.Available(5)
- result.isDefined mustEqual true
- result.get.GUID mustEqual 5
- result.get.Policy mustEqual AvailabilityPolicy.Leased
- result.get.Object mustEqual None
- val result2 : Option[SecureKey] = obj.Get(5)
- result2.get.GUID mustEqual 5
- result2.get.Policy mustEqual AvailabilityPolicy.Leased
- result2.get.Object mustEqual None
- }
-
- "check a number (assigned)" in {
- val obj = MaxNumberSource()
- val result : Option[LoanedKey] = obj.Available(5)
- result.isDefined mustEqual true
- result.get.GUID mustEqual 5
- result.get.Policy mustEqual AvailabilityPolicy.Leased
- result.get.Object = new TestClass()
- val result2 : Option[SecureKey] = obj.Get(5)
- result2.get.GUID mustEqual 5
- result2.get.Policy mustEqual AvailabilityPolicy.Leased
- result2.get.Object mustEqual result.get.Object
- }
-
- "check a number (assigned and returned)" in {
- val obj = MaxNumberSource()
- val test = new TestClass()
- val result : Option[LoanedKey] = obj.Available(5)
- result.get.Policy mustEqual AvailabilityPolicy.Leased
- result.get.Object = test
- val result2 : Option[SecureKey] = obj.Get(5)
- result2.get.Policy mustEqual AvailabilityPolicy.Leased
- result2.get.Object.get === test
- obj.Return(5) mustEqual Some(test)
- val result3 : Option[SecureKey] = obj.Get(5)
- result3.get.Policy mustEqual AvailabilityPolicy.Available
- result3.get.Object mustEqual None
- }
-
- "clear" in {
- val obj = MaxNumberSource()
- val test1 = new TestClass()
- val test2 = new TestClass()
- obj.Available(5) //no assignment
- obj.Available(10).get.Object = test1
- obj.Available(15).get.Object = test2
- obj.Restrict(15)
- obj.Restrict(20).get.Object = test1
- obj.CountUsed mustEqual 4
-
- val list : List[IdentifiableEntity] = obj.Clear()
- obj.CountUsed mustEqual 0
- list.size mustEqual 3
- list.count(obj => { obj == test1 }) mustEqual 2
- list.count(obj => { obj == test2 }) mustEqual 1
- }
- }
-
"LimitedNumberSource" should {
import net.psforever.objects.guid.source.LimitedNumberSource
"construct" in {
diff --git a/common/src/test/scala/objects/UniqueNumberSystemTest.scala b/common/src/test/scala/objects/UniqueNumberSystemTest.scala
new file mode 100644
index 000000000..a685b0355
--- /dev/null
+++ b/common/src/test/scala/objects/UniqueNumberSystemTest.scala
@@ -0,0 +1,288 @@
+// Copyright (c) 2017 PSForever
+package objects
+
+import akka.actor.{ActorRef, ActorSystem, Props}
+import net.psforever.objects.entity.IdentifiableEntity
+import net.psforever.objects.guid.NumberPoolHub
+import net.psforever.objects.guid.actor.{NumberPoolActor, Register, UniqueNumberSystem, Unregister}
+import net.psforever.objects.guid.selector.RandomSelector
+import net.psforever.objects.guid.source.LimitedNumberSource
+
+import scala.concurrent.duration.Duration
+import scala.util.{Failure, Success}
+
+class AllocateNumberPoolActors extends ActorTest(ActorSystem("test")) {
+ "AllocateNumberPoolActors" in {
+ val src : LimitedNumberSource = LimitedNumberSource(6000)
+ val guid : NumberPoolHub = new NumberPoolHub(src)
+ guid.AddPool("pool1", (1001 to 2000).toList)
+ guid.AddPool("pool2", (3001 to 4000).toList)
+ guid.AddPool("pool3", (5001 to 6000).toList)
+ val actorMap = UniqueNumberSystemTest.AllocateNumberPoolActors(guid)
+ assert(actorMap.size == 4)
+ assert(actorMap.get("generic").isDefined) //automatically generated
+ assert(actorMap.get("pool1").isDefined)
+ assert(actorMap.get("pool2").isDefined)
+ assert(actorMap.get("pool3").isDefined)
+ }
+}
+
+class UniqueNumberSystemTest extends ActorTest(ActorSystem("test")) {
+ "UniqueNumberSystem" should {
+ "constructor" in {
+ val src : LimitedNumberSource = LimitedNumberSource(6000)
+ val guid : NumberPoolHub = new NumberPoolHub(src)
+ guid.AddPool("pool1", (1001 to 2000).toList)
+ guid.AddPool("pool2", (3001 to 4000).toList)
+ guid.AddPool("pool3", (5001 to 6000).toList)
+ system.actorOf(Props(classOf[UniqueNumberSystem], guid, UniqueNumberSystemTest.AllocateNumberPoolActors(guid)), "uns")
+ //as long as it constructs ...
+
+ }
+ }
+}
+
+class UniqueNumberSystemTest1 extends ActorTest(ActorSystem("test")) {
+ class EntityTestClass extends IdentifiableEntity
+
+ "UniqueNumberSystem" should {
+ "Register (success)" in {
+ val src : LimitedNumberSource = LimitedNumberSource(6000)
+ val guid : NumberPoolHub = new NumberPoolHub(src)
+ val pool1 = (1001 to 2000).toList
+ val pool2 = (3001 to 4000).toList
+ val pool3 = (5001 to 6000).toList
+ guid.AddPool("pool1", pool1).Selector = new RandomSelector
+ guid.AddPool("pool2", pool2).Selector = new RandomSelector
+ guid.AddPool("pool3", pool3).Selector = new RandomSelector
+ val uns = system.actorOf(Props(classOf[UniqueNumberSystem], guid, UniqueNumberSystemTest.AllocateNumberPoolActors(guid)), "uns")
+ assert(src.CountUsed == 0)
+ //pool1
+ for(_ <- 1 to 100) {
+ val testObj = new EntityTestClass()
+ uns ! Register(testObj, "pool1")
+ val msg = receiveOne(Duration.create(100, "ms"))
+ assert(msg.isInstanceOf[Success[_]])
+ assert(pool1.contains(testObj.GUID.guid))
+ }
+ //pool2
+ for(_ <- 1 to 100) {
+ val testObj = new EntityTestClass()
+ uns ! Register(testObj, "pool2")
+ val msg = receiveOne(Duration.create(100, "ms"))
+ assert(msg.isInstanceOf[Success[_]])
+ assert(pool2.contains(testObj.GUID.guid))
+ }
+ //pool3
+ for(_ <- 1 to 100) {
+ val testObj = new EntityTestClass()
+ uns ! Register(testObj, "pool3")
+ val msg = receiveOne(Duration.create(100, "ms"))
+ assert(msg.isInstanceOf[Success[_]])
+ assert(pool3.contains(testObj.GUID.guid))
+ }
+ assert(src.CountUsed == 300)
+ }
+ }
+}
+
+class UniqueNumberSystemTest2 extends ActorTest(ActorSystem("test")) {
+ class EntityTestClass extends IdentifiableEntity
+
+ "UniqueNumberSystem" should {
+ "Register (success; already registered)" in {
+ val src : LimitedNumberSource = LimitedNumberSource(6000)
+ val guid : NumberPoolHub = new NumberPoolHub(src)
+ guid.AddPool("pool1", (1001 to 2000).toList).Selector = new RandomSelector
+ guid.AddPool("pool2", (3001 to 4000).toList).Selector = new RandomSelector
+ guid.AddPool("pool3", (5001 to 6000).toList).Selector = new RandomSelector
+ val uns = system.actorOf(Props(classOf[UniqueNumberSystem], guid, UniqueNumberSystemTest.AllocateNumberPoolActors(guid)), "uns")
+ val testObj = new EntityTestClass()
+ assert(!testObj.HasGUID)
+ assert(src.CountUsed == 0)
+
+ uns ! Register(testObj, "pool1")
+ val msg1 = receiveOne(Duration.create(100, "ms"))
+ assert(msg1.isInstanceOf[Success[_]])
+ assert(testObj.HasGUID)
+ assert(src.CountUsed == 1)
+
+ val id = testObj.GUID.guid
+ uns ! Register(testObj, "pool2") //different pool; makes no difference
+ val msg2 = receiveOne(Duration.create(100, "ms"))
+ assert(msg2.isInstanceOf[Success[_]])
+ assert(testObj.HasGUID)
+ assert(src.CountUsed == 1)
+ assert(testObj.GUID.guid == id) //unchanged
+ }
+ }
+ //a log.warn should have been generated during this test
+}
+
+class UniqueNumberSystemTest3 extends ActorTest(ActorSystem("test")) {
+ class EntityTestClass extends IdentifiableEntity
+
+ "UniqueNumberSystem" should {
+ "Register (failure; no pool)" in {
+ val src : LimitedNumberSource = LimitedNumberSource(6000)
+ val guid : NumberPoolHub = new NumberPoolHub(src)
+ guid.AddPool("pool1", (1001 to 2000).toList).Selector = new RandomSelector
+ guid.AddPool("pool2", (3001 to 4000).toList).Selector = new RandomSelector
+ guid.AddPool("pool3", (5001 to 6000).toList).Selector = new RandomSelector
+ val uns = system.actorOf(Props(classOf[UniqueNumberSystem], guid, UniqueNumberSystemTest.AllocateNumberPoolActors(guid)), "uns")
+ val testObj = new EntityTestClass()
+ assert(!testObj.HasGUID)
+ assert(src.CountUsed == 0)
+
+ uns ! Register(testObj, "pool4")
+ val msg1 = receiveOne(Duration.create(100, "ms"))
+ assert(msg1.isInstanceOf[Failure[_]])
+ assert(!testObj.HasGUID)
+ assert(src.CountUsed == 0)
+ }
+ }
+}
+
+class UniqueNumberSystemTest4 extends ActorTest(ActorSystem("test")) {
+ class EntityTestClass extends IdentifiableEntity
+
+ "UniqueNumberSystem" should {
+ "Register (failure; empty pool)" in {
+ val src : LimitedNumberSource = LimitedNumberSource(6000)
+ val guid : NumberPoolHub = new NumberPoolHub(src)
+ guid.AddPool("pool1", (1001 to 2000).toList).Selector = new RandomSelector
+ guid.AddPool("pool2", (3001 to 4000).toList).Selector = new RandomSelector
+ guid.AddPool("pool3", (5001 to 6000).toList).Selector = new RandomSelector
+ guid.AddPool("pool4", 50 :: Nil).Selector = new RandomSelector //list of one element; can not add an empty list
+ val uns = system.actorOf(Props(classOf[UniqueNumberSystem], guid, UniqueNumberSystemTest.AllocateNumberPoolActors(guid)), "uns")
+
+ val testObj1 = new EntityTestClass()
+ uns ! Register(testObj1, "pool4")
+ val msg1 = receiveOne(Duration.create(100, "ms"))
+ assert(msg1.isInstanceOf[Success[_]]) //pool4 is now empty
+
+ val testObj2 = new EntityTestClass()
+ uns ! Register(testObj2, "pool4")
+ val msg2 = receiveOne(Duration.create(100, "ms"))
+ assert(msg2.isInstanceOf[Failure[_]])
+ }
+ }
+}
+
+class UniqueNumberSystemTest5 extends ActorTest(ActorSystem("test")) {
+ class EntityTestClass extends IdentifiableEntity
+
+ "UniqueNumberSystem" should {
+ "Unregister (success)" in {
+ val src : LimitedNumberSource = LimitedNumberSource(6000)
+ val guid : NumberPoolHub = new NumberPoolHub(src)
+ val pool2 = (3001 to 4000).toList
+ guid.AddPool("pool1", (1001 to 2000).toList).Selector = new RandomSelector
+ guid.AddPool("pool2", pool2).Selector = new RandomSelector
+ guid.AddPool("pool3", (5001 to 6000).toList).Selector = new RandomSelector
+ val uns = system.actorOf(Props(classOf[UniqueNumberSystem], guid, UniqueNumberSystemTest.AllocateNumberPoolActors(guid)), "uns")
+ val testObj = new EntityTestClass()
+ assert(!testObj.HasGUID)
+ assert(src.CountUsed == 0)
+
+ uns ! Register(testObj, "pool2")
+ val msg1 = receiveOne(Duration.create(100, "ms"))
+ assert(msg1.isInstanceOf[Success[_]])
+ assert(testObj.HasGUID)
+ assert(pool2.contains(testObj.GUID.guid))
+ assert(src.CountUsed == 1)
+
+ uns ! Unregister(testObj)
+ val msg2 = receiveOne(Duration.create(100, "ms"))
+ assert(msg2.isInstanceOf[Success[_]])
+ assert(!testObj.HasGUID)
+ assert(src.CountUsed == 0)
+ }
+ }
+}
+
+class UniqueNumberSystemTest6 extends ActorTest(ActorSystem("test")) {
+ class EntityTestClass extends IdentifiableEntity
+
+ "UniqueNumberSystem" should {
+ "Unregister (success; object not registered at all)" in {
+ val src : LimitedNumberSource = LimitedNumberSource(6000)
+ val guid : NumberPoolHub = new NumberPoolHub(src)
+ guid.AddPool("pool1", (1001 to 2000).toList).Selector = new RandomSelector
+ guid.AddPool("pool2", (3001 to 4000).toList).Selector = new RandomSelector
+ guid.AddPool("pool3", (5001 to 6000).toList).Selector = new RandomSelector
+ val uns = system.actorOf(Props(classOf[UniqueNumberSystem], guid, UniqueNumberSystemTest.AllocateNumberPoolActors(guid)), "uns")
+ val testObj = new EntityTestClass()
+ assert(!testObj.HasGUID)
+ assert(src.CountUsed == 0)
+
+ uns ! Unregister(testObj)
+ val msg1 = receiveOne(Duration.create(100, "ms"))
+ assert(msg1.isInstanceOf[Success[_]])
+ assert(!testObj.HasGUID)
+ assert(src.CountUsed == 0)
+ }
+ }
+}
+
+class UniqueNumberSystemTest7 extends ActorTest(ActorSystem("test")) {
+ class EntityTestClass extends IdentifiableEntity
+
+ "UniqueNumberSystem" should {
+ "Unregister (failure; number not in system)" in {
+ val src : LimitedNumberSource = LimitedNumberSource(6000)
+ val guid : NumberPoolHub = new NumberPoolHub(src)
+ guid.AddPool("pool1", (1001 to 2000).toList).Selector = new RandomSelector
+ guid.AddPool("pool2", (3001 to 4000).toList).Selector = new RandomSelector
+ guid.AddPool("pool3", (5001 to 6000).toList).Selector = new RandomSelector
+ val uns = system.actorOf(Props(classOf[UniqueNumberSystem], guid, UniqueNumberSystemTest.AllocateNumberPoolActors(guid)), "uns")
+ val testObj = new EntityTestClass()
+ testObj.GUID = net.psforever.packet.game.PlanetSideGUID(6001) //fake registering; number too high
+ assert(testObj.HasGUID)
+ assert(src.CountUsed == 0)
+
+ uns ! Unregister(testObj)
+ val msg1 = receiveOne(Duration.create(100, "ms"))
+ assert(msg1.isInstanceOf[Failure[_]])
+ assert(testObj.HasGUID)
+ assert(src.CountUsed == 0)
+ }
+ }
+}
+
+class UniqueNumberSystemTest8 extends ActorTest(ActorSystem("test")) {
+ class EntityTestClass extends IdentifiableEntity
+
+ "UniqueNumberSystem" should {
+ "Unregister (failure; object is not registered to that number)" in {
+ val src : LimitedNumberSource = LimitedNumberSource(6000)
+ val guid : NumberPoolHub = new NumberPoolHub(src)
+ guid.AddPool("pool1", (1001 to 2000).toList).Selector = new RandomSelector
+ guid.AddPool("pool2", (3001 to 4000).toList).Selector = new RandomSelector
+ guid.AddPool("pool3", (5001 to 6000).toList).Selector = new RandomSelector
+ val uns = system.actorOf(Props(classOf[UniqueNumberSystem], guid, UniqueNumberSystemTest.AllocateNumberPoolActors(guid)), "uns")
+ val testObj = new EntityTestClass()
+ testObj.GUID = net.psforever.packet.game.PlanetSideGUID(3500) //fake registering
+ assert(testObj.HasGUID)
+ assert(src.CountUsed == 0)
+
+ uns ! Unregister(testObj)
+ val msg1 = receiveOne(Duration.create(100, "ms"))
+ assert(msg1.isInstanceOf[Failure[_]])
+ assert(testObj.HasGUID)
+ assert(src.CountUsed == 0)
+ }
+ }
+}
+
+object UniqueNumberSystemTest {
+ /**
+ * @see `UniqueNumberSystem.AllocateNumberPoolActors(NumberPoolHub)(implicit ActorContext)`
+ */
+ def AllocateNumberPoolActors(poolSource : NumberPoolHub)(implicit system : ActorSystem) : Map[String, ActorRef] = {
+ poolSource.Pools.map({ case ((pname, pool)) =>
+ pname -> system.actorOf(Props(classOf[NumberPoolActor], pool), pname)
+ }).toMap
+ }
+}
+
diff --git a/pslogin/src/main/scala/AvatarService.scala b/pslogin/src/main/scala/AvatarService.scala
deleted file mode 100644
index 377a92496..000000000
--- a/pslogin/src/main/scala/AvatarService.scala
+++ /dev/null
@@ -1,228 +0,0 @@
-// Copyright (c) 2016 PSForever.net to present
-import akka.actor.Actor
-import akka.event.{ActorEventBus, SubchannelClassification}
-import akka.util.Subclassification
-import net.psforever.objects.equipment.Equipment
-import net.psforever.packet.game.objectcreate.ConstructorData
-import net.psforever.types.ExoSuitType
-import net.psforever.packet.game.{PlanetSideGUID, PlayerStateMessageUpstream}
-import net.psforever.types.Vector3
-
-sealed trait Action
-
-sealed trait Response
-
-final case class Join(channel : String)
-final case class Leave()
-final case class LeaveAll()
-
-object AvatarAction {
- final case class ArmorChanged(player_guid : PlanetSideGUID, suit : ExoSuitType.Value, subtype : Int) extends Action
- //final case class DropItem(pos : Vector3, orient : Vector3, item : PlanetSideGUID) extends Action
- final case class EquipmentInHand(player_guid : PlanetSideGUID, slot : Int, item : Equipment) extends Action
- final case class EquipmentOnGround(player_guid : PlanetSideGUID, pos : Vector3, orient : Vector3, item : Equipment) extends Action
- final case class LoadPlayer(player_guid : PlanetSideGUID, pdata : ConstructorData) extends Action
-// final case class LoadMap(msg : PlanetSideGUID) extends Action
-// final case class unLoadMap(msg : PlanetSideGUID) extends Action
- final case class ObjectDelete(player_guid : PlanetSideGUID, item_guid : PlanetSideGUID, unk : Int = 0) extends Action
- final case class ObjectHeld(player_guid : PlanetSideGUID, slot : Int) extends Action
- final case class PlanetsideAttribute(player_guid : PlanetSideGUID, attribute_type : Int, attribute_value : Long) extends Action
- final case class PlayerState(player_guid : PlanetSideGUID, msg : PlayerStateMessageUpstream, spectator : Boolean, weaponInHand : Boolean) extends Action
- final case class Reload(player_guid : PlanetSideGUID, mag : Int) extends Action
-// final case class PlayerStateShift(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action
-// final case class DestroyDisplay(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action
-// final case class HitHintReturn(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action
-// final case class ChangeWeapon(unk1 : Int, sessionId : Long) extends Action
-}
-
-object AvatarServiceResponse {
- final case class ArmorChanged(suit : ExoSuitType.Value, subtype : Int) extends Response
- //final case class DropItem(pos : Vector3, orient : Vector3, item : PlanetSideGUID) extends Response
- final case class EquipmentInHand(slot : Int, item : Equipment) extends Response
- final case class EquipmentOnGround(pos : Vector3, orient : Vector3, item : Equipment) extends Response
- final case class LoadPlayer(pdata : ConstructorData) extends Response
-// final case class unLoadMap() extends Response
-// final case class LoadMap() extends Response
- final case class ObjectDelete(item_guid : PlanetSideGUID, unk : Int) extends Response
- final case class ObjectHeld(slot : Int) extends Response
- final case class PlanetSideAttribute(attribute_type : Int, attribute_value : Long) extends Response
- final case class PlayerState(msg : PlayerStateMessageUpstream, spectator : Boolean, weaponInHand : Boolean) extends Response
- final case class Reload(mag : Int) extends Response
-// final case class PlayerStateShift(itemID : PlanetSideGUID) extends Response
-// final case class DestroyDisplay(itemID : PlanetSideGUID) extends Response
-// final case class HitHintReturn(itemID : PlanetSideGUID) extends Response
-// final case class ChangeWeapon(facingYaw : Int) extends Response
-}
-
-final case class AvatarServiceMessage(forChannel : String, actionMessage : Action)
-
-final case class AvatarServiceResponse(toChannel : String, avatar_guid : PlanetSideGUID, replyMessage : Response)
-
-/*
- /avatar/
- */
-
-class AvatarEventBus extends ActorEventBus with SubchannelClassification {
- type Event = AvatarServiceResponse
- type Classifier = String
-
- protected def classify(event: Event): Classifier = event.toChannel
-
- protected def subclassification = new Subclassification[Classifier] {
- def isEqual(x: Classifier, y: Classifier) = x == y
- def isSubclass(x: Classifier, y: Classifier) = x.startsWith(y)
- }
-
- protected def publish(event: Event, subscriber: Subscriber): Unit = {
- subscriber ! event
- }
-}
-
-class AvatarService extends Actor {
- //import AvatarServiceResponse._
- private [this] val log = org.log4s.getLogger
-
- override def preStart = {
- log.info("Starting...")
- }
-
- val AvatarEvents = new AvatarEventBus
-
- /*val channelMap = Map(
- AvatarMessageType.CMT_OPEN -> AvatarPath("local")
- )*/
-
- def receive = {
- case Join(channel) =>
- val path = "/Avatar/" + channel
- val who = sender()
-
- log.info(s"$who has joined $path")
-
- AvatarEvents.subscribe(who, path)
- case Leave() =>
- AvatarEvents.unsubscribe(sender())
- case LeaveAll() =>
- AvatarEvents.unsubscribe(sender())
-
- case AvatarServiceMessage(forChannel, action) =>
- action match {
- case AvatarAction.ArmorChanged(player_guid, suit, subtype) =>
- AvatarEvents.publish(
- AvatarServiceResponse("/Avatar/" + forChannel, player_guid, AvatarServiceResponse.ArmorChanged(suit, subtype))
- )
- case AvatarAction.EquipmentInHand(player_guid, slot, obj) =>
- AvatarEvents.publish(
- AvatarServiceResponse("/Avatar/" + forChannel, player_guid, AvatarServiceResponse.EquipmentInHand(slot, obj))
- )
- case AvatarAction.EquipmentOnGround(player_guid, pos, orient, obj) =>
- AvatarEvents.publish(
- AvatarServiceResponse("/Avatar/" + forChannel, player_guid, AvatarServiceResponse.EquipmentOnGround(pos, orient, obj))
- )
- case AvatarAction.LoadPlayer(player_guid, pdata) =>
- AvatarEvents.publish(
- AvatarServiceResponse("/Avatar/" + forChannel, player_guid, AvatarServiceResponse.LoadPlayer(pdata))
- )
- case AvatarAction.ObjectDelete(player_guid, item_guid, unk) =>
- AvatarEvents.publish(
- AvatarServiceResponse("/Avatar/" + forChannel, player_guid, AvatarServiceResponse.ObjectDelete(item_guid, unk))
- )
- case AvatarAction.ObjectHeld(player_guid, slot) =>
- AvatarEvents.publish(
- AvatarServiceResponse("/Avatar/" + forChannel, player_guid, AvatarServiceResponse.ObjectHeld(slot))
- )
- case AvatarAction.PlanetsideAttribute(guid, attribute_type, attribute_value) =>
- AvatarEvents.publish(
- AvatarServiceResponse("/Avatar/" + forChannel, guid, AvatarServiceResponse.PlanetSideAttribute(attribute_type, attribute_value))
- )
- case AvatarAction.PlayerState(guid, msg, spectator, weapon) =>
- AvatarEvents.publish(
- AvatarServiceResponse("/Avatar/" + forChannel, guid, AvatarServiceResponse.PlayerState(msg, spectator, weapon))
- )
- case AvatarAction.Reload(player_guid, mag) =>
- AvatarEvents.publish(
- AvatarServiceResponse("/Avatar/" + forChannel, player_guid, AvatarServiceResponse.Reload(mag))
- )
- case _ => ;
- }
-
- /*
- case AvatarService.PlayerStateMessage(msg) =>
- // log.info(s"NEW: ${m}")
- val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(msg.avatar_guid)
- if (playerOpt.isDefined) {
- val player: PlayerAvatar = playerOpt.get
- AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, msg.avatar_guid,
- AvatarServiceReply.PlayerStateMessage(msg.pos, msg.vel, msg.facingYaw, msg.facingPitch, msg.facingYawUpper, msg.is_crouching, msg.is_jumping, msg.jump_thrust, msg.is_cloaked)
- ))
-
- }
- case AvatarService.LoadMap(msg) =>
- val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(msg.guid)
- if (playerOpt.isDefined) {
- val player: PlayerAvatar = playerOpt.get
- AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, PlanetSideGUID(msg.guid),
- AvatarServiceReply.LoadMap()
- ))
- }
- case AvatarService.unLoadMap(msg) =>
- val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(msg.guid)
- if (playerOpt.isDefined) {
- val player: PlayerAvatar = playerOpt.get
- AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, PlanetSideGUID(msg.guid),
- AvatarServiceReply.unLoadMap()
- ))
- }
- case AvatarService.ObjectHeld(msg) =>
- val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(msg.guid)
- if (playerOpt.isDefined) {
- val player: PlayerAvatar = playerOpt.get
- AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, PlanetSideGUID(msg.guid),
- AvatarServiceReply.ObjectHeld()
- ))
- }
- case AvatarService.PlanetsideAttribute(guid, attribute_type, attribute_value) =>
- val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(guid)
- if (playerOpt.isDefined) {
- val player: PlayerAvatar = playerOpt.get
- AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, guid,
- AvatarServiceReply.PlanetSideAttribute(attribute_type, attribute_value)
- ))
- }
- case AvatarService.PlayerStateShift(killer, guid) =>
- val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(guid)
- if (playerOpt.isDefined) {
- val player: PlayerAvatar = playerOpt.get
- AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, guid,
- AvatarServiceReply.PlayerStateShift(killer)
- ))
- }
- case AvatarService.DestroyDisplay(killer, victim) =>
- val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(victim)
- if (playerOpt.isDefined) {
- val player: PlayerAvatar = playerOpt.get
- AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, victim,
- AvatarServiceReply.DestroyDisplay(killer)
- ))
- }
- case AvatarService.HitHintReturn(source_guid,victim_guid) =>
- val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(source_guid)
- if (playerOpt.isDefined) {
- val player: PlayerAvatar = playerOpt.get
- AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, victim_guid,
- AvatarServiceReply.DestroyDisplay(source_guid)
- ))
- }
- case AvatarService.ChangeWeapon(unk1, sessionId) =>
- val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(sessionId)
- if (playerOpt.isDefined) {
- val player: PlayerAvatar = playerOpt.get
- AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, PlanetSideGUID(player.guid),
- AvatarServiceReply.ChangeWeapon(unk1)
- ))
- }
- */
- case msg =>
- log.info(s"Unhandled message $msg from $sender")
- }
-}
diff --git a/pslogin/src/main/scala/PsLogin.scala b/pslogin/src/main/scala/PsLogin.scala
index c1403fc43..6e172cbe8 100644
--- a/pslogin/src/main/scala/PsLogin.scala
+++ b/pslogin/src/main/scala/PsLogin.scala
@@ -3,7 +3,7 @@ import java.net.InetAddress
import java.io.File
import java.util.Locale
-import akka.actor.{ActorRef, ActorSystem, Props}
+import akka.actor.{ActorContext, ActorRef, ActorSystem, Props}
import akka.routing.RandomPool
import ch.qos.logback.classic.LoggerContext
import ch.qos.logback.classic.joran.JoranConfigurator
@@ -12,11 +12,14 @@ import ch.qos.logback.core.status._
import ch.qos.logback.core.util.StatusPrinter
import com.typesafe.config.ConfigFactory
import net.psforever.crypto.CryptoInterface
-import net.psforever.objects.zones.{InterstellarCluster, TerminalObjectBuilder, Zone, ZoneMap}
+import net.psforever.objects.zones._
import net.psforever.objects.guid.TaskResolver
+import net.psforever.objects.serverobject.builders.{DoorObjectBuilder, IFFLockObjectBuilder, TerminalObjectBuilder}
import org.slf4j
import org.fusesource.jansi.Ansi._
import org.fusesource.jansi.Ansi.Color._
+import services.avatar._
+import services.local._
import scala.collection.JavaConverters._
import scala.concurrent.Await
@@ -202,6 +205,7 @@ object PsLogin {
val serviceManager = ServiceManager.boot
serviceManager ! ServiceManager.Register(RandomPool(50).props(Props[TaskResolver]), "taskResolver")
serviceManager ! ServiceManager.Register(Props[AvatarService], "avatar")
+ serviceManager ! ServiceManager.Register(Props[LocalService], "local")
serviceManager ! ServiceManager.Register(Props(classOf[InterstellarCluster], createContinents()), "galaxy")
/** Create two actors for handling the login and world server endpoints */
@@ -222,11 +226,38 @@ object PsLogin {
def createContinents() : List[Zone] = {
val map13 = new ZoneMap("map13") {
import net.psforever.objects.GlobalDefinitions._
- LocalObject(TerminalObjectBuilder(orderTerminal, 853))
- LocalObject(TerminalObjectBuilder(orderTerminal, 855))
- LocalObject(TerminalObjectBuilder(orderTerminal, 860))
+
+ LocalObject(DoorObjectBuilder(door, 330))
+ LocalObject(DoorObjectBuilder(door, 332))
+ LocalObject(DoorObjectBuilder(door, 372))
+ LocalObject(DoorObjectBuilder(door, 373))
+ LocalObject(IFFLockObjectBuilder(lock_external, 556))
+ LocalObject(IFFLockObjectBuilder(lock_external, 558))
+ LocalObject(TerminalObjectBuilder(cert_terminal, 186))
+ LocalObject(TerminalObjectBuilder(cert_terminal, 187))
+ LocalObject(TerminalObjectBuilder(cert_terminal, 188))
+ LocalObject(TerminalObjectBuilder(order_terminal, 853))
+ LocalObject(TerminalObjectBuilder(order_terminal, 855))
+ LocalObject(TerminalObjectBuilder(order_terminal, 860))
+
+ LocalBases = 30
+
+ ObjectToBase(330, 29)
+ ObjectToBase(332, 29)
+ ObjectToBase(556, 29)
+ ObjectToBase(558, 29)
+ DoorToLock(330, 558)
+ DoorToLock(332, 556)
+ }
+ val home3 = new Zone("home3", map13, 13) {
+ override def Init(implicit context : ActorContext) : Unit = {
+ super.Init(context)
+
+ import net.psforever.types.PlanetSideEmpire
+ Base(2).get.Faction = PlanetSideEmpire.VS //HART building C
+ Base(29).get.Faction = PlanetSideEmpire.NC //South Villa Gun Tower
+ }
}
- val home3 = Zone("home3", map13, 13)
home3 ::
Nil
diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala
index 87bbdde91..c202a572f 100644
--- a/pslogin/src/main/scala/WorldSessionActor.scala
+++ b/pslogin/src/main/scala/WorldSessionActor.scala
@@ -2,24 +2,30 @@
import java.util.concurrent.atomic.AtomicInteger
import akka.actor.{Actor, ActorRef, Cancellable, MDCContextAware}
-import net.psforever.packet.{PlanetSideGamePacket, _}
+import net.psforever.packet._
import net.psforever.packet.control._
-import net.psforever.packet.game.{ObjectCreateDetailedMessage, _}
+import net.psforever.packet.game._
import scodec.Attempt.{Failure, Successful}
import scodec.bits._
import org.log4s.MDC
import MDCContextAware.Implicits._
import ServiceManager.Lookup
import net.psforever.objects._
+import net.psforever.objects.serverobject.doors.Door
import net.psforever.objects.zones.{InterstellarCluster, Zone}
import net.psforever.objects.entity.IdentifiableEntity
import net.psforever.objects.equipment._
import net.psforever.objects.guid.{Task, TaskResolver}
import net.psforever.objects.guid.actor.{Register, Unregister}
import net.psforever.objects.inventory.{GridInventory, InventoryItem}
-import net.psforever.objects.terminals.Terminal
+import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject}
+import net.psforever.objects.serverobject.locks.IFFLock
+import net.psforever.objects.serverobject.terminals.Terminal
import net.psforever.packet.game.objectcreate._
import net.psforever.types._
+import services._
+import services.avatar._
+import services.local._
import scala.annotation.tailrec
import scala.util.Success
@@ -31,18 +37,22 @@ class WorldSessionActor extends Actor with MDCContextAware {
var sessionId : Long = 0
var leftRef : ActorRef = ActorRef.noSender
var rightRef : ActorRef = ActorRef.noSender
- var avatarService = Actor.noSender
- var taskResolver = Actor.noSender
- var galaxy = Actor.noSender
+ var avatarService : ActorRef = ActorRef.noSender
+ var localService : ActorRef = ActorRef.noSender
+ var taskResolver : ActorRef = Actor.noSender
+ var galaxy : ActorRef = Actor.noSender
var continent : Zone = null
+ var progressBarValue : Option[Float] = None
var clientKeepAlive : Cancellable = WorldSessionActor.DefaultCancellable
+ var progressBarUpdate : Cancellable = WorldSessionActor.DefaultCancellable
override def postStop() = {
if(clientKeepAlive != null)
clientKeepAlive.cancel()
- avatarService ! Leave()
+ avatarService ! Service.Leave()
+ localService ! Service.Leave()
LivePlayerList.Remove(sessionId) match {
case Some(tplayer) =>
if(tplayer.HasGUID) {
@@ -64,11 +74,13 @@ class WorldSessionActor extends Actor with MDCContextAware {
if(pipe.hasNext) {
rightRef = pipe.next
rightRef !> HelloFriend(sessionId, pipe)
- } else {
+ }
+ else {
rightRef = sender()
}
context.become(Started)
ServiceManager.serviceManager ! Lookup("avatar")
+ ServiceManager.serviceManager ! Lookup("local")
ServiceManager.serviceManager ! Lookup("taskResolver")
ServiceManager.serviceManager ! Lookup("galaxy")
@@ -81,6 +93,9 @@ class WorldSessionActor extends Actor with MDCContextAware {
case ServiceManager.LookupResult("avatar", endpoint) =>
avatarService = endpoint
log.info("ID: " + sessionId + " Got avatar service " + endpoint)
+ case ServiceManager.LookupResult("local", endpoint) =>
+ localService = endpoint
+ log.info("ID: " + sessionId + " Got local service " + endpoint)
case ServiceManager.LookupResult("taskResolver", endpoint) =>
taskResolver = endpoint
log.info("ID: " + sessionId + " Got task resolver service " + endpoint)
@@ -206,6 +221,48 @@ class WorldSessionActor extends Actor with MDCContextAware {
case _ => ;
}
+ case LocalServiceResponse(_, guid, reply) =>
+ reply match {
+ case LocalServiceResponse.DoorOpens(door_guid) =>
+ if(player.GUID != guid) {
+ sendResponse(PacketCoding.CreateGamePacket(0, GenericObjectStateMsg(door_guid, 16)))
+ }
+
+ case LocalServiceResponse.DoorCloses(door_guid) => //door closes for everyone
+ sendResponse(PacketCoding.CreateGamePacket(0, GenericObjectStateMsg(door_guid, 17)))
+
+ case LocalServiceResponse.HackClear(target_guid, unk1, unk2) =>
+ sendResponse(PacketCoding.CreateGamePacket(0, HackMessage(0, target_guid, guid, 0, unk1, HackState.HackCleared, unk2)))
+
+ case LocalServiceResponse.HackObject(target_guid, unk1, unk2) =>
+ if(player.GUID != guid) {
+ sendResponse(PacketCoding.CreateGamePacket(0, HackMessage(0, target_guid, guid, 100, unk1, HackState.Hacked, unk2)))
+ }
+
+ case LocalServiceResponse.TriggerSound(sound, pos, unk, volume) =>
+ sendResponse(PacketCoding.CreateGamePacket(0, TriggerSoundMessage(sound, pos, unk, volume)))
+ }
+
+ case Door.DoorMessage(tplayer, msg, order) =>
+ val door_guid = msg.object_guid
+ order match {
+ case Door.OpenEvent() =>
+ continent.GUID(door_guid) match {
+ case Some(door : Door) =>
+ sendResponse(PacketCoding.CreateGamePacket(0, GenericObjectStateMsg(door_guid, 16)))
+ localService ! LocalServiceMessage(continent.Id, LocalAction.DoorOpens (tplayer.GUID, continent, door) )
+
+ case _ =>
+ log.warn(s"door $door_guid wanted to be opened but could not be found")
+ }
+
+ case Door.CloseEvent() =>
+ sendResponse(PacketCoding.CreateGamePacket(0, GenericObjectStateMsg(door_guid, 17)))
+ localService ! LocalServiceMessage(continent.Id, LocalAction.DoorCloses(tplayer.GUID, door_guid))
+
+ case Door.NoEvent() => ;
+ }
+
case Terminal.TerminalMessage(tplayer, msg, order) =>
order match {
case Terminal.BuyExosuit(exosuit, subtype) =>
@@ -312,6 +369,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
avatarService ! AvatarServiceMessage(tplayer.Continent, AvatarAction.EquipmentOnGround(tplayer.GUID, pos, orient, obj))
})
}
+ sendResponse(PacketCoding.CreateGamePacket(0, ItemTransactionResultMessage (msg.terminal_guid, TransactionType.Buy, true)))
case Terminal.BuyEquipment(item) => ;
tplayer.Fit(item) match {
@@ -382,6 +440,31 @@ class WorldSessionActor extends Actor with MDCContextAware {
PutEquipmentInSlot(tplayer, entry.obj, entry.start)
})
//TODO drop items on ground
+ sendResponse(PacketCoding.CreateGamePacket(0, ItemTransactionResultMessage (msg.terminal_guid, TransactionType.InfantryLoadout, true)))
+
+ case Terminal.LearnCertification(cert, cost) =>
+ if(!player.Certifications.contains(cert)) {
+ log.info(s"$tplayer is learning the $cert certification for $cost points")
+ tplayer.Certifications += cert
+ sendResponse(PacketCoding.CreateGamePacket(0, PlanetsideAttributeMessage(tplayer.GUID, 24, cert.id.toLong)))
+ sendResponse(PacketCoding.CreateGamePacket(0, ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Learn, true)))
+ }
+ else {
+ log.warn(s"$tplayer already knows the $cert certification, so he can't learn it")
+ sendResponse(PacketCoding.CreateGamePacket(0, ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Learn, false)))
+ }
+
+ case Terminal.SellCertification(cert, cost) =>
+ if(player.Certifications.contains(cert)) {
+ log.info(s"$tplayer is forgetting the $cert certification for $cost points")
+ tplayer.Certifications -= cert
+ sendResponse(PacketCoding.CreateGamePacket(0, PlanetsideAttributeMessage(tplayer.GUID, 25, cert.id.toLong)))
+ sendResponse(PacketCoding.CreateGamePacket(0, ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Sell, true)))
+ }
+ else {
+ log.warn(s"$tplayer doesn't know what a $cert certification is, so he can't forget it")
+ sendResponse(PacketCoding.CreateGamePacket(0, ItemTransactionResultMessage(msg.terminal_guid, TransactionType.Learn, false)))
+ }
case Terminal.NoDeal() =>
log.warn(s"$tplayer made a request but the terminal rejected the order $msg")
@@ -389,7 +472,9 @@ class WorldSessionActor extends Actor with MDCContextAware {
}
case ListAccountCharacters =>
+ import net.psforever.objects.definition.converter.CharacterSelectConverter
val gen : AtomicInteger = new AtomicInteger(1)
+ val converter : CharacterSelectConverter = new CharacterSelectConverter
//load characters
SetCharacterSelectScreenGUID(player, gen)
@@ -398,7 +483,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
val armor = player.Armor
player.Spawn
sendResponse(PacketCoding.CreateGamePacket(0,
- ObjectCreateMessage(ObjectClass.avatar, player.GUID, player.Definition.Packet.ConstructorData(player).get)
+ ObjectCreateDetailedMessage(ObjectClass.avatar, player.GUID, converter.DetailedConstructorData(player).get)
))
if(health > 0) { //player can not be dead; stay spawned as alive
player.Health = health
@@ -477,6 +562,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
LivePlayerList.Assign(continent.Number, sessionId, guid)
sendResponse(PacketCoding.CreateGamePacket(0, SetCurrentAvatarMessage(guid,0,0)))
sendResponse(PacketCoding.CreateGamePacket(0, CreateShortcutMessage(guid, 1, 0, true, Shortcut.MEDKIT)))
+ sendResponse(PacketCoding.CreateGamePacket(0, ChatMsg(ChatMessageType.CMT_EXPANSIONS, true, "", "1 on", None))) //CC on
case Zone.ItemFromGround(tplayer, item) =>
val obj_guid = item.GUID
@@ -503,6 +589,35 @@ class WorldSessionActor extends Actor with MDCContextAware {
continent.Actor ! Zone.DropItemOnGround(item, item.Position, item.Orientation) //restore
}
+ case ItemHacking(tplayer, target, tool_guid, delta, completeAction, tickAction) =>
+ progressBarUpdate.cancel
+ if(progressBarValue.isDefined) {
+ val progressBarVal : Float = progressBarValue.get + delta
+ val vis = if(progressBarVal == 0L) { //hack state for progress bar visibility
+ HackState.Start
+ }
+ else if(progressBarVal > 100L) {
+ HackState.Finished
+ }
+ else {
+ HackState.Ongoing
+ }
+ sendResponse(PacketCoding.CreateGamePacket(0, HackMessage(1, target.GUID, player.GUID, progressBarVal.toInt, 0L, vis, 8L)))
+ if(progressBarVal > 100) { //done
+ progressBarValue = None
+ log.info(s"Hacked a $target")
+ sendResponse(PacketCoding.CreateGamePacket(0, HackMessage(0, target.GUID, player.GUID, 100, 1114636288L, HackState.Hacked, 8L)))
+ completeAction()
+ }
+ else { //continue next tick
+ tickAction.getOrElse(() => Unit)()
+ progressBarValue = Some(progressBarVal)
+ import scala.concurrent.duration._
+ import scala.concurrent.ExecutionContext.Implicits.global
+ progressBarUpdate = context.system.scheduler.scheduleOnce(250 milliseconds, self, ItemHacking(tplayer, target, tool_guid, delta, completeAction))
+ }
+ }
+
case ResponseToSelf(pkt) =>
log.info(s"Received a direct message: $pkt")
sendResponse(pkt)
@@ -625,6 +740,11 @@ class WorldSessionActor extends Actor with MDCContextAware {
log.info(s"New world login to $server with Token:$token. $clientVersion")
self ! ListAccountCharacters
+ import scala.concurrent.duration._
+ import scala.concurrent.ExecutionContext.Implicits.global
+ clientKeepAlive.cancel
+ clientKeepAlive = context.system.scheduler.schedule(0 seconds, 500 milliseconds, self, PokeClient())
+
case msg @ CharacterCreateRequestMessage(name, head, voice, gender, empire) =>
log.info("Handling " + msg)
sendResponse(PacketCoding.CreateGamePacket(0, ActionResultMessage(true, None)))
@@ -641,11 +761,6 @@ class WorldSessionActor extends Actor with MDCContextAware {
//TODO if yes, get continent guid accessors
//TODO if no, get sanctuary guid accessors and reset the player's expectations
galaxy ! InterstellarCluster.GetWorld("home3")
-
- import scala.concurrent.duration._
- import scala.concurrent.ExecutionContext.Implicits.global
- clientKeepAlive.cancel
- clientKeepAlive = context.system.scheduler.schedule(0 seconds, 500 milliseconds, self, PokeClient())
case default =>
log.error("Unsupported " + default + " in " + msg)
}
@@ -672,7 +787,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
)
})
//render Equipment that was dropped into zone before the player arrived
- continent.EquipmentOnGround.toList.foreach(item => {
+ continent.EquipmentOnGround.foreach(item => {
val definition = item.Definition
sendResponse(
PacketCoding.CreateGamePacket(0,
@@ -685,7 +800,8 @@ class WorldSessionActor extends Actor with MDCContextAware {
)
})
- avatarService ! Join(player.Continent)
+ avatarService ! Service.Join(player.Continent)
+ localService ! Service.Join(player.Continent)
self ! SetCurrentAvatar(player)
case msg @ PlayerStateMessageUpstream(avatar_guid, pos, vel, yaw, pitch, yaw_upper, seq_time, unk3, is_crouching, is_jumping, unk4, is_cloaking, unk5, unk6) =>
@@ -747,6 +863,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
case msg @ ChangeFireStateMessage_Stop(item_guid) =>
log.info("ChangeFireState_Stop: " + msg)
+ progressBarUpdate.cancel
case msg @ EmoteMsg(avatar_guid, emote) =>
log.info("Emote: " + msg)
@@ -916,15 +1033,53 @@ class WorldSessionActor extends Actor with MDCContextAware {
log.info("UseItem: " + msg)
// TODO: Not all fields in the response are identical to source in real packet logs (but seems to be ok)
// TODO: Not all incoming UseItemMessage's respond with another UseItemMessage (i.e. doors only send out GenericObjectStateMsg)
- if (itemType != 121) sendResponse(PacketCoding.CreateGamePacket(0, UseItemMessage(avatar_guid, unk1, object_guid, unk2, unk3, unk4, unk5, unk6, unk7, unk8, itemType)))
- if (itemType == 121 && !unk3){ // TODO : medkit use ?!
- sendResponse(PacketCoding.CreateGamePacket(0, UseItemMessage(avatar_guid, unk1, object_guid, 0, unk3, unk4, unk5, unk6, unk7, unk8, itemType)))
- sendResponse(PacketCoding.CreateGamePacket(0, PlanetsideAttributeMessage(avatar_guid, 0, 100))) // avatar with 100 hp
- sendResponse(PacketCoding.CreateGamePacket(0, ObjectDeleteMessage(PlanetSideGUID(unk1), 2)))
- }
- if (unk1 == 0 && !unk3 && unk7 == 25) {
- // TODO: This should only actually be sent to doors upon opening; may break non-door items upon use
- sendResponse(PacketCoding.CreateGamePacket(0, GenericObjectStateMsg(object_guid, 16)))
+ continent.GUID(object_guid) match {
+ case Some(door : Door) =>
+ continent.Map.DoorToLock.get(object_guid.guid) match { //check for IFF Lock
+ case Some(lock_guid) =>
+ val lock_hacked = continent.GUID(lock_guid).get.asInstanceOf[IFFLock].HackedBy match {
+ case Some((tplayer, _, _)) =>
+ tplayer.Faction == player.Faction
+ case None =>
+ false
+ }
+ continent.Map.ObjectToBase.get(lock_guid) match { //check for associated base
+ case Some(base_id) =>
+ if(continent.Base(base_id).get.Faction == player.Faction || lock_hacked) { //either base allegiance aligns or locks is hacked
+ door.Actor ! Door.Use(player, msg)
+ }
+ case None =>
+ if(lock_hacked) { //is lock hacked? this may be a weird case
+ door.Actor ! Door.Use(player, msg)
+ }
+ }
+ case None =>
+ door.Actor ! Door.Use(player, msg) //let door open freely
+ }
+
+ case Some(panel : IFFLock) =>
+ player.Slot(player.DrawnSlot).Equipment match {
+ case Some(tool : SimpleItem) =>
+ if(tool.Definition == GlobalDefinitions.remote_electronics_kit) {
+ //TODO get player hack level (for now, presume 15s in intervals of 4/s)
+ progressBarValue = Some(-2.66f)
+ self ! WorldSessionActor.ItemHacking(player, panel, tool.GUID, 2.66f, FinishHackingDoor(panel, 1114636288L))
+ log.info("Hacking a door~")
+ }
+ case _ => ;
+ }
+
+ case Some(obj : PlanetSideGameObject) =>
+ if(itemType != 121) {
+ sendResponse(PacketCoding.CreateGamePacket(0, UseItemMessage(avatar_guid, unk1, object_guid, unk2, unk3, unk4, unk5, unk6, unk7, unk8, itemType)))
+ }
+ else if(itemType == 121 && !unk3) { // TODO : medkit use ?!
+ sendResponse(PacketCoding.CreateGamePacket(0, UseItemMessage(avatar_guid, unk1, object_guid, 0, unk3, unk4, unk5, unk6, unk7, unk8, itemType)))
+ sendResponse(PacketCoding.CreateGamePacket(0, PlanetsideAttributeMessage(avatar_guid, 0, 100))) // avatar with 100 hp
+ sendResponse(PacketCoding.CreateGamePacket(0, ObjectDeleteMessage(PlanetSideGUID(unk1), 2)))
+ }
+
+ case None => ;
}
case msg @ UnuseItemMessage(player_guid, item) =>
@@ -1030,6 +1185,9 @@ class WorldSessionActor extends Actor with MDCContextAware {
case msg @ TargetingImplantRequest(list) =>
log.info("TargetingImplantRequest: "+msg)
+ case msg @ ActionCancelMessage(u1, u2, u3) =>
+ log.info("Cancelled: "+msg)
+
case default => log.error(s"Unhandled GamePacket $pkt")
}
@@ -1175,7 +1333,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
}
def Execute(resolver : ActorRef) : Unit = {
- localAccessor ! Register(localObject, resolver)
+ localAccessor ! Register(localObject, "dynamic", resolver)
}
})
}
@@ -1471,6 +1629,22 @@ class WorldSessionActor extends Actor with MDCContextAware {
}
}
+ /**
+ * The process of hacking the `Door` `IFFLock` is completed.
+ * Pass the message onto the lock and onto the local events system.
+ * @param target the `IFFLock` belonging to the door that is being hacked
+ * @param unk na;
+ * used by `HackingMessage` as `unk5`
+ * @see `HackMessage`
+ */
+ //TODO add params here depending on which params in HackMessage are important
+ //TODO sound should be centered on IFFLock, not on player
+ private def FinishHackingDoor(target : IFFLock, unk : Long)() : Unit = {
+ target.Actor ! CommonMessages.Hack(player)
+ localService ! LocalServiceMessage(continent.Id, LocalAction.TriggerSound(player.GUID, TriggeredSound.HackDoor, player.Position, 30, 0.49803925f))
+ localService ! LocalServiceMessage(continent.Id, LocalAction.HackTemporarily(player.GUID, continent, target, unk))
+ }
+
def failWithError(error : String) = {
log.error(error)
sendResponse(PacketCoding.CreateControlPacket(ConnectionClose()))
@@ -1502,6 +1676,23 @@ object WorldSessionActor {
private final case class ListAccountCharacters()
private final case class SetCurrentAvatar(tplayer : Player)
+ /**
+ * A message that indicates the user is using a remote electronics kit to hack some server object.
+ * Each time this message is sent for a given hack attempt counts as a single "tick" of progress.
+ * The process of "making progress" with a hack involves sending this message repeatedly until the progress is 100 or more.
+ * @param tplayer the player
+ * @param target the object being hacked
+ * @param tool_guid the REK
+ * @param delta how much the progress bar value changes each tick
+ * @param completeAction a custom action performed once the hack is completed
+ * @param tickAction an optional action is is performed for each tick of progress
+ */
+ private final case class ItemHacking(tplayer : Player,
+ target : PlanetSideServerObject,
+ tool_guid : PlanetSideGUID,
+ delta : Float,
+ completeAction : () => Unit,
+ tickAction : Option[() => Unit] = None)
/**
* A placeholder `Cancellable` object.
*/
diff --git a/pslogin/src/main/scala/services/Service.scala b/pslogin/src/main/scala/services/Service.scala
new file mode 100644
index 000000000..eed17c79a
--- /dev/null
+++ b/pslogin/src/main/scala/services/Service.scala
@@ -0,0 +1,34 @@
+// Copyright (c) 2017 PSForever
+package services
+
+import akka.event.{ActorEventBus, SubchannelClassification}
+import akka.util.Subclassification
+import net.psforever.packet.game.PlanetSideGUID
+
+object Service {
+ final val defaultPlayerGUID : PlanetSideGUID = PlanetSideGUID(0)
+
+ final case class Join(channel : String)
+ final case class Leave()
+ final case class LeaveAll()
+}
+
+trait GenericEventBusMsg {
+ def toChannel : String
+}
+
+class GenericEventBus[A <: GenericEventBusMsg] extends ActorEventBus with SubchannelClassification {
+ type Event = A
+ type Classifier = String
+
+ protected def classify(event: Event): Classifier = event.toChannel
+
+ protected def subclassification = new Subclassification[Classifier] {
+ def isEqual(x: Classifier, y: Classifier) = x == y
+ def isSubclass(x: Classifier, y: Classifier) = x.startsWith(y)
+ }
+
+ protected def publish(event: Event, subscriber: Subscriber): Unit = {
+ subscriber ! event
+ }
+}
diff --git a/pslogin/src/main/scala/services/avatar/AvatarAction.scala b/pslogin/src/main/scala/services/avatar/AvatarAction.scala
new file mode 100644
index 000000000..451b38732
--- /dev/null
+++ b/pslogin/src/main/scala/services/avatar/AvatarAction.scala
@@ -0,0 +1,28 @@
+// Copyright (c) 2017 PSForever
+package services.avatar
+
+import net.psforever.objects.equipment.Equipment
+import net.psforever.packet.game.{PlanetSideGUID, PlayerStateMessageUpstream}
+import net.psforever.packet.game.objectcreate.ConstructorData
+import net.psforever.types.{ExoSuitType, Vector3}
+
+object AvatarAction {
+ trait Action
+
+ final case class ArmorChanged(player_guid : PlanetSideGUID, suit : ExoSuitType.Value, subtype : Int) extends Action
+ //final case class DropItem(pos : Vector3, orient : Vector3, item : PlanetSideGUID) extends Action
+ final case class EquipmentInHand(player_guid : PlanetSideGUID, slot : Int, item : Equipment) extends Action
+ final case class EquipmentOnGround(player_guid : PlanetSideGUID, pos : Vector3, orient : Vector3, item : Equipment) extends Action
+ final case class LoadPlayer(player_guid : PlanetSideGUID, pdata : ConstructorData) extends Action
+// final case class LoadMap(msg : PlanetSideGUID) extends Action
+// final case class unLoadMap(msg : PlanetSideGUID) extends Action
+ final case class ObjectDelete(player_guid : PlanetSideGUID, item_guid : PlanetSideGUID, unk : Int = 0) extends Action
+ final case class ObjectHeld(player_guid : PlanetSideGUID, slot : Int) extends Action
+ final case class PlanetsideAttribute(player_guid : PlanetSideGUID, attribute_type : Int, attribute_value : Long) extends Action
+ final case class PlayerState(player_guid : PlanetSideGUID, msg : PlayerStateMessageUpstream, spectator : Boolean, weaponInHand : Boolean) extends Action
+ final case class Reload(player_guid : PlanetSideGUID, mag : Int) extends Action
+// final case class PlayerStateShift(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action
+// final case class DestroyDisplay(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action
+// final case class HitHintReturn(killer : PlanetSideGUID, victim : PlanetSideGUID) extends Action
+// final case class ChangeWeapon(unk1 : Int, sessionId : Long) extends Action
+}
diff --git a/pslogin/src/main/scala/services/avatar/AvatarService.scala b/pslogin/src/main/scala/services/avatar/AvatarService.scala
new file mode 100644
index 000000000..5b656c23c
--- /dev/null
+++ b/pslogin/src/main/scala/services/avatar/AvatarService.scala
@@ -0,0 +1,150 @@
+// Copyright (c) 2017 PSForever
+package services.avatar
+
+import akka.actor.Actor
+import services.{GenericEventBus, Service}
+
+class AvatarService extends Actor {
+ //import AvatarServiceResponse._
+ private [this] val log = org.log4s.getLogger
+
+ override def preStart = {
+ log.info("Starting...")
+ }
+
+ val AvatarEvents = new GenericEventBus[AvatarServiceResponse] //AvatarEventBus
+
+ def receive = {
+ case Service.Join(channel) =>
+ val path = s"/$channel/Avatar"
+ val who = sender()
+
+ log.info(s"$who has joined $path")
+
+ AvatarEvents.subscribe(who, path)
+ case Service.Leave() =>
+ AvatarEvents.unsubscribe(sender())
+ case Service.LeaveAll() =>
+ AvatarEvents.unsubscribe(sender())
+
+ case AvatarServiceMessage(forChannel, action) =>
+ action match {
+ case AvatarAction.ArmorChanged(player_guid, suit, subtype) =>
+ AvatarEvents.publish(
+ AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarServiceResponse.ArmorChanged(suit, subtype))
+ )
+ case AvatarAction.EquipmentInHand(player_guid, slot, obj) =>
+ AvatarEvents.publish(
+ AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarServiceResponse.EquipmentInHand(slot, obj))
+ )
+ case AvatarAction.EquipmentOnGround(player_guid, pos, orient, obj) =>
+ AvatarEvents.publish(
+ AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarServiceResponse.EquipmentOnGround(pos, orient, obj))
+ )
+ case AvatarAction.LoadPlayer(player_guid, pdata) =>
+ AvatarEvents.publish(
+ AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarServiceResponse.LoadPlayer(pdata))
+ )
+ case AvatarAction.ObjectDelete(player_guid, item_guid, unk) =>
+ AvatarEvents.publish(
+ AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarServiceResponse.ObjectDelete(item_guid, unk))
+ )
+ case AvatarAction.ObjectHeld(player_guid, slot) =>
+ AvatarEvents.publish(
+ AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarServiceResponse.ObjectHeld(slot))
+ )
+ case AvatarAction.PlanetsideAttribute(guid, attribute_type, attribute_value) =>
+ AvatarEvents.publish(
+ AvatarServiceResponse(s"/$forChannel/Avatar", guid, AvatarServiceResponse.PlanetSideAttribute(attribute_type, attribute_value))
+ )
+ case AvatarAction.PlayerState(guid, msg, spectator, weapon) =>
+ AvatarEvents.publish(
+ AvatarServiceResponse(s"/$forChannel/Avatar", guid, AvatarServiceResponse.PlayerState(msg, spectator, weapon))
+ )
+ case AvatarAction.Reload(player_guid, mag) =>
+ AvatarEvents.publish(
+ AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarServiceResponse.Reload(mag))
+ )
+ case _ => ;
+ }
+
+ /*
+ case AvatarService.PlayerStateMessage(msg) =>
+ // log.info(s"NEW: ${m}")
+ val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(msg.avatar_guid)
+ if (playerOpt.isDefined) {
+ val player: PlayerAvatar = playerOpt.get
+ AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, msg.avatar_guid,
+ AvatarServiceReply.PlayerStateMessage(msg.pos, msg.vel, msg.facingYaw, msg.facingPitch, msg.facingYawUpper, msg.is_crouching, msg.is_jumping, msg.jump_thrust, msg.is_cloaked)
+ ))
+
+ }
+ case AvatarService.LoadMap(msg) =>
+ val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(msg.guid)
+ if (playerOpt.isDefined) {
+ val player: PlayerAvatar = playerOpt.get
+ AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, PlanetSideGUID(msg.guid),
+ AvatarServiceReply.LoadMap()
+ ))
+ }
+ case AvatarService.unLoadMap(msg) =>
+ val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(msg.guid)
+ if (playerOpt.isDefined) {
+ val player: PlayerAvatar = playerOpt.get
+ AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, PlanetSideGUID(msg.guid),
+ AvatarServiceReply.unLoadMap()
+ ))
+ }
+ case AvatarService.ObjectHeld(msg) =>
+ val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(msg.guid)
+ if (playerOpt.isDefined) {
+ val player: PlayerAvatar = playerOpt.get
+ AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, PlanetSideGUID(msg.guid),
+ AvatarServiceReply.ObjectHeld()
+ ))
+ }
+ case AvatarService.PlanetsideAttribute(guid, attribute_type, attribute_value) =>
+ val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(guid)
+ if (playerOpt.isDefined) {
+ val player: PlayerAvatar = playerOpt.get
+ AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, guid,
+ AvatarServiceReply.PlanetSideAttribute(attribute_type, attribute_value)
+ ))
+ }
+ case AvatarService.PlayerStateShift(killer, guid) =>
+ val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(guid)
+ if (playerOpt.isDefined) {
+ val player: PlayerAvatar = playerOpt.get
+ AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, guid,
+ AvatarServiceReply.PlayerStateShift(killer)
+ ))
+ }
+ case AvatarService.DestroyDisplay(killer, victim) =>
+ val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(victim)
+ if (playerOpt.isDefined) {
+ val player: PlayerAvatar = playerOpt.get
+ AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, victim,
+ AvatarServiceReply.DestroyDisplay(killer)
+ ))
+ }
+ case AvatarService.HitHintReturn(source_guid,victim_guid) =>
+ val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(source_guid)
+ if (playerOpt.isDefined) {
+ val player: PlayerAvatar = playerOpt.get
+ AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, victim_guid,
+ AvatarServiceReply.DestroyDisplay(source_guid)
+ ))
+ }
+ case AvatarService.ChangeWeapon(unk1, sessionId) =>
+ val playerOpt: Option[PlayerAvatar] = PlayerMasterList.getPlayer(sessionId)
+ if (playerOpt.isDefined) {
+ val player: PlayerAvatar = playerOpt.get
+ AvatarEvents.publish(AvatarMessage("/Avatar/" + player.continent, PlanetSideGUID(player.guid),
+ AvatarServiceReply.ChangeWeapon(unk1)
+ ))
+ }
+ */
+ case msg =>
+ log.info(s"Unhandled message $msg from $sender")
+ }
+}
diff --git a/pslogin/src/main/scala/services/avatar/AvatarServiceMessage.scala b/pslogin/src/main/scala/services/avatar/AvatarServiceMessage.scala
new file mode 100644
index 000000000..e3e35cd38
--- /dev/null
+++ b/pslogin/src/main/scala/services/avatar/AvatarServiceMessage.scala
@@ -0,0 +1,4 @@
+// Copyright (c) 2017 PSForever
+package services.avatar
+
+final case class AvatarServiceMessage(forChannel : String, actionMessage : AvatarAction.Action)
diff --git a/pslogin/src/main/scala/services/avatar/AvatarServiceResponse.scala b/pslogin/src/main/scala/services/avatar/AvatarServiceResponse.scala
new file mode 100644
index 000000000..29e05ed62
--- /dev/null
+++ b/pslogin/src/main/scala/services/avatar/AvatarServiceResponse.scala
@@ -0,0 +1,34 @@
+// Copyright (c) 2017 PSForever
+package services.avatar
+
+import net.psforever.objects.equipment.Equipment
+import net.psforever.packet.game.{PlanetSideGUID, PlayerStateMessageUpstream}
+import net.psforever.packet.game.objectcreate.ConstructorData
+import net.psforever.types.{ExoSuitType, Vector3}
+import services.GenericEventBusMsg
+
+final case class AvatarServiceResponse(toChannel : String,
+ avatar_guid : PlanetSideGUID,
+ replyMessage : AvatarServiceResponse.Response
+ ) extends GenericEventBusMsg
+
+object AvatarServiceResponse {
+ trait Response
+
+ final case class ArmorChanged(suit : ExoSuitType.Value, subtype : Int) extends Response
+ //final case class DropItem(pos : Vector3, orient : Vector3, item : PlanetSideGUID) extends Response
+ final case class EquipmentInHand(slot : Int, item : Equipment) extends Response
+ final case class EquipmentOnGround(pos : Vector3, orient : Vector3, item : Equipment) extends Response
+ final case class LoadPlayer(pdata : ConstructorData) extends Response
+// final case class unLoadMap() extends Response
+// final case class LoadMap() extends Response
+ final case class ObjectDelete(item_guid : PlanetSideGUID, unk : Int) extends Response
+ final case class ObjectHeld(slot : Int) extends Response
+ final case class PlanetSideAttribute(attribute_type : Int, attribute_value : Long) extends Response
+ final case class PlayerState(msg : PlayerStateMessageUpstream, spectator : Boolean, weaponInHand : Boolean) extends Response
+ final case class Reload(mag : Int) extends Response
+// final case class PlayerStateShift(itemID : PlanetSideGUID) extends Response
+// final case class DestroyDisplay(itemID : PlanetSideGUID) extends Response
+// final case class HitHintReturn(itemID : PlanetSideGUID) extends Response
+// final case class ChangeWeapon(facingYaw : Int) extends Response
+}
diff --git a/pslogin/src/main/scala/services/local/LocalAction.scala b/pslogin/src/main/scala/services/local/LocalAction.scala
new file mode 100644
index 000000000..4003fd9b0
--- /dev/null
+++ b/pslogin/src/main/scala/services/local/LocalAction.scala
@@ -0,0 +1,18 @@
+// Copyright (c) 2017 PSForever
+package services.local
+
+import net.psforever.objects.serverobject.PlanetSideServerObject
+import net.psforever.objects.serverobject.doors.Door
+import net.psforever.objects.zones.Zone
+import net.psforever.packet.game.{PlanetSideGUID, TriggeredSound}
+import net.psforever.types.Vector3
+
+object LocalAction {
+ trait Action
+
+ final case class DoorOpens(player_guid : PlanetSideGUID, continent : Zone, door : Door) extends Action
+ final case class DoorCloses(player_guid : PlanetSideGUID, door_guid : PlanetSideGUID) extends Action
+ final case class HackClear(player_guid : PlanetSideGUID, target : PlanetSideServerObject, unk1 : Long, unk2 : Long = 8L) extends Action
+ final case class HackTemporarily(player_guid : PlanetSideGUID, continent : Zone, target : PlanetSideServerObject, unk1 : Long, unk2 : Long = 8L) extends Action
+ final case class TriggerSound(player_guid : PlanetSideGUID, sound : TriggeredSound.Value, pos : Vector3, unk : Int, volume : Float) extends Action
+}
diff --git a/pslogin/src/main/scala/services/local/LocalService.scala b/pslogin/src/main/scala/services/local/LocalService.scala
new file mode 100644
index 000000000..56acf25d8
--- /dev/null
+++ b/pslogin/src/main/scala/services/local/LocalService.scala
@@ -0,0 +1,73 @@
+// Copyright (c) 2017 PSForever
+package services.local
+
+import akka.actor.{Actor, Props}
+import services.local.support.{DoorCloseActor, HackClearActor}
+import services.{GenericEventBus, Service}
+
+class LocalService extends Actor {
+ //import LocalService._
+ private val doorCloser = context.actorOf(Props[DoorCloseActor], "local-door-closer")
+ private val hackClearer = context.actorOf(Props[HackClearActor], "local-hack-clearer")
+ private [this] val log = org.log4s.getLogger
+
+ override def preStart = {
+ log.info("Starting...")
+ }
+
+ val LocalEvents = new GenericEventBus[LocalServiceResponse]
+
+ def receive = {
+ case Service.Join(channel) =>
+ val path = s"/$channel/LocalEnvironment"
+ val who = sender()
+ log.info(s"$who has joined $path")
+ LocalEvents.subscribe(who, path)
+ case Service.Leave() =>
+ LocalEvents.unsubscribe(sender())
+ case Service.LeaveAll() =>
+ LocalEvents.unsubscribe(sender())
+
+ case LocalServiceMessage(forChannel, action) =>
+ action match {
+ case LocalAction.DoorOpens(player_guid, zone, door) =>
+ doorCloser ! DoorCloseActor.DoorIsOpen(door, zone)
+ LocalEvents.publish(
+ LocalServiceResponse(s"/$forChannel/LocalEnvironment", player_guid, LocalServiceResponse.DoorOpens(door.GUID))
+ )
+ case LocalAction.DoorCloses(player_guid, door_guid) =>
+ LocalEvents.publish(
+ LocalServiceResponse(s"/$forChannel/LocalEnvironment", player_guid, LocalServiceResponse.DoorCloses(door_guid))
+ )
+ case LocalAction.HackClear(player_guid, target, unk1, unk2) =>
+ LocalEvents.publish(
+ LocalServiceResponse(s"/$forChannel/LocalEnvironment", player_guid, LocalServiceResponse.HackClear(target.GUID, unk1, unk2))
+ )
+ case LocalAction.HackTemporarily(player_guid, zone, target, unk1, unk2) =>
+ hackClearer ! HackClearActor.ObjectIsHacked(target, zone, unk1, unk2)
+ LocalEvents.publish(
+ LocalServiceResponse(s"/$forChannel/LocalEnvironment", player_guid, LocalServiceResponse.HackObject(target.GUID, unk1, unk2))
+ )
+ case LocalAction.TriggerSound(player_guid, sound, pos, unk, volume) =>
+ LocalEvents.publish(
+ LocalServiceResponse(s"/$forChannel/LocalEnvironment", player_guid, LocalServiceResponse.TriggerSound(sound, pos, unk, volume))
+ )
+ case _ => ;
+ }
+
+ //response from DoorCloseActor
+ case DoorCloseActor.CloseTheDoor(door_guid, zone_id) =>
+ LocalEvents.publish(
+ LocalServiceResponse(s"/$zone_id/LocalEnvironment", Service.defaultPlayerGUID, LocalServiceResponse.DoorCloses(door_guid))
+ )
+
+ //response from HackClearActor
+ case HackClearActor.ClearTheHack(target_guid, zone_id, unk1, unk2) =>
+ LocalEvents.publish(
+ LocalServiceResponse(s"/$zone_id/LocalEnvironment", Service.defaultPlayerGUID, LocalServiceResponse.HackClear(target_guid, unk1, unk2))
+ )
+
+ case msg =>
+ log.info(s"Unhandled message $msg from $sender")
+ }
+}
diff --git a/pslogin/src/main/scala/services/local/LocalServiceMessage.scala b/pslogin/src/main/scala/services/local/LocalServiceMessage.scala
new file mode 100644
index 000000000..fc6dd20aa
--- /dev/null
+++ b/pslogin/src/main/scala/services/local/LocalServiceMessage.scala
@@ -0,0 +1,4 @@
+// Copyright (c) 2017 PSForever
+package services.local
+
+final case class LocalServiceMessage(forChannel : String, actionMessage : LocalAction.Action)
diff --git a/pslogin/src/main/scala/services/local/LocalServiceResponse.scala b/pslogin/src/main/scala/services/local/LocalServiceResponse.scala
new file mode 100644
index 000000000..736732bc0
--- /dev/null
+++ b/pslogin/src/main/scala/services/local/LocalServiceResponse.scala
@@ -0,0 +1,21 @@
+// Copyright (c) 2017 PSForever
+package services.local
+
+import net.psforever.packet.game.{PlanetSideGUID, TriggeredSound}
+import net.psforever.types.Vector3
+import services.GenericEventBusMsg
+
+final case class LocalServiceResponse(toChannel : String,
+ avatar_guid : PlanetSideGUID,
+ replyMessage : LocalServiceResponse.Response
+ ) extends GenericEventBusMsg
+
+object LocalServiceResponse {
+ trait Response
+
+ final case class DoorOpens(door_guid : PlanetSideGUID) extends Response
+ final case class DoorCloses(door_guid : PlanetSideGUID) extends Response
+ final case class HackClear(target_guid : PlanetSideGUID, unk1 : Long, unk2 : Long) extends Response
+ final case class HackObject(target_guid : PlanetSideGUID, unk1 : Long, unk2 : Long) extends Response
+ final case class TriggerSound(sound : TriggeredSound.Value, pos : Vector3, unk : Int, volume : Float) extends Response
+}
diff --git a/pslogin/src/main/scala/services/local/support/DoorCloseActor.scala b/pslogin/src/main/scala/services/local/support/DoorCloseActor.scala
new file mode 100644
index 000000000..a2ca622cf
--- /dev/null
+++ b/pslogin/src/main/scala/services/local/support/DoorCloseActor.scala
@@ -0,0 +1,135 @@
+// Copyright (c) 2017 PSForever
+package services.local.support
+
+import akka.actor.{Actor, Cancellable}
+import net.psforever.objects.serverobject.doors.Door
+import net.psforever.objects.zones.Zone
+import net.psforever.packet.game.PlanetSideGUID
+
+import scala.annotation.tailrec
+import scala.concurrent.duration._
+
+/**
+ * Close an opened door after a certain amount of time has passed.
+ * This `Actor` is intended to sit on top of the event system that handles broadcast messaging regarding doors opening.
+ * @see `LocalService`
+ */
+class DoorCloseActor() extends Actor {
+ /** The periodic `Executor` that checks for doors to be closed */
+ private var doorCloserTrigger : Cancellable = DoorCloseActor.DefaultCloser
+ /** A `List` of currently open doors */
+ private var openDoors : List[DoorCloseActor.DoorEntry] = Nil
+ //private[this] val log = org.log4s.getLogger
+
+ def receive : Receive = {
+ case DoorCloseActor.DoorIsOpen(door, zone, time) =>
+ openDoors = openDoors :+ DoorCloseActor.DoorEntry(door, zone, time)
+ if(openDoors.size == 1) { //we were the only entry so the event must be started from scratch
+ import scala.concurrent.ExecutionContext.Implicits.global
+ doorCloserTrigger = context.system.scheduler.scheduleOnce(DoorCloseActor.timeout, self, DoorCloseActor.TryCloseDoors())
+ }
+
+ case DoorCloseActor.TryCloseDoors() =>
+ doorCloserTrigger.cancel
+ val now : Long = System.nanoTime
+ val (doorsToClose, doorsLeftOpen) = PartitionEntries(openDoors, now)
+ openDoors = doorsLeftOpen
+ doorsToClose.foreach(entry => {
+ entry.door.Open = false //permissible break from synchronization
+ context.parent ! DoorCloseActor.CloseTheDoor(entry.door.GUID, entry.zone.Id) //call up to the main event system
+ })
+
+ if(doorsLeftOpen.nonEmpty) {
+ val short_timeout : FiniteDuration = math.max(1, DoorCloseActor.timeout_time - (now - doorsLeftOpen.head.time)) nanoseconds
+ import scala.concurrent.ExecutionContext.Implicits.global
+ doorCloserTrigger = context.system.scheduler.scheduleOnce(short_timeout, self, DoorCloseActor.TryCloseDoors())
+ }
+
+ case _ => ;
+ }
+
+ /**
+ * Iterate over entries in a `List` until an entry that does not exceed the time limit is discovered.
+ * Separate the original `List` into two:
+ * a `List` of elements that have exceeded the time limit,
+ * and a `List` of elements that still satisfy the time limit.
+ * As newer entries to the `List` will always resolve later than old ones,
+ * and newer entries are always added to the end of the main `List`,
+ * processing in order is always correct.
+ * @param list the `List` of entries to divide
+ * @param now the time right now (in nanoseconds)
+ * @see `List.partition`
+ * @return a `Tuple` of two `Lists`, whose qualifications are explained above
+ */
+ private def PartitionEntries(list : List[DoorCloseActor.DoorEntry], now : Long) : (List[DoorCloseActor.DoorEntry], List[DoorCloseActor.DoorEntry]) = {
+ val n : Int = recursivePartitionEntries(list.iterator, now)
+ (list.take(n), list.drop(n)) //take and drop so to always return new lists
+ }
+
+ /**
+ * Mark the index where the `List` of elements can be divided into two:
+ * a `List` of elements that have exceeded the time limit,
+ * and a `List` of elements that still satisfy the time limit.
+ * @param iter the `Iterator` of entries to divide
+ * @param now the time right now (in nanoseconds)
+ * @param index a persistent record of the index where list division should occur;
+ * defaults to 0
+ * @return the index where division will occur
+ */
+ @tailrec private def recursivePartitionEntries(iter : Iterator[DoorCloseActor.DoorEntry], now : Long, index : Int = 0) : Int = {
+ if(!iter.hasNext) {
+ index
+ }
+ else {
+ val entry = iter.next()
+ if(now - entry.time >= DoorCloseActor.timeout_time) {
+ recursivePartitionEntries(iter, now, index + 1)
+ }
+ else {
+ index
+ }
+ }
+ }
+}
+
+object DoorCloseActor {
+ /** The wait before an open door closes; as a Long for calculation simplicity */
+ private final val timeout_time : Long = 5000000000L //nanoseconds (5s)
+ /** The wait before an open door closes; as a `FiniteDuration` for `Executor` simplicity */
+ private final val timeout : FiniteDuration = timeout_time nanoseconds
+
+ private final val DefaultCloser : Cancellable = new Cancellable() {
+ override def cancel : Boolean = true
+ override def isCancelled : Boolean = true
+ }
+
+ /**
+ * Message that carries information about a door that has been opened.
+ * @param door the door object
+ * @param zone the zone in which the door resides
+ * @param time when the door was opened
+ * @see `DoorEntry`
+ */
+ final case class DoorIsOpen(door : Door, zone : Zone, time : Long = System.nanoTime())
+ /**
+ * Message that carries information about a door that needs to close.
+ * Prompting, as compared to `DoorIsOpen` which is reactionary.
+ * @param door_guid the door
+ * @param zone_id the zone in which the door resides
+ */
+ final case class CloseTheDoor(door_guid : PlanetSideGUID, zone_id : String)
+ /**
+ * Internal message used to signal a test of the queued door information.
+ */
+ private final case class TryCloseDoors()
+
+ /**
+ * Entry of door information.
+ * The `zone` is maintained separately to ensure that any message resulting in an attempt to close doors is targetted.
+ * @param door the door object
+ * @param zone the zone in which the door resides
+ * @param time when the door was opened
+ * @see `DoorIsOpen`
+ */
+ private final case class DoorEntry(door : Door, zone : Zone, time : Long)
+}
diff --git a/pslogin/src/main/scala/services/local/support/HackClearActor.scala b/pslogin/src/main/scala/services/local/support/HackClearActor.scala
new file mode 100644
index 000000000..76a7e7f92
--- /dev/null
+++ b/pslogin/src/main/scala/services/local/support/HackClearActor.scala
@@ -0,0 +1,136 @@
+// Copyright (c) 2017 PSForever
+package services.local.support
+
+import akka.actor.{Actor, Cancellable}
+import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject}
+import net.psforever.objects.zones.Zone
+import net.psforever.packet.game.PlanetSideGUID
+
+import scala.annotation.tailrec
+import scala.concurrent.duration._
+
+/**
+ * Restore original functionality to an object that has been hacked after a certain amount of time has passed.
+ * This `Actor` is intended to sit on top of the event system that handles broadcast messaging regarding hacking events.
+ * @see `LocalService`
+ */
+class HackClearActor() extends Actor {
+ /** The periodic `Executor` that checks for server objects to be unhacked */
+ private var clearTrigger : Cancellable = HackClearActor.DefaultClearer
+ /** A `List` of currently hacked server objects */
+ private var hackedObjects : List[HackClearActor.HackEntry] = Nil
+ //private[this] val log = org.log4s.getLogger
+
+ def receive : Receive = {
+ case HackClearActor.ObjectIsHacked(target, zone, unk1, unk2, time) =>
+ hackedObjects = hackedObjects :+ HackClearActor.HackEntry(target, zone, unk1, unk2, time)
+ if(hackedObjects.size == 1) { //we were the only entry so the event must be started from scratch
+ import scala.concurrent.ExecutionContext.Implicits.global
+ clearTrigger = context.system.scheduler.scheduleOnce(HackClearActor.timeout, self, HackClearActor.TryClearHacks())
+ }
+
+ case HackClearActor.TryClearHacks() =>
+ clearTrigger.cancel
+ val now : Long = System.nanoTime
+ //TODO we can just walk across the list of doors and extract only the first few entries
+ val (unhackObjects, stillHackedObjects) = PartitionEntries(hackedObjects, now)
+ hackedObjects = stillHackedObjects
+ unhackObjects.foreach(entry => {
+ entry.target.Actor ! CommonMessages.ClearHack()
+ context.parent ! HackClearActor.ClearTheHack(entry.target.GUID, entry.zone.Id, entry.unk1, entry.unk2) //call up to the main event system
+ })
+
+ if(stillHackedObjects.nonEmpty) {
+ val short_timeout : FiniteDuration = math.max(1, HackClearActor.timeout_time - (now - stillHackedObjects.head.time)) nanoseconds
+ import scala.concurrent.ExecutionContext.Implicits.global
+ clearTrigger = context.system.scheduler.scheduleOnce(short_timeout, self, HackClearActor.TryClearHacks())
+ }
+
+ case _ => ;
+ }
+
+ /**
+ * Iterate over entries in a `List` until an entry that does not exceed the time limit is discovered.
+ * Separate the original `List` into two:
+ * a `List` of elements that have exceeded the time limit,
+ * and a `List` of elements that still satisfy the time limit.
+ * As newer entries to the `List` will always resolve later than old ones,
+ * and newer entries are always added to the end of the main `List`,
+ * processing in order is always correct.
+ * @param list the `List` of entries to divide
+ * @param now the time right now (in nanoseconds)
+ * @see `List.partition`
+ * @return a `Tuple` of two `Lists`, whose qualifications are explained above
+ */
+ private def PartitionEntries(list : List[HackClearActor.HackEntry], now : Long) : (List[HackClearActor.HackEntry], List[HackClearActor.HackEntry]) = {
+ val n : Int = recursivePartitionEntries(list.iterator, now)
+ (list.take(n), list.drop(n)) //take and drop so to always return new lists
+ }
+
+ /**
+ * Mark the index where the `List` of elements can be divided into two:
+ * a `List` of elements that have exceeded the time limit,
+ * and a `List` of elements that still satisfy the time limit.
+ * @param iter the `Iterator` of entries to divide
+ * @param now the time right now (in nanoseconds)
+ * @param index a persistent record of the index where list division should occur;
+ * defaults to 0
+ * @return the index where division will occur
+ */
+ @tailrec private def recursivePartitionEntries(iter : Iterator[HackClearActor.HackEntry], now : Long, index : Int = 0) : Int = {
+ if(!iter.hasNext) {
+ index
+ }
+ else {
+ val entry = iter.next()
+ if(now - entry.time >= HackClearActor.timeout_time) {
+ recursivePartitionEntries(iter, now, index + 1)
+ }
+ else {
+ index
+ }
+ }
+ }
+}
+
+object HackClearActor {
+ /** The wait before a server object is to unhack; as a Long for calculation simplicity */
+ private final val timeout_time : Long = 60000000000L //nanoseconds (60s)
+ /** The wait before a server object is to unhack; as a `FiniteDuration` for `Executor` simplicity */
+ private final val timeout : FiniteDuration = timeout_time nanoseconds
+
+ private final val DefaultClearer : Cancellable = new Cancellable() {
+ override def cancel : Boolean = true
+ override def isCancelled : Boolean = true
+ }
+
+ /**
+ * Message that carries information about a server object that has been hacked.
+ * @param target the server object
+ * @param zone the zone in which the object resides
+ * @param time when the object was hacked
+ * @see `HackEntry`
+ */
+ final case class ObjectIsHacked(target : PlanetSideServerObject, zone : Zone, unk1 : Long, unk2 : Long, time : Long = System.nanoTime())
+ /**
+ * Message that carries information about a server object that needs its functionality restored.
+ * Prompting, as compared to `ObjectIsHacked` which is reactionary.
+ * @param door_guid the server object
+ * @param zone_id the zone in which the object resides
+ */
+ final case class ClearTheHack(door_guid : PlanetSideGUID, zone_id : String, unk1 : Long, unk2 : Long)
+ /**
+ * Internal message used to signal a test of the queued door information.
+ */
+ private final case class TryClearHacks()
+
+ /**
+ * Entry of hacked server object information.
+ * The `zone` is maintained separately to ensure that any message resulting in an attempt to close doors is targetted.
+ * @param target the server object
+ * @param zone the zone in which the object resides
+ * @param time when the object was hacked
+ * @see `ObjectIsHacked`
+ */
+ private final case class HackEntry(target : PlanetSideServerObject, zone : Zone, unk1 : Long, unk2 : Long, time : Long)
+}